Compare commits

...

14 Commits

Author SHA1 Message Date
DrSmugleaf
dbb45f1c13 Version: 148.4.0 2023-08-21 23:19:25 -07:00
Leon Friedrich
6284e16b64 Add recursive PVS overrides and remove IsOverride() (#4262) 2023-08-21 23:17:53 -07:00
DrSmugleaf
f6c55085fe Version: 148.3.0 2023-08-21 19:01:15 -07:00
DrSmugleaf
30eafd26e7 Fix test checking that Robust's and .NET's colors are equal (#4287) 2023-08-21 16:26:06 -07:00
Pieter-Jan Briers
63423d96b4 Fixes for new color PR (#4278)
Undo change to violet color

add to named color list
2023-08-21 23:06:20 +02:00
Morb
474334aff2 Make DiscordRichPresence icon CVars server-side with replication (#4272) 2023-08-21 10:44:40 +02:00
Leon Friedrich
be102f86bf Add IntegrationInstance fields for common dependencies (#4283) 2023-08-21 14:35:27 +10:00
Tom Leys
d7962c7190 Add implementation of Random.Pick(ValueList<T> ..) (#4257) 2023-08-21 13:56:18 +10:00
Leon Friedrich
7fe9385c3b Change default value of EntityLastModifiedTick from zero to one. (#4282)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2023-08-21 13:55:41 +10:00
Kara
a9d9d1348a Tile texture reload command (#4268) 2023-08-21 13:46:58 +10:00
Leon Friedrich
4eaf624555 Allow pre-startup components to be shut down. (#4281) 2023-08-21 13:45:57 +10:00
Leon Friedrich
e37c131fb4 Prevent invalid prototypes from being spawned (#4279) 2023-08-21 12:29:02 +10:00
PrPleGoo
9df4606492 Added colors (#4278) 2023-08-20 18:48:00 +02:00
Pieter-Jan Briers
3be8070274 Happy eyeballs delay can be configured. 2023-08-20 17:45:43 +02:00
24 changed files with 420 additions and 142 deletions

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,6 +54,31 @@ END TEMPLATE-->
*None yet*
## 148.4.0
### New features
* Add recursive PVS overrides and remove IsOverride()
## 148.3.0
### New features
* Happy eyeballs delay can be configured.
* Added more colors.
* Allow pre-startup components to be shut down.
* Added tile texture reload command.
* Add implementation of Random.Pick(ValueList<T> ..).
* Add IntegrationInstance fields for common dependencies.
### Bugfixes
* Prevent invalid prototypes from being spawned.
* Change default value of EntityLastModifiedTick from zero to one.
* Make DiscordRichPresence icon CVars server-side with replication.
## 148.2.0
### New features

View File

@@ -558,3 +558,6 @@ cmd-vfs_ls-help = Usage: vfs_list <path>
cmd-vfs_ls-err-args = Need exactly 1 argument.
cmd-vfs_ls-hint-path = <path>
cmd-reloadtiletextures-desc = Reloads the tile texture atlas to allow hot reloading tile sprites
cmd-reloadtiletextures-help = Usage: reloadtiletextures

View File

@@ -68,6 +68,7 @@ namespace Robust.Client
deps.Register<IComponentFactory, ComponentFactory>();
deps.Register<ITileDefinitionManager, ClydeTileDefinitionManager>();
deps.Register<IClydeTileDefinitionManager, ClydeTileDefinitionManager>();
deps.Register<ClydeTileDefinitionManager, ClydeTileDefinitionManager>();
deps.Register<GameController, GameController>();
deps.Register<IGameController, GameController>();
deps.Register<IGameControllerInternal, GameController>();

View File

@@ -3,13 +3,16 @@ using System.Collections.Generic;
using System.Linq;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Map;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
@@ -52,8 +55,11 @@ namespace Robust.Client.Map
_genTextureAtlas();
}
private void _genTextureAtlas()
internal void _genTextureAtlas()
{
_tileRegions.Clear();
_tileTextureAtlas = null;
var defList = TileDefs.Where(t => t.Sprite != null).ToList();
// If there are no tile definitions, we do nothing.
@@ -144,4 +150,17 @@ namespace Robust.Client.Map
_tileTextureAtlas = Texture.LoadFromImage(sheet, "Tile Atlas");
}
}
public sealed class ReloadTileTexturesCommand : LocalizedCommands
{
[Dependency] private readonly ClydeTileDefinitionManager _tile = default!;
public override string Command => "reloadtiletextures";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_tile._genTextureAtlas();
}
}
}

View File

@@ -81,30 +81,35 @@ namespace Robust.Server.GameObjects
private protected override EntityUid CreateEntity(string? prototypeName, EntityUid uid = default, IEntityLoadContext? context = null)
{
var entity = base.CreateEntity(prototypeName, uid, context);
if (prototypeName == null)
return base.CreateEntity(prototypeName, uid, context);
if (!string.IsNullOrWhiteSpace(prototypeName))
{
var prototype = PrototypeManager.Index<EntityPrototype>(prototypeName);
if (!PrototypeManager.TryIndex<EntityPrototype>(prototypeName, out var prototype))
throw new EntityCreationException($"Attempted to spawn an entity with an invalid prototype: {prototypeName}");
// At this point in time, all data configure on the entity *should* be purely from the prototype.
// As such, we can reset the modified ticks to Zero,
// which indicates "not different from client's own deserialization".
// So the initial data for the component or even the creation doesn't have to be sent over the wire.
foreach (var (netId, component) in GetNetComponents(entity))
{
// Make sure to ONLY get components that are defined in the prototype.
// Others could be instantiated directly by AddComponent (e.g. ContainerManager).
// And those aren't guaranteed to exist on the client, so don't clear them.
var compName = ComponentFactory.GetComponentName(component.GetType());
if (prototype.Components.ContainsKey(compName))
component.ClearTicks();
}
}
var entity = base.CreateEntity(prototype, uid, context);
// At this point in time, all data configure on the entity *should* be purely from the prototype.
// As such, we can reset the modified ticks to Zero,
// which indicates "not different from client's own deserialization".
// So the initial data for the component or even the creation doesn't have to be sent over the wire.
ClearTicks(entity, prototype);
return entity;
}
private void ClearTicks(EntityUid entity, EntityPrototype prototype)
{
foreach (var (netId, component) in GetNetComponents(entity))
{
// Make sure to ONLY get components that are defined in the prototype.
// Others could be instantiated directly by AddComponent (e.g. ContainerManager).
// And those aren't guaranteed to exist on the client, so don't clear them.
var compName = ComponentFactory.GetComponentName(netId);
if (prototype.Components.ContainsKey(compName))
component.ClearTicks();
}
}
public override EntityStringRepresentation ToPrettyString(EntityUid uid)
{
TryGetComponent(uid, out ActorComponent? actor);

View File

@@ -77,11 +77,21 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
/// </summary>
private readonly HashSet<TIndex> _globalOverrides = new();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent along with all of their children.
/// </summary>
private readonly HashSet<TIndex> _globalRecursiveOverrides = new();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent.
/// </summary>
public HashSet<TIndex>.Enumerator GlobalOverridesEnumerator => _globalOverrides.GetEnumerator();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent along with all of their children.
/// </summary>
public HashSet<TIndex>.Enumerator GlobalRecursiveOverridesEnumerator => _globalRecursiveOverrides.GetEnumerator();
/// <summary>
/// List of <see cref="TIndex"/> that should always get sent to a certain <see cref="ICommonSession"/>.
/// </summary>
@@ -203,8 +213,11 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
{
switch (location)
{
case GlobalOverride _:
_globalOverrides.Add(index);
case GlobalOverride global:
if (global.Recursive)
_globalRecursiveOverrides.Add(index);
else
_globalOverrides.Add(index);
break;
case GridChunkLocation gridChunkLocation:
// might be gone due to grid-deletions
@@ -239,8 +252,11 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
// since we can find the index, we can assume the dicts will be there too & dont need to do any checks. gaming.
switch (location)
{
case GlobalOverride _:
_globalOverrides.Remove(index);
case GlobalOverride global:
if (global.Recursive)
_globalRecursiveOverrides.Remove(index);
else
_globalOverrides.Remove(index);
break;
case GridChunkLocation gridChunkLocation:
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Remove(index);
@@ -376,15 +392,10 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
#region UpdateIndex
private bool IsOverride(TIndex index)
private bool TryGetLocation(TIndex index, out IIndexLocation? location)
{
if (_locationChangeBuffer.TryGetValue(index, out var change) &&
change is GlobalOverride or LocalOverride) return true;
if (_indexLocations.TryGetValue(index, out var indexLoc) &&
indexLoc is GlobalOverride or LocalOverride) return true;
return false;
return _locationChangeBuffer.TryGetValue(index, out location)
|| _indexLocations.TryGetValue(index, out location);
}
/// <summary>
@@ -392,15 +403,25 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
/// </summary>
/// <param name="index">The <see cref="TIndex"/> to update.</param>
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
public void UpdateIndex(TIndex index, bool removeFromOverride = false)
/// <param name="recursive">If true, this will also recursively send any children of the given index.</param>
public void AddGlobalOverride(TIndex index, bool removeFromOverride, bool recursive)
{
if(!removeFromOverride && IsOverride(index))
if (!TryGetLocation(index, out var oldLocation))
{
RegisterUpdate(index, new GlobalOverride(recursive));
return;
}
if (!removeFromOverride && oldLocation is LocalOverride)
return;
if (_indexLocations.TryGetValue(index, out var oldLocation) &&
oldLocation is GlobalOverride) return;
if (oldLocation is GlobalOverride global &&
(!removeFromOverride || global.Recursive == recursive))
{
return;
}
RegisterUpdate(index, new GlobalOverride());
RegisterUpdate(index, new GlobalOverride(recursive));
}
/// <summary>
@@ -411,12 +432,20 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
public void UpdateIndex(TIndex index, ICommonSession session, bool removeFromOverride = false)
{
if(!removeFromOverride && IsOverride(index))
if (!TryGetLocation(index, out var oldLocation))
{
RegisterUpdate(index, new LocalOverride(session));
return;
}
if (!removeFromOverride || oldLocation is GlobalOverride)
return;
if (_indexLocations.TryGetValue(index, out var oldLocation) &&
oldLocation is LocalOverride local &&
local.Session == session) return;
if (oldLocation is LocalOverride local &&
(!removeFromOverride || local.Session == session))
{
return;
}
RegisterUpdate(index, new LocalOverride(session));
}
@@ -429,34 +458,42 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
public void UpdateIndex(TIndex index, EntityCoordinates coordinates, bool removeFromOverride = false)
{
if(!removeFromOverride && IsOverride(index))
if (!removeFromOverride
&& TryGetLocation(index, out var oldLocation)
&& oldLocation is GlobalOverride or LocalOverride)
{
return;
}
if (!_entityManager.TryGetComponent(coordinates.EntityId, out TransformComponent? xform))
return;
var gridIdOpt = coordinates.GetGridUid(_entityManager);
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
if (xform.GridUid is { } gridId && gridId.IsValid())
{
var gridIndices = GetChunkIndices(coordinates.Position);
UpdateIndex(index, gridId, gridIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
return;
}
var mapCoordinates = coordinates.ToMap(_entityManager, _transformSystem);
var mapIndices = GetChunkIndices(coordinates.Position);
UpdateIndex(index, mapCoordinates.MapId, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
var worldPos = _transformSystem.GetWorldMatrix(xform).Transform(coordinates.Position);
var mapIndices = GetChunkIndices(worldPos);
UpdateIndex(index, xform.MapID, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
}
public IChunkIndexLocation GetChunkIndex(EntityCoordinates coordinates)
{
var gridIdOpt = coordinates.GetGridUid(_entityManager);
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
if (!_entityManager.TryGetComponent(coordinates.EntityId, out TransformComponent? xform))
return new MapChunkLocation(default, default);
if (xform.GridUid is { } gridId && gridId.IsValid())
{
var gridIndices = GetChunkIndices(coordinates.Position);
return new GridChunkLocation(gridId, gridIndices);
}
var mapCoordinates = coordinates.ToMap(_entityManager, _transformSystem);
var mapIndices = GetChunkIndices(coordinates.Position);
return new MapChunkLocation(mapCoordinates.MapId, mapIndices);
var worldPos = _transformSystem.GetWorldMatrix(xform).Transform(coordinates.Position);
var mapIndices = GetChunkIndices(worldPos);
return new MapChunkLocation(xform.MapID, mapIndices);
}
/// <summary>
@@ -469,11 +506,14 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
/// <param name="forceDirty">If true, this will mark the previous chunk as dirty even if the entity did not move from that chunk.</param>
public void UpdateIndex(TIndex index, EntityUid gridId, Vector2i chunkIndices, bool removeFromOverride = false, bool forceDirty = false)
{
if(!removeFromOverride && IsOverride(index))
_locationChangeBuffer.TryGetValue(index, out var bufferedLocation);
_indexLocations.TryGetValue(index, out var oldLocation);
//removeFromOverride is false 99% of the time.
if ((bufferedLocation ?? oldLocation) is GlobalOverride or LocalOverride && !removeFromOverride)
return;
if (_indexLocations.TryGetValue(index, out var oldLocation) &&
oldLocation is GridChunkLocation oldGrid &&
if (oldLocation is GridChunkLocation oldGrid &&
oldGrid.ChunkIndices == chunkIndices &&
oldGrid.GridId == gridId)
{
@@ -497,22 +537,26 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
/// <param name="forceDirty">If true, this will mark the previous chunk as dirty even if the entity did not move from that chunk.</param>
public void UpdateIndex(TIndex index, MapId mapId, Vector2i chunkIndices, bool removeFromOverride = false, bool forceDirty = false)
{
if(!removeFromOverride && IsOverride(index))
_locationChangeBuffer.TryGetValue(index, out var bufferedLocation);
_indexLocations.TryGetValue(index, out var oldLocation);
//removeFromOverride is false 99% of the time.
if ((bufferedLocation ?? oldLocation) is GlobalOverride or LocalOverride && !removeFromOverride)
return;
if (_indexLocations.TryGetValue(index, out var oldLocation) &&
oldLocation is MapChunkLocation oldMap &&
// Is this entity just returning to its old location?
if (oldLocation is MapChunkLocation oldMap &&
oldMap.ChunkIndices == chunkIndices &&
oldMap.MapId == mapId)
{
_locationChangeBuffer.Remove(index);
if (bufferedLocation != null)
_locationChangeBuffer.Remove(index);
if (forceDirty)
_dirtyChunks.Add(oldMap);
return;
}
RegisterUpdate(index, new MapChunkLocation(mapId, chunkIndices));
}
@@ -584,7 +628,18 @@ public struct GridChunkLocation : IIndexLocation, IChunkIndexLocation, IEquatabl
}
}
public struct GlobalOverride : IIndexLocation { }
public struct GlobalOverride : IIndexLocation
{
/// <summary>
/// If true, this will also send all children of the override.
/// </summary>
public readonly bool Recursive;
public GlobalOverride(bool recursive)
{
Recursive = recursive;
}
}
public struct LocalOverride : IIndexLocation
{

View File

@@ -12,20 +12,23 @@ public sealed class PvsOverrideSystem : EntitySystem
[Shared.IoC.Dependency] private readonly PvsSystem _pvs = default!;
/// <summary>
/// Used to ensure that an entity is always sent to every client. Overrides any client-specific overrides.
/// Used to ensure that an entity is always sent to every client. By default this overrides any client-specific overrides.
/// </summary>
public void AddGlobalOverride(EntityUid uid)
/// <param name="removeExistingOverride">Whether or not to supersede existing overrides.</param>
/// <param name="recursive">If true, this will also recursively send any children of the given index.</param>
public void AddGlobalOverride(EntityUid uid, bool removeExistingOverride = true, bool recursive = false)
{
_pvs.EntityPVSCollection.UpdateIndex(uid, true);
_pvs.EntityPVSCollection.AddGlobalOverride(uid, removeExistingOverride, recursive);
}
/// <summary>
/// Used to ensure that an entity is always sent to a specific client. Overrides any global or pre-existing
/// client-specific overrides.
/// </summary>
public void AddSessionOverride(EntityUid uid, ICommonSession session)
/// <param name="removeExistingOverride">Whether or not to supersede existing overrides.</param>
public void AddSessionOverride(EntityUid uid, ICommonSession session,bool removeExistingOverride = true)
{
_pvs.EntityPVSCollection.UpdateIndex(uid, session, true);
_pvs.EntityPVSCollection.UpdateIndex(uid, session, removeExistingOverride);
}
/// <summary>

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.Extensions.ObjectPool;
using Robust.Server.Configuration;
@@ -13,11 +12,9 @@ using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Players;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
@@ -107,6 +104,7 @@ internal sealed partial class PvsSystem : EntitySystem
private EntityQuery<EyeComponent> _eyeQuery;
private EntityQuery<TransformComponent> _xformQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
public override void Initialize()
{
@@ -114,6 +112,7 @@ internal sealed partial class PvsSystem : EntitySystem
_eyeQuery = GetEntityQuery<EyeComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
_metaQuery = GetEntityQuery<MetaDataComponent>();
_entityPvsCollection = RegisterPVSCollection<EntityUid>();
@@ -387,7 +386,7 @@ internal sealed partial class PvsSystem : EntitySystem
pvsCollection.AddGrid(gridId);
}
_entityPvsCollection.UpdateIndex(gridId);
_entityPvsCollection.AddGlobalOverride(gridId, true, false);
}
private void OnMapDestroyed(MapChangedEvent e)
@@ -407,7 +406,7 @@ internal sealed partial class PvsSystem : EntitySystem
if(e.Map == MapId.Nullspace) return;
var uid = _mapManager.GetMapEntityId(e.Map);
_entityPvsCollection.UpdateIndex(uid);
_entityPvsCollection.AddGlobalOverride(uid, true, false);
}
#endregion
@@ -749,6 +748,16 @@ internal sealed partial class PvsSystem : EntitySystem
}
globalEnumerator.Dispose();
var globalRecursiveEnumerator = _entityPvsCollection.GlobalRecursiveOverridesEnumerator;
while (globalRecursiveEnumerator.MoveNext())
{
var uid = globalRecursiveEnumerator.Current;
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, lastSeen, in mQuery, in tQuery, in fromTick,
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget, true);
}
globalRecursiveEnumerator.Dispose();
var localEnumerator = _entityPvsCollection.GetElementsForSession(session);
while (localEnumerator.MoveNext())
{
@@ -764,7 +773,7 @@ internal sealed partial class PvsSystem : EntitySystem
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
}
var expandEvent = new ExpandPvsEvent(session, new List<EntityUid>());
var expandEvent = new ExpandPvsEvent(session);
RaiseLocalEvent(ref expandEvent);
foreach (var entityUid in expandEvent.Entities)
{
@@ -772,6 +781,12 @@ internal sealed partial class PvsSystem : EntitySystem
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
}
foreach (var entityUid in expandEvent.RecursiveEntities)
{
RecursivelyAddOverride(in entityUid, lastAcked, lastSent, visibleEnts, lastSeen, in mQuery, in tQuery, in fromTick,
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget, true);
}
var entityStates = new List<EntityState>(entStateCount);
foreach (var (uid, visiblity) in visibleEnts)
@@ -898,8 +913,7 @@ internal sealed partial class PvsSystem : EntitySystem
}
}
public bool RecursivelyAddOverride(
in EntityUid uid,
public bool RecursivelyAddOverride(in EntityUid uid,
Dictionary<EntityUid, PvsEntityVisibility>? lastAcked,
Dictionary<EntityUid, PvsEntityVisibility>? lastSent,
Dictionary<EntityUid, PvsEntityVisibility> toSend,
@@ -911,34 +925,74 @@ internal sealed partial class PvsSystem : EntitySystem
ref int enteredEntityCount,
ref int entStateCount,
in int newEntityBudget,
in int enteredEntityBudget)
in int enteredEntityBudget,
bool addChildren = false)
{
//are we valid?
//sometimes uids gets added without being valid YET (looking at you mapmanager) (mapcreate & gridcreated fire before the uids becomes valid)
if (!uid.IsValid()) return false;
if (!uid.IsValid())
return false;
var parent = transQuery.GetComponent(uid).ParentUid;
var xform = transQuery.GetComponent(uid);
var parent = xform.ParentUid;
if (parent.IsValid() && !RecursivelyAddOverride(in parent, lastAcked, lastSent, toSend, lastSeen, in metaQuery, in transQuery, in fromTick,
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget))
return false;
//did we already get added?
if (toSend.ContainsKey(uid)) return true;
// Note that we check this AFTER adding parents. This is because while this entity may already have been added
// to the toSend set, it doesn't guarantee that its parents have been. E.g., if a player ghost just teleported
// to follow a far away entity, the player's own entity is still being sent, but we need to ensure that we also
// send the new parents, which may otherwise be delayed because of the PVS budget..
if (!toSend.ContainsKey(uid))
{
// TODO PERFORMANCE.
// ProcessEntry() unnecessarily checks lastSent.ContainsKey() and maybe lastSeen.Contains(). Given that at this
// point the budgets are just ignored, this should just bypass those checks. But then again 99% of the time this
// is just the player's own entity + maybe a singularity. So currently not all that performance intensive.
var (entered, _) = ProcessEntry(in uid, lastAcked, lastSent, lastSeen, ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
AddToSendSet(in uid, metaQuery.GetComponent(uid), toSend, fromTick, in entered, ref entStateCount);
}
// TODO PERFORMANCE.
// ProcessEntry() unnecessarily checks lastSent.ContainsKey() and maybe lastSeen.Contains(). Given that at this
// point the budgets are just ignored, this should just bypass those checks. But then again 99% of the time this
// is just the player's own entity + maybe a singularity. So currently not all that performance intensive.
var (entered, _) = ProcessEntry(in uid, lastAcked, lastSent, lastSeen, ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
if (addChildren)
{
RecursivelyAddChildren(xform, lastAcked, lastSent, toSend, lastSeen, fromTick, ref newEntityCount,
ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
}
AddToSendSet(in uid, metaQuery.GetComponent(uid), toSend, fromTick, in entered, ref entStateCount);
return true;
}
private void RecursivelyAddChildren(TransformComponent xform,
Dictionary<EntityUid, PvsEntityVisibility>? lastAcked,
Dictionary<EntityUid, PvsEntityVisibility>? lastSent,
Dictionary<EntityUid, PvsEntityVisibility> toSend,
Dictionary<EntityUid, GameTick> lastSeen,
in GameTick fromTick,
ref int newEntityCount,
ref int enteredEntityCount,
ref int entStateCount,
in int newEntityBudget,
in int enteredEntityBudget)
{
foreach (var child in xform.ChildEntities)
{
if (!_xformQuery.TryGetComponent(child, out var childXform))
continue;
if (!toSend.ContainsKey(child))
{
var (entered, _) = ProcessEntry(in child, lastAcked, lastSent, lastSeen, ref newEntityCount,
ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
AddToSendSet(in child, _metaQuery.GetComponent(child), toSend, fromTick, in entered, ref entStateCount);
}
RecursivelyAddChildren(childXform, lastAcked, lastSent, toSend, lastSeen, fromTick, ref newEntityCount,
ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
}
}
private (bool Entered, bool ShouldAdd) ProcessEntry(in EntityUid uid,
Dictionary<EntityUid, PvsEntityVisibility>? lastAcked,
Dictionary<EntityUid, PvsEntityVisibility>? lastSent,
@@ -1034,26 +1088,20 @@ internal sealed partial class PvsSystem : EntitySystem
while (query.MoveNext(out var uid, out var md))
{
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
if (md.EntityLastModifiedTick <= fromTick)
continue;
var state = GetEntityState(player, uid, fromTick, md);
// Temporary debugging code.
// TODO REMOVE TEMPORARY CODE
if (state.Empty)
{
var msg = $"{nameof(GetEntityState)} returned an empty state while enumerating all. Entity {ToPrettyString(uid)}. Net component Data:";
foreach (var (_, cmp) in EntityManager.GetNetComponents(uid))
{
msg += $"\nName: {_factory.GetComponentName(cmp.GetType())}" +
$"Enabled: {cmp.NetSyncEnabled}, " +
$"Lifestage: {cmp.LifeStage}, " +
$"OwnerOnly: {cmp.SendOnlyToOwner}, " +
$"SessionSpecific: {cmp.SessionSpecific}, " +
$"LastModified: {cmp.LastModifiedTick}";
}
Log.Error(msg);
Log.Error($@"{nameof(GetEntityState)} returned an empty state while enumerating entities.
Tick: {fromTick}--{_gameTiming.CurTick}
Entity: {ToPrettyString(uid)}
Last modified: {md.EntityLastModifiedTick}
Metadata last modified: {md.LastModifiedTick}
Transform last modified: {Transform(uid).LastModifiedTick}");
}
stateEntities.Add(state);
@@ -1077,26 +1125,21 @@ internal sealed partial class PvsSystem : EntitySystem
continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick || md.EntityLastModifiedTick == GameTick.Zero);
DebugTools.Assert(md.EntityLastModifiedTick > fromTick || md.EntityLastModifiedTick == GameTick.Zero);
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick);
DebugTools.Assert(md.EntityLastModifiedTick > fromTick);
var state = GetEntityState(player, uid, fromTick, md);
// Temporary debugging code.
// TODO REMOVE TEMPORARY CODE
if (state.Empty)
{
var msg = $"{nameof(GetEntityState)} returned an empty state for new entity {ToPrettyString(uid)}. Net component Data:";
foreach (var (_, cmp) in EntityManager.GetNetComponents(uid))
{
msg += $"\nName: {_factory.GetComponentName(cmp.GetType())}" +
$"Enabled: {cmp.NetSyncEnabled}, " +
$"Lifestage: {cmp.LifeStage}, " +
$"OwnerOnly: {cmp.SendOnlyToOwner}, " +
$"SessionSpecific: {cmp.SessionSpecific}, " +
$"LastModified: {cmp.LastModifiedTick}";
}
Log.Error(msg);
Log.Error($@"{nameof(GetEntityState)} returned an empty state for a new entity.
Tick: {fromTick}--{_gameTiming.CurTick}
Entity: {ToPrettyString(uid)}
Last modified: {md.EntityLastModifiedTick}
Metadata last modified: {md.LastModifiedTick}
Transform last modified: {Transform(uid).LastModifiedTick}");
continue;
}
stateEntities.Add(state);
@@ -1109,8 +1152,9 @@ internal sealed partial class PvsSystem : EntitySystem
continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick || md.EntityLastModifiedTick == GameTick.Zero);
DebugTools.Assert(md.EntityLastModifiedTick > fromTick || md.EntityLastModifiedTick == GameTick.Zero);
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick);
DebugTools.Assert(md.EntityLastModifiedTick > fromTick);
var state = GetEntityState(player, uid, fromTick, md);
if (!state.Empty)
@@ -1330,11 +1374,20 @@ internal sealed partial class PvsSystem : EntitySystem
public readonly struct ExpandPvsEvent
{
public readonly IPlayerSession Session;
public readonly List<EntityUid> Entities;
public ExpandPvsEvent(IPlayerSession session, List<EntityUid> entities)
/// <summary>
/// List of entities that will get added to this session's PVS set.
/// </summary>
public readonly List<EntityUid> Entities = new();
/// <summary>
/// List of entities that will get added to this session's PVS set. Unlike <see cref="Entities"/> this will also
/// recursively add all children of the given entity.
/// </summary>
public readonly List<EntityUid> RecursiveEntities = new();
public ExpandPvsEvent(IPlayerSession session)
{
Session = session;
Entities = entities;
}
}

View File

@@ -29,7 +29,6 @@ using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using JetBrains.Annotations;
using Robust.Shared.Utility;
using SysVector3 = System.Numerics.Vector3;
@@ -1638,6 +1637,11 @@ namespace Robust.Shared.Maths
/// </summary>
public static Color RoyalBlue => new(65, 105, 225, 255);
/// <summary>
/// Gets the system color with (R, G, B, A) = (204, 71, 120, 255).
/// </summary>
public static Color Ruber => new(204, 71, 120, 255);
/// <summary>
/// Gets the system color with (R, G, B, A) = (139, 69, 19, 255).
/// </summary>
@@ -1653,6 +1657,11 @@ namespace Robust.Shared.Maths
/// </summary>
public static Color SandyBrown => new(244, 164, 96, 255);
/// <summary>
/// Gets the system color with (R, G, B, A) = (0, 66, 153, 255).
/// </summary>
public static Color SeaBlue => new(0, 66, 153, 255);
/// <summary>
/// Gets the system color with (R, G, B, A) = (46, 139, 87, 255).
/// </summary>
@@ -1733,6 +1742,16 @@ namespace Robust.Shared.Maths
/// </summary>
public static Color Violet => new(238, 130, 238, 255);
/// <summary>
/// Gets the system color with (R, G, B, A) = (126, 3, 168, 255).
/// </summary>
public static Color BetterViolet => new(126, 3, 168, 255);
/// <summary>
/// Gets the system color with (R, G, B, A) = (255, 153, 0, 255).
/// </summary>
public static Color VividGamboge => new(255, 153, 0, 255);
/// <summary>
/// Gets the system color with (R, G, B, A) = (245, 222, 179, 255).
/// </summary>
@@ -1767,6 +1786,7 @@ namespace Robust.Shared.Maths
["aquamarine"] = Aquamarine,
["azure"] = Azure,
["beige"] = Beige,
["betterviolet"] = BetterViolet,
["bisque"] = Bisque,
["black"] = Black,
["blanchedalmond"] = BlanchedAlmond,
@@ -1877,9 +1897,11 @@ namespace Robust.Shared.Maths
["red"] = Red,
["rosybrown"] = RosyBrown,
["royalblue"] = RoyalBlue,
["ruber"] = Ruber,
["saddlebrown"] = SaddleBrown,
["salmon"] = Salmon,
["sandybrown"] = SandyBrown,
["seablue"] = SeaBlue,
["seagreen"] = SeaGreen,
["seashell"] = SeaShell,
["sienna"] = Sienna,
@@ -1896,6 +1918,7 @@ namespace Robust.Shared.Maths
["tomato"] = Tomato,
["turquoise"] = Turquoise,
["violet"] = Violet,
["vividgamboge"] = VividGamboge,
["wheat"] = Wheat,
["white"] = White,
["whitesmoke"] = WhiteSmoke,

View File

@@ -283,6 +283,12 @@ namespace Robust.Shared
/// </summary>
public static readonly CVarDef<float> NetFakeDuplicates = CVarDef.Create("net.fakeduplicates", 0f, CVar.CHEAT);
/// <summary>
/// When using Happy Eyeballs to try both IPv6 over IPv4, the delay that IPv4 gets to get less priority.
/// </summary>
public static readonly CVarDef<float> NetHappyEyeballsDelay =
CVarDef.Create("net.happy_eyeballs_delay", 0.025f, CVar.CLIENTONLY);
/**
* SUS
*/
@@ -1189,10 +1195,10 @@ namespace Robust.Shared
CVarDef.Create("discord.enabled", true, CVar.CLIENTONLY);
public static readonly CVarDef<string> DiscordRichPresenceMainIconId =
CVarDef.Create("discord.rich_main_icon_id", "devstation", CVar.CLIENTONLY);
CVarDef.Create("discord.rich_main_icon_id", "devstation", CVar.SERVER | CVar.REPLICATED);
public static readonly CVarDef<string> DiscordRichPresenceSecondIconId =
CVarDef.Create("discord.rich_second_icon_id", "logo", CVar.CLIENTONLY);
CVarDef.Create("discord.rich_second_icon_id", "logo", CVar.SERVER | CVar.REPLICATED);
/*
* RES

View File

@@ -100,9 +100,15 @@ namespace Robust.Shared.GameObjects
/// </remarks>
internal void LifeShutdown(IEntityManager entManager)
{
// Starting allows a component to remove itself in it's own Startup function.
DebugTools.Assert(LifeStage == ComponentLifeStage.Starting || LifeStage == ComponentLifeStage.Running);
DebugTools.Assert(LifeStage is >= ComponentLifeStage.Initializing and < ComponentLifeStage.Stopping);
if (LifeStage <= ComponentLifeStage.Initialized)
{
// Component was never started, no shutdown logic necessary. Simply mark it as stopped.
LifeStage = ComponentLifeStage.Stopped;
return;
}
LifeStage = ComponentLifeStage.Stopping;
entManager.EventBus.RaiseComponentEvent(this, CompShutdownInstance);
LifeStage = ComponentLifeStage.Stopped;

View File

@@ -328,6 +328,12 @@ namespace Robust.Shared.GameObjects
return GetRegistration(componentType).Name;
}
[Pure]
public string GetComponentName(ushort netID)
{
return GetRegistration(netID).Name;
}
public ComponentRegistration GetRegistration(ushort netID)
{
if (_networkedComponents is null)

View File

@@ -71,7 +71,7 @@ namespace Robust.Shared.GameObjects
// Every entity starts at tick 1, because they are conceptually created in the time between 0->1
[ViewVariables]
public GameTick EntityLastModifiedTick { get; internal set; } = GameTick.Zero;
public GameTick EntityLastModifiedTick { get; internal set; } = GameTick.First;
/// <summary>
/// This is the tick at which the client last applied state data received from the server.

View File

@@ -131,7 +131,7 @@ namespace Robust.Shared.GameObjects
foreach (var comp in comps)
{
if (comp is { LifeStage: < ComponentLifeStage.Initialized })
if (comp is { LifeStage: ComponentLifeStage.Added })
comp.LifeInitialize(this, CompIdx.Index(comp.GetType()));
}
@@ -490,7 +490,7 @@ namespace Robust.Shared.GameObjects
return;
}
if (component.Running)
if (component.LifeStage >= ComponentLifeStage.Initialized && component.LifeStage <= ComponentLifeStage.Running)
component.LifeShutdown(this);
#if EXCEPTION_TOLERANCE
}

View File

@@ -751,8 +751,17 @@ namespace Robust.Shared.GameObjects
if (prototypeName == null)
return AllocEntity(out _, uid);
PrototypeManager.TryIndex<EntityPrototype>(prototypeName, out var prototype);
if (!PrototypeManager.TryIndex<EntityPrototype>(prototypeName, out var prototype))
throw new EntityCreationException($"Attempted to spawn an entity with an invalid prototype: {prototypeName}");
return CreateEntity(prototype, uid, context);
}
/// <summary>
/// Allocates an entity and loads components but does not do initialization.
/// </summary>
private protected EntityUid CreateEntity(EntityPrototype prototype, EntityUid uid = default, IEntityLoadContext? context = null)
{
var entity = AllocEntity(prototype, out var metadata, uid);
try
{
@@ -764,7 +773,7 @@ namespace Robust.Shared.GameObjects
// Exception during entity loading.
// Need to delete the entity to avoid corrupt state causing crashes later.
DeleteEntity(entity);
throw new EntityCreationException($"Exception inside CreateEntity with prototype {prototypeName}", e);
throw new EntityCreationException($"Exception inside CreateEntity with prototype {prototype.ID}", e);
}
}

View File

@@ -156,6 +156,17 @@ namespace Robust.Shared.GameObjects
/// </exception>
[Pure]
string GetComponentName(Type componentType);
/// <summary>
/// Gets the name of a component, throwing an exception if it does not exist.
/// </summary>
/// <param name="netID">The network ID corresponding to the component.</param>
/// <returns>The registered name of the component</returns>
/// <exception cref="UnknownComponentException">
/// Thrown if no component with id <see cref="netID"/> exists.
/// </exception>
[Pure]
string GetComponentName(ushort netID);
/// <summary>
/// Gets the registration belonging to a component, throwing an exception if it does not exist.

View File

@@ -324,7 +324,8 @@ namespace Robust.Shared.Network
{
DebugTools.AssertNotNull(second);
// Connecting via second peer is delayed by 25ms to give an advantage to IPv6, if it works.
await Task.Delay(25, cancellationToken);
var delay = TimeSpan.FromSeconds(_config.GetCVar(CVars.NetHappyEyeballsDelay));
await Task.Delay(delay, cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Collections;
using Robust.Shared.Utility;
namespace Robust.Shared.Random
@@ -29,6 +30,12 @@ namespace Robust.Shared.Random
return list[index];
}
public static ref T Pick<T>(this IRobustRandom random, ValueList<T> list)
{
var index = random.Next(list.Count);
return ref list[index];
}
/// <summary>Picks a random element from a collection.</summary>
/// <remarks>
/// This is O(n).

View File

@@ -13,6 +13,7 @@ using Moq;
using NUnit.Framework;
using Robust.Client;
using Robust.Client.GameStates;
using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Client.UserInterface;
using Robust.Server;
@@ -26,7 +27,10 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using ServerProgram = Robust.Server.Program;
@@ -259,6 +263,38 @@ namespace Robust.UnitTesting
public virtual IntegrationOptions? Options { get; internal set; }
public IEntityManager EntMan { get; private set; } = default!;
public IPrototypeManager ProtoMan { get; private set; } = default!;
public IConfigurationManager CfgMan { get; private set; } = default!;
public ISharedPlayerManager PlayerMan { get; private set; } = default!;
public IGameTiming Timing { get; private set; } = default!;
public IMapManager MapMan { get; private set; } = default!;
protected virtual void ResolveIoC(IDependencyCollection deps)
{
EntMan = deps.Resolve<IEntityManager>();
ProtoMan = deps.Resolve<IPrototypeManager>();
CfgMan = deps.Resolve<IConfigurationManager>();
PlayerMan = deps.Resolve<ISharedPlayerManager>();
Timing = deps.Resolve<IGameTiming>();
MapMan = deps.Resolve<IMapManager>();
}
public T System<T>() where T : IEntitySystem
{
return EntMan.System<T>();
}
public TransformComponent Transform(EntityUid uid)
{
return EntMan.GetComponent<TransformComponent>(uid);
}
public MetaDataComponent MetaData(EntityUid uid)
{
return EntMan.GetComponent<MetaDataComponent>(uid);
}
/// <summary>
/// Whether the instance is still alive.
/// "Alive" indicates that it is able to receive and process commands.
@@ -688,6 +724,7 @@ namespace Robust.UnitTesting
server.SetupMainLoop();
GameLoop.RunInit();
ResolveIoC(deps);
return server;
}
@@ -695,6 +732,11 @@ namespace Robust.UnitTesting
public sealed class ClientIntegrationInstance : IntegrationInstance
{
public LocalPlayer? Player => ((IPlayerManager) PlayerMan).LocalPlayer;
public ICommonSession? Session => Player?.Session;
public NetUserId? User => Session?.UserId;
public EntityUid? AttachedEntity => Session?.AttachedEntity;
public ClientIntegrationInstance(ClientIntegrationOptions? options) : base(options)
{
ClientOptions = options;
@@ -859,6 +901,7 @@ namespace Robust.UnitTesting
client.StartupContinue(GameController.DisplayMode.Headless);
GameLoop.RunInit();
ResolveIoC(deps);
return client;
}

View File

@@ -65,12 +65,12 @@ public sealed class PvsSystemTests : RobustIntegrationTest
var mapCoords = new EntityCoordinates(map, new Vector2(2, 2));
await server.WaitPost(() =>
{
player = sEntMan.SpawnEntity("", gridCoords);
other = sEntMan.SpawnEntity("", gridCoords);
player = sEntMan.SpawnEntity(null, gridCoords);
other = sEntMan.SpawnEntity(null, gridCoords);
otherXform = sEntMan.GetComponent<TransformComponent>(other);
// Ensure map PVS chunk is not empty
sEntMan.SpawnEntity("", mapCoords);
sEntMan.SpawnEntity(null, mapCoords);
// Attach player.
var session = (IPlayerSession) sPlayerMan.Sessions.First();

View File

@@ -84,7 +84,7 @@ public sealed class DeletionNetworkingTests : RobustIntegrationTest
await server.WaitPost(() =>
{
var coords = new EntityCoordinates(grid1, new Vector2(0.5f, 0.5f));
player = sEntMan.SpawnEntity("", coords);
player = sEntMan.SpawnEntity(null, coords);
var session = (IPlayerSession) sPlayerMan.Sessions.First();
session.AttachToEntity(player);
session.JoinGame();
@@ -107,10 +107,10 @@ public sealed class DeletionNetworkingTests : RobustIntegrationTest
var coords = new EntityCoordinates(grid2, new Vector2(0.5f, 0.5f));
await server.WaitPost(() =>
{
entA = sEntMan.SpawnEntity("", coords);
entB = sEntMan.SpawnEntity("", coords);
childA = sEntMan.SpawnEntity("", new EntityCoordinates(entA, default));
childB = sEntMan.SpawnEntity("", new EntityCoordinates(entB, default));
entA = sEntMan.SpawnEntity(null, coords);
entB = sEntMan.SpawnEntity(null, coords);
childA = sEntMan.SpawnEntity(null, new EntityCoordinates(entA, default));
childB = sEntMan.SpawnEntity(null, new EntityCoordinates(entB, default));
});
await RunTicks();
@@ -122,10 +122,10 @@ public sealed class DeletionNetworkingTests : RobustIntegrationTest
EntityUid clientChildB = default;
await client.WaitPost(() =>
{
entC = cEntMan.SpawnEntity("", coords);
childC = cEntMan.SpawnEntity("", new EntityCoordinates(entC, default));
clientChildA = cEntMan.SpawnEntity("", new EntityCoordinates(entA, default));
clientChildB = cEntMan.SpawnEntity("", new EntityCoordinates(entB, default));
entC = cEntMan.SpawnEntity(null, coords);
childC = cEntMan.SpawnEntity(null, new EntityCoordinates(entC, default));
clientChildA = cEntMan.SpawnEntity(null, new EntityCoordinates(entA, default));
clientChildB = cEntMan.SpawnEntity(null, new EntityCoordinates(entB, default));
});
await RunTicks();

View File

@@ -202,7 +202,9 @@ namespace Robust.UnitTesting.Shared.Maths
Assert.That(sysColor, Is.EqualTo((System.Drawing.Color) color));
}
static IEnumerable<string> DefaultColorNames => Color.GetAllDefaultColors().Select(e => e.Key);
static IEnumerable<string> DefaultColorNames => Color.GetAllDefaultColors()
.Where(e => System.Drawing.Color.FromName(e.Key).IsKnownColor)
.Select(e => e.Key);
[Test]
public void GetAllDefaultColorsFromName([ValueSource(nameof(DefaultColorNames))] string colorName)

View File

@@ -74,7 +74,7 @@ public sealed class BroadphaseNetworkingTest : RobustIntegrationTest
await server.WaitPost(() =>
{
var coords = new EntityCoordinates(grid1, new Vector2(0.5f, 0.5f));
player = sEntMan.SpawnEntity("", coords);
player = sEntMan.SpawnEntity(null, coords);
// Enable physics
var physics = sEntMan.AddComponent<PhysicsComponent>(player);