Compare commits

...

55 Commits

Author SHA1 Message Date
metalgearsloth
7a636b3b87 Version: 0.45.0.0 2022-08-25 23:33:10 +10:00
Leon Friedrich
98ce017b4a Physics get-colliding tweaks (#3183) 2022-08-25 23:29:46 +10:00
Leon Friedrich
26b04f0d66 Fix container init IDs (#3184) 2022-08-25 23:28:48 +10:00
Leon Friedrich
f4f2dea688 Fix load grid errors (#3181) 2022-08-25 23:28:29 +10:00
metalgearsloth
e9a0f9a4c1 Bandaid maploader for loading onto an existing one (#3160) 2022-08-24 15:49:46 +10:00
Leon Friedrich
de438ae94c Prevent players from attaching to terminating entities (#3175) 2022-08-24 01:42:48 +10:00
metalgearsloth
9ec77f20ee Version: 0.44.0.0 2022-08-23 16:35:59 +10:00
Leon Friedrich
da01040b52 Hopefully fix container issues? (#3174) 2022-08-23 16:35:02 +10:00
metalgearsloth
dce2a5ddb2 Fix physics grid reparent crash (#3122) 2022-08-23 11:33:36 +10:00
Acruid
5c99fbabf2 Map serialization postmapinit bugfix (#3165) 2022-08-23 10:52:23 +10:00
Leon Friedrich
9d0846c0e9 Add PVS/entity-state debug commands (#3169) 2022-08-23 10:50:23 +10:00
metalgearsloth
035ecfb098 Fix joint collision (#3161)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
2022-08-23 10:48:19 +10:00
metalgearsloth
3693f5aee7 Physics component cleanup (#3158) 2022-08-23 10:45:28 +10:00
ElectroJr
b859815b07 Version: 0.43.1.1 2022-08-22 17:03:25 -04:00
Leon Friedrich
3701ca83e4 Only request full state on missing metadata. (#3170) 2022-08-23 07:01:41 +10:00
Leon Friedrich
1473f1d34c Fix entity state exception tolerance (#3171) 2022-08-23 07:01:31 +10:00
ElectroJr
889c140fb9 Version: 0.43.1.0 2022-08-22 13:32:57 -04:00
metalgearsloth
c8259915f8 LoadBP completion results (#3159) 2022-08-23 02:58:56 +10:00
Leon Friedrich
a2a25fb296 Add session info for client-side ToPrettyString() (#3162) 2022-08-23 02:56:09 +10:00
Leon Friedrich
938a9929ea Fix deferred component remove error (#3163) 2022-08-23 02:55:59 +10:00
wrexbe
7726075b9b Fix gamestate debug display (#3167) 2022-08-23 02:55:41 +10:00
Leon Friedrich
03b3d1bbe7 Maybe fix PVS bug (#3164) 2022-08-23 02:54:50 +10:00
wrexbe
17ec51b74c Add reusing username+id in tests (#3143) 2022-08-22 05:57:07 +10:00
Leon Friedrich
e92998d1ec Remove a PVS warning on full release (#3154) 2022-08-22 05:50:52 +10:00
metalgearsloth
6691512136 Version: 0.43.0.2 2022-08-21 17:10:05 +10:00
metalgearsloth
a80f4ad76c Add broadphase AABB back in (#3157) 2022-08-21 16:56:01 +10:00
Leon Friedrich
2c6f4cd80c Fix PVS/container bug (#3155) 2022-08-21 16:06:44 +10:00
ElectroJr
51a4c6dcf2 Version: 0.43.0.1 2022-08-20 19:49:03 -04:00
Leon Friedrich
1f402e581a Fix state interpolation (#3153) 2022-08-21 09:47:15 +10:00
ElectroJr
17ea92bfda Version: 0.43.0.0 2022-08-20 18:31:48 -04:00
Leon Friedrich
6a8266af7e Revert "Revert "PVS & client state handling changes"" (#3152) 2022-08-21 08:30:28 +10:00
ElectroJr
f6b7606648 Version: 0.42.0.0 2022-08-20 15:28:34 -04:00
Leon Friedrich
9cd8adae93 Revert "PVS & client state handling changes" (#3151) 2022-08-21 05:27:16 +10:00
ElectroJr
c5ba8b75c8 Version: 0.41.0.0 2022-08-20 13:42:20 -04:00
Leon Friedrich
3d73cc7289 Make AudioSystem accept nullable SoundSpecifier (#3133) 2022-08-21 03:40:49 +10:00
Leon Friedrich
11aa062ee0 Un-revert BUI PRs (#3139) 2022-08-21 03:40:29 +10:00
Leon Friedrich
b4358a9e33 PVS & client state handling changes (#3000) 2022-08-21 03:40:18 +10:00
Leon Friedrich
0cce4714a1 Make a specific EntityUid error more descriptive (#3132) 2022-08-20 16:30:11 +10:00
metalgearsloth
9c4e6a6595 Hotfix tile placement brrrrt (#3140) 2022-08-20 16:29:38 +10:00
DrSmugleaf
1ddd541fe9 Add IEntityManager.Systems proxy methods (#3150) 2022-08-20 16:28:36 +10:00
Leon Friedrich
e45aa3f2fe Assert that components are not added to terminating entities (#3144) 2022-08-20 16:28:09 +10:00
Leon Friedrich
99efdb6061 Include stack trace in deletion errors (#3145) 2022-08-20 16:27:54 +10:00
DrSmugleaf
32f0ffdc79 Move ContainerHelpers methods to SharedContainerSystem (#3149) 2022-08-20 07:57:56 +02:00
DrSmugleaf
cf166483c9 Change state manager methods to return the new state (#3137) 2022-08-15 22:05:48 +02:00
Pieter-Jan Briers
49631867f4 Fix sRGB conversion in WindowRoot background color. 2022-08-15 16:51:23 +02:00
metalgearsloth
9f56eaec9a Obsolote EntitySystem.Get<T> (#3142) 2022-08-15 16:22:06 +02:00
DrSmugleaf
04f2b732a5 Add TryFirstOrNull and TryFirstOrDefault without predicates (#3141) 2022-08-15 16:21:40 +02:00
metalgearsloth
b8cfabc339 Version: 0.40.3.3 2022-08-15 14:06:22 +10:00
metalgearsloth
1cdd39202f Make tile grid placement somewhat bearable (#3131)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
2022-08-15 14:05:27 +10:00
Leon Friedrich
3290720b4c Don't error on missing entity suffixes (#3124) 2022-08-15 14:04:50 +10:00
Leon Friedrich
49badb06cb Fix fixture init/state bug. (#3125) 2022-08-15 14:03:57 +10:00
Visne
2c6941e73b Version: 0.40.3.2 2022-08-15 02:49:34 +02:00
wrexbe
5e5883cb88 Fix map schema (#3138) 2022-08-15 02:40:56 +02:00
ElectroJr
02c504445e Version: 0.40.3.1 2022-08-14 19:21:42 -04:00
wrexbe
4d5075a792 Update mapfile_validators.py (#3136) 2022-08-15 09:20:12 +10:00
96 changed files with 2870 additions and 1807 deletions

View File

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

View File

@@ -0,0 +1,16 @@
# Loc strings for various entity state & client-side PVS related commands
cmd-reset-ent-help = Usage: resetent <Entity UID>
cmd-reset-ent-desc = Reset an entity to the most recently received server state. This will also reset entities that have been detached to null-space.
cmd-reset-all-ents-help = Usage: resetallents
cmd-reset-all-ents-desc = Resets all entities to the most recently received server state. This only impacts entities that have not been detached to null-space.
cmd-detach-ent-help = Usage: detachent <Entity UID>
cmd-detach-ent-desc = Detach an entity to null-space, as if it had left PVS range.
cmd-local-delete-help = Usage: localdelete <Entity UID>
cmd-local-delete-desc = Deletes an entity. Unlike the normal delete command, this is CLIENT-SIDE. Unless the entity is a client-side entity, this will likely cause errors.
cmd-full-state-reset-help = Usage: fullstatereset
cmd-full-state-reset-desc = Discards any entity state information and requests a full-state from the server.

View File

@@ -1,10 +1,14 @@
### Localization for engine console commands
## generic
## generic command errors
cmd-invalid-arg-number-error = Invalid number of arguments.
cmd-parse-failure-integer = {$arg} is not a valid integer.
cmd-parse-failure-float = {$arg} is not a valid float.
cmd-parse-failure-bool = {$arg} is not a valid bool.
cmd-parse-failure-uid = {$arg} is not a valid entity UID.
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
## 'help' command
@@ -147,6 +151,8 @@ cmd-hint-loadmap-y-position = [y-position]
cmd-hint-loadmap-rotation = [rotation]
cmd-hint-loadmap-uids = [float]
cmd-hint-savebp-id = <Grid EntityID>
## 'flushcookies' command
# Note: the flushcookies command is from Robust.Client.WebView, it's not in the main engine code.

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading;
using Robust.Client.Timing;
using Robust.LoaderApi;
using Robust.Shared;
using Robust.Shared.IoC;
@@ -13,7 +14,7 @@ namespace Robust.Client
{
private IGameLoop? _mainLoop;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
private static bool _hasStarted;

View File

@@ -517,7 +517,7 @@ namespace Robust.Client
using (_prof.Group("Entity"))
{
// The last real tick is the current tick! This way we won't be in "prediction" mode.
_gameTiming.LastRealTick = _gameTiming.CurTick;
_gameTiming.LastRealTick = _gameTiming.LastProcessedTick = _gameTiming.CurTick;
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false);
}
}

View File

@@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using Prometheus;
using Robust.Client.GameStates;
using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
@@ -19,8 +18,7 @@ namespace Robust.Client.GameObjects
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientNetManager _networkManager = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
protected override int NextEntityUid { get; set; } = EntityUid.ClientUid + 1;
@@ -47,6 +45,30 @@ namespace Robust.Client.GameObjects
base.StartEntity(entity);
}
/// <inheritdoc />
public override void Dirty(EntityUid uid)
{
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.Dirty(uid);
}
/// <inheritdoc />
public override void Dirty(Component component)
{
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.Dirty(component);
}
public override EntityStringRepresentation ToPrettyString(EntityUid uid)
{
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
return base.ToPrettyString(uid) with { Session = _playerManager.LocalPlayer.Session };
else
return base.ToPrettyString(uid);
}
#region IEntityNetworkManager impl
public override IEntityNetworkManager EntityNetManager => this;
@@ -67,7 +89,7 @@ namespace Robust.Client.GameObjects
{
using (histogram?.WithLabels("EntityNet").NewTimer())
{
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameTiming.LastRealTick)
{
var (_, msg) = _queue.Take();
// Logger.DebugS("net.ent", "Dispatching: {0}: {1}", seq, msg);
@@ -103,7 +125,7 @@ namespace Robust.Client.GameObjects
private void HandleEntityNetworkMessage(MsgEntity message)
{
if (message.SourceTick <= _gameStateManager.CurServerTick)
if (message.SourceTick <= _gameTiming.LastRealTick)
{
DispatchMsgEntity(message);
return;

View File

@@ -1470,11 +1470,9 @@ namespace Robust.Client.GameObjects
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (curState == null)
if (curState is not SpriteComponentState thestate)
return;
var thestate = (SpriteComponentState)curState;
Visible = thestate.Visible;
DrawDepth = thestate.DrawDepth;
scale = thestate.Scale;

View File

@@ -1,11 +1,10 @@
using System;
using System;
using System.Collections.Generic;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
@@ -17,14 +16,12 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEntityNetworkManager _netMan = default!;
private readonly Dictionary<object, BoundUserInterface> _openInterfaces =
internal readonly Dictionary<Enum, BoundUserInterface> _openInterfaces =
new();
private readonly Dictionary<object, PrototypeData> _interfaces = new();
[DataField("interfaces", readOnly: true)]
private List<PrototypeData> _interfaceData = new();
internal readonly Dictionary<Enum, PrototypeData> _interfaces = new();
[ViewVariables]
public IEnumerable<BoundUserInterface> Interfaces => _openInterfaces.Values;
@@ -72,7 +69,7 @@ namespace Robust.Client.GameObjects
// TODO: This type should be cached, but I'm too lazy.
var type = _reflectionManager.LooseGetType(data.ClientType);
var boundInterface =
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[] {this, wrapped.UiKey});
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new object[] {this, wrapped.UiKey});
boundInterface.Open();
_openInterfaces[wrapped.UiKey] = boundInterface;
@@ -81,7 +78,7 @@ namespace Robust.Client.GameObjects
_entityManager.EventBus.RaiseLocalEvent(Owner, new BoundUIOpenedEvent(wrapped.UiKey, Owner, playerSession), true);
}
internal void Close(object uiKey, bool remoteCall)
internal void Close(Enum uiKey, bool remoteCall)
{
if (!_openInterfaces.TryGetValue(uiKey, out var boundUserInterface))
{
@@ -98,10 +95,9 @@ namespace Robust.Client.GameObjects
_entityManager.EventBus.RaiseLocalEvent(Owner, new BoundUIClosedEvent(uiKey, Owner, playerSession), true);
}
internal void SendMessage(BoundUserInterfaceMessage message, object uiKey)
internal void SendMessage(BoundUserInterfaceMessage message, Enum uiKey)
{
EntitySystem.Get<UserInterfaceSystem>()
.Send(new BoundUIWrapMessage(Owner, message, uiKey));
_netMan.SendSystemNetworkMessage(new BoundUIWrapMessage(Owner, message, uiKey));
}
}
@@ -111,14 +107,15 @@ namespace Robust.Client.GameObjects
public abstract class BoundUserInterface : IDisposable
{
protected ClientUserInterfaceComponent Owner { get; }
protected object UiKey { get; }
public readonly Enum UiKey;
/// <summary>
/// The last received state object sent from the server.
/// </summary>
protected BoundUserInterfaceState? State { get; private set; }
protected BoundUserInterface(ClientUserInterfaceComponent owner, object uiKey)
protected BoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey)
{
Owner = owner;
UiKey = uiKey;
@@ -149,7 +146,7 @@ namespace Robust.Client.GameObjects
/// <summary>
/// Invoked to close the UI.
/// </summary>
protected void Close()
public void Close()
{
Owner.Close(UiKey, false);
}
@@ -157,7 +154,7 @@ namespace Robust.Client.GameObjects
/// <summary>
/// Sends a message to the server-side UI.
/// </summary>
protected void SendMessage(BoundUserInterfaceMessage message)
public void SendMessage(BoundUserInterfaceMessage message)
{
Owner.SendMessage(message, UiKey);
}

View File

@@ -96,16 +96,21 @@ namespace Robust.Client.GameObjects
public override void FrameUpdate(float frameTime)
{
var spriteQuery = GetEntityQuery<SpriteComponent>();
var metaQuery = GetEntityQuery<MetaDataComponent>();
while (_queuedUpdates.TryDequeue(out var appearance))
{
if (appearance.Deleted)
continue;
UnmarkDirty(appearance);
// If the entity is no longer within the clients PVS, don't bother updating.
if ((metaQuery.GetComponent(appearance.Owner).Flags & MetaDataFlags.Detached) != 0)
continue;
// Sprite comp is allowed to be null, so that things like spriteless point-lights can use this system
spriteQuery.TryGetComponent(appearance.Owner, out var sprite);
OnChangeData(appearance.Owner, sprite, appearance);
UnmarkDirty(appearance);
}
}

View File

@@ -475,9 +475,9 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
{
if (_timing.IsFirstTimePredicted)
if (_timing.IsFirstTimePredicted || sound == null)
return Play(sound, Filter.Local(), source, audioParams);
else
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Client.Player;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
@@ -9,6 +11,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using static Robust.Shared.Containers.ContainerManagerComponent;
namespace Robust.Client.GameObjects
@@ -34,11 +37,9 @@ namespace Robust.Client.GameObjects
private void HandleEntityInitialized(EntityInitializedMessage ev)
{
if (!ExpectedEntities.TryGetValue(ev.Entity, out var container))
if (!RemoveExpectedEntity(ev.Entity, out var container))
return;
RemoveExpectedEntity(ev.Entity);
if (container.Deleted)
return;
@@ -61,7 +62,7 @@ namespace Robust.Client.GameObjects
goto skip;
}
container.EmptyContainer(true, entMan: EntityManager);
EmptyContainer(container, true);
container.Shutdown();
toDelete.Add(id);
@@ -114,13 +115,13 @@ namespace Robust.Client.GameObjects
foreach (var entityUid in removedExpected)
{
RemoveExpectedEntity(entityUid);
RemoveExpectedEntity(entityUid, out _);
}
// Add new entities.
foreach (var entity in entityUids)
{
if (!EntityManager.EntityExists(entity))
if (!EntityManager.TryGetComponent(entity, out MetaDataComponent? meta))
{
AddExpectedEntity(entity, container);
continue;
@@ -133,14 +134,17 @@ namespace Robust.Client.GameObjects
// from the container. It would then subsequently be parented to the container without ever being
// re-inserted, leading to the client seeing what should be hidden entities attached to
// containers/players.
if (Transform(entity).MapID == MapId.Nullspace)
if ((meta.Flags & MetaDataFlags.Detached) != 0)
{
AddExpectedEntity(entity, container);
continue;
}
if (!container.ContainedEntities.Contains(entity))
container.Insert(entity);
if (container.Contains(entity))
continue;
RemoveExpectedEntity(entity, out _);
container.Insert(entity);
}
}
}
@@ -158,7 +162,7 @@ namespace Robust.Client.GameObjects
if (message.OldParent != null && message.OldParent.Value.IsValid())
return;
if (!ExpectedEntities.TryGetValue(message.Entity, out var container))
if (!RemoveExpectedEntity(message.Entity, out var container))
return;
if (xform.ParentUid != container.Owner)
@@ -168,8 +172,6 @@ namespace Robust.Client.GameObjects
return;
}
RemoveExpectedEntity(message.Entity);
if (container.Deleted)
return;
@@ -189,20 +191,33 @@ namespace Robust.Client.GameObjects
public void AddExpectedEntity(EntityUid uid, IContainer container)
{
if (ExpectedEntities.ContainsKey(uid))
return;
DebugTools.Assert(!TryComp(uid, out MetaDataComponent? meta) ||
(meta.Flags & ( MetaDataFlags.Detached | MetaDataFlags.InContainer) ) == MetaDataFlags.Detached,
$"Adding entity {ToPrettyString(uid)} to list of expected entities for container {container.ID} in {ToPrettyString(container.Owner)}, despite it already being in a container.");
ExpectedEntities.Add(uid, container);
if (!ExpectedEntities.TryAdd(uid, container))
{
DebugTools.Assert(ExpectedEntities[uid] == container,
$"Expecting entity {ToPrettyString(uid)} to be present in two containers. New: {container.ID} in {ToPrettyString(container.Owner)}. Old: {ExpectedEntities[uid].ID} in {ToPrettyString(ExpectedEntities[uid].Owner)}");
DebugTools.Assert(ExpectedEntities[uid].ExpectedEntities.Contains(uid),
$"Entity {ToPrettyString(uid)} is expected, but not expected in the given container? Container: {ExpectedEntities[uid].ID} in {ToPrettyString(ExpectedEntities[uid].Owner)}");
return;
}
DebugTools.Assert(!container.ExpectedEntities.Contains(uid),
$"Contained entity {ToPrettyString(uid)} was not yet expected by the system, but was already expected by the container: {container.ID} in {ToPrettyString(container.Owner)}");
container.ExpectedEntities.Add(uid);
}
public void RemoveExpectedEntity(EntityUid uid)
public bool RemoveExpectedEntity(EntityUid uid, [NotNullWhen(true)] out IContainer? container)
{
if (!ExpectedEntities.TryGetValue(uid, out var container))
return;
if (!ExpectedEntities.Remove(uid, out container))
return false;
ExpectedEntities.Remove(uid);
DebugTools.Assert(container.ExpectedEntities.Contains(uid),
$"While removing expected contained entity {ToPrettyString(uid)}, the entity was missing from the container expected set. Container: {container.ID} in {ToPrettyString(container.Owner)}");
container.ExpectedEntities.Remove(uid);
return true;
}
public override void FrameUpdate(float frameTime)

View File

@@ -1,4 +1,4 @@
using JetBrains.Annotations;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Extensions.ObjectPool;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
@@ -15,7 +16,7 @@ namespace Robust.Client.GameStates;
/// </summary>
internal sealed class ClientDirtySystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
private readonly Dictionary<GameTick, HashSet<EntityUid>> _dirtyEntities = new();
@@ -49,14 +50,14 @@ internal sealed class ClientDirtySystem : EntitySystem
_dirtyEntities.Clear();
}
public IEnumerable<EntityUid> GetDirtyEntities(GameTick currentTick)
public IEnumerable<EntityUid> GetDirtyEntities()
{
_dirty.Clear();
// This is just to avoid collection being modified during iteration unfortunately.
foreach (var (tick, dirty) in _dirtyEntities)
{
if (tick < currentTick) continue;
if (tick < _timing.LastRealTick) continue;
foreach (var ent in dirty)
{
_dirty.Add(ent);

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -12,154 +14,144 @@ namespace Robust.Client.GameStates
/// <inheritdoc />
internal sealed class GameStateProcessor : IGameStateProcessor
{
private readonly IGameTiming _timing;
private readonly IClientGameTiming _timing;
private readonly List<GameState> _stateBuffer = new();
private GameState? _lastFullState;
private bool _waitingForFull = true;
private int _interpRatio;
private GameTick _highestFromSequence;
private readonly Dictionary<EntityUid, Dictionary<uint, ComponentState>> _lastStateFullRep
private readonly Dictionary<GameTick, List<EntityUid>> _pvsDetachMessages = new();
public GameState? LastFullState { get; private set; }
public bool WaitingForFull => LastFullStateRequested.HasValue;
public GameTick? LastFullStateRequested
{
get => _lastFullStateRequested;
set
{
_lastFullStateRequested = value;
LastFullState = null;
}
}
public GameTick? _lastFullStateRequested = GameTick.Zero;
private int _bufferSize;
/// <summary>
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
/// </summary>
internal readonly Dictionary<EntityUid, Dictionary<ushort, ComponentState>> _lastStateFullRep
= new();
/// <inheritdoc />
public int MinBufferSize => Interpolation ? 3 : 2;
public int MinBufferSize => Interpolation ? 2 : 1;
/// <inheritdoc />
public int TargetBufferSize => MinBufferSize + InterpRatio;
/// <inheritdoc />
public int CurrentBufferSize => CalculateBufferSize(_timing.CurTick);
public int TargetBufferSize => MinBufferSize + BufferSize;
/// <inheritdoc />
public bool Interpolation { get; set; }
/// <inheritdoc />
public int InterpRatio
public int BufferSize
{
get => _interpRatio;
set => _interpRatio = value < 0 ? 0 : value;
get => _bufferSize;
set => _bufferSize = value < 0 ? 0 : value;
}
/// <inheritdoc />
public bool Extrapolation { get; set; }
/// <inheritdoc />
public bool Logging { get; set; }
public GameTick LastProcessedRealState { get; set; }
/// <summary>
/// Constructs a new instance of <see cref="GameStateProcessor"/>.
/// </summary>
/// <param name="timing">Timing information of the current state.</param>
public GameStateProcessor(IGameTiming timing)
public GameStateProcessor(IClientGameTiming timing)
{
_timing = timing;
}
/// <inheritdoc />
public void AddNewState(GameState state)
public bool AddNewState(GameState state)
{
// any state from tick 0 is a full state, and needs to be handled different
if (state.FromSequence == GameTick.Zero)
{
// this is a newer full state, so discard the older one.
if (_lastFullState == null || (_lastFullState != null && _lastFullState.ToSequence < state.ToSequence))
{
_lastFullState = state;
if (Logging)
Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
return;
}
}
// NOTE: DispatchTick will be modifying CurTick, this is NOT thread safe.
var lastTick = new GameTick(_timing.CurTick.Value - 1);
if (state.ToSequence <= lastTick && !_waitingForFull) // CurTick isn't set properly when WaitingForFull
// Check for old states.
if (state.ToSequence <= _timing.LastRealTick)
{
if (Logging)
Logger.DebugS("net.state", $"Received Old GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
Logger.DebugS("net.state", $"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return;
return false;
}
// lets check for a duplicate state now.
for (var i = 0; i < _stateBuffer.Count; i++)
// Check for a duplicate states.
foreach (var bufferState in _stateBuffer)
{
var iState = _stateBuffer[i];
if (state.ToSequence != iState.ToSequence)
if (state.ToSequence != bufferState.ToSequence)
continue;
if (Logging)
Logger.DebugS("net.state", $"Received Dupe GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
Logger.DebugS("net.state", $"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return;
return false;
}
// Are we expecting a full state?
if (!WaitingForFull)
{
// This is a good state that we will be using.
_stateBuffer.Add(state);
if (Logging)
Logger.DebugS("net.state", $"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return true;
}
// this is a good state that we will be using.
_stateBuffer.Add(state);
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value)
{
LastFullState = state;
if (Logging)
Logger.DebugS("net.state", $"Received New GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
if (Logging)
Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
return true;
}
if (LastFullState == null || state.ToSequence <= LastFullState.ToSequence)
return false;
_stateBuffer.Add(state);
return true;
}
/// <inheritdoc />
public bool ProcessTickStates(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState)
/// <summary>
/// Attempts to get the current and next states to apply.
/// </summary>
/// <remarks>
/// If the processor is not currently waiting for a full state, the states to apply depends on <see
/// cref="IGameTiming.LastProcessedTick"/>.
/// </remarks>
/// <returns>Returns true if the states should be applied.</returns>
public bool TryGetServerState([NotNullWhen(true)] out GameState? curState, out GameState? nextState)
{
bool applyNextState;
if (_waitingForFull)
{
applyNextState = CalculateFullState(out curState, out nextState, TargetBufferSize);
}
else // this will be how almost all states are calculated
{
applyNextState = CalculateDeltaState(curTick, out curState, out nextState);
}
var applyNextState = WaitingForFull
? TryGetFullState(out curState, out nextState)
: TryGetDeltaState(out curState, out nextState);
if (applyNextState && !curState!.Extrapolated)
LastProcessedRealState = curState.ToSequence;
if (!_waitingForFull)
if (curState != null)
{
if (!applyNextState)
_timing.CurTick = LastProcessedRealState;
// This will slightly speed up or slow down the client tickrate based on the contents of the buffer.
// CalcNextState should have just cleaned out any old states, so the buffer contains [t-1(last), t+0(cur), t+1(next), t+2, t+3, ..., t+n]
// we can use this info to properly time our tickrate so it does not run fast or slow compared to the server.
_timing.TickTimingAdjustment = (CurrentBufferSize - (float)TargetBufferSize) * 0.10f;
}
else
{
_timing.TickTimingAdjustment = 0f;
}
if (applyNextState)
{
DebugTools.Assert(curState!.Extrapolated || curState.FromSequence <= LastProcessedRealState,
DebugTools.Assert(curState.FromSequence <= curState.ToSequence,
"Tried to apply a non-extrapolated state that has too high of a FromSequence!");
if (Logging)
{
Logger.DebugS("net.state", $"Applying State: ext={curState!.Extrapolated}, cTick={_timing.CurTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
}
Logger.DebugS("net.state", $"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
}
var cState = curState!;
curState = cState;
return applyNextState;
}
public void UpdateFullRep(GameState state)
{
// Logger.Debug($"UPDATE FULL REP: {string.Join(", ", state.EntityStates?.Select(e => e.Uid) ?? Enumerable.Empty<EntityUid>())}");
// Note: the most recently received server state currently doesn't include pvs-leave messages (detaching
// transform to null-space). This is because a client should never predict an entity being moved back from
// null-space, so there should be no need to reset it back there.
if (state.FromSequence == GameTick.Zero)
{
@@ -178,7 +170,7 @@ namespace Robust.Client.GameStates
{
if (!_lastStateFullRep.TryGetValue(entityState.Uid, out var compData))
{
compData = new Dictionary<uint, ComponentState>();
compData = new Dictionary<ushort, ComponentState>();
_lastStateFullRep.Add(entityState.Uid, compData);
}
@@ -196,167 +188,138 @@ namespace Robust.Client.GameStates
}
}
private bool CalculateFullState([NotNullWhen(true)] out GameState? curState, out GameState? nextState, int targetBufferSize)
private bool TryGetFullState([NotNullWhen(true)] out GameState? curState, out GameState? nextState)
{
if (_lastFullState != null)
nextState = null;
curState = null;
if (LastFullState == null)
return false;
// remove any old states we find to keep the buffer clean
// also look for the next state if we are interpolating.
var nextTick = LastFullState.ToSequence + 1;
for (var i = 0; i < _stateBuffer.Count; i++)
{
if (Logging)
Logger.DebugS("net", $"Resync CurTick to: {_lastFullState.ToSequence}");
var state = _stateBuffer[i];
var curTick = _timing.CurTick = _lastFullState.ToSequence;
if (Interpolation)
if (state.ToSequence < LastFullState.ToSequence)
{
// look for the next state
var lastTick = new GameTick(curTick.Value - 1);
var nextTick = new GameTick(curTick.Value + 1);
nextState = null;
for (var i = 0; i < _stateBuffer.Count; i++)
{
var state = _stateBuffer[i];
if (state.ToSequence == nextTick)
{
nextState = state;
}
else if (state.ToSequence < lastTick) // remove any old states we find to keep the buffer clean
{
_stateBuffer.RemoveSwap(i);
i--;
}
}
// we let the buffer fill up before starting to tick
if (nextState != null && _stateBuffer.Count >= targetBufferSize)
{
curState = _lastFullState;
_waitingForFull = false;
return true;
}
_stateBuffer.RemoveSwap(i);
i--;
}
else if (_stateBuffer.Count >= targetBufferSize)
else if (Interpolation && state.ToSequence == nextTick)
{
curState = _lastFullState;
nextState = default;
_waitingForFull = false;
return true;
nextState = state;
}
}
if (Logging)
Logger.DebugS("net", $"Have FullState, filling buffer... ({_stateBuffer.Count}/{targetBufferSize})");
// we let the buffer fill up before starting to tick
if (_stateBuffer.Count >= TargetBufferSize)
{
if (Logging)
Logger.DebugS("net", $"Resync CurTick to: {LastFullState.ToSequence}");
// waiting for full state or buffer to fill
curState = default;
nextState = default;
curState = LastFullState;
return true;
}
// waiting for buffer to fill
if (Logging)
Logger.DebugS("net", $"Have FullState, filling buffer... ({_stateBuffer.Count}/{TargetBufferSize})");
return false;
}
private bool CalculateDeltaState(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState)
internal void AddLeavePvsMessage(MsgStateLeavePvs message)
{
var lastTick = new GameTick(curTick.Value - 1);
var nextTick = new GameTick(curTick.Value + 1);
// Late message may still need to be processed,
DebugTools.Assert(message.Entities.Count > 0);
_pvsDetachMessages.TryAdd(message.Tick, message.Entities);
}
public List<(GameTick Tick, List<EntityUid> Entities)> GetEntitiesToDetach(GameTick toTick, int budget)
{
var result = new List<(GameTick Tick, List<EntityUid> Entities)>();
foreach (var (tick, entities) in _pvsDetachMessages)
{
if (tick > toTick)
continue;
if (budget >= entities.Count)
{
budget -= entities.Count;
_pvsDetachMessages.Remove(tick);
result.Add((tick, entities));
continue;
}
var index = entities.Count - budget;
result.Add((tick, entities.GetRange(index, budget)));
entities.RemoveRange(index, budget);
break;
}
return result;
}
private bool TryGetDeltaState(out GameState? curState, out GameState? nextState)
{
curState = null;
nextState = null;
var targetCurTick = _timing.LastProcessedTick + 1;
var targetNextTick = _timing.LastProcessedTick + 2;
GameTick? futureStateLowestFromSeq = null;
uint lastStateInput = 0;
for (var i = 0; i < _stateBuffer.Count; i++)
{
var state = _stateBuffer[i];
// remember there are no duplicate ToSequence states in the list.
if (state.ToSequence == curTick)
if (state.ToSequence == targetCurTick && state.FromSequence <= _timing.LastRealTick)
{
curState = state;
_highestFromSequence = state.FromSequence;
continue;
}
else if (Interpolation && state.ToSequence == nextTick)
{
if (Interpolation && state.ToSequence == targetNextTick)
nextState = state;
if (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence)
{
futureStateLowestFromSeq = state.FromSequence;
}
}
else if (state.ToSequence > curTick)
if (state.ToSequence > targetCurTick && (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence))
{
if (futureStateLowestFromSeq == null || futureStateLowestFromSeq.Value > state.FromSequence)
{
futureStateLowestFromSeq = state.FromSequence;
}
futureStateLowestFromSeq = state.FromSequence;
continue;
}
else if (state.ToSequence == lastTick)
{
lastStateInput = state.LastProcessedInput;
}
else if (state.ToSequence < _highestFromSequence) // remove any old states we find to keep the buffer clean
// remove any old states we find to keep the buffer clean
if (state.ToSequence <= _timing.LastRealTick)
{
_stateBuffer.RemoveSwap(i);
i--;
}
}
// Make sure we can ACTUALLY apply this state.
// Can happen that we can't if there is a hole and we're doing extrapolation.
if (curState != null && curState.FromSequence > LastProcessedRealState)
curState = null;
// can't find current state, but we do have a future state.
if (!Extrapolation && curState == null && futureStateLowestFromSeq != null
&& futureStateLowestFromSeq <= LastProcessedRealState)
{
//this is not actually extrapolation
curState = ExtrapolateState(_highestFromSequence, curTick, lastStateInput);
return true; // keep moving, we have a future state
}
// we won't extrapolate, and curState was not found, buffer is empty
if (!Extrapolation && curState == null)
return false;
// we found both the states to interpolate between, this should almost always be true.
if (Interpolation && curState != null)
return true;
if (!Interpolation && curState != null && nextState != null)
return true;
if (curState == null)
{
curState = ExtrapolateState(_highestFromSequence, curTick, lastStateInput);
}
if (nextState == null && Interpolation)
{
nextState = ExtrapolateState(_highestFromSequence, nextTick, lastStateInput);
}
return true;
}
/// <summary>
/// Generates a completely fake GameState.
/// </summary>
private static GameState ExtrapolateState(GameTick fromSequence, GameTick toSequence, uint lastInput)
{
var state = new GameState(fromSequence, toSequence, lastInput, default, default, default, null);
state.Extrapolated = true;
return state;
// Even if we can't find current state, maybe we have a future state?
return curState != null || (futureStateLowestFromSeq != null && futureStateLowestFromSeq <= _timing.LastRealTick);
}
/// <inheritdoc />
public void Reset()
{
_stateBuffer.Clear();
_lastFullState = null;
_waitingForFull = true;
LastFullState = null;
LastFullStateRequested = GameTick.Zero;
}
public void MergeImplicitData(Dictionary<EntityUid, Dictionary<uint, ComponentState>> data)
public void RequestFullState()
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = _timing.LastRealTick;
}
public void MergeImplicitData(Dictionary<EntityUid, Dictionary<ushort, ComponentState>> data)
{
foreach (var (uid, compData) in data)
{
@@ -372,20 +335,39 @@ namespace Robust.Client.GameStates
}
}
public Dictionary<uint, ComponentState> GetLastServerStates(EntityUid entity)
public Dictionary<ushort, ComponentState> GetLastServerStates(EntityUid entity)
{
return _lastStateFullRep[entity];
}
public bool TryGetLastServerStates(EntityUid entity,
[NotNullWhen(true)] out Dictionary<uint, ComponentState>? dictionary)
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary)
{
return _lastStateFullRep.TryGetValue(entity, out dictionary);
}
public int CalculateBufferSize(GameTick fromTick)
{
return _stateBuffer.Count(s => s.ToSequence >= fromTick);
bool foundState;
var nextTick = fromTick;
do
{
foundState = false;
foreach (var state in _stateBuffer)
{
if (state.ToSequence > nextTick && state.FromSequence <= nextTick)
{
foundState = true;
nextTick += 1;
}
}
}
while (foundState);
return (int) (nextTick.Value - fromTick.Value);
}
}
}

View File

@@ -1,7 +1,8 @@
using System;
using System;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
namespace Robust.Client.GameStates
@@ -27,18 +28,10 @@ namespace Robust.Client.GameStates
int TargetBufferSize { get; }
/// <summary>
/// Number of game states currently in the state buffer.
/// Number of applicable game states currently in the state buffer.
/// </summary>
int CurrentBufferSize { get; }
/// <summary>
/// The current tick of the last server game state applied.
/// </summary>
/// <remarks>
/// Use this to synchronize server-sent simulation events with the client's game loop.
/// </remarks>
GameTick CurServerTick { get; }
/// <summary>
/// If the buffer size is this many states larger than the target buffer size,
/// apply the overflow of states in a single tick.
@@ -57,6 +50,11 @@ namespace Robust.Client.GameStates
/// </summary>
event Action<GameStateAppliedArgs> GameStateApplied;
/// <summary>
/// This is invoked whenever a pvs-leave message is received.
/// </summary>
public event Action<MsgStateLeavePvs>? PvsLeave;
/// <summary>
/// One time initialization of the service.
/// </summary>
@@ -78,6 +76,11 @@ namespace Robust.Client.GameStates
/// <param name="message">Message being dispatched.</param>
void InputCommandDispatched(FullInputCmdMessage message);
/// <summary>
/// Requests a full state from the server. This should override even implicit entity data.
/// </summary>
public void RequestFullState();
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@@ -17,8 +17,8 @@ namespace Robust.Client.GameStates
/// Minimum number of states needed in the buffer for everything to work.
/// </summary>
/// <remarks>
/// With interpolation enabled minimum is 3 states in buffer for the system to work (last, cur, next).
/// Without interpolation enabled minimum is 2 states in buffer for the system to work (last, cur).
/// With interpolation enabled minimum is 2 states in buffer for the system to work (cur, next).
/// Without interpolation enabled minimum is 2 states in buffer for the system to work (cur).
/// </remarks>
int MinBufferSize { get; }
@@ -28,12 +28,6 @@ namespace Robust.Client.GameStates
/// </summary>
int TargetBufferSize { get; }
/// <summary>
/// Number of game states currently in the state buffer.
/// </summary>
/// <seealso cref="CalculateBufferSize"/>
int CurrentBufferSize { get; }
/// <summary>
/// Is frame interpolation turned on?
/// </summary>
@@ -46,29 +40,22 @@ namespace Robust.Client.GameStates
/// For Lan, set this to 0. For Excellent net conditions, set this to 1. For normal network conditions,
/// set this to 2. For worse conditions, set it higher.
/// </remarks>
int InterpRatio { get; set; }
/// <summary>
/// If the client clock runs ahead of the server and the buffer gets emptied, should fake extrapolated states be generated?
/// </summary>
bool Extrapolation { get; set; }
int BufferSize { get; set; }
/// <summary>
/// Is debug logging enabled? This will dump debug info about every state to the log.
/// </summary>
bool Logging { get; set; }
/// <summary>
/// The last REAL server tick that has been processed.
/// i.e. not incremented on extrapolation.
/// </summary>
GameTick LastProcessedRealState { get; set; }
/// <summary>
/// Adds a new state into the processor. These are usually from networking or replays.
/// </summary>
/// <param name="state">Newly received state.</param>
void AddNewState(GameState state);
/// <returns>Returns true if the state was accepted and should be acknowledged</returns>
bool AddNewState(GameState state);
//> usually from replays
//replays when
/// <summary>
/// Calculates the current and next state to apply for a given game tick.
@@ -77,7 +64,7 @@ namespace Robust.Client.GameStates
/// <param name="curState">Current state for the given tick. This can be null.</param>
/// <param name="nextState">Current state for tick + 1. This can be null.</param>
/// <returns>Was the function able to correctly calculate the states for the given tick?</returns>
bool ProcessTickStates(GameTick curTick, [NotNullWhen(true)] out GameState? curState, out GameState? nextState);
bool TryGetServerState([NotNullWhen(true)] out GameState? curState, out GameState? nextState);
/// <summary>
/// Resets the processor back to its initial state.
@@ -96,21 +83,22 @@ namespace Robust.Client.GameStates
/// The data to merge.
/// It's a dictionary of entity ID -> (component net ID -> ComponentState)
/// </param>
void MergeImplicitData(Dictionary<EntityUid, Dictionary<uint, ComponentState>> data);
void MergeImplicitData(Dictionary<EntityUid, Dictionary<ushort, ComponentState>> data);
/// <summary>
/// Get the last state data from the server for an entity.
/// </summary>
/// <returns>Dictionary (net ID -> ComponentState)</returns>
Dictionary<uint, ComponentState> GetLastServerStates(EntityUid entity);
Dictionary<ushort, ComponentState> GetLastServerStates(EntityUid entity);
/// <summary>
/// Calculate the size of the game state buffer from a given tick.
/// Calculate the number of applicable states in the game state buffer from a given tick.
/// This includes only applicable states. If there is a gap, future buffers are not included.
/// </summary>
/// <param name="fromTick">The tick to calculate from.</param>
int CalculateBufferSize(GameTick fromTick);
bool TryGetLastServerStates(EntityUid entity,
[NotNullWhen(true)] out Dictionary<uint, ComponentState>? dictionary);
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);
}
}

View File

@@ -1,17 +1,17 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Configuration;
using Robust.Client.Timing;
using Robust.Shared.Collections;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
@@ -21,21 +21,20 @@ namespace Robust.Client.GameStates
/// </summary>
sealed class NetEntityOverlay : Overlay
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
private const int TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
private const uint TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
private const int _maxEnts = 128; // maximum number of entities to track.
/// <inheritdoc />
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
private readonly Font _font;
private readonly int _lineHeight;
private readonly List<NetEntity> _netEnts = new();
private readonly Dictionary<EntityUid, NetEntData> _netEnts = new();
public NetEntityOverlay()
{
@@ -45,87 +44,58 @@ namespace Robust.Client.GameStates
_lineHeight = _font.GetLineHeight(1);
_gameStateManager.GameStateApplied += HandleGameStateApplied;
_gameStateManager.PvsLeave += OnPvsLeave;
}
private void OnPvsLeave(MsgStateLeavePvs msg)
{
if (msg.Tick.Value + TrafficHistorySize < _gameTiming.LastRealTick.Value)
return;
foreach (var uid in msg.Entities)
{
if (!_netEnts.TryGetValue(uid, out var netEnt))
continue;
if (netEnt.LastUpdate < msg.Tick)
{
netEnt.InPVS = false;
netEnt.LastUpdate = msg.Tick;
}
netEnt.Traffic.Add(msg.Tick, NetEntData.EntState.PvsLeave);
}
}
private void HandleGameStateApplied(GameStateAppliedArgs args)
{
if(_gameTiming.InPrediction) // we only care about real server states.
return;
// Shift traffic history down one
for (var i = 0; i < _netEnts.Count; i++)
{
var traffic = _netEnts[i].Traffic;
for (int j = 1; j < TrafficHistorySize; j++)
{
traffic[j - 1] = traffic[j];
}
traffic[^1] = 0;
}
var gameState = args.AppliedState;
if(gameState.EntityStates.HasContents)
if (!gameState.EntityStates.HasContents)
return;
foreach (var entityState in gameState.EntityStates.Span)
{
// Loop over every entity that gets updated this state and record the traffic
foreach (var entityState in gameState.EntityStates.Span)
if (!_netEnts.TryGetValue(entityState.Uid, out var netEnt))
{
var newEnt = true;
for(var i=0;i<_netEnts.Count;i++)
{
var netEnt = _netEnts[i];
if (netEnt.Id != entityState.Uid)
continue;
//TODO: calculate size of state and record it here.
netEnt.Traffic[^1] = 1;
netEnt.LastUpdate = gameState.ToSequence;
newEnt = false;
_netEnts[i] = netEnt; // copy struct back
break;
}
if (!newEnt)
if (_netEnts.Count >= _maxEnts)
continue;
var newNetEnt = new NetEntity(entityState.Uid);
newNetEnt.Traffic[^1] = 1;
newNetEnt.LastUpdate = gameState.ToSequence;
_netEnts.Add(newNetEnt);
_netEnts[entityState.Uid] = netEnt = new();
}
}
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange*2, pvsRange*2));
int timeout = _gameTiming.TickRate * 3;
for (int i = 0; i < _netEnts.Count; i++)
{
var netEnt = _netEnts[i];
if(_entityManager.EntityExists(netEnt.Id))
if (!netEnt.InPVS && netEnt.LastUpdate < gameState.ToSequence)
{
//TODO: Whoever is working on PVS remake, change the InPVS detection.
var uid = netEnt.Id;
var position = _entityManager.GetComponent<TransformComponent>(uid).MapPosition;
netEnt.InPVS = !pvsEnabled || (pvsBox.Contains(position.Position) && position.MapId == pvsCenter.MapId);
_netEnts[i] = netEnt; // copy struct back
continue;
netEnt.InPVS = true;
netEnt.Traffic.Add(gameState.ToSequence, NetEntData.EntState.PvsEnter);
}
else
netEnt.Traffic.Add(gameState.ToSequence, NetEntData.EntState.Data);
netEnt.Exists = false;
if (netEnt.LastUpdate.Value + timeout < _gameTiming.LastRealTick.Value)
{
_netEnts.RemoveAt(i);
i--;
continue;
}
if (netEnt.LastUpdate < gameState.ToSequence)
netEnt.LastUpdate = gameState.ToSequence;
_netEnts[i] = netEnt; // copy struct back
//TODO: calculate size of state and record it here.
}
}
@@ -139,145 +109,128 @@ namespace Robust.Client.GameStates
case OverlaySpace.ScreenSpace:
DrawScreen(args);
break;
case OverlaySpace.WorldSpace:
DrawWorld(args);
break;
}
}
private void DrawWorld(in OverlayDrawArgs args)
{
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
if(!pvsEnabled)
return;
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange * 2, pvsRange * 2));
var worldHandle = args.WorldHandle;
worldHandle.DrawRect(pvsBox, Color.Red, false);
}
private void DrawScreen(in OverlayDrawArgs args)
{
// remember, 0,0 is top left of ui with +X right and +Y down
var screenHandle = args.ScreenHandle;
for (int i = 0; i < _netEnts.Count; i++)
int i = 0;
foreach (var (uid, netEnt) in _netEnts)
{
var netEnt = _netEnts[i];
var uid = netEnt.Id;
if (!_entityManager.EntityExists(uid))
{
_netEnts.RemoveSwap(i);
i--;
_netEnts.Remove(uid);
continue;
}
var xPos = 100;
var yPos = 10 + _lineHeight * i;
var name = $"({netEnt.Id}) {_entityManager.GetComponent<MetaDataComponent>(uid).EntityPrototype?.ID}";
var color = CalcTextColor(ref netEnt);
var yPos = 10 + _lineHeight * i++;
var name = $"({uid}) {_entityManager.GetComponent<MetaDataComponent>(uid).EntityPrototype?.ID}";
var color = netEnt.TextColor(_gameTiming);
screenHandle.DrawString(_font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
DrawTrafficBox(screenHandle, ref netEnt, xPos, yPos);
DrawTrafficBox(screenHandle, netEnt, xPos, yPos);
}
}
private void DrawTrafficBox(DrawingHandleScreen handle, ref NetEntity netEntity, int x, int y)
private void DrawTrafficBox(DrawingHandleScreen handle, NetEntData netEntity, int x, int y)
{
handle.DrawRect(UIBox2.FromDimensions(x+1, y, TrafficHistorySize + 1, _lineHeight), new Color(32, 32, 32, 128));
handle.DrawRect(UIBox2.FromDimensions(x + 1, y, TrafficHistorySize + 1, _lineHeight), new Color(32, 32, 32, 128));
handle.DrawRect(UIBox2.FromDimensions(x, y, TrafficHistorySize + 2, _lineHeight), Color.Gray.WithAlpha(0.15f), false);
var traffic = netEntity.Traffic;
//TODO: Local peak size, actually scale the peaks
for (int i = 0; i < TrafficHistorySize; i++)
for (uint i = 1; i <= TrafficHistorySize; i++)
{
if(traffic[i] == 0)
if (!traffic.TryGetValue(_gameTiming.LastRealTick + (i - TrafficHistorySize), out var tickData))
continue;
var color = tickData switch
{
NetEntData.EntState.Data => Color.Green,
NetEntData.EntState.PvsLeave => Color.Orange,
NetEntData.EntState.PvsEnter => Color.Cyan,
_ => throw new Exception("Unexpected value")
};
var xPos = x + 1 + i;
var yPosA = y + 1;
var yPosB = yPosA + _lineHeight - 1;
handle.DrawLine(new Vector2(xPos, yPosA), new Vector2(xPos, yPosB), Color.Green);
handle.DrawLine(new Vector2(xPos, yPosA), new Vector2(xPos, yPosB), color);
}
}
private Color CalcTextColor(ref NetEntity ent)
{
if(!ent.Exists)
return Color.Gray; // Entity is deleted, will be removed from list soon.
if(!ent.InPVS)
return Color.Red; // Entity still exists outside PVS, but not updated anymore.
if(_gameTiming.LastRealTick < ent.LastUpdate + _gameTiming.TickRate)
return Color.Blue; //Entity in PVS generating ongoing traffic.
return Color.Green; // Entity in PVS, but not updated recently.
}
protected override void DisposeBehavior()
{
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
_gameStateManager.PvsLeave -= OnPvsLeave;
base.DisposeBehavior();
}
private struct NetEntity
private sealed class NetEntData
{
public GameTick LastUpdate;
public readonly EntityUid Id;
public readonly int[] Traffic;
public bool Exists;
public bool InPVS;
public GameTick LastUpdate = GameTick.Zero;
public readonly OverflowDictionary<GameTick, EntState> Traffic = new((int) TrafficHistorySize);
public bool Exists = true;
public bool InPVS = true;
public NetEntity(EntityUid id)
public Color TextColor(IClientGameTiming timing)
{
LastUpdate = GameTick.Zero;
Id = id;
Traffic = new int[TrafficHistorySize];
Exists = true;
InPVS = true;
if (!InPVS)
return Color.Orange; // Entity still exists outside PVS, but not updated anymore.
if (timing.LastRealTick < LastUpdate + timing.TickRate)
return Color.Blue; //Entity in PVS generating ongoing traffic.
return Color.Green; // Entity in PVS, but not updated recently.
}
public enum EntState : byte
{
Nothing = 0,
Data = 1,
PvsLeave = 2,
PvsEnter = 3
}
}
private sealed class NetEntityReportCommand : IConsoleCommand
{
public string Command => "net_entityreport";
public string Help => "net_entityreport <0|1>";
public string Help => "net_entityreport";
public string Description => "Toggles the net entity report panel.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 1 arguments.");
return;
}
if (!byte.TryParse(args[0], out var iValue))
{
shell.WriteError("Invalid argument: Needs to be 0 or 1.");
return;
}
var bValue = iValue > 0;
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if(bValue && !overlayMan.HasOverlay(typeof(NetEntityOverlay)))
if (!overlayMan.HasOverlay(typeof(NetEntityOverlay)))
{
overlayMan.AddOverlay(new NetEntityOverlay());
shell.WriteLine("Enabled network entity report overlay.");
}
else if(!bValue && overlayMan.HasOverlay(typeof(NetEntityOverlay)))
else
{
overlayMan.RemoveOverlay(typeof(NetEntityOverlay));
shell.WriteLine("Disabled network entity report overlay.");
}
}
}
private sealed class NetShowGraphCommand : IConsoleCommand
{
// Yeah commands should be localized, but I'm lazy and this is really just a debug command.
public string Command => "net_refresh";
public string Help => "net_refresh";
public string Description => "requests a full server state";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
IoCManager.Resolve<IClientGameStateManager>().RequestFullState();
}
}
}
}

View File

@@ -10,6 +10,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Client.Player;
namespace Robust.Client.GameStates
{
@@ -28,6 +29,7 @@ namespace Robust.Client.GameStates
private const int MidrangePayloadBps = 33600 / 8; // mid-range line
private const int BytesPerPixel = 2; // If you are running the game on a DSL connection, you can scale the graph to fit your absurd bandwidth.
private const int LowerGraphOffset = 100; // Offset on the Y axis in pixels of the lower lag/interp graph.
private const int LeftMargin = 500; // X offset, to avoid interfering with the f3 menu.
private const int MsPerPixel = 4; // Latency Milliseconds per pixel, for scaling the graph.
/// <inheritdoc />
@@ -84,38 +86,46 @@ namespace Robust.Client.GameStates
var sb = new StringBuilder();
foreach (var entState in entStates.Span)
{
if (entState.Uid == WatchEntId)
{
if(entState.ComponentChanges.HasContents)
{
sb.Append($"\n Changes:");
foreach (var compChange in entState.ComponentChanges.Span)
{
var registration = _componentFactory.GetRegistration(compChange.NetID);
var create = compChange.Created ? 'C' : '\0';
var mod = !(compChange.Created || compChange.Created) ? 'M' : '\0';
var del = compChange.Deleted ? 'D' : '\0';
sb.Append($"\n [{create}{mod}{del}]{compChange.NetID}:{registration.Name}");
if (entState.Uid != WatchEntId)
continue;
if(compChange.State is not null)
sb.Append($"\n STATE:{compChange.State.GetType().Name}");
}
}
if (!entState.ComponentChanges.HasContents)
{
sb.Append("\n Entered PVS");
break;
}
sb.Append($"\n Changes:");
foreach (var compChange in entState.ComponentChanges.Span)
{
var registration = _componentFactory.GetRegistration(compChange.NetID);
var create = compChange.Created ? 'C' : '\0';
var mod = !(compChange.Created || compChange.Created) ? 'M' : '\0';
var del = compChange.Deleted ? 'D' : '\0';
sb.Append($"\n [{create}{mod}{del}]{compChange.NetID}:{registration.Name}");
if (compChange.State is not null)
sb.Append($"\n STATE:{compChange.State.GetType().Name}");
}
}
entStateString = sb.ToString();
}
foreach (var ent in args.Detached)
{
if (ent != WatchEntId)
continue;
conShell.WriteLine($"watchEnt: Left PVS at tick {args.AppliedState.ToSequence}, eid={WatchEntId}" + "\n");
}
var entDeletes = args.AppliedState.EntityDeletions;
if (entDeletes.HasContents)
{
var sb = new StringBuilder();
foreach (var entDelete in entDeletes.Span)
{
if (entDelete == WatchEntId)
{
entDelString = "\n Deleted";
}
}
}
@@ -155,17 +165,16 @@ namespace Robust.Client.GameStates
{
// remember, 0,0 is top left of ui with +X right and +Y down
var leftMargin = 300;
var width = HistorySize;
var height = 500;
var drawSizeThreshold = Math.Min(_totalHistoryPayload / HistorySize, 300);
var handle = args.ScreenHandle;
// bottom payload line
handle.DrawLine(new Vector2(leftMargin, height), new Vector2(leftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
handle.DrawLine(new Vector2(LeftMargin, height), new Vector2(LeftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
// bottom lag line
handle.DrawLine(new Vector2(leftMargin, height + LowerGraphOffset), new Vector2(leftMargin + width, height + LowerGraphOffset), Color.DarkGray.WithAlpha(0.8f));
handle.DrawLine(new Vector2(LeftMargin, height + LowerGraphOffset), new Vector2(LeftMargin + width, height + LowerGraphOffset), Color.DarkGray.WithAlpha(0.8f));
int lastLagY = -1;
int lastLagMs = -1;
@@ -175,7 +184,7 @@ namespace Robust.Client.GameStates
var state = _history[i];
// draw the payload size
var xOff = leftMargin + i;
var xOff = LeftMargin + i;
var yoff = height - state.Payload / BytesPerPixel;
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LightGreen.WithAlpha(0.8f));
@@ -211,25 +220,25 @@ namespace Robust.Client.GameStates
// average payload line
var avgyoff = height - drawSizeThreshold / BytesPerPixel;
handle.DrawLine(new Vector2(leftMargin, avgyoff), new Vector2(leftMargin + width, avgyoff), Color.DarkGray.WithAlpha(0.8f));
handle.DrawLine(new Vector2(LeftMargin, avgyoff), new Vector2(LeftMargin + width, avgyoff), Color.DarkGray.WithAlpha(0.8f));
// top payload warning line
var warnYoff = height - _warningPayloadSize / BytesPerPixel;
handle.DrawLine(new Vector2(leftMargin, warnYoff), new Vector2(leftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
handle.DrawLine(new Vector2(LeftMargin, warnYoff), new Vector2(LeftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
// mid payload line
var midYoff = height - _midrangePayloadSize / BytesPerPixel;
handle.DrawLine(new Vector2(leftMargin, midYoff), new Vector2(leftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
handle.DrawLine(new Vector2(LeftMargin, midYoff), new Vector2(LeftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
// payload text
handle.DrawString(_font, new Vector2(leftMargin + width, warnYoff), "56K");
handle.DrawString(_font, new Vector2(leftMargin + width, midYoff), "33.6K");
handle.DrawString(_font, new Vector2(LeftMargin + width, warnYoff), "56K");
handle.DrawString(_font, new Vector2(LeftMargin + width, midYoff), "33.6K");
// interp text info
if(lastLagY != -1)
handle.DrawString(_font, new Vector2(leftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
handle.DrawString(_font, new Vector2(LeftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
handle.DrawString(_font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
}
protected override void DisposeBehavior()
@@ -242,32 +251,19 @@ namespace Robust.Client.GameStates
private sealed class NetShowGraphCommand : IConsoleCommand
{
public string Command => "net_graph";
public string Help => "net_graph <0|1>";
public string Help => "net_graph";
public string Description => "Toggles the net statistics pannel.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
return;
}
if (!byte.TryParse(args[0], out var iValue))
{
shell.WriteLine("Invalid argument: Needs to be 0 or 1.");
return;
}
var bValue = iValue > 0;
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if(bValue && !overlayMan.HasOverlay(typeof(NetGraphOverlay)))
if(!overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
overlayMan.AddOverlay(new NetGraphOverlay());
shell.WriteLine("Enabled network overlay.");
}
else if(overlayMan.HasOverlay(typeof(NetGraphOverlay)))
else
{
overlayMan.RemoveOverlay(typeof(NetGraphOverlay));
shell.WriteLine("Disabled network overlay.");
@@ -283,13 +279,12 @@ namespace Robust.Client.GameStates
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
EntityUid eValue;
if (args.Length == 0)
{
shell.WriteError("Invalid argument amount. Expected 1 argument.");
return;
eValue = IoCManager.Resolve<IPlayerManager>().LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
}
if (!EntityUid.TryParse(args[0], out var eValue))
else if (!EntityUid.TryParse(args[0], out eValue))
{
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
return;
@@ -297,12 +292,13 @@ namespace Robust.Client.GameStates
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (overlayMan.HasOverlay(typeof(NetGraphOverlay)))
if (!overlayMan.TryGetOverlay(out NetGraphOverlay? overlay))
{
var netOverlay = overlayMan.GetOverlay<NetGraphOverlay>();
netOverlay.WatchEntId = eValue;
overlay = new();
overlayMan.AddOverlay(overlay);
}
overlay.WatchEntId = eValue;
}
}
}

View File

@@ -95,7 +95,7 @@ namespace Robust.Client.Graphics.Clyde
}
// Clear screen to correct color.
ClearFramebuffer(_userInterfaceManager.GetMainClearColor());
ClearFramebuffer(ConvertClearFromSrgb(_userInterfaceManager.GetMainClearColor()));
using (DebugGroup("UI"))
using (_prof.Group("UI"))
@@ -325,7 +325,7 @@ namespace Robust.Client.Graphics.Clyde
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
name: nameof(entityPostRenderTarget));
}
_renderHandle.UseRenderTarget(entityPostRenderTarget);
_renderHandle.Clear(default, 0, ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
@@ -449,7 +449,8 @@ namespace Robust.Client.Graphics.Clyde
{
BindRenderTargetFull(RtToLoaded(rt));
if (clearColor is not null)
ClearFramebuffer(clearColor.Value);
ClearFramebuffer(ConvertClearFromSrgb(clearColor.Value));
SetViewportImmediate(Box2i.FromDimensions(Vector2i.Zero, rt.Size));
_updateUniformConstants(rt.Size);
CalcScreenMatrices(rt.Size, out var proj, out var view);

View File

@@ -382,6 +382,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
// NOTE: sRGB IS IN LINEAR IF FRAMEBUFFER_SRGB IS ACTIVE.
private void ClearFramebuffer(Color color, int stencil = 0, ClearBufferMask mask = ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit)
{
GL.ClearColor(color.ConvertOpenTK());
@@ -392,6 +393,14 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
}
private Color ConvertClearFromSrgb(Color color)
{
if (!_hasGLSrgb)
return color;
return Color.FromSrgb(color);
}
private (GLShaderProgram, LoadedShader) ActivateShaderInstance(ClydeHandle handle)
{
var instance = _shaderInstances[handle];

View File

@@ -16,8 +16,8 @@ namespace Robust.Client.Graphics
bool RemoveOverlay(Type overlayClass);
bool RemoveOverlay<T>() where T : Overlay;
bool TryGetOverlay(Type overlayClass, out Overlay? overlay);
bool TryGetOverlay<T>(out T? overlay) where T : Overlay;
bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay);
bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay;
Overlay GetOverlay(Type overlayClass);
T GetOverlay<T>() where T : Overlay;

View File

@@ -55,6 +55,9 @@ namespace Robust.Client.Placement
/// </summary>
private bool _placenextframe;
// Massive hack to avoid creating a billion grids for now.
private bool _gridFrameBuffer;
/// <summary>
/// Allows various types of placement as singular, line, or grid placement where placement mode allows this type of placement
/// </summary>
@@ -259,6 +262,7 @@ namespace Robust.Client.Placement
if (!CurrentPermission!.IsTile)
HandlePlacement();
_gridFrameBuffer = false;
_placenextframe = false;
return true;
}))
@@ -394,6 +398,7 @@ namespace Robust.Client.Placement
DeactivateSpecialPlacement();
break;
case PlacementTypes.Grid:
_gridFrameBuffer = true;
foreach (var coordinate in CurrentMode!.GridCoordinates())
{
RequestPlacement(coordinate);
@@ -570,8 +575,10 @@ namespace Robust.Client.Placement
_pendingTileChanges.RemoveAll(c => c.Item2 < _time.RealTime);
// continues tile placement but placement of entities only occurs on mouseUp
if (_placenextframe && CurrentPermission!.IsTile)
if (_placenextframe && CurrentPermission!.IsTile && !_gridFrameBuffer)
{
HandlePlacement();
}
}
private void ActivateLineMode()

View File

@@ -97,7 +97,8 @@ namespace Robust.Client.Player
entMan.EventBus.RaiseLocalEvent(previous.Value, new PlayerDetachedEvent(previous.Value), true);
}
ControlledEntity = default;
ControlledEntity = null;
InternalSession.AttachedEntity = null;
if (previous != null)
{

View File

@@ -8,8 +8,8 @@ namespace Robust.Client.State
event Action<StateChangedEventArgs> OnStateChanged;
State CurrentState { get; }
void RequestStateChange<T>() where T : State, new();
T RequestStateChange<T>() where T : State, new();
void FrameUpdate(FrameEventArgs e);
void RequestStateChange(Type type);
State RequestStateChange(Type type);
}
}

View File

@@ -1,6 +1,6 @@
using Robust.Shared.Log;
using System;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.State
@@ -22,23 +22,20 @@ namespace Robust.Client.State
CurrentState?.FrameUpdate(e);
}
public void RequestStateChange<T>() where T : State, new()
public T RequestStateChange<T>() where T : State, new()
{
RequestStateChange(typeof(T));
return (T) RequestStateChange(typeof(T));
}
public void RequestStateChange(Type type)
public State RequestStateChange(Type type)
{
if(!typeof(State).IsAssignableFrom(type))
throw new ArgumentException($"Needs to be derived from {typeof(State).FullName}", nameof(type));
if (CurrentState?.GetType() != type)
{
SwitchToState(type);
}
return CurrentState?.GetType() == type ? CurrentState : SwitchToState(type);
}
private void SwitchToState(Type type)
private State SwitchToState(Type type)
{
Logger.Debug($"Switching to state {type}");
@@ -51,6 +48,8 @@ namespace Robust.Client.State
CurrentState.Startup();
OnStateChanged?.Invoke(new StateChangedEventArgs(old, CurrentState));
return CurrentState;
}
}
}

View File

@@ -10,6 +10,14 @@ namespace Robust.Client.Timing
{
[Dependency] private readonly IClientNetManager _netManager = default!;
public override bool InPrediction => !ApplyingState && CurTick > LastRealTick;
/// <inheritdoc />
public GameTick LastRealTick { get; set; }
/// <inheritdoc />
public GameTick LastProcessedTick { get; set; }
public override TimeSpan ServerTime
{
get

View File

@@ -5,6 +5,20 @@ namespace Robust.Client.Timing
{
public interface IClientGameTiming : IGameTiming
{
/// <summary>
/// This is functionally the clients "current-tick" before prediction, and represents the target value for <see
/// cref="LastRealTick"/>. This value should increment by at least one every tick. It may increase by more than
/// that if we apply several server states within a single tick.
/// </summary>
GameTick LastProcessedTick { get; set; }
/// <summary>
/// The last real non-extrapolated server state that was applied. Without networking issues, this tick should
/// always correspond to <see cref="LastRealTick"/>, however if there is a missing states or the buffer has run
/// out, this value may be smaller..
/// </summary>
GameTick LastRealTick { get; set; }
void StartPastPrediction();
void EndPastPrediction();

View File

@@ -1,10 +1,11 @@
using System;
using System;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.Profiling;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
@@ -19,7 +20,7 @@ namespace Robust.Client.UserInterface.CustomControls
private readonly Control[] _monitors = new Control[Enum.GetNames<DebugMonitor>().Length];
//TODO: Think about a factory for this
public DebugMonitors(IGameTiming gameTiming, IPlayerManager playerManager, IEyeManager eyeManager,
public DebugMonitors(IClientGameTiming gameTiming, IPlayerManager playerManager, IEyeManager eyeManager,
IInputManager inputManager, IStateManager stateManager, IClyde displayManager, IClientNetManager netManager,
IMapManager mapManager)
{

View File

@@ -1,6 +1,7 @@
using System;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Timing;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
@@ -10,13 +11,13 @@ namespace Robust.Client.UserInterface.CustomControls
{
public sealed class DebugTimePanel : PanelContainer
{
private readonly IGameTiming _gameTiming;
private readonly IClientGameTiming _gameTiming;
private readonly IClientGameStateManager _gameState;
private readonly char[] _textBuffer = new char[256];
private readonly Label _contents;
public DebugTimePanel(IGameTiming gameTiming, IClientGameStateManager gameState)
public DebugTimePanel(IClientGameTiming gameTiming, IClientGameStateManager gameState)
{
_gameTiming = gameTiming;
_gameState = gameState;
@@ -53,7 +54,7 @@ namespace Robust.Client.UserInterface.CustomControls
// This is why there's a -1 on Pred:.
_contents.TextMemory = FormatHelpers.FormatIntoMem(_textBuffer,
$@"Paused: {_gameTiming.Paused}, CurTick: {_gameTiming.CurTick}/{_gameTiming.CurTick - 1}, CurServerTick: {_gameState.CurServerTick}, Pred: {_gameTiming.CurTick.Value - _gameState.CurServerTick.Value - 1}
$@"Paused: {_gameTiming.Paused}, CurTick: {_gameTiming.CurTick}, LastProcessed: {_gameTiming.LastProcessedTick}, LastRealTick: {_gameTiming.LastRealTick}, Pred: {_gameTiming.CurTick.Value - _gameTiming.LastRealTick.Value - 1}
CurTime: {_gameTiming.CurTime:hh\:mm\:ss\.ff}, RealTime: {_gameTiming.RealTime:hh\:mm\:ss\.ff}, CurFrame: {_gameTiming.CurFrame}
ServerTime: {_gameTiming.ServerTime}, TickTimingAdjustment: {_gameTiming.TickTimingAdjustment}");
}

View File

@@ -5,6 +5,7 @@ using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
@@ -26,7 +27,7 @@ namespace Robust.Client.UserInterface
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IFontManager _fontManager = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;

View File

@@ -646,9 +646,6 @@ namespace Robust.Server
ServerCurTick.Set(_time.CurTick.Value);
ServerCurTime.Set(_time.CurTime.TotalSeconds);
// These are always the same on the server, there is no prediction.
_time.LastRealTick = _time.CurTick;
_systemConsole.UpdateTick();
using (TickUsage.WithLabels("PreEngine").NewTimer())

View File

@@ -75,11 +75,11 @@ namespace Robust.Server.Console.Commands
}
}
public sealed class SaveBp : IConsoleCommand
public sealed class SaveGridCommand : IConsoleCommand
{
public string Command => "savebp";
public string Command => "savegrid";
public string Description => "Serializes a grid to disk.";
public string Help => "savebp <gridID> <Path>";
public string Help => "savegrid <gridID> <Path>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
@@ -104,16 +104,30 @@ namespace Robust.Server.Console.Commands
return;
}
IoCManager.Resolve<IMapLoader>().SaveBlueprint(gridId, args[1]);
IoCManager.Resolve<IMapLoader>().SaveGrid(gridId, args[1]);
shell.WriteLine("Save successful. Look in the user data directory.");
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
switch (args.Length)
{
case 1:
return CompletionResult.FromHint(Loc.GetString("cmd-hint-savebp-id"));
case 2:
var res = IoCManager.Resolve<IResourceManager>();
var opts = CompletionHelper.UserFilePath(args[1], res.UserData);
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));
}
return CompletionResult.Empty;
}
}
public sealed class LoadBp : IConsoleCommand
public sealed class LoadGridCommand : IConsoleCommand
{
public string Command => "loadbp";
public string Description => "Loads a blueprint from disk into the game.";
public string Help => "loadbp <MapID> <Path> [x y] [rotation] [storeUids]";
public string Command => "loadgrid";
public string Description => "Loads a grid from a file into an existing map.";
public string Help => "loadgrid <MapID> <Path> [x y] [rotation] [storeUids]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
@@ -148,15 +162,15 @@ namespace Robust.Server.Console.Commands
var loadOptions = new MapLoadOptions();
if (args.Length >= 4)
{
if (!int.TryParse(args[2], out var x))
if (!float.TryParse(args[2], out var x))
{
shell.WriteError($"{args[2]} is not a valid integer.");
shell.WriteError($"{args[2]} is not a valid float.");
return;
}
if (!int.TryParse(args[3], out var y))
if (!float.TryParse(args[3], out var y))
{
shell.WriteError($"{args[3]} is not a valid integer.");
shell.WriteError($"{args[3]} is not a valid float.");
return;
}
@@ -167,7 +181,7 @@ namespace Robust.Server.Console.Commands
{
if (!float.TryParse(args[4], out var rotation))
{
shell.WriteError($"{args[4]} is not a valid integer.");
shell.WriteError($"{args[4]} is not a valid float.");
return;
}
@@ -178,7 +192,7 @@ namespace Robust.Server.Console.Commands
{
if (!bool.TryParse(args[5], out var storeUids))
{
shell.WriteError($"{args[5]} is not a valid boolean..");
shell.WriteError($"{args[5]} is not a valid boolean.");
return;
}
@@ -186,7 +200,12 @@ namespace Robust.Server.Console.Commands
}
var mapLoader = IoCManager.Resolve<IMapLoader>();
mapLoader.LoadBlueprint(mapId, args[1], loadOptions);
mapLoader.LoadGrid(mapId, args[1], loadOptions);
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return LoadMap.GetCompletionResult(shell, args);
}
}
@@ -258,7 +277,7 @@ namespace Robust.Server.Console.Commands
public string Description => Loc.GetString("cmd-loadmap-desc");
public string Help => Loc.GetString("cmd-loadmap-help");
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public static CompletionResult GetCompletionResult(IConsoleShell shell, string[] args)
{
switch (args.Length)
{
@@ -282,6 +301,11 @@ namespace Robust.Server.Console.Commands
return CompletionResult.Empty;
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return GetCompletionResult(shell, args);
}
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2 || args.Length > 6)

View File

@@ -1,14 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using static Robust.Shared.GameObjects.SharedUserInterfaceComponent;
namespace Robust.Server.GameObjects
@@ -22,16 +18,10 @@ namespace Robust.Server.GameObjects
[ComponentReference(typeof(SharedUserInterfaceComponent))]
public sealed class ServerUserInterfaceComponent : SharedUserInterfaceComponent, ISerializationHooks
{
private readonly Dictionary<object, BoundUserInterface> _interfaces =
internal readonly Dictionary<Enum, BoundUserInterface> _interfaces =
new();
[DataField("interfaces", readOnly: true)]
private List<PrototypeData> _interfaceData = new();
/// <summary>
/// Enumeration of all the interfaces this component provides.
/// </summary>
public IEnumerable<BoundUserInterface> Interfaces => _interfaces.Values;
public IReadOnlyDictionary<Enum, BoundUserInterface> Interfaces => _interfaces;
void ISerializationHooks.AfterDeserialization()
{
@@ -42,35 +32,12 @@ namespace Robust.Server.GameObjects
_interfaces[prototypeData.UiKey] = new BoundUserInterface(prototypeData, this);
}
}
}
public BoundUserInterface GetBoundUserInterface(object uiKey)
{
return _interfaces[uiKey];
}
public bool TryGetBoundUserInterface(object uiKey,
[NotNullWhen(true)] out BoundUserInterface? boundUserInterface)
{
return _interfaces.TryGetValue(uiKey, out boundUserInterface);
}
public BoundUserInterface? GetBoundUserInterfaceOrNull(object uiKey)
{
return TryGetBoundUserInterface(uiKey, out var boundUserInterface)
? boundUserInterface
: null;
}
public bool HasBoundUserInterface(object uiKey)
{
return _interfaces.ContainsKey(uiKey);
}
internal void SendToSession(IPlayerSession session, BoundUserInterfaceMessage message, object uiKey)
{
EntitySystem.Get<UserInterfaceSystem>()
.SendTo(session, new BoundUIWrapMessage(Owner, message, uiKey));
}
[RegisterComponent]
public sealed class ActiveUserInterfaceComponent : Component
{
public HashSet<BoundUserInterface> Interfaces = new();
}
/// <summary>
@@ -79,275 +46,92 @@ namespace Robust.Server.GameObjects
[PublicAPI]
public sealed class BoundUserInterface
{
private bool _isActive;
public float InteractionRangeSqrd;
public object UiKey { get; }
public ServerUserInterfaceComponent Owner { get; }
private readonly HashSet<IPlayerSession> _subscribedSessions = new();
private BoundUserInterfaceState? _lastState;
public Enum UiKey { get; }
public ServerUserInterfaceComponent Component { get; }
public EntityUid Owner => Component.Owner;
internal readonly HashSet<IPlayerSession> _subscribedSessions = new();
internal BoundUIWrapMessage? LastStateMsg;
public bool RequireInputValidation;
private bool _stateDirty;
internal bool StateDirty;
private readonly Dictionary<IPlayerSession, BoundUserInterfaceState> _playerStateOverrides =
internal readonly Dictionary<IPlayerSession, BoundUIWrapMessage> PlayerStateOverrides =
new();
/// <summary>
/// All of the sessions currently subscribed to this UserInterface.
/// </summary>
public IReadOnlyCollection<IPlayerSession> SubscribedSessions => _subscribedSessions;
public IReadOnlySet<IPlayerSession> SubscribedSessions => _subscribedSessions;
[Obsolete("Use system events")]
public event Action<ServerBoundUserInterfaceMessage>? OnReceiveMessage;
public event Action<IPlayerSession>? OnClosed;
public BoundUserInterface(PrototypeData data, ServerUserInterfaceComponent owner)
{
RequireInputValidation = data.RequireInputValidation;
UiKey = data.UiKey;
Owner = owner;
Component = owner;
// One Abs(), because negative values imply no limit
InteractionRangeSqrd = data.InteractionRange * MathF.Abs(data.InteractionRange);
}
/// <summary>
/// Sets a state. This can be used for stateful UI updating, which can be easier to implement,
/// but is more costly on bandwidth.
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
/// Pretty much how NanoUI did it back in ye olde BYOND.
/// </summary>
/// <param name="state">
/// The state object that will be sent to all current and future client.
/// This can be null.
/// </param>
/// <param name="session">
/// The player session to send this new state to.
/// Set to null for sending it to every subscribed player session.
/// </param>
public void SetState(BoundUserInterfaceState state, IPlayerSession? session = null)
[Obsolete("Use UserInterfaceSystem")]
public void SetState(BoundUserInterfaceState state, IPlayerSession? session = null, bool clearOverrides = true)
{
if (session == null)
{
_lastState = state;
_playerStateOverrides.Clear();
}
else
{
_playerStateOverrides[session] = state;
}
_stateDirty = true;
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().SetUiState(this, state, session, clearOverrides);
}
/// <summary>
/// Switches between closed and open for a specific client.
/// </summary>
/// <param name="session">The player session to toggle the UI on.</param>
/// <exception cref="ArgumentException">
/// Thrown if the session's status is <c>Connecting</c> or <c>Disconnected</c>
/// </exception>
/// <exception cref="ArgumentNullException">Thrown if <see cref="session"/> is null.</exception>
[Obsolete("Use UserInterfaceSystem")]
public void Toggle(IPlayerSession session)
{
if (_subscribedSessions.Contains(session))
{
Close(session);
}
else
{
Open(session);
}
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().ToggleUi(this, session);
}
/// <summary>
/// Opens this interface for a specific client.
/// </summary>
/// <param name="session">The player session to open the UI on.</param>
/// <exception cref="ArgumentException">
/// Thrown if the session's status is <c>Connecting</c> or <c>Disconnected</c>
/// </exception>
/// <exception cref="ArgumentNullException">Thrown if <see cref="session"/> is null.</exception>
[Obsolete("Use UserInterfaceSystem")]
public bool Open(IPlayerSession session)
{
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
if (session.Status == SessionStatus.Connecting || session.Status == SessionStatus.Disconnected)
{
throw new ArgumentException("Invalid session status.", nameof(session));
}
if (_subscribedSessions.Contains(session))
{
return false;
}
_subscribedSessions.Add(session);
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(Owner.Owner, new BoundUIOpenedEvent(UiKey, Owner.Owner, session), true);
SendMessage(new OpenBoundInterfaceMessage(), session);
if (_lastState != null)
{
SendMessage(new UpdateBoundStateMessage(_lastState));
}
if (!_isActive)
{
_isActive = true;
EntitySystem.Get<UserInterfaceSystem>()
.ActivateInterface(this);
}
session.PlayerStatusChanged += OnSessionOnPlayerStatusChanged;
return true;
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().OpenUi(this, session);
}
private void OnSessionOnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
{
if (args.NewStatus == SessionStatus.Disconnected)
{
CloseShared(args.Session);
}
}
/// <summary>
/// Close this interface for a specific client.
/// </summary>
/// <param name="session">The session to close the UI on.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="session"/> is null.</exception>
[Obsolete("Use UserInterfaceSystem")]
public bool Close(IPlayerSession session)
{
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
if (!_subscribedSessions.Contains(session))
{
return false;
}
var msg = new CloseBoundInterfaceMessage();
SendMessage(msg, session);
CloseShared(session);
return true;
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().CloseUi(this, session);
}
public void CloseShared(IPlayerSession session)
{
var owner = Owner.Owner;
OnClosed?.Invoke(session);
_subscribedSessions.Remove(session);
_playerStateOverrides.Remove(session);
session.PlayerStatusChanged -= OnSessionOnPlayerStatusChanged;
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(owner, new BoundUIClosedEvent(UiKey, owner, session), true);
if (_subscribedSessions.Count == 0)
{
EntitySystem.Get<UserInterfaceSystem>()
.DeactivateInterface(this);
_isActive = false;
}
}
/// <summary>
/// Closes this interface for any clients that have it open.
/// </summary>
[Obsolete("Use UserInterfaceSystem")]
public void CloseAll()
{
foreach (var session in _subscribedSessions.ToArray())
Close(session);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().CloseAll(this);
}
/// <summary>
/// Returns whether or not a session has this UI open.
/// </summary>
/// <param name="session">The session to check.</param>
/// <returns>True if the player has this UI open, false otherwise.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="session"/> is null.</exception>
[Obsolete("Just check SubscribedSessions.Contains")]
public bool SessionHasOpen(IPlayerSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
return _subscribedSessions.Contains(session);
}
/// <summary>
/// Sends a message to ALL sessions that currently have the UI open.
/// </summary>
/// <param name="message">The message to send.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="message"/> is null.</exception>
[Obsolete("Use UserInterfaceSystem")]
public void SendMessage(BoundUserInterfaceMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
foreach (var session in _subscribedSessions)
{
Owner.SendToSession(session, message, UiKey);
}
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().SendUiMessage(this, message);
}
/// <summary>
/// Sends a message to a specific session.
/// </summary>
/// <param name="message">The message to send.</param>
/// <param name="session">The session to send the message to.</param>
/// <exception cref="ArgumentNullException">Thrown if either argument is null.</exception>
/// <exception cref="ArgumentException">Thrown if the session does not have this UI open.</exception>
[Obsolete("Use UserInterfaceSystem")]
public void SendMessage(BoundUserInterfaceMessage message, IPlayerSession session)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
AssertContains(session);
Owner.SendToSession(session, message, UiKey);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<UserInterfaceSystem>().TrySendUiMessage(this, message, session);
}
internal void ReceiveMessage(ServerBoundUserInterfaceMessage message)
internal void InvokeOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
OnReceiveMessage?.Invoke(message);
}
private void AssertContains(IPlayerSession session)
{
if (!SessionHasOpen(session))
{
throw new ArgumentException("Player session does not have this UI open.");
}
}
public void DispatchPendingState()
{
if (!_stateDirty)
{
return;
}
foreach (var playerSession in _subscribedSessions)
{
if (!_playerStateOverrides.ContainsKey(playerSession) && _lastState != null)
{
SendMessage(new UpdateBoundStateMessage(_lastState), playerSession);
}
}
foreach (var (player, state) in _playerStateOverrides)
{
SendMessage(new UpdateBoundStateMessage(state), player);
}
_stateDirty = false;
}
}
[PublicAPI]

View File

@@ -50,8 +50,8 @@ namespace Robust.Server.GameObjects
// Null by default.
forceKicked = null;
// Cannot attach to a deleted/nonexisting entity.
if (EntityManager.Deleted(uid))
// Cannot attach to a deleted, nonexisting or terminating entity.
if (!TryComp(uid, out MetaDataComponent? meta) || meta.EntityLifeStage > EntityLifeStage.MapInitialized)
{
return false;
}

View File

@@ -119,8 +119,11 @@ namespace Robust.Server.GameObjects
}
/// <inheritdoc />
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
{
if (sound == null)
return null;
var filter = Filter.Pvs(source, entityManager: EntityManager).RemoveWhereAttachedEntity(e => e == user);
return Play(sound, filter, source, audioParams);
}

View File

@@ -4,10 +4,11 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.ViewVariables;
using Robust.Shared.Utility;
namespace Robust.Server.GameObjects
{
@@ -18,9 +19,9 @@ namespace Robust.Server.GameObjects
private readonly List<IPlayerSession> _sessionCache = new();
// List of all bound user interfaces that have at least one player looking at them.
[ViewVariables]
private readonly List<BoundUserInterface> _activeInterfaces = new();
private Dictionary<IPlayerSession, List<BoundUserInterface>> _openInterfaces = new();
[Dependency] private readonly IPlayerManager _playerMan = default!;
/// <inheritdoc />
public override void Initialize()
@@ -29,19 +30,39 @@ namespace Robust.Server.GameObjects
SubscribeNetworkEvent<BoundUIWrapMessage>(OnMessageReceived);
SubscribeLocalEvent<ServerUserInterfaceComponent, ComponentShutdown>(OnUserInterfaceShutdown);
_playerMan.PlayerStatusChanged += OnPlayerStatusChanged;
}
public override void Shutdown()
{
base.Shutdown();
_playerMan.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
{
if (args.NewStatus != SessionStatus.Disconnected)
return;
if (!_openInterfaces.TryGetValue(args.Session, out var buis))
return;
foreach (var bui in buis.ToArray())
{
CloseShared(bui, args.Session);
}
}
private void OnUserInterfaceShutdown(EntityUid uid, ServerUserInterfaceComponent component, ComponentShutdown args)
{
foreach (var bui in component.Interfaces)
{
DeactivateInterface(bui);
}
}
if (!TryComp(uid, out ActiveUserInterfaceComponent? activeUis))
return;
internal void SendTo(IPlayerSession session, BoundUIWrapMessage msg)
{
RaiseNetworkEvent(msg, session.ConnectedClient);
foreach (var bui in activeUis.Interfaces)
{
DeactivateInterface(bui, activeUis);
}
}
/// <summary>
@@ -53,13 +74,13 @@ namespace Robust.Server.GameObjects
if (!TryComp(uid, out ServerUserInterfaceComponent? uiComp) || args.SenderSession is not IPlayerSession session)
return;
if (!uiComp.TryGetBoundUserInterface(msg.UiKey, out var ui))
if (!uiComp._interfaces.TryGetValue(msg.UiKey, out var ui))
{
Logger.DebugS("go.comp.ui", "Got BoundInterfaceMessageWrapMessage for unknown UI key: {0}", msg.UiKey);
return;
}
if (!ui.SessionHasOpen(session))
if (!ui.SubscribedSessions.Contains(session))
{
Logger.DebugS("go.comp.ui", $"UI {msg.UiKey} got BoundInterfaceMessageWrapMessage from a client who was not subscribed: {session}", msg.UiKey);
return;
@@ -68,7 +89,7 @@ namespace Robust.Server.GameObjects
// if they want to close the UI, we can go home early.
if (msg.Message is CloseBoundInterfaceMessage)
{
ui.CloseShared(session);
CloseShared(ui, session);
return;
}
@@ -93,24 +114,45 @@ namespace Robust.Server.GameObjects
// Once we have populated our message's wrapped message, we will wrap it up into a message that can be sent
// to old component-code.
var WrappedUnwrappedMessageMessageMessage = new ServerBoundUserInterfaceMessage(message, session);
ui.ReceiveMessage(WrappedUnwrappedMessageMessageMessage);
ui.InvokeOnReceiveMessage(WrappedUnwrappedMessageMessageMessage);
}
/// <inheritdoc />
public override void Update(float frameTime)
{
var query = GetEntityQuery<TransformComponent>();
foreach (var userInterface in _activeInterfaces.ToList())
foreach (var (activeUis, xform) in EntityQuery<ActiveUserInterfaceComponent, TransformComponent>())
{
CheckRange(userInterface, query);
userInterface.DispatchPendingState();
foreach (var ui in activeUis.Interfaces)
{
CheckRange(activeUis, ui, xform, query);
if (!ui.StateDirty)
continue;
ui.StateDirty = false;
foreach (var (player, state) in ui.PlayerStateOverrides)
{
RaiseNetworkEvent(state, player.ConnectedClient);
}
if (ui.LastStateMsg == null)
continue;
foreach (var session in ui.SubscribedSessions)
{
if (!ui.PlayerStateOverrides.ContainsKey(session))
RaiseNetworkEvent(ui.LastStateMsg, session.ConnectedClient);
}
}
}
}
/// <summary>
/// Verify that the subscribed clients are still in range of the interface.
/// </summary>
private void CheckRange(BoundUserInterface ui, EntityQuery<TransformComponent> query)
private void CheckRange(ActiveUserInterfaceComponent activeUis, BoundUserInterface ui, TransformComponent transform, EntityQuery<TransformComponent> query)
{
if (ui.InteractionRangeSqrd <= 0)
return;
@@ -119,7 +161,6 @@ namespace Robust.Server.GameObjects
_sessionCache.Clear();
_sessionCache.AddRange(ui.SubscribedSessions);
var transform = query.GetComponent(ui.Owner.Owner);
var uiPos = _xformSys.GetWorldPosition(transform, query);
var uiMap = transform.MapID;
@@ -127,175 +168,321 @@ namespace Robust.Server.GameObjects
{
// The component manages the set of sessions, so this invalid session should be removed soon.
if (!query.TryGetComponent(session.AttachedEntity, out var xform))
{
continue;
}
if (uiMap != xform.MapID)
{
ui.Close(session);
CloseUi(ui, session, activeUis);
continue;
}
var distanceSquared = (uiPos - _xformSys.GetWorldPosition(xform, query)).LengthSquared;
if (distanceSquared > ui.InteractionRangeSqrd)
{
ui.Close(session);
}
CloseUi(ui, session, activeUis);
}
}
internal void DeactivateInterface(BoundUserInterface userInterface)
private void DeactivateInterface(BoundUserInterface ui, ActiveUserInterfaceComponent? activeUis = null)
{
_activeInterfaces.Remove(userInterface);
if (!Resolve(ui.Component.Owner, ref activeUis, false))
return;
activeUis.Interfaces.Remove(ui);
if (activeUis.Interfaces.Count == 0)
RemCompDeferred(activeUis.Owner, activeUis);
}
internal void ActivateInterface(BoundUserInterface userInterface)
private void ActivateInterface(BoundUserInterface ui)
{
_activeInterfaces.Add(userInterface);
EnsureComp<ActiveUserInterfaceComponent>(ui.Component.Owner).Interfaces.Add(ui);
}
#region Proxy Methods
public bool HasUi(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
#region Get BUI
public bool HasUi(EntityUid uid, Enum uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
return ui.HasBoundUserInterface(uiKey);
return ui._interfaces.ContainsKey(uiKey);
}
public BoundUserInterface GetUi(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
public BoundUserInterface GetUi(EntityUid uid, Enum uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
throw new InvalidOperationException($"Cannot get {typeof(BoundUserInterface)} from an entity without {typeof(ServerUserInterfaceComponent)}!");
return ui.GetBoundUserInterface(uiKey);
return ui._interfaces[uiKey];
}
public BoundUserInterface? GetUiOrNull(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
public BoundUserInterface? GetUiOrNull(EntityUid uid, Enum uiKey, ServerUserInterfaceComponent? ui = null)
{
return TryGetUi(uid, uiKey, out var bui, ui)
? bui
: null;
}
public bool TryGetUi(EntityUid uid, object uiKey, [NotNullWhen(true)] out BoundUserInterface? bui, ServerUserInterfaceComponent? ui = null)
public bool TryGetUi(EntityUid uid, Enum uiKey, [NotNullWhen(true)] out BoundUserInterface? bui, ServerUserInterfaceComponent? ui = null)
{
bui = null;
return Resolve(uid, ref ui, false) && ui.TryGetBoundUserInterface(uiKey, out bui);
return Resolve(uid, ref ui, false) && ui._interfaces.TryGetValue(uiKey, out bui);
}
#endregion
public bool IsUiOpen(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
public bool IsUiOpen(EntityUid uid, Enum uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui, false))
return false;
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return bui.SubscribedSessions.Count > 0;
}
public bool TrySetUiState(EntityUid uid, object uiKey, BoundUserInterfaceState state, IPlayerSession? session = null, ServerUserInterfaceComponent? ui = null)
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui, false))
return false;
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
bui.SetState(state, session);
return true;
return bui.SubscribedSessions.Contains(session);
}
public bool TryToggleUi(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
/// <summary>
/// Sets a state. This can be used for stateful UI updating.
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
/// Pretty much how NanoUI did it back in ye olde BYOND.
/// </summary>
/// <param name="state">
/// The state object that will be sent to all current and future client.
/// This can be null.
/// </param>
/// <param name="session">
/// The player session to send this new state to.
/// Set to null for sending it to every subscribed player session.
/// </param>
public bool TrySetUiState(EntityUid uid,
Enum uiKey,
BoundUserInterfaceState state,
IPlayerSession? session = null,
ServerUserInterfaceComponent? ui = null,
bool clearOverrides = true)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
bui.Toggle(session);
return true;
}
public bool TryOpen(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
return bui.Open(session);
}
public bool TryClose(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return bui.Close(session);
}
public bool TryCloseAll(EntityUid uid, object uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
bui.CloseAll();
SetUiState(bui, state, session, clearOverrides);
return true;
}
public bool SessionHasOpenUi(EntityUid uid, object uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
/// <summary>
/// Sets a state. This can be used for stateful UI updating.
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
/// Pretty much how NanoUI did it back in ye olde BYOND.
/// </summary>
/// <param name="state">
/// The state object that will be sent to all current and future client.
/// This can be null.
/// </param>
/// <param name="session">
/// The player session to send this new state to.
/// Set to null for sending it to every subscribed player session.
/// </param>
public void SetUiState(BoundUserInterface bui, BoundUserInterfaceState state, IPlayerSession? session = null, bool clearOverrides = true)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
return bui.SessionHasOpen(session);
}
public bool TrySendUiMessage(EntityUid uid, object uiKey, BoundUserInterfaceMessage message, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
bui.SendMessage(message);
return true;
}
public bool TrySendUiMessage(EntityUid uid, object uiKey, BoundUserInterfaceMessage message, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
if (!TryGetUi(uid, uiKey, out var bui))
return false;
try
var msg = new BoundUIWrapMessage(bui.Component.Owner, new UpdateBoundStateMessage(state), bui.UiKey);
if (session == null)
{
bui.SendMessage(message, session);
bui.LastStateMsg = msg;
if (clearOverrides)
bui.PlayerStateOverrides.Clear();
}
catch (ArgumentException)
else
{
return false;
bui.PlayerStateOverrides[session] = msg;
}
bui.StateDirty = true;
}
/// <summary>
/// Switches between closed and open for a specific client.
/// </summary>
public bool TryToggleUi(EntityUid uid, Enum uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
ToggleUi(bui, session);
return true;
}
/// <summary>
/// Switches between closed and open for a specific client.
/// </summary>
public void ToggleUi(BoundUserInterface bui, IPlayerSession session)
{
if (bui._subscribedSessions.Contains(session))
CloseUi(bui, session);
else
OpenUi(bui, session);
}
#region Open
public bool TryOpen(EntityUid uid, Enum uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return OpenUi(bui, session);
}
/// <summary>
/// Opens this interface for a specific client.
/// </summary>
public bool OpenUi(BoundUserInterface bui, IPlayerSession session)
{
if (session.Status == SessionStatus.Connecting || session.Status == SessionStatus.Disconnected)
return false;
if (!bui._subscribedSessions.Add(session))
return false;
_openInterfaces.GetOrNew(session).Add(bui);
RaiseLocalEvent(bui.Component.Owner, new BoundUIOpenedEvent(bui.UiKey, bui.Component.Owner, session));
RaiseNetworkEvent(new BoundUIWrapMessage(bui.Component.Owner, new OpenBoundInterfaceMessage(), bui.UiKey), session.ConnectedClient);
// Fun fact, clients needs to have BUIs open before they can receive the state.....
if (bui.LastStateMsg != null)
RaiseNetworkEvent(bui.LastStateMsg, session.ConnectedClient);
ActivateInterface(bui);
return true;
}
#endregion
#region Close
public bool TryClose(EntityUid uid, Enum uiKey, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return CloseUi(bui, session);
}
/// <summary>
/// Close this interface for a specific client.
/// </summary>
public bool CloseUi(BoundUserInterface bui, IPlayerSession session, ActiveUserInterfaceComponent? activeUis = null)
{
if (!bui._subscribedSessions.Remove(session))
return false;
RaiseNetworkEvent(new BoundUIWrapMessage(bui.Component.Owner, new CloseBoundInterfaceMessage(), bui.UiKey), session.ConnectedClient);
CloseShared(bui, session, activeUis);
return true;
}
private void CloseShared(BoundUserInterface bui, IPlayerSession session, ActiveUserInterfaceComponent? activeUis = null)
{
var owner = bui.Component.Owner;
bui._subscribedSessions.Remove(session);
bui.PlayerStateOverrides.Remove(session);
if (_openInterfaces.TryGetValue(session, out var buis))
buis.Remove(bui);
RaiseLocalEvent(owner, new BoundUIClosedEvent(bui.UiKey, owner, session));
if (bui._subscribedSessions.Count == 0)
DeactivateInterface(bui, activeUis);
}
/// <summary>
/// Closes this all interface for any clients that have any open.
/// </summary>
public bool TryCloseAll(EntityUid uid, ActiveUserInterfaceComponent? aui = null)
{
if (!Resolve(uid, ref aui, false))
return false;
foreach (var ui in aui.Interfaces)
{
CloseAll(ui);
}
return true;
}
/// <summary>
/// Closes this specific interface for any clients that have it open.
/// </summary>
public bool TryCloseAll(EntityUid uid, Enum uiKey, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
CloseAll(bui);
return true;
}
/// <summary>
/// Closes this interface for any clients that have it open.
/// </summary>
public void CloseAll(BoundUserInterface bui)
{
foreach (var session in bui.SubscribedSessions.ToArray())
{
CloseUi(bui, session);
}
}
#endregion
#region SendMessage
/// <summary>
/// Send a BUI message to all connected player sessions.
/// </summary>
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
SendUiMessage(bui, message);
return true;
}
/// <summary>
/// Send a BUI message to all connected player sessions.
/// </summary>
public void SendUiMessage(BoundUserInterface bui, BoundUserInterfaceMessage message)
{
var msg = new BoundUIWrapMessage(bui.Component.Owner, message, bui.UiKey);
foreach (var session in bui.SubscribedSessions)
{
RaiseNetworkEvent(msg, session.ConnectedClient);
}
}
/// <summary>
/// Send a BUI message to a specific player session.
/// </summary>
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, IPlayerSession session, ServerUserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return TrySendUiMessage(bui, message, session);
}
/// <summary>
/// Send a BUI message to a specific player session.
/// </summary>
public bool TrySendUiMessage(BoundUserInterface bui, BoundUserInterfaceMessage message, IPlayerSession session)
{
if (!bui.SubscribedSessions.Contains(session))
return false;
RaiseNetworkEvent(new BoundUIWrapMessage(bui.Component.Owner, message, bui.UiKey), session.ConnectedClient);
return true;
}

View File

@@ -1,3 +1,7 @@
using System;
using Robust.Shared.Players;
using Robust.Shared.Timing;
namespace Robust.Server.GameStates
{
/// <summary>
@@ -16,5 +20,9 @@ namespace Robust.Server.GameStates
void SendGameStateUpdate();
ushort TransformNetId { get; set; }
Action<ICommonSession, GameTick, GameTick>? ClientAck { get; set; }
Action<ICommonSession, GameTick, GameTick>? ClientRequestFull { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
namespace Robust.Server.GameStates;
namespace Robust.Server.GameStates;
public enum PVSEntityVisiblity : byte
{

View File

@@ -11,10 +11,11 @@ using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -27,15 +28,16 @@ internal sealed partial class PVSSystem : EntitySystem
[Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
[Shared.IoC.Dependency] private readonly IConfigurationManager _configManager = default!;
[Shared.IoC.Dependency] private readonly IServerEntityManager _serverEntManager = default!;
[Shared.IoC.Dependency] private readonly IServerGameStateManager _stateManager = default!;
[Shared.IoC.Dependency] private readonly SharedTransformSystem _transform = default!;
[Shared.IoC.Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
[Shared.IoC.Dependency] private readonly IServerGameStateManager _serverGameStateManager = default!;
public const float ChunkSize = 8;
public const int TickBuffer = 10;
private static TransformComponentState _transformCullState =
new(Vector2.Zero, Angle.Zero, EntityUid.Invalid, false, false);
// TODO make this a cvar. Make it in terms of seconds and tie it to tick rate?
public const int TickBuffer = 20;
// Note: If a client has ping higher than TickBuffer / TickRate, then the server will treat every entity as if it
// had entered PVS for the first time. Note that due to the PVS budget, this buffer is easily overwhelmed.
/// <summary>
/// Maximum number of pooled objects
@@ -58,18 +60,16 @@ internal sealed partial class PVSSystem : EntitySystem
/// </summary>
public HashSet<ICommonSession> SeenAllEnts = new();
/// <summary>
/// All <see cref="Robust.Shared.GameObjects.EntityUid"/>s a <see cref="ICommonSession"/> saw last iteration.
/// </summary>
private readonly Dictionary<ICommonSession, OverflowDictionary<GameTick, Dictionary<EntityUid, PVSEntityVisiblity>>> _playerVisibleSets = new();
private readonly Dictionary<ICommonSession, SessionPVSData> _playerVisibleSets = new();
private PVSCollection<EntityUid> _entityPvsCollection = default!;
public PVSCollection<EntityUid> EntityPVSCollection => _entityPvsCollection;
private readonly List<IPVSCollection> _pvsCollections = new();
private readonly ObjectPool<Dictionary<EntityUid, PVSEntityVisiblity>> _visSetPool
= new DefaultObjectPool<Dictionary<EntityUid, PVSEntityVisiblity>>(
new DictPolicy<EntityUid, PVSEntityVisiblity>(), MaxVisPoolSize*TickBuffer);
new DictPolicy<EntityUid, PVSEntityVisiblity>(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<EntityUid>> _uidSetPool
= new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), MaxVisPoolSize);
@@ -97,10 +97,14 @@ internal sealed partial class PVSSystem : EntitySystem
private readonly List<(uint, IChunkIndexLocation)> _chunkList = new(64);
private readonly List<MapGrid> _gridsPool = new(8);
private ISawmill _sawmill = default!;
public override void Initialize()
{
base.Initialize();
_sawmill = Logger.GetSawmill("PVS");
_entityPvsCollection = RegisterPVSCollection<EntityUid>();
SubscribeLocalEvent<MapChangedEvent>(ev =>
@@ -123,6 +127,9 @@ internal sealed partial class PVSSystem : EntitySystem
_configManager.OnValueChanged(CVars.NetPVS, SetPvs, true);
_configManager.OnValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
_serverGameStateManager.ClientAck += OnClientAck;
_serverGameStateManager.ClientRequestFull += OnClientRequestFull;
InitializeDirty();
}
@@ -158,9 +165,76 @@ internal sealed partial class PVSSystem : EntitySystem
_configManager.UnsubValueChanged(CVars.NetPVS, SetPvs);
_configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged);
_serverGameStateManager.ClientAck -= OnClientAck;
_serverGameStateManager.ClientRequestFull -= OnClientRequestFull;
ShutdownDirty();
}
private void OnClientRequestFull(ICommonSession session, GameTick tick, GameTick lastAcked)
{
if (!_playerVisibleSets.TryGetValue(session, out var sessionData))
return;
// TODO rate limit this?
_sawmill.Warning($"Client {session} requested full state on tick {tick}. Last Acked: {lastAcked}. They probably encountered a PVS / missing meta-data exception.");
sessionData.LastSeenAt.Clear();
if (sessionData.Overflow != null)
{
_visSetPool.Return(sessionData.Overflow.Value.SentEnts);
sessionData.Overflow = null;
}
// return last acked to pool, but only if it is not still in the OverflowDictionary.
if (sessionData.LastAcked != null && _gameTiming.CurTick.Value - lastAcked.Value > TickBuffer)
_visSetPool.Return(sessionData.LastAcked);
sessionData.LastAcked = null;
sessionData.RequestedFull = true;
}
private void OnClientAck(ICommonSession session, GameTick ackedTick, GameTick lastAckedTick)
{
if (!_playerVisibleSets.TryGetValue(session, out var sessionData))
return;
if (sessionData.Overflow != null && sessionData.Overflow.Value.Tick < ackedTick)
{
var (overflowTick, overflowEnts) = sessionData.Overflow.Value;
sessionData.Overflow = null;
if (overflowTick == ackedTick)
{
ProcessAckedTick(sessionData, overflowEnts, ackedTick, lastAckedTick);
return;
}
// Even though the acked tick is newer, we have no guarantee that the client received the cached set, so
// we just discard it.
_visSetPool.Return(overflowEnts);
}
if (sessionData.SentEntities.TryGetValue(ackedTick, out var ackedData))
ProcessAckedTick(sessionData, ackedData, ackedTick, lastAckedTick);
}
private void ProcessAckedTick(SessionPVSData sessionData, Dictionary<EntityUid, PVSEntityVisiblity> ackedData, GameTick tick, GameTick lastAckedTick)
{
// return last acked to pool, but only if it is not still in the OverflowDictionary.
if (sessionData.LastAcked != null && _gameTiming.CurTick.Value - lastAckedTick.Value > TickBuffer)
_visSetPool.Return(sessionData.LastAcked);
sessionData.LastAcked = ackedData;
foreach (var ent in ackedData.Keys)
{
sessionData.LastSeenAt[ent] = tick;
}
// The client acked a tick. If they requested a full state, this ack happened some time after that, so we can safely set this to false
sessionData.RequestedFull = false;
}
private void OnViewsizeChanged(float obj)
{
_viewSize = obj * 2;
@@ -171,7 +245,6 @@ internal sealed partial class PVSSystem : EntitySystem
CullingEnabled = value;
}
public void ProcessCollections()
{
foreach (var collection in _pvsCollections)
@@ -228,6 +301,15 @@ internal sealed partial class PVSSystem : EntitySystem
private void OnEntityDeleted(EntityUid e)
{
_entityPvsCollection.RemoveIndex(EntityManager.CurrentTick, e);
var previousTick = _gameTiming.CurTick - 1;
foreach (var sessionData in _playerVisibleSets.Values)
{
sessionData.LastSeenAt.Remove(e);
if (sessionData.SentEntities.TryGetValue(previousTick, out var ents))
ents.Remove(e);
}
}
private void OnEntityMove(ref MoveEvent ev)
@@ -269,25 +351,39 @@ internal sealed partial class PVSSystem : EntitySystem
{
if (e.NewStatus == SessionStatus.InGame)
{
_playerVisibleSets.Add(e.Session, new OverflowDictionary<GameTick, Dictionary<EntityUid, PVSEntityVisiblity>>(TickBuffer, _visSetPool.Return));
_playerVisibleSets.Add(e.Session, new());
foreach (var pvsCollection in _pvsCollections)
{
pvsCollection.AddPlayer(e.Session);
}
return;
}
else if (e.NewStatus == SessionStatus.Disconnected)
if (e.NewStatus != SessionStatus.Disconnected)
return;
foreach (var pvsCollection in _pvsCollections)
{
var overflowDict = _playerVisibleSets[e.Session];
_playerVisibleSets.Remove(e.Session);
foreach (var (_, playerVisSet) in overflowDict)
{
_visSetPool.Return(playerVisSet);
}
foreach (var pvsCollection in _pvsCollections)
{
pvsCollection.RemovePlayer(e.Session);
}
pvsCollection.RemovePlayer(e.Session);
}
if (!_playerVisibleSets.Remove(e.Session, out var data))
return;
if (data.Overflow != null)
_visSetPool.Return(data.Overflow.Value.SentEnts);
data.Overflow = null;
if (data.LastAcked != null)
_visSetPool.Return(data.LastAcked);
foreach (var (_, visSet) in data.SentEntities)
{
if (visSet != data.LastAcked)
_visSetPool.Return(visSet);
}
data.LastAcked = null;
}
private void OnGridRemoved(GridRemovalEvent ev)
@@ -566,7 +662,7 @@ internal sealed partial class PVSSystem : EntitySystem
return true;
}
public (List<EntityState>? updates, List<EntityUid>? deletions) CalculateEntityStates(IPlayerSession session,
public (List<EntityState>? updates, List<EntityUid>? deletions, List<EntityUid>? leftPvs, GameTick fromTick) CalculateEntityStates(IPlayerSession session,
GameTick fromTick, GameTick toTick,
(Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[] chunkCache,
HashSet<int> chunkIndices, EntityQuery<MetaDataComponent> mQuery, EntityQuery<TransformComponent> tQuery,
@@ -575,8 +671,13 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(session.Status == SessionStatus.InGame);
var enteredEntityBudget = _netConfigManager.GetClientCVar(session.ConnectedClient, CVars.NetPVSEntityBudget);
var entitiesSent = 0;
_playerVisibleSets[session].TryGetValue(fromTick, out var playerVisibleSet);
var sessionData = _playerVisibleSets[session];
sessionData.SentEntities.TryGetValue(toTick - 1, out var lastSent);
var lastAcked = sessionData.LastAcked;
var lastSeen = sessionData.LastSeenAt;
var visibleEnts = _visSetPool.Get();
DebugTools.Assert(visibleEnts.Count == 0);
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
foreach (var i in chunkIndices)
@@ -585,7 +686,7 @@ internal sealed partial class PVSSystem : EntitySystem
if(!cache.HasValue) continue;
foreach (var rootNode in cache.Value.tree.RootNodes)
{
RecursivelyAddTreeNode(in rootNode, cache.Value.tree, playerVisibleSet, visibleEnts, fromTick,
RecursivelyAddTreeNode(in rootNode, cache.Value.tree, lastAcked, lastSent, visibleEnts, fromTick,
ref entitiesSent, cache.Value.metadata, in enteredEntityBudget);
}
}
@@ -594,7 +695,7 @@ internal sealed partial class PVSSystem : EntitySystem
while (globalEnumerator.MoveNext())
{
var uid = globalEnumerator.Current;
RecursivelyAddOverride(in uid, playerVisibleSet, visibleEnts, fromTick,
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
globalEnumerator.Dispose();
@@ -603,14 +704,14 @@ internal sealed partial class PVSSystem : EntitySystem
while (localEnumerator.MoveNext())
{
var uid = localEnumerator.Current;
RecursivelyAddOverride(in uid, playerVisibleSet, visibleEnts, fromTick,
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
localEnumerator.Dispose();
foreach (var viewerEntity in viewerEntities)
{
RecursivelyAddOverride(in viewerEntity, playerVisibleSet, visibleEnts, fromTick,
RecursivelyAddOverride(in viewerEntity, lastAcked, lastSent, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
@@ -618,56 +719,92 @@ internal sealed partial class PVSSystem : EntitySystem
RaiseLocalEvent(ref expandEvent);
foreach (var entityUid in expandEvent.Entities)
{
RecursivelyAddOverride(in entityUid, playerVisibleSet, visibleEnts, fromTick,
RecursivelyAddOverride(in entityUid, lastAcked, lastSent, visibleEnts, fromTick,
ref entitiesSent, mQuery, tQuery, in enteredEntityBudget);
}
var entityStates = new List<EntityState>();
foreach (var (entityUid, visiblity) in visibleEnts)
foreach (var (uid, visiblity) in visibleEnts)
{
if (sessionData.RequestedFull)
{
entityStates.Add(GetFullEntityState(session, uid, mQuery.GetComponent(uid)));
continue;
}
if (visiblity == PVSEntityVisiblity.StayedUnchanged)
continue;
var @new = visiblity == PVSEntityVisiblity.Entered;
var state = GetEntityState(session, entityUid, @new ? GameTick.Zero : fromTick, mQuery.GetComponent(entityUid).Flags);
var entered = visiblity == PVSEntityVisiblity.Entered;
var entFromTick = entered ? lastSeen.GetValueOrDefault(uid) : fromTick;
var state = GetEntityState(session, uid, entFromTick, mQuery.GetComponent(uid));
//this entity is not new & nothing changed
if(!@new && state.Empty) continue;
entityStates.Add(state);
if (entered || !state.Empty)
entityStates.Add(state);
}
if(playerVisibleSet != null)
// tell a client to detach entities that have left their view
var leftView = ProcessLeavePVS(visibleEnts, lastSent);
if (sessionData.SentEntities.Add(toTick, visibleEnts, out var oldEntry))
{
foreach (var (entityUid, _) in playerVisibleSet)
if (oldEntry.Value.Key > fromTick && sessionData.Overflow == null)
{
// it was deleted, so we dont need to exit pvs
if (deletions.Contains(entityUid)) continue;
// The clients last ack is too late, the overflow dictionary size has been exceeded, and we will no
// longer have information about the sent entities. This means we would no longer be able to add
// entities to _ackedEnts.
//
// If the client has enough latency, this result in a situation where we must constantly assume that every entity
// that needs to get sent to the client is being received by them for the first time.
//
// In order to avoid this, while also keeping the overflow dictionary limited in size, we keep a single
// overflow state, so we can at least periodically update the acked entities.
//TODO: HACK: somehow an entity left the view, transform does not exist (deleted?), but was not in the
// deleted list. This seems to happen with the map entity on round restart.
if (!EntityManager.EntityExists(entityUid))
continue;
// This is pretty shit and there is probably a better way of doing this.
sessionData.Overflow = oldEntry.Value;
entityStates.Add(new EntityState(entityUid, new NetListAsArray<ComponentChange>(new[]
{
ComponentChange.Changed(_stateManager.TransformNetId, _transformCullState),
}), true));
#if !FULL_RELEASE
// This happens relatively frequently for the current TickBuffer value, and doesn't really provide any
// useful info when not debugging/testing locally. Hence disabled on FULL_RELEASE.
_sawmill.Warning($"Client {session} exceeded tick buffer.");
#endif
}
else if (oldEntry.Value.Value != lastAcked)
_visSetPool.Return(oldEntry.Value.Value);
}
_playerVisibleSets[session].Add(toTick, visibleEnts);
if (deletions.Count == 0) deletions = default;
if (entityStates.Count == 0) entityStates = default;
return (entityStates, deletions);
return (entityStates, deletions, leftView, sessionData.RequestedFull ? GameTick.Zero : fromTick);
}
/// <summary>
/// Figure out what entities are no longer visible to the client. These entities are sent reliably to the client
/// in a separate net message.
/// </summary>
private List<EntityUid>? ProcessLeavePVS(
Dictionary<EntityUid, PVSEntityVisiblity> visibleEnts,
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent)
{
if (lastSent == null)
return null;
var leftView = new List<EntityUid>();
foreach (var uid in lastSent.Keys)
{
if (!visibleEnts.ContainsKey(uid))
leftView.Add(uid);
}
return leftView.Count > 0 ? leftView : null;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void RecursivelyAddTreeNode(in EntityUid nodeIndex,
private bool RecursivelyAddTreeNode(in EntityUid nodeIndex,
RobustTree<EntityUid> tree,
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
Dictionary<EntityUid, PVSEntityVisiblity>? lastAcked,
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent,
Dictionary<EntityUid, PVSEntityVisiblity> toSend,
GameTick fromTick,
ref int totalEnteredEntities,
@@ -683,12 +820,12 @@ internal sealed partial class PVSSystem : EntitySystem
if (nodeIndex.IsValid() && !toSend.ContainsKey(nodeIndex))
{
//are we new?
var (entered, budgetFail) = ProcessEntry(in nodeIndex, previousVisibleEnts,
var (entered, budgetFull) = ProcessEntry(in nodeIndex, lastAcked, lastSent,
ref totalEnteredEntities, in enteredEntityBudget);
if (budgetFail) return;
AddToSendSet(in nodeIndex, metaDataCache[nodeIndex], toSend, fromTick, entered);
if (budgetFull) return true;
}
var node = tree[nodeIndex];
@@ -697,15 +834,19 @@ internal sealed partial class PVSSystem : EntitySystem
{
foreach (var child in node.Children)
{
RecursivelyAddTreeNode(in child, tree, previousVisibleEnts, toSend, fromTick,
ref totalEnteredEntities, metaDataCache, in enteredEntityBudget);
if (RecursivelyAddTreeNode(in child, tree, lastAcked, lastSent, toSend, fromTick,
ref totalEnteredEntities, metaDataCache, in enteredEntityBudget))
return true;
}
}
return false;
}
public bool RecursivelyAddOverride(
in EntityUid uid,
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
Dictionary<EntityUid, PVSEntityVisiblity>? lastAcked,
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent,
Dictionary<EntityUid, PVSEntityVisiblity> toSend,
GameTick fromTick,
ref int totalEnteredEntities,
@@ -721,29 +862,40 @@ internal sealed partial class PVSSystem : EntitySystem
if (toSend.ContainsKey(uid)) return true;
var parent = transQuery.GetComponent(uid).ParentUid;
if (parent.IsValid() && !RecursivelyAddOverride(in parent, previousVisibleEnts, toSend, fromTick,
if (parent.IsValid() && !RecursivelyAddOverride(in parent, lastAcked, lastSent, toSend, fromTick,
ref totalEnteredEntities, metaQuery, transQuery, in enteredEntityBudget))
return false;
var (entered, _) = ProcessEntry(in uid, previousVisibleEnts,
var (entered, _) = ProcessEntry(in uid, lastAcked, lastSent,
ref totalEnteredEntities, in enteredEntityBudget);
AddToSendSet(in uid, metaQuery.GetComponent(uid), toSend, fromTick, entered);
return true;
}
private (bool entered, bool budgetFail) ProcessEntry(in EntityUid uid,
Dictionary<EntityUid, PVSEntityVisiblity>? previousVisibleEnts,
private (bool entering, bool budgetFull) ProcessEntry(in EntityUid uid,
Dictionary<EntityUid, PVSEntityVisiblity>? lastAcked,
Dictionary<EntityUid, PVSEntityVisiblity>? lastSent,
ref int totalEnteredEntities, in int enteredEntityBudget)
{
var entered = previousVisibleEnts?.Remove(uid) == false;
var enteredSinceLastSent = lastSent == null || !lastSent.ContainsKey(uid);
if (entered)
var entered = enteredSinceLastSent || // OR, entered since last ack:
lastAcked == null || !lastAcked.ContainsKey(uid);
// If the entity is entering, but we already sent this entering entity, in the last message, we won't add it to
// the budget. Chances are the packet will arrive in a nice and orderly fashion, and the client will stick to
// their requested budget. However this can cause issues if a packet gets dropped, because a player may create
// 2x or more times the normal entity creation budget.
//
// The fix for that would be to just also give the PVS budget a client-side aspect that controls entity creation
// rate.
if (enteredSinceLastSent)
{
if (totalEnteredEntities >= enteredEntityBudget)
// TODO: should we separate this budget into "entered-but-seen" and "completely-new"?
// completely new entities are significantly more intensive for both server sending and client processing.
if (totalEnteredEntities++ >= enteredEntityBudget)
return (entered, true);
totalEnteredEntities++;
}
return (entered, false);
@@ -757,7 +909,7 @@ internal sealed partial class PVSSystem : EntitySystem
return;
}
if (metaDataComponent.EntityLastModifiedTick < fromTick)
if (metaDataComponent.EntityLastModifiedTick <= fromTick)
{
//entity has been sent before and hasnt been updated since
toSend.Add(uid, PVSEntityVisiblity.StayedUnchanged);
@@ -771,7 +923,7 @@ internal sealed partial class PVSSystem : EntitySystem
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
public (List<EntityState>? updates, List<EntityUid>? deletions) GetAllEntityStates(ICommonSession player, GameTick fromTick, GameTick toTick)
public (List<EntityState>?, List<EntityUid>?, List<EntityUid>?, GameTick fromTick) GetAllEntityStates(ICommonSession player, GameTick fromTick, GameTick toTick)
{
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
// no point sending an empty collection
@@ -791,10 +943,10 @@ internal sealed partial class PVSSystem : EntitySystem
foreach (var md in EntityManager.EntityQuery<MetaDataComponent>(true))
{
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
stateEntities.Add(GetEntityState(player, md.Owner, GameTick.Zero, md.Flags));
stateEntities.Add(GetEntityState(player, md.Owner, GameTick.Zero, md));
}
return (stateEntities.Count == 0 ? default : stateEntities, deletions);
return (stateEntities.Count == 0 ? default : stateEntities, deletions, null, fromTick);
}
// Just get the relevant entities that have been dirtied
@@ -819,8 +971,8 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, uid, GameTick.Zero, md.Flags));
if (md.EntityLastModifiedTick > fromTick)
stateEntities.Add(GetEntityState(player, uid, GameTick.Zero, md));
}
foreach (var uid in dirty)
@@ -832,8 +984,8 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, uid, fromTick, md.Flags));
if (md.EntityLastModifiedTick > fromTick)
stateEntities.Add(GetEntityState(player, uid, fromTick, md));
}
}
}
@@ -842,7 +994,7 @@ internal sealed partial class PVSSystem : EntitySystem
{
if (stateEntities.Count == 0) stateEntities = default;
return (stateEntities, deletions);
return (stateEntities, deletions, null, fromTick);
}
stateEntities = new List<EntityState>(EntityManager.EntityCount);
@@ -853,13 +1005,13 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, md.Owner, fromTick, md.Flags));
stateEntities.Add(GetEntityState(player, md.Owner, fromTick, md));
}
// no point sending an empty collection
if (stateEntities.Count == 0) stateEntities = default;
return (stateEntities, deletions);
return (stateEntities, deletions, null, fromTick);
}
/// <summary>
@@ -868,20 +1020,27 @@ internal sealed partial class PVSSystem : EntitySystem
/// <param name="player">The player to generate this state for.</param>
/// <param name="entityUid">Uid of the entity to generate the state from.</param>
/// <param name="fromTick">Only provide delta changes from this tick.</param>
/// <param name="flags">Any applicable metadata flags</param>
/// <param name="meta">The entity's metadata component</param>
/// <param name="includeImplicit">If true, the state will include even the implicit component data</param>
/// <returns>New entity State for the given entity.</returns>
private EntityState GetEntityState(ICommonSession player, EntityUid entityUid, GameTick fromTick, MetaDataFlags flags)
private EntityState GetEntityState(ICommonSession player, EntityUid entityUid, GameTick fromTick, MetaDataComponent meta)
{
var bus = EntityManager.EventBus;
var changed = new List<ComponentChange>();
// Whether this entity has any component states that are only for a specific session.
// TODO: This GetComp is probably expensive, less expensive than before, but ideally we'd cache it somewhere or something from a previous getcomp
// Probably still needs tweaking but checking for add / changed states up front should do most of the work.
var specificStates = (flags & MetaDataFlags.EntitySpecific) == MetaDataFlags.EntitySpecific;
// Whether this entity has any component states that should only be sent to specific sessions.
var entitySpecific = (meta.Flags & MetaDataFlags.EntitySpecific) == MetaDataFlags.EntitySpecific;
foreach (var (netId, component) in EntityManager.GetNetComponents(entityUid))
{
DebugTools.Assert(component.Initialized);
if (!component.NetSyncEnabled)
continue;
if (component.Deleted || !component.Initialized)
{
_sawmill.Error("Entity manager returned deleted or uninitialized components while sending entity data");
continue;
}
// NOTE: When LastModifiedTick or CreationTick are 0 it means that the relevant data is
// "not different from entity creation".
@@ -892,37 +1051,20 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(component.LastModifiedTick >= component.CreationTick);
var addState = false;
var changeState = false;
var addState = component.CreationTick != GameTick.Zero && component.CreationTick > fromTick;
var changedState = component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick > fromTick;
// We'll check the properties first; if we ever have specific states then doing the struct event is expensive.
if (component.CreationTick != GameTick.Zero && component.CreationTick >= fromTick && !component.Deleted)
addState = true;
else if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick >= fromTick)
changeState = true;
if (!addState && !changeState)
if (!(addState || changedState))
continue;
if (specificStates && !EntityManager.CanGetComponentState(bus, component, player))
if (component.SendOnlyToOwner && player.AttachedEntity != component.Owner)
continue;
if (addState)
{
ComponentState? state = null;
if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero &&
component.LastModifiedTick >= fromTick)
state = EntityManager.GetComponentState(bus, component);
if (entitySpecific && !EntityManager.CanGetComponentState(bus, component, player))
continue;
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChange.Added(netId, state));
}
else
{
DebugTools.Assert(changeState);
changed.Add(ComponentChange.Changed(netId, EntityManager.GetComponentState(bus, component)));
}
var state = changedState ? EntityManager.GetComponentState(bus, component) : null;
changed.Add(ComponentChange.Added(netId, state, component.LastModifiedTick));
}
foreach (var netId in _serverEntManager.GetDeletedComponents(entityUid, fromTick))
@@ -930,7 +1072,38 @@ internal sealed partial class PVSSystem : EntitySystem
changed.Add(ComponentChange.Removed(netId));
}
return new EntityState(entityUid, changed.ToArray());
return new EntityState(entityUid, changed.ToArray(), meta.EntityLastModifiedTick);
}
/// <summary>
/// Variant of <see cref="GetEntityState"/> that includes all entity data, including data that can be inferred implicitly from the entity prototype.
/// </summary>
private EntityState GetFullEntityState(ICommonSession player, EntityUid entityUid, MetaDataComponent meta)
{
var bus = EntityManager.EventBus;
var changed = new List<ComponentChange>();
var entitySpecific = (meta.Flags & MetaDataFlags.EntitySpecific) == MetaDataFlags.EntitySpecific;
foreach (var (netId, component) in EntityManager.GetNetComponents(entityUid))
{
if (!component.NetSyncEnabled)
continue;
if (component.SendOnlyToOwner && player.AttachedEntity != component.Owner)
continue;
if (entitySpecific && !EntityManager.CanGetComponentState(bus, component, player))
continue;
changed.Add(ComponentChange.Added(netId, EntityManager.GetComponentState(bus, component), component.LastModifiedTick));
}
foreach (var netId in _serverEntManager.GetDeletedComponents(entityUid, GameTick.Zero))
{
changed.Add(ComponentChange.Removed(netId));
}
return new EntityState(entityUid, changed.ToArray(), meta.EntityLastModifiedTick);
}
private EntityUid[] GetSessionViewers(ICommonSession session)
@@ -1030,6 +1203,39 @@ internal sealed partial class PVSSystem : EntitySystem
return true;
}
}
/// <summary>
/// Session data class used to avoid having to lock session dictionaries.
/// </summary>
private sealed class SessionPVSData
{
/// <summary>
/// All <see cref="EntityUid"/>s that this session saw during the last <see cref="TickBuffer"/> ticks.
/// </summary>
public readonly OverflowDictionary<GameTick, Dictionary<EntityUid, PVSEntityVisiblity>> SentEntities = new(TickBuffer);
/// <summary>
/// The most recently acked entities
/// </summary>
public Dictionary<EntityUid, PVSEntityVisiblity>? LastAcked = new();
/// <summary>
/// Stores the last tick at which a given entity was acked by a player. Used to avoid re-sending the whole entity
/// state when an item re-enters PVS.
/// </summary>
public readonly Dictionary<EntityUid, GameTick> LastSeenAt = new();
/// <summary>
/// <see cref="_sentData"/> overflow in case a player's last ack is more than <see cref="TickBuffer"/> ticks behind the current tick.
/// </summary>
public (GameTick Tick, Dictionary<EntityUid, PVSEntityVisiblity> SentEnts)? Overflow;
/// <summary>
/// If true, the client has explicitly requested a full state. Unlike the first state, we will send them
/// all data, not just data that cannot be implicitly inferred from entity prototypes.
/// </summary>
public bool RequestedFull = false;
}
}
[ByRefEvent]

View File

@@ -22,6 +22,7 @@ using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SharpZstd.Interop;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Players;
namespace Robust.Server.GameStates
{
@@ -51,6 +52,9 @@ namespace Robust.Server.GameStates
public ushort TransformNetId { get; set; }
public Action<ICommonSession, GameTick, GameTick>? ClientAck { get; set; }
public Action<ICommonSession, GameTick, GameTick>? ClientRequestFull { get; set; }
public void PostInject()
{
_logger = Logger.GetSawmill("PVS");
@@ -60,7 +64,9 @@ namespace Robust.Server.GameStates
public void Initialize()
{
_networkManager.RegisterNetMessage<MsgState>();
_networkManager.RegisterNetMessage<MsgStateLeavePvs>();
_networkManager.RegisterNetMessage<MsgStateAck>(HandleStateAck);
_networkManager.RegisterNetMessage<MsgStateRequestFull>(HandleFullStateRequest);
_networkManager.Connected += HandleClientConnected;
_networkManager.Disconnect += HandleClientDisconnect;
@@ -118,10 +124,7 @@ namespace Robust.Server.GameStates
private void HandleClientConnected(object? sender, NetChannelArgs e)
{
if (!_ackedStates.ContainsKey(e.Channel.ConnectionId))
_ackedStates.Add(e.Channel.ConnectionId, GameTick.Zero);
else
_ackedStates[e.Channel.ConnectionId] = GameTick.Zero;
_ackedStates[e.Channel.ConnectionId] = GameTick.Zero;
}
private void HandleClientDisconnect(object? sender, NetChannelArgs e)
@@ -129,38 +132,36 @@ namespace Robust.Server.GameStates
_ackedStates.Remove(e.Channel.ConnectionId);
}
private void HandleStateAck(MsgStateAck msg)
private void HandleFullStateRequest(MsgStateRequestFull msg)
{
Ack(msg.MsgChannel.ConnectionId, msg.Sequence);
if (!_playerManager.TryGetSessionById(msg.MsgChannel.UserId, out var session) ||
!_ackedStates.TryGetValue(msg.MsgChannel.ConnectionId, out var lastAcked))
return;
ClientRequestFull?.Invoke(session, msg.Tick, lastAcked);
// Update acked tick so that OnClientAck doesn't get invoked by any late acks.
_ackedStates[msg.MsgChannel.ConnectionId] = _gameTiming.CurTick;
}
private void Ack(long uniqueIdentifier, GameTick stateAcked)
private void HandleStateAck(MsgStateAck msg)
{
DebugTools.Assert(_networkManager.IsServer);
if (_playerManager.TryGetSessionById(msg.MsgChannel.UserId, out var session))
Ack(msg.MsgChannel.ConnectionId, msg.Sequence, session);
}
if (_ackedStates.TryGetValue(uniqueIdentifier, out var lastAck))
{
if (stateAcked > lastAck) // most of the time this is true
{
_ackedStates[uniqueIdentifier] = stateAcked;
}
else if (stateAcked == GameTick.Zero) // client signaled they need a full state
{
//Performance/Abuse: Should this be rate limited?
_ackedStates[uniqueIdentifier] = GameTick.Zero;
}
private void Ack(long uniqueIdentifier, GameTick stateAcked, IPlayerSession playerSession)
{
if (!_ackedStates.TryGetValue(uniqueIdentifier, out var lastAck) || stateAcked <= lastAck)
return;
//else stateAcked was out of order or client is being silly, just ignore
}
else
DebugTools.Assert("How did the client send us an ack without being connected?");
ClientAck?.Invoke(playerSession, stateAcked, lastAck);
_ackedStates[uniqueIdentifier] = stateAcked;
}
/// <inheritdoc />
public void SendGameStateUpdate()
{
DebugTools.Assert(_networkManager.IsServer);
if (!_networkManager.IsConnected)
{
// Prevent deletions piling up if we have no clients.
@@ -257,7 +258,7 @@ namespace Robust.Server.GameStates
DebugTools.Assert("Why does this channel not have an entry?");
}
var (entStates, deletions) = _pvs.CullingEnabled
var (entStates, deletions, leftPvs, fromTick) = _pvs.CullingEnabled
? _pvs.CalculateEntityStates(session, lastAck, _gameTiming.CurTick, chunkCache,
playerChunks[sessionIndex], metadataQuery, transformQuery, viewerEntities[sessionIndex])
: _pvs.GetAllEntityStates(session, lastAck, _gameTiming.CurTick);
@@ -267,7 +268,7 @@ namespace Robust.Server.GameStates
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
var lastInputCommand = inputSystem.GetLastInputCommand(session);
var lastSystemMessage = _entityNetworkManager.GetLastMessageSequence(session);
var state = new GameState(lastAck, _gameTiming.CurTick, Math.Max(lastInputCommand, lastSystemMessage),
var state = new GameState(fromTick, _gameTiming.CurTick, Math.Max(lastInputCommand, lastSystemMessage),
entStates, playerStates, deletions, mapData);
InterlockedHelper.Min(ref oldestAckValue, lastAck.Value);
@@ -277,6 +278,8 @@ namespace Robust.Server.GameStates
stateUpdateMessage.State = state;
stateUpdateMessage.CompressionContext = resources.CompressionContext;
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
// If the state is too big we let Lidgren send it reliably.
// This is to avoid a situation where a state is so large that it consistently gets dropped
// (or, well, part of it).
@@ -286,11 +289,15 @@ namespace Robust.Server.GameStates
// TODO: remove this lock by having a single state object per session that contains all per-session state needed.
lock (_ackedStates)
{
_ackedStates[channel.ConnectionId] = _gameTiming.CurTick;
Ack(channel.ConnectionId, _gameTiming.CurTick, session);
}
}
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
// separately, we send PVS detach / left-view messages reliably. This is not resistant to packet loss,
// but unlike game state it doesn't really matter. This also significantly reduces the size of game
// state messages PVS chunks move out of view.
if (leftPvs != null && leftPvs.Count > 0)
_networkManager.ServerSendMessage(new MsgStateLeavePvs() { Entities = leftPvs, Tick = _gameTiming.CurTick }, channel);
}
if (_pvs.CullingEnabled)

View File

@@ -8,9 +8,9 @@ namespace Robust.Server.Maps
{
public interface IMapLoader
{
(IReadOnlyList<EntityUid> entities, EntityUid? gridId) LoadBlueprint(MapId mapId, string path);
(IReadOnlyList<EntityUid> entities, EntityUid? gridId) LoadBlueprint(MapId mapId, string path, MapLoadOptions options);
void SaveBlueprint(EntityUid gridId, string yamlPath);
(IReadOnlyList<EntityUid> entities, EntityUid? gridId) LoadGrid(MapId mapId, string path);
(IReadOnlyList<EntityUid> entities, EntityUid? gridId) LoadGrid(MapId mapId, string path, MapLoadOptions options);
void SaveGrid(EntityUid gridId, string yamlPath);
(IReadOnlyList<EntityUid> entities, IReadOnlyList<EntityUid> gridIds) LoadMap(MapId mapId, string path);
(IReadOnlyList<EntityUid> entities, IReadOnlyList<EntityUid> gridIds) LoadMap(MapId mapId, string path, MapLoadOptions options);

View File

@@ -44,5 +44,11 @@ namespace Robust.Server.Maps
private Angle _rotation = Angle.Zero;
public Matrix3 TransformMatrix { get; set; } = Matrix3.Identity;
/// <summary>
/// If there is a map entity serialized should we also load it.
/// This should be set to false if you want to load a map file onto an existing map.
/// </summary>
public bool LoadMap { get; set; } = true;
}
}

View File

@@ -51,7 +51,7 @@ namespace Robust.Server.Maps
public event Action<YamlStream, string>? LoadedMapData;
/// <inheritdoc />
public void SaveBlueprint(EntityUid gridId, string yamlPath)
public void SaveGrid(EntityUid gridId, string yamlPath)
{
var grid = _mapManager.GetGrid(gridId);
@@ -71,9 +71,9 @@ namespace Robust.Server.Maps
}
/// <inheritdoc />
public (IReadOnlyList<EntityUid> entities, EntityUid? gridId) LoadBlueprint(MapId mapId, string path)
public (IReadOnlyList<EntityUid> entities, EntityUid? gridId) LoadGrid(MapId mapId, string path)
{
return LoadBlueprint(mapId, path, DefaultLoadOptions);
return LoadGrid(mapId, path, DefaultLoadOptions);
}
private ResourcePath Rooted(string path)
@@ -81,8 +81,13 @@ namespace Robust.Server.Maps
return new ResourcePath(path).ToRootedPath();
}
public (IReadOnlyList<EntityUid> entities, EntityUid? gridId) LoadBlueprint(MapId mapId, string path, MapLoadOptions options)
public (IReadOnlyList<EntityUid> entities, EntityUid? gridId) LoadGrid(MapId mapId, string path, MapLoadOptions options)
{
DebugTools.Assert(_mapManager.MapExists(mapId));
var oldLoadMapOpt = options.LoadMap; // lets not mutate the default options
options.LoadMap = false;
var resPath = Rooted(path);
if (!TryGetReader(resPath, out var reader)) return (Array.Empty<EntityUid>(), null);
@@ -104,11 +109,13 @@ namespace Robust.Server.Maps
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager,
_prototypeManager, _serializationManager, _componentFactory, data.RootNode.ToDataNodeCast<MappingDataNode>(), mapId, options);
context.LogErrorOnMap = true;
context.Deserialize();
grid = context.Grids.FirstOrDefault();
entities = context.Entities;
PostDeserialize(mapId, context);
options.LoadMap = oldLoadMapOpt;
}
return (entities, grid?.GridEntityId);
@@ -266,6 +273,11 @@ namespace Robust.Server.Maps
private readonly List<(EntityUid, MappingDataNode)> _entitiesToDeserialize
= new();
/// <summary>
/// If true, this will log an error when encountering a map entity. E.g., when using the loadgrid command to load a map file.
/// </summary>
public bool LogErrorOnMap = false;
private bool IsBlueprintMode => GridIDMap.Count == 1;
private readonly MappingDataNode RootNode;
@@ -625,7 +637,7 @@ namespace Robust.Server.Maps
foreach (var grid in Grids)
{
var transform = _xformQuery!.Value.GetComponent(grid.GridEntityId);
if (transform.ParentUid.IsValid())
if (transform.MapUid?.IsValid() == true)
continue;
var mapOffset = transform.LocalPosition;
@@ -768,10 +780,18 @@ namespace Robust.Server.Maps
_serverEntityManager.FinishEntityLoad(entity, metaQuery.GetComponent(entity).EntityPrototype, this);
if (mapQuery.HasComponent(entity))
if (!mapQuery.HasComponent(entity))
continue;
if (LogErrorOnMap)
Logger.ErrorS("map", "Found an additional map entity while loading a map/grid. Either you are using loadgrid to load a map file, or your map file contains more than one map entity.");
if ((_loadOptions?.LoadMap ?? true) && TargetMapUid == null)
{
DebugTools.Assert(TargetMapUid == null);
TargetMapUid = entity;
// error on any additional map entities.
LogErrorOnMap = true;
}
}
}
@@ -797,9 +817,25 @@ namespace Robust.Server.Maps
private void FinishEntitiesInitialization()
{
// Ideally MapLoader would just be topdown and I could just set the root to null instead
// then we'd have a nice clean init, but instead it's done per stage and we need to make sure it gets
// handled per stage.
var query = _serverEntityManager.GetEntityQuery<MetaDataComponent>();
foreach (var entity in Entities)
var mapQuery = _serverEntityManager.GetEntityQuery<MapComponent>();
for (var i = 0; i < Entities.Count; i++)
{
var entity = Entities[i];
// If we're loading a map but not 'loading the map' then kill it
if (TargetMapUid == null && mapQuery.HasComponent(entity))
{
_serverEntityManager.DeleteEntity(entity);
Entities.RemoveSwap(i);
i--;
continue;
}
_serverEntityManager.FinishEntityInitialization(entity, query.GetComponent(entity));
}
}
@@ -855,7 +891,10 @@ namespace Robust.Server.Maps
meta.Add("name", "DemoStation");
meta.Add("author", "Space-Wizards");
var isPostInit = false;
//TODO: MapId is null when saveBP is used, another reason this jumbled mess needs to be rewritten
var isPostInit = MapId is not null && _mapManager.IsMapInitialized(MapId.Value);
//TODO: This is a workaround to make SaveBP function
foreach (var grid in Grids)
{
if (_mapManager.IsMapInitialized(grid.ParentMapId))

View File

@@ -45,12 +45,16 @@ namespace Robust.Server.Physics
public override void Initialize()
{
base.Initialize();
_logger = Shared.Log.Logger.GetSawmill("gsplit");
_logger = Logger.GetSawmill("gsplit");
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoval);
SubscribeNetworkEvent<RequestGridNodesMessage>(OnDebugRequest);
SubscribeNetworkEvent<StopGridNodesMessage>(OnDebugStopRequest);
var configManager = IoCManager.Resolve<IConfigurationManager>();
#if !FULL_RELEASE
// It makes mapping painful
configManager.OverrideDefault(CVars.GridSplitting, false);
#endif
configManager.OnValueChanged(CVars.GridSplitting, SetSplitAllowed, true);
}

View File

@@ -131,27 +131,24 @@ namespace Robust.Server.Placement
private void PlaceNewTile(ushort tileType, EntityCoordinates coordinates)
{
var mapCoordinates = coordinates.ToMap(_entityManager);
if (!coordinates.IsValid(_entityManager)) return;
if (mapCoordinates.MapId == MapId.Nullspace) return;
IMapGrid? grid;
var gridCoordinate = coordinates.AlignWithClosestGridTile(entityManager: _entityManager, mapManager: _mapManager);
_mapManager.TryGetGrid(coordinates.EntityId, out grid);
if (!gridCoordinate.IsValid(_entityManager)) return;
if (grid == null)
_mapManager.TryFindGridAt(coordinates.ToMap(_entityManager), out grid);
var closest = _mapManager.IsGrid(gridCoordinate.EntityId);
if (closest) // stick to existing grid
if (grid != null) // stick to existing grid
{
if (!_mapManager.TryGetGrid(gridCoordinate.EntityId, out var grid)) return;
grid.SetTile(gridCoordinate, new Tile(tileType));
grid.SetTile(coordinates, new Tile(tileType));
}
else if (tileType != 0) // create a new grid
{
var newGrid = _mapManager.CreateGrid(mapCoordinates.MapId);
newGrid.WorldPosition = mapCoordinates.Position + (newGrid.TileSize / 2f); // assume bottom left tile origin
var tilePos = newGrid.WorldToTile(mapCoordinates.Position);
var newGrid = _mapManager.CreateGrid(coordinates.GetMapId(_entityManager));
newGrid.WorldPosition = coordinates.Position + (newGrid.TileSize / 2f); // assume bottom left tile origin
var tilePos = newGrid.WorldToTile(coordinates.Position);
newGrid.SetTile(tilePos, new Tile(tileType));
}
}

View File

@@ -70,13 +70,13 @@ namespace Robust.Shared
/// Whether to interpolate between server game states for render frames on the client.
/// </summary>
public static readonly CVarDef<bool> NetInterp =
CVarDef.Create("net.interp", true, CVar.ARCHIVE);
CVarDef.Create("net.interp", true, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// The target number of game states to keep buffered up to smooth out against network inconsistency.
/// The target number of game states to keep buffered up to smooth out network inconsistency.
/// </summary>
public static readonly CVarDef<int> NetInterpRatio =
CVarDef.Create("net.interp_ratio", 0, CVar.ARCHIVE);
public static readonly CVarDef<int> NetBufferSize =
CVarDef.Create("net.buffer_size", 0, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// Enable verbose game state/networking logging.
@@ -137,6 +137,12 @@ namespace Robust.Shared
public static readonly CVarDef<int> NetPVSEntityBudget =
CVarDef.Create("net.pvs_budget", 50, CVar.ARCHIVE | CVar.REPLICATED);
/// <summary>
/// The amount of pvs-exiting entities that a client will process in a single tick.
/// </summary>
public static readonly CVarDef<int> NetPVSEntityExitBudget =
CVarDef.Create("net.pvs_exit_budget", 75, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// ZSTD compression level to use when compressing game states.
/// </summary>

View File

@@ -1,9 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Utility;
namespace Robust.Shared.Collections;
@@ -76,6 +74,36 @@ public sealed class OverflowDictionary<TKey, TValue> : IDictionary<TKey, TValue>
}
}
/// <summary>
/// Variant of <see cref="Add(TKey, TValue)"/> that also returns any entry that was removed to make room for the new entry.
/// </summary>
public bool Add(TKey key, TValue value, [NotNullWhen(true)] out (TKey Key, TValue Value)? old)
{
if (_dict.ContainsKey(key))
throw new InvalidOperationException("Tried inserting duplicate key.");
if (Count == Capacity)
{
var startIndex = GetArrayStartIndex();
var entry = _insertionQueue[startIndex];
_dict.Remove(entry, out var oldValue);
Array.Clear(_insertionQueue, startIndex, 1);
_valueDisposer?.Invoke(oldValue!);
old = (entry, oldValue!);
}
else
old = null;
_dict.Add(key, value);
_insertionQueue[_currentIndex++] = key;
if (_currentIndex == Capacity)
{
_currentIndex = 0;
}
return old != null;
}
public bool Remove(TKey key)
{
//it doesnt make sense for my usecase so i left this unimplemented. i cba to bother with moving all the entries in the array around etc.

View File

@@ -139,7 +139,7 @@ namespace Robust.Shared.Configuration
/// <inheritdoc />
public void TickProcessMessages()
{
if(!_timing.InSimulation || _timing.InPrediction)
if (!_timing.InSimulation || _timing.InPrediction)
return;
// _netVarsMessages is not in any particular ordering.
@@ -150,7 +150,7 @@ namespace Robust.Shared.Configuration
{
var msg = _netVarsMessages[i];
if (msg.Tick > _timing.LastRealTick)
if (msg.Tick > _timing.CurTick)
continue;
toApply.Add(msg);
@@ -168,8 +168,8 @@ namespace Robust.Shared.Configuration
{
ApplyNetVarChange(msg.MsgChannel, msg.NetworkedVars, msg.Tick);
if(msg.Tick != default && msg.Tick < _timing.LastRealTick)
_sawmill.Warning($"{msg.MsgChannel}: Received late nwVar message ({msg.Tick} < {_timing.LastRealTick} ).");
if(msg.Tick != default && msg.Tick < _timing.CurTick)
_sawmill.Warning($"{msg.MsgChannel}: Received late nwVar message ({msg.Tick} < {_timing.CurTick} ).");
}
}

View File

@@ -61,6 +61,7 @@ namespace Robust.Shared.Containers
DebugTools.Assert(transform == null || transform.Owner == toinsert);
DebugTools.Assert(ownerTransform == null || ownerTransform.Owner == Owner);
DebugTools.Assert(meta == null || meta.Owner == toinsert);
DebugTools.Assert(!ExpectedEntities.Contains(toinsert));
IoCManager.Resolve(ref entMan);
//Verify we can insert into this container

View File

@@ -4,6 +4,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Robust.Shared.Containers
{
@@ -38,6 +39,8 @@ namespace Robust.Shared.Containers
/// <inheritdoc />
protected override void InternalInsert(EntityUid toinsert, EntityUid oldParent, IEntityManager entMan)
{
// Why TF is this even a list??????
DebugTools.Assert(!_containerList.Contains(toinsert));
_containerList.Add(toinsert);
base.InternalInsert(toinsert, oldParent, entMan);
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -69,115 +68,74 @@ namespace Robust.Shared.Containers
/// <summary>
/// Attempts to remove an entity from its container, if any.
/// <see cref="SharedContainerSystem.TryRemoveFromContainer"/>
/// </summary>
/// <param name="entity">Entity that might be inside a container.</param>
/// <param name="force">Whether to forcibly remove the entity from the container.</param>
/// <param name="wasInContainer">Whether the entity was actually inside a container or not.</param>
/// <returns>If the entity could be removed. Also returns false if it wasn't inside a container.</returns>
[Obsolete("Use SharedContainerSystem.TryRemoveFromContainer() instead")]
public static bool TryRemoveFromContainer(this EntityUid entity, bool force, out bool wasInContainer, IEntityManager? entMan = null)
{
IoCManager.Resolve(ref entMan);
DebugTools.Assert(entMan.EntityExists(entity));
if (TryGetContainer(entity, out var container, entMan))
{
wasInContainer = true;
if (!force)
return container.Remove(entity, entMan);
container.ForceRemove(entity, entMan);
return true;
}
wasInContainer = false;
return false;
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SharedContainerSystem>()
.TryRemoveFromContainer(entity, force, out wasInContainer);
}
/// <summary>
/// Attempts to remove an entity from its container, if any.
/// <see cref="SharedContainerSystem.TryRemoveFromContainer"/>
/// </summary>
/// <param name="entity">Entity that might be inside a container.</param>
/// <param name="force">Whether to forcibly remove the entity from the container.</param>
/// <returns>If the entity could be removed. Also returns false if it wasn't inside a container.</returns>
[Obsolete("Use SharedContainerSystem.TryRemoveFromContainer() instead")]
public static bool TryRemoveFromContainer(this EntityUid entity, bool force = false, IEntityManager? entMan = null)
{
return TryRemoveFromContainer(entity, force, out _, entMan);
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SharedContainerSystem>()
.TryRemoveFromContainer(entity, force);
}
/// <summary>
/// Attempts to remove all entities in a container.
/// <see cref="SharedContainerSystem.EmptyContainer"/>
/// </summary>
[Obsolete("Use SharedContainerSystem.EmptyContainer() instead")]
public static void EmptyContainer(this IContainer container, bool force = false, EntityCoordinates? moveTo = null,
bool attachToGridOrMap = false, IEntityManager? entMan = null)
{
IoCManager.Resolve(ref entMan);
foreach (var entity in container.ContainedEntities.ToArray())
{
if (entMan.Deleted(entity))
continue;
if (force)
container.ForceRemove(entity, entMan);
else
container.Remove(entity, entMan);
if (moveTo.HasValue)
entMan.GetComponent<TransformComponent>(entity).Coordinates = moveTo.Value;
if(attachToGridOrMap)
entMan.GetComponent<TransformComponent>(entity).AttachToGridOrMap();
}
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SharedContainerSystem>()
.EmptyContainer(container, force, moveTo, attachToGridOrMap);
}
/// <summary>
/// Attempts to remove and delete all entities in a container.
/// <see cref="SharedContainerSystem.CleanContainer"/>
/// </summary>
[Obsolete("Use SharedContainerSystem.CleanContainer() instead")]
public static void CleanContainer(this IContainer container, IEntityManager? entMan = null)
{
IoCManager.Resolve(ref entMan);
foreach (var ent in container.ContainedEntities.ToArray())
{
if (entMan.Deleted(ent)) continue;
container.ForceRemove(ent, entMan);
entMan.DeleteEntity(ent);
}
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SharedContainerSystem>()
.CleanContainer(container);
}
/// <summary>
/// <see cref="SharedContainerSystem.AttachParentToContainerOrGrid"/>
/// </summary>
[Obsolete("Use SharedContainerSystem.AttachParentToContainerOrGrid() instead")]
public static void AttachParentToContainerOrGrid(this TransformComponent transform, IEntityManager? entMan = null)
{
IoCManager.Resolve(ref entMan);
if (transform.Parent == null
|| !TryGetContainer(transform.Parent.Owner, out var container, entMan)
|| !TryInsertIntoContainer(transform, container, entMan))
transform.AttachToGridOrMap();
}
private static bool TryInsertIntoContainer(this TransformComponent transform, IContainer container, IEntityManager? entMan = null)
{
IoCManager.Resolve(ref entMan);
if (container.Insert(transform.Owner, entMan)) return true;
if (entMan.GetComponent<TransformComponent>(container.Owner).Parent != null
&& TryGetContainer(container.Owner, out var newContainer, entMan))
return TryInsertIntoContainer(transform, newContainer, entMan);
return false;
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SharedContainerSystem>()
.AttachParentToContainerOrGrid(transform);
}
/// <summary>
/// <see cref="SharedContainerSystem.TryGetManagerComp"/>
/// </summary>
[Obsolete("Use SharedContainerSystem.TryGetManagerComp() instead")]
private static bool TryGetManagerComp(this EntityUid entity, [NotNullWhen(true)] out IContainerManager? manager, IEntityManager? entMan = null)
{
IoCManager.Resolve(ref entMan);
DebugTools.Assert(entMan.EntityExists(entity));
if (entMan.TryGetComponent(entity, out manager))
return true;
// RECURSION ALERT
if (entMan.GetComponent<TransformComponent>(entity).Parent != null)
return TryGetManagerComp(entMan.GetComponent<TransformComponent>(entity).ParentUid, out manager, entMan);
return false;
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SharedContainerSystem>()
.TryGetManagerComp(entity, out manager);
}
/// <summary>

View File

@@ -29,10 +29,14 @@ namespace Robust.Shared.Containers
void ISerializationHooks.AfterDeserialization()
{
foreach (var (_, container) in Containers)
// TODO remove ISerializationHooks I guess the IDs can be set by a custom serializer for the dictionary? But
// the component??? Maybe other systems need to stop assuming that containers have been initialized during
// their own init.
foreach (var (id, container) in Containers)
{
var baseContainer = (BaseContainer) container;
baseContainer.Manager = this;
baseContainer.ID = id;
}
}
@@ -50,19 +54,6 @@ namespace Robust.Shared.Containers
Containers.Clear();
}
/// <inheritdoc />
protected override void Initialize()
{
base.Initialize();
foreach (var container in Containers)
{
var baseContainer = (BaseContainer)container.Value;
baseContainer.Manager = this;
baseContainer.ID = container.Key;
}
}
/// <inheritdoc />
public override ComponentState GetComponentState()
{
@@ -144,7 +135,11 @@ namespace Robust.Shared.Containers
{
foreach (var container in Containers.Values)
{
if (container.Contains(entity)) container.ForceRemove(entity);
if (container.Contains(entity))
{
container.ForceRemove(entity);
return;
}
}
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Robust.Shared.Containers
@@ -309,6 +311,114 @@ namespace Robust.Shared.Containers
return container != null;
}
/// <summary>
/// Attempts to remove an entity from its container, if any.
/// </summary>
/// <param name="entity">Entity that might be inside a container.</param>
/// <param name="force">Whether to forcibly remove the entity from the container.</param>
/// <param name="wasInContainer">Whether the entity was actually inside a container or not.</param>
/// <returns>If the entity could be removed. Also returns false if it wasn't inside a container.</returns>
public bool TryRemoveFromContainer(EntityUid entity, bool force, out bool wasInContainer)
{
DebugTools.Assert(Exists(entity));
if (TryGetContainingContainer(entity, out var container))
{
wasInContainer = true;
if (!force)
return container.Remove(entity);
container.ForceRemove(entity);
return true;
}
wasInContainer = false;
return false;
}
/// <summary>
/// Attempts to remove an entity from its container, if any.
/// </summary>
/// <param name="entity">Entity that might be inside a container.</param>
/// <param name="force">Whether to forcibly remove the entity from the container.</param>
/// <returns>If the entity could be removed. Also returns false if it wasn't inside a container.</returns>
public bool TryRemoveFromContainer(EntityUid entity, bool force = false)
{
return TryRemoveFromContainer(entity, force, out _);
}
/// <summary>
/// Attempts to remove all entities in a container.
/// </summary>
public void EmptyContainer(IContainer container, bool force = false, EntityCoordinates? moveTo = null,
bool attachToGridOrMap = false)
{
foreach (var entity in container.ContainedEntities.ToArray())
{
if (Deleted(entity))
continue;
if (force)
container.ForceRemove(entity);
else
container.Remove(entity);
if (moveTo.HasValue)
Transform(entity).Coordinates = moveTo.Value;
if (attachToGridOrMap)
Transform(entity).AttachToGridOrMap();
}
}
/// <summary>
/// Attempts to remove and delete all entities in a container.
/// </summary>
public void CleanContainer(IContainer container)
{
foreach (var ent in container.ContainedEntities.ToArray())
{
if (Deleted(ent)) continue;
container.ForceRemove(ent);
Del(ent);
}
}
public void AttachParentToContainerOrGrid(TransformComponent transform)
{
if (transform.Parent == null
|| !TryGetContainingContainer(transform.Parent.Owner, out var container)
|| !TryInsertIntoContainer(transform, container))
transform.AttachToGridOrMap();
}
private bool TryInsertIntoContainer(TransformComponent transform, IContainer container)
{
if (container.Insert(transform.Owner)) return true;
if (Transform(container.Owner).Parent != null
&& TryGetContainingContainer(container.Owner, out var newContainer))
return TryInsertIntoContainer(transform, newContainer);
return false;
}
internal bool TryGetManagerComp(EntityUid entity, [NotNullWhen(true)] out IContainerManager? manager)
{
DebugTools.Assert(Exists(entity));
if (TryComp(entity, out manager))
return true;
// RECURSION ALERT
var transform = Transform(entity);
if (transform.ParentUid.IsValid())
return TryGetManagerComp(transform.ParentUid, out manager);
return false;
}
#endregion
// Eject entities from their parent container if the parent change is done by the transform only.

View File

@@ -22,9 +22,9 @@ namespace Robust.Shared.GameObjects
public virtual string Name => IoCManager.Resolve<IComponentFactory>().GetComponentName(GetType());
/// <inheritdoc />
[ViewVariables]
[DataField("netsync")]
public bool NetSyncEnabled { get; set; } = true;
public bool NetSyncEnabled { get; } = true;
//readonly. If you want to make it writable, you need to add the component to the entity's net-components
/// <inheritdoc />
[ViewVariables]
@@ -34,6 +34,13 @@ namespace Robust.Shared.GameObjects
[ViewVariables]
public ComponentLifeStage LifeStage { get; private set; } = ComponentLifeStage.PreAdd;
/// <summary>
/// If true, and if this is a networked component, then component data will only be sent to players if their
/// controlled entity is the owner of this component. This is a faster alternative to <see
/// cref="MetaDataFlags.EntitySpecific"/>.
/// </summary>
public virtual bool SendOnlyToOwner => false;
/// <summary>
/// Increases the life stage from <see cref="ComponentLifeStage.PreAdd" /> to <see cref="ComponentLifeStage.Added" />,
/// after raising a <see cref="ComponentAdd"/> event.

View File

@@ -44,7 +44,6 @@ namespace Robust.Shared.GameObjects
public sealed class PhysicsComponent : Component, IPhysBody, ILookupWorldBox2Component
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[DataField("status", readOnly: true)]
private BodyStatus _bodyStatus = BodyStatus.OnGround;
@@ -57,6 +56,35 @@ namespace Robust.Shared.GameObjects
[ViewVariables]
internal BroadphaseComponent? Broadphase { get; set; }
/// <summary>
/// Debugging VV
/// </summary>
[ViewVariables]
private Box2? _broadphaseAABB
{
get
{
Box2? aabb = null;
if (Broadphase == null)
{
return aabb;
}
var tree = Broadphase.Tree;
foreach (var (_, fixture) in IoCManager.Resolve<IEntityManager>().GetComponent<FixturesComponent>(Owner).Fixtures)
{
foreach (var proxy in fixture.Proxies)
{
aabb = aabb?.Union(tree.GetProxy(proxy.ProxyId)!.AABB) ?? tree.GetProxy(proxy.ProxyId)!.AABB;
}
}
return aabb;
}
}
/// <summary>
/// Store the body's index within the island so we can lookup its data.
/// Key is Island's ID and value is our index.
@@ -71,11 +99,6 @@ namespace Robust.Shared.GameObjects
public bool IgnoreCCD { get; set; }
// TODO: Placeholder; look it's disgusting but my main concern is stopping fixtures being serialized every tick
// on physics bodies for massive shuttle perf savings.
[Obsolete("Use FixturesComponent instead.")]
public IReadOnlyList<Fixture> Fixtures => _entMan.GetComponent<FixturesComponent>(Owner).Fixtures.Values.ToList();
public int FixtureCount => _entMan.GetComponent<FixturesComponent>(Owner).Fixtures.Count;
[ViewVariables] public int ContactCount => Contacts.Count;
@@ -83,7 +106,7 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Linked-list of all of our contacts.
/// </summary>
internal LinkedList<Contact> Contacts = new();
internal readonly LinkedList<Contact> Contacts = new();
[DataField("ignorePaused"), ViewVariables(VVAccess.ReadWrite)]
public bool IgnorePaused { get; set; }
@@ -118,7 +141,7 @@ namespace Robust.Shared.GameObjects
Force = Vector2.Zero;
Torque = 0.0f;
_sysMan.GetEntitySystem<SharedBroadphaseSystem>().RegenerateContacts(this);
_entMan.EntitySysManager.GetEntitySystem<SharedBroadphaseSystem>().RegenerateContacts(this);
var ev = new PhysicsBodyTypeChangedEvent(Owner, _bodyType, oldType, this);
_entMan.EventBus.RaiseLocalEvent(Owner, ref ev, true);
@@ -633,14 +656,6 @@ namespace Robust.Shared.GameObjects
private bool _predict;
public IEnumerable<PhysicsComponent> GetBodiesIntersecting()
{
foreach (var entity in _sysMan.GetEntitySystem<SharedPhysicsSystem>().GetCollidingEntities(_entMan.GetComponent<TransformComponent>(Owner).MapID, GetWorldAABB()))
{
yield return entity;
}
}
/// <summary>
/// Gets a local point relative to the body's origin given a world point.
/// Note that the vector only takes the rotation into account, not the position.
@@ -819,10 +834,10 @@ namespace Robust.Shared.GameObjects
{
// Check if either: the joint even allows collisions OR the other body on the joint is actually the other body we're checking.
if (!joint.CollideConnected &&
(aUid == joint.BodyAUid &&
((aUid == joint.BodyAUid &&
bUid == joint.BodyBUid) ||
(bUid == joint.BodyAUid ||
aUid == joint.BodyBUid)) return false;
(bUid == joint.BodyAUid &&
aUid == joint.BodyBUid))) return false;
}
}
@@ -841,9 +856,9 @@ namespace Robust.Shared.GameObjects
// View variables conveniences properties.
[ViewVariables]
private Vector2 _mapLinearVelocity => _sysMan.GetEntitySystem<SharedPhysicsSystem>().GetMapLinearVelocity(Owner, this);
private Vector2 _mapLinearVelocity => _entMan.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>().GetMapLinearVelocity(Owner, this);
[ViewVariables]
private float _mapAngularVelocity => _sysMan.GetEntitySystem<SharedPhysicsSystem>().GetMapAngularVelocity(Owner, this);
private float _mapAngularVelocity => _entMan.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>().GetMapAngularVelocity(Owner, this);
}
/// <summary>

View File

@@ -41,6 +41,7 @@ namespace Robust.Shared.GameObjects
internal set => _mapIndex = value;
}
[ViewVariables(VVAccess.ReadOnly)]
internal bool MapPaused { get; set; } = false;
/// <inheritdoc />
@@ -50,6 +51,7 @@ namespace Robust.Shared.GameObjects
set => this.MapPaused = value;
}
[ViewVariables(VVAccess.ReadOnly)]
internal bool MapPreInit { get; set; } = false;
/// <inheritdoc />

View File

@@ -73,7 +73,8 @@ namespace Robust.Shared.GameObjects
protected override void OnRemove()
{
_mapManager.TrueGridDelete((MapGrid)_mapGrid!);
if (_mapGrid != null)
_mapManager.TrueGridDelete((MapGrid)_mapGrid);
base.OnRemove();
}

View File

@@ -7,6 +7,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects
@@ -60,6 +61,12 @@ namespace Robust.Shared.GameObjects
[ViewVariables]
public GameTick EntityLastModifiedTick { get; internal set; } = new(1);
/// <summary>
/// This is the tick at which the client last applied state data received from the server.
/// </summary>
[ViewVariables]
public GameTick LastStateApplied { get; internal set; } = GameTick.Zero;
/// <summary>
/// The in-game name of this entity.
/// </summary>
@@ -134,7 +141,18 @@ namespace Robust.Shared.GameObjects
public EntityLifeStage EntityLifeStage { get; internal set; }
[ViewVariables]
public MetaDataFlags Flags { get; internal set; }
public MetaDataFlags Flags
{
get => _flags;
internal set
{
// In container and detached to null are mutually exclusive flags.
DebugTools.Assert((value & (MetaDataFlags.InContainer | MetaDataFlags.Detached)) != (MetaDataFlags.InContainer | MetaDataFlags.Detached));
_flags = value;
}
}
internal MetaDataFlags _flags;
/// <summary>
/// The sum of our visibility layer and our parent's visibility layers.
@@ -185,13 +203,21 @@ namespace Robust.Shared.GameObjects
public enum MetaDataFlags : byte
{
None = 0,
/// <summary>
/// Whether the entity has states specific to a particular player.
/// Whether the entity has states specific to particular players. This will cause many state-attempt events to
/// be raised, and is generally somewhat expensive.
/// </summary>
EntitySpecific = 1 << 0,
/// <summary>
/// Whether the entity is currently inside of a container.
/// </summary>
InContainer = 1 << 1,
/// <summary>
/// Used by clients to indicate that an entity has left their visible set.
/// </summary>
Detached = 1 << 2,
}
}

View File

@@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Players;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -11,13 +10,14 @@ namespace Robust.Shared.GameObjects
[NetworkedComponent]
public abstract class SharedUserInterfaceComponent : Component
{
[DataDefinition]
public sealed class PrototypeData : ISerializationHooks
{
public object UiKey { get; set; } = default!;
[DataField("interfaces")]
internal List<PrototypeData> _interfaceData = new();
[DataDefinition]
public sealed class PrototypeData
{
[DataField("key", readOnly: true, required: true)]
private string _uiKeyRaw = default!;
public Enum UiKey { get; set; } = default!;
[DataField("type", readOnly: true, required: true)]
public string ClientType { get; set; } = default!;
@@ -38,19 +38,6 @@ namespace Robust.Shared.GameObjects
/// </remarks>
[DataField("requireInputValidation")]
public bool RequireInputValidation = true;
void ISerializationHooks.AfterDeserialization()
{
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
if (reflectionManager.TryParseEnumReference(_uiKeyRaw, out var @enum))
{
UiKey = @enum;
return;
}
UiKey = _uiKeyRaw;
}
}
}
@@ -62,9 +49,9 @@ namespace Robust.Shared.GameObjects
{
public readonly ICommonSession Sender;
public readonly EntityUid Target;
public readonly object UiKey;
public readonly Enum UiKey;
public BoundUserInterfaceMessageAttempt(ICommonSession sender, EntityUid target, object uiKey)
public BoundUserInterfaceMessageAttempt(ICommonSession sender, EntityUid target, Enum uiKey)
{
Sender = sender;
Target = target;
@@ -85,7 +72,7 @@ namespace Robust.Shared.GameObjects
/// The UI of this message.
/// Only set when the message is raised as a directed event.
/// </summary>
public object UiKey { get; set; } = default!;
public Enum UiKey { get; set; } = default!;
/// <summary>
/// The Entity receiving the message.
@@ -126,9 +113,9 @@ namespace Robust.Shared.GameObjects
{
public readonly EntityUid Entity;
public readonly BoundUserInterfaceMessage Message;
public readonly object UiKey;
public readonly Enum UiKey;
public BoundUIWrapMessage(EntityUid entity, BoundUserInterfaceMessage message, object uiKey)
public BoundUIWrapMessage(EntityUid entity, BoundUserInterfaceMessage message, Enum uiKey)
{
Message = message;
UiKey = uiKey;
@@ -143,7 +130,7 @@ namespace Robust.Shared.GameObjects
public sealed class BoundUIOpenedEvent : BoundUserInterfaceMessage
{
public BoundUIOpenedEvent(object uiKey, EntityUid uid, ICommonSession session)
public BoundUIOpenedEvent(Enum uiKey, EntityUid uid, ICommonSession session)
{
UiKey = uiKey;
Entity = uid;
@@ -153,7 +140,7 @@ namespace Robust.Shared.GameObjects
public sealed class BoundUIClosedEvent : BoundUserInterfaceMessage
{
public BoundUIClosedEvent(object uiKey, EntityUid uid, ICommonSession session)
public BoundUIClosedEvent(Enum uiKey, EntityUid uid, ICommonSession session)
{
UiKey = uiKey;
Entity = uid;

View File

@@ -226,7 +226,7 @@ namespace Robust.Shared.GameObjects
newComponent.Owner = uid;
if (!uid.IsValid() || !EntityExists(uid))
throw new ArgumentException("Entity is not valid.", nameof(uid));
throw new ArgumentException($"Entity {uid} is not valid.", nameof(uid));
if (newComponent == null) throw new ArgumentNullException(nameof(newComponent));
@@ -252,6 +252,10 @@ namespace Robust.Shared.GameObjects
private void AddComponentInternal<T>(EntityUid uid, T component, bool overwrite, bool skipInit) where T : Component
{
DebugTools.Assert(component is MetaDataComponent ||
GetComponent<MetaDataComponent>(uid).EntityLifeStage < EntityLifeStage.Terminating,
$"Attempted to add a {typeof(T).Name} component to an entity ({ToPrettyString(uid)}) while it is terminating");
// get interface aliases for mapping
var reg = _componentFactory.GetRegistration(component);
@@ -542,15 +546,15 @@ namespace Robust.Shared.GameObjects
var entityUid = component.Owner;
// ReSharper disable once InvertIf
if (reg.NetID != null)
if (reg.NetID != null && _netComponents.TryGetValue(entityUid, out var netSet))
{
var netSet = _netComponents[entityUid];
if (netSet.Count == 1)
_netComponents.Remove(entityUid);
else
netSet.Remove(reg.NetID.Value);
Dirty(entityUid);
if (component.NetSyncEnabled)
Dirty(entityUid);
}
foreach (var refType in reg.References)
@@ -1119,6 +1123,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public ComponentState GetComponentState(IEventBus eventBus, IComponent component)
{
DebugTools.Assert(component.NetSyncEnabled, $"Attempting to get component state for an un-synced component: {component.GetType()}");
var getState = new ComponentGetState();
eventBus.RaiseComponentEvent(component, ref getState);

View File

@@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
namespace Robust.Shared.GameObjects;
public partial class EntityManager
{
public T System<T>() where T : IEntitySystem
{
return _entitySystemManager.GetEntitySystem<T>();
}
public T? SystemOrNull<T>() where T : IEntitySystem
{
return _entitySystemManager.GetEntitySystemOrNull<T>();
}
public bool TrySystem<T>([NotNullWhen(true)] out T? entitySystem) where T : IEntitySystem
{
return _entitySystemManager.TryGetEntitySystem(out entitySystem);
}
}

View File

@@ -6,6 +6,7 @@ using Prometheus;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
@@ -27,6 +28,7 @@ namespace Robust.Shared.GameObjects
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ISerializationManager _serManager = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly ProfManager _prof = default!;
#endregion Dependencies
@@ -250,19 +252,17 @@ namespace Robust.Shared.GameObjects
/// <remarks>
/// Calling Dirty on a component will call this directly.
/// </remarks>
public void Dirty(EntityUid uid)
public virtual void Dirty(EntityUid uid)
{
var currentTick = CurrentTick;
// We want to retrieve MetaDataComponent even if its Deleted flag is set.
if (!_entTraitArray[CompIdx.ArrayIndex<MetaDataComponent>()].TryGetValue(uid, out var component))
throw new KeyNotFoundException($"Entity {uid} does not exist, cannot dirty it.");
var metadata = (MetaDataComponent)component;
if (metadata.EntityLastModifiedTick == currentTick) return;
if (metadata.EntityLastModifiedTick == _gameTiming.CurTick) return;
metadata.EntityLastModifiedTick = currentTick;
metadata.EntityLastModifiedTick = _gameTiming.CurTick;
if (metadata.EntityLifeStage > EntityLifeStage.Initializing)
{
@@ -270,7 +270,7 @@ namespace Robust.Shared.GameObjects
}
}
public void Dirty(Component component)
public virtual void Dirty(Component component)
{
var owner = component.Owner;
@@ -313,7 +313,7 @@ namespace Robust.Shared.GameObjects
#if !EXCEPTION_TOLERANCE
throw new InvalidOperationException(msg);
#else
Logger.Error(msg);
Logger.Error($"{msg}. Stack: {Environment.StackTrace}");
#endif
}

View File

@@ -1,6 +1,7 @@
using Robust.Shared.Serialization;
using System;
using NetSerializer;
using Robust.Shared.Timing;
namespace Robust.Shared.GameObjects
{
@@ -13,10 +14,13 @@ namespace Robust.Shared.GameObjects
public bool Empty => ComponentChanges.Value is null or { Count: 0 };
public EntityState(EntityUid uid, NetListAsArray<ComponentChange> changedComponents, bool hide = false)
public readonly GameTick EntityLastModified;
public EntityState(EntityUid uid, NetListAsArray<ComponentChange> changedComponents, GameTick lastModified)
{
Uid = uid;
ComponentChanges = changedComponents;
EntityLastModified = lastModified;
}
}
@@ -45,12 +49,15 @@ namespace Robust.Shared.GameObjects
/// </summary>
public readonly ushort NetID;
public ComponentChange(ushort netId, bool created, bool deleted, ComponentState? state)
public readonly GameTick LastModifiedTick;
public ComponentChange(ushort netId, bool created, bool deleted, ComponentState? state, GameTick lastModifiedTick)
{
Deleted = deleted;
State = state;
NetID = netId;
Created = created;
LastModifiedTick = lastModifiedTick;
}
public override string ToString()
@@ -58,19 +65,19 @@ namespace Robust.Shared.GameObjects
return $"{(Deleted ? "D" : "C")} {NetID} {State?.GetType().Name}";
}
public static ComponentChange Added(ushort netId, ComponentState? state)
public static ComponentChange Added(ushort netId, ComponentState? state, GameTick lastModifiedTick)
{
return new(netId, true, false, state);
return new(netId, true, false, state, lastModifiedTick);
}
public static ComponentChange Changed(ushort netId, ComponentState state)
public static ComponentChange Changed(ushort netId, ComponentState state, GameTick lastModifiedTick)
{
return new(netId, false, false, state);
return new(netId, false, false, state, lastModifiedTick);
}
public static ComponentChange Removed(ushort netId)
{
return new(netId, false, true, null);
return new(netId, false, true, null, default);
}
}
}

View File

@@ -137,6 +137,7 @@ namespace Robust.Shared.GameObjects
/// </summary>
/// <typeparam name="T">entity system to get</typeparam>
/// <returns></returns>
[Obsolete]
public static T Get<T>() where T : IEntitySystem
{
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<T>();
@@ -148,6 +149,7 @@ namespace Robust.Shared.GameObjects
/// <typeparam name="T">Type of entity system to find.</typeparam>
/// <param name="entitySystem">instance matching the specified type (if exists).</param>
/// <returns>If an instance of the specified entity system type exists.</returns>
[Obsolete]
public static bool TryGet<T>([NotNullWhen(true)] out T? entitySystem) where T : IEntitySystem
{
return IoCManager.Resolve<IEntitySystemManager>().TryGetEntitySystem(out entitySystem);

View File

@@ -0,0 +1,28 @@
using System.Diagnostics.CodeAnalysis;
namespace Robust.Shared.GameObjects;
public partial interface IEntityManager
{
/// <summary>
/// Get an entity system of the specified type.
/// </summary>
/// <typeparam name="T">The type of entity system to find.</typeparam>
/// <returns>The <see cref="IEntitySystem"/> instance matching the specified type.</returns>
T System<T>() where T : IEntitySystem;
/// <summary>
/// Get an entity system of the specified type, or null if it is not registered.
/// </summary>
/// <typeparam name="T">The type of entity system to find.</typeparam>
/// <returns>The <see cref="IEntitySystem"/> instance matching the specified type, or null.</returns>
T? SystemOrNull<T>() where T : IEntitySystem;
/// <summary>
/// Tries to get an entity system of the specified type.
/// </summary>
/// <typeparam name="T">Type of entity system to find.</typeparam>
/// <param name="entitySystem">instance matching the specified type (if exists).</param>
/// <returns>If an instance of the specified entity system type exists.</returns>
bool TrySystem<T>([NotNullWhen(true)] out T? entitySystem) where T : IEntitySystem;
}

View File

@@ -50,8 +50,10 @@ namespace Robust.Shared.GameObjects
/// <param name="audioParams">Audio parameters to apply when playing the sound.</param>
public abstract IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, AudioParams? audioParams = null);
public IPlayingAudioStream? PlayGlobal(SoundSpecifier sound, Filter playerFilter, AudioParams? audioParams = null)
=> PlayGlobal(GetSound(sound), playerFilter, audioParams ?? sound.Params);
public IPlayingAudioStream? PlayGlobal(SoundSpecifier? sound, Filter playerFilter, AudioParams? audioParams = null)
{
return sound == null ? null : PlayGlobal(GetSound(sound), playerFilter, audioParams ?? sound.Params);
}
/// <summary>
/// Play an audio file following an entity.
@@ -69,8 +71,10 @@ namespace Robust.Shared.GameObjects
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
/// <param name="audioParams">Audio parameters to apply when playing the sound. Defaults to using the sound specifier's parameters</param>
public IPlayingAudioStream? Play(SoundSpecifier sound, Filter playerFilter, EntityUid uid, AudioParams? audioParams = null)
=> Play(GetSound(sound), playerFilter, uid, audioParams ?? sound.Params);
public IPlayingAudioStream? Play(SoundSpecifier? sound, Filter playerFilter, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : Play(GetSound(sound), playerFilter, uid, audioParams ?? sound.Params);
}
/// <summary>
/// Play an audio file following an entity for every entity in PVS range.
@@ -78,8 +82,10 @@ namespace Robust.Shared.GameObjects
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
/// <param name="audioParams">Audio parameters to apply when playing the sound. Defaults to using the sound specifier's parameters</param>
public IPlayingAudioStream? PlayPvs(SoundSpecifier sound, EntityUid uid, AudioParams? audioParams = null)
=> Play(sound, Filter.Pvs(uid, entityManager: EntityManager), uid, audioParams);
public IPlayingAudioStream? PlayPvs(SoundSpecifier? sound, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : Play(sound, Filter.Pvs(uid, entityManager: EntityManager), uid, audioParams);
}
/// <summary>
/// Plays a predicted sound following an entity. The server will send the sound to every player in PVS range,
@@ -90,7 +96,7 @@ namespace Robust.Shared.GameObjects
/// <param name="source">The UID of the entity "emitting" the audio.</param>
/// <param name="user">The UID of the user that initiated this sound. This is usually some player's controlled entity.</param>
/// <param name="audioParams">Audio parameters to apply when playing the sound. Defaults to using the sound specifier's parameters</param>
public abstract IPlayingAudioStream? PlayPredicted(SoundSpecifier sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null);
public abstract IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
@@ -109,8 +115,11 @@ namespace Robust.Shared.GameObjects
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams">Audio parameters to apply when playing the sound.</param>
public IPlayingAudioStream? Play(SoundSpecifier sound, Filter playerFilter, EntityCoordinates coordinates,
AudioParams? audioParams = null) => Play(GetSound(sound), playerFilter, coordinates, audioParams ?? sound.Params);
public IPlayingAudioStream? Play(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
return sound == null ? null : Play(GetSound(sound), playerFilter, coordinates, audioParams ?? sound.Params);
}
protected EntityCoordinates GetFallbackCoordinates(MapCoordinates mapCoordinates)
{

View File

@@ -62,7 +62,7 @@ namespace Robust.Shared.GameObjects
return state.found;
}
public IEnumerable<PhysicsComponent> GetCollidingEntities(PhysicsComponent body, Vector2 offset, bool approximate = true)
public IEnumerable<PhysicsComponent> GetCollidingEntities(PhysicsComponent body, bool approximate = true)
{
var broadphase = body.Broadphase;
if (broadphase == null || !EntityManager.TryGetComponent(body.Owner, out FixturesComponent? manager))
@@ -178,33 +178,45 @@ namespace Robust.Shared.GameObjects
return bodies;
}
public IEnumerable<PhysicsComponent> GetCollidingEntities(PhysicsComponent body)
public HashSet<PhysicsComponent> GetContactingEntities(PhysicsComponent body, bool approximate = false)
{
// HashSet to ensure that we only return each entity once, instead of once per colliding fixture.
var result = new HashSet<PhysicsComponent>();
var node = body.Contacts.First;
while (node != null)
{
var contact = node.Value;
node = node.Next;
if (!approximate && !contact.IsTouching)
continue;
var bodyA = contact.FixtureA!.Body;
var bodyB = contact.FixtureB!.Body;
var other = body == bodyA ? bodyB : bodyA;
yield return other;
result.Add(body == bodyA ? bodyB : bodyA);
}
return result;
}
// TODO: This will get every body but we don't need to do that
/// <summary>
/// Checks whether a body is colliding
/// </summary>
/// <param name="body"></param>
/// <param name="offset"></param>
/// <returns></returns>
public bool IsColliding(PhysicsComponent body, Vector2 offset, bool approximate)
public bool IsInContact(PhysicsComponent body, bool approximate = false)
{
return GetCollidingEntities(body, offset, approximate).Any();
var node = body.Contacts.First;
while (node != null)
{
if (approximate || node.Value.IsTouching)
return true;
node = node.Next;
}
return false;
}
#region RayCast

View File

@@ -171,7 +171,7 @@ namespace Robust.Shared.GameObjects
if (args.OldMapId != xform.MapID)
return;
_broadphase.UpdateBroadphase(uid, args.OldMapId, xform: xform);
if (body != null)
@@ -237,7 +237,8 @@ namespace Robust.Shared.GameObjects
DestroyContacts(body, oldMap); // This can modify body.Awake
DebugTools.Assert(body.Contacts.Count == 0);
if (fixturesQuery.TryGetComponent(uid, out var fixtures) && body._canCollide)
// TODO: When we cull sharedphysicsmapcomponent we can probably remove this grid check.
if (!MapManager.IsGrid(uid.Value) && fixturesQuery.TryGetComponent(uid, out var fixtures) && body._canCollide)
{
// TODO If not deleting, update world position+rotation while iterating through children and pass into UpdateBodyBroadphase
_broadphase.UpdateBodyBroadphase(body, fixtures, xform, newBroadphase, xformQuery, oldMoveBuffer);

View File

@@ -734,7 +734,7 @@ public abstract partial class SharedTransformSystem
DebugTools.Assert(!xform.Anchored);
}
public void DetachParentToNull(TransformComponent xform, EntityQuery<TransformComponent> xformQuery, EntityQuery<MetaDataComponent> metaQuery)
public void DetachParentToNull(TransformComponent xform, EntityQuery<TransformComponent> xformQuery, EntityQuery<MetaDataComponent> metaQuery, TransformComponent? oldConcrete = null)
{
var oldParent = xform._parent;
@@ -763,7 +763,7 @@ public abstract partial class SharedTransformSystem
RaiseLocalEvent(xform.Owner, ref anchorStateChangedEvent, true);
}
var oldConcrete = xformQuery.GetComponent(oldParent);
oldConcrete ??= xformQuery.GetComponent(oldParent);
oldConcrete._children.Remove(xform.Owner);
xform._parent = EntityUid.Invalid;

View File

@@ -7,17 +7,10 @@ using Robust.Shared.Timing;
namespace Robust.Shared.GameStates
{
[DebuggerDisplay("GameState from={FromSequence} to={ToSequence} ext={Extrapolated}")]
[DebuggerDisplay("GameState from={FromSequence} to={ToSequence}")]
[Serializable, NetSerializable]
public sealed class GameState
{
/// <summary>
/// An extrapolated state that was created artificially by the client.
/// It does not contain any real data from the server.
/// </summary>
[field:NonSerialized]
public bool Extrapolated { get; set; }
/// <summary>
/// The serialized size in bytes of this game state.
/// </summary>

View File

@@ -1,4 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
@@ -104,7 +104,6 @@ namespace Robust.Shared.Localization
&& !bundle.TryGetMsg(locId, "suffix", null, out var err, out suffix))
{
suffix = null;
allErrors.AddRange(err);
}
WriteWarningForErrs(allErrors, locId);

View File

@@ -35,10 +35,15 @@ namespace Robust.Shared.Map
var gridId = coords.GetGridUid(entityManager);
if (gridId != null || !mapManager.GridExists(gridId))
if (!mapManager.GridExists(gridId))
{
var mapCoords = coords.ToMap(entityManager);
if (mapManager.TryFindGridAt(mapCoords, out var mapGrid))
{
return new EntityCoordinates(mapGrid.GridEntityId, mapGrid.WorldToLocal(mapCoords.Position));
}
// create a box around the cursor
var gridSearchBox = Box2.UnitCentered.Scale(searchBoxSize).Translated(mapCoords.Position);

View File

@@ -120,8 +120,11 @@ internal partial class MapManager
{
if (kvEntity.Value == newMapEntity)
{
if (mapId == kvEntity.Key)
return;
throw new InvalidOperationException(
$"Entity {newMapEntity} is already the root node of map {kvEntity.Key}.");
$"Entity {newMapEntity} is already the root node of another map {kvEntity.Key}.");
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
@@ -27,7 +27,7 @@ namespace Robust.Shared.Network.Messages
public GameState State;
public ZStdCompressionContext CompressionContext;
private bool _hasWritten;
internal bool _hasWritten;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
@@ -70,7 +70,7 @@ namespace Robust.Shared.Network.Messages
// We compress the state.
if (stateStream.Length > CompressionThreshold)
{
var sw = Stopwatch.StartNew();
// var sw = Stopwatch.StartNew();
stateStream.Position = 0;
var buf = ArrayPool<byte>.Shared.Rent(ZStd.CompressBound((int)stateStream.Length));
var length = CompressionContext.Compress2(buf, stateStream.AsSpan());
@@ -79,7 +79,7 @@ namespace Robust.Shared.Network.Messages
buffer.Write(buf.AsSpan(0, length));
var elapsed = sw.Elapsed;
// var elapsed = sw.Elapsed;
// System.Console.WriteLine(
// $"From: {State.FromSequence} To: {State.ToSequence} Size: {length} B Before: {stateStream.Length} B time: {elapsed}");
@@ -94,8 +94,7 @@ namespace Robust.Shared.Network.Messages
buffer.Write(stateStream.AsSpan());
}
_hasWritten = false;
_hasWritten = true;
MsgSize = buffer.LengthBytes;
}
@@ -106,13 +105,7 @@ namespace Robust.Shared.Network.Messages
/// <returns></returns>
public bool ShouldSendReliably()
{
// This check will be true in integration tests.
// TODO: Maybe handle this better so that packet loss integration testing can be done?
if (!_hasWritten)
{
return true;
}
DebugTools.Assert(_hasWritten, "Attempted to determine sending method before determining packet size.");
return MsgSize > ReliableThreshold;
}

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Timing;
#nullable disable
namespace Robust.Shared.Network.Messages;
/// <summary>
/// Message containing a list of entities that have left a clients view.
/// </summary>
/// <remarks>
/// These messages are only sent if PVS is enabled. These messages are sent separately from the main game state.
/// </remarks>
public sealed class MsgStateLeavePvs : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Entity;
public List<EntityUid> Entities;
public GameTick Tick;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
Tick = buffer.ReadGameTick();
var length = buffer.ReadInt32();
Entities = new(length);
for (int i = 0; i < length; i++)
{
Entities.Add(buffer.ReadEntityUid());
}
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(Tick);
buffer.Write(Entities.Count);
foreach (var ent in Entities)
{
buffer.Write(ent);
}
}
public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered;
}

View File

@@ -0,0 +1,25 @@
using Lidgren.Network;
using Robust.Shared.Timing;
#nullable disable
namespace Robust.Shared.Network.Messages;
public sealed class MsgStateRequestFull : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Entity;
public GameTick Tick;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
Tick = buffer.ReadGameTick();
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(Tick);
}
public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered;
}

View File

@@ -22,6 +22,8 @@ namespace Robust.Shared.Physics.Broadphase
return proxy.AABB;
}
public int Count => _tree.NodeCount;
public Box2 GetFatAabb(DynamicTree.Proxy proxy)
{
return _tree.GetFatAabb(proxy);

View File

@@ -25,6 +25,7 @@ namespace Robust.Shared.Physics
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FixturesComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<FixturesComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<FixturesComponent, ComponentHandleState>(OnHandleState);
@@ -288,11 +289,11 @@ namespace Robust.Shared.Physics
if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? physics))
{
DebugTools.Assert(false);
Logger.ErrorS("physics", $"Tried to apply fixture state for {uid} which has name {nameof(PhysicsComponent)}");
Logger.ErrorS("physics", $"Tried to apply fixture state for an entity without physics: {ToPrettyString(uid)}");
return;
}
component.SerializedFixtures.Clear();
var toAddFixtures = new ValueList<Fixture>();
var toRemoveFixtures = new ValueList<Fixture>();
var computeProperties = false;

View File

@@ -7,6 +7,8 @@ namespace Robust.Shared.Physics {
public interface IBroadPhase
{
int Count { get; }
Box2 GetFatAabb(DynamicTree.Proxy proxy);
DynamicTree.Proxy AddProxy(ref FixtureProxy proxy);

View File

@@ -284,7 +284,7 @@ namespace Robust.Shared.Physics
var otherTransform = bodyQuery.GetComponent(colliding.GridEntityId).GetTransform(xformQuery.GetComponent(colliding.GridEntityId));
// Get Grid2 AABB in grid1 ref
var aabb1 = grid.LocalAABB.Union(invWorldMatrix.TransformBox(otherGridBounds));
var aabb1 = grid.LocalAABB.Intersect(invWorldMatrix.TransformBox(otherGridBounds));
// TODO: AddPair has a nasty check in there that's O(n) but that's also a general physics problem.
var ourChunks = mapGrid.GetLocalMapChunks(aabb1);

View File

@@ -49,12 +49,12 @@ namespace Robust.Shared.Physics
public abstract class SharedJointSystem : EntitySystem
{
[Dependency] private readonly SharedContainerSystem Container = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
// To avoid issues with component states we'll queue up all dirty joints and check it every tick to see if
// we can delete the component.
private HashSet<JointComponent> _dirtyJoints = new();
protected HashSet<Joint> AddedJoints = new();
private readonly HashSet<JointComponent> _dirtyJoints = new();
protected readonly HashSet<Joint> AddedJoints = new();
private ISawmill _sawmill = default!;
@@ -466,7 +466,7 @@ namespace Robust.Shared.Physics
// Wake up connected bodies.
if (EntityManager.TryGetComponent<PhysicsComponent>(bodyAUid, out var bodyA) &&
MetaData(bodyAUid).EntityLifeStage < EntityLifeStage.Terminating &&
!Container.IsEntityInContainer(bodyAUid))
!_container.IsEntityInContainer(bodyAUid))
{
bodyA.CanCollide = true;
bodyA.Awake = true;
@@ -474,7 +474,7 @@ namespace Robust.Shared.Physics
if (EntityManager.TryGetComponent<PhysicsComponent>(bodyBUid, out var bodyB) &&
MetaData(bodyBUid).EntityLifeStage < EntityLifeStage.Terminating &&
!Container.IsEntityInContainer(bodyBUid))
!_container.IsEntityInContainer(bodyBUid))
{
bodyB.CanCollide = true;
bodyB.Awake = true;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using Robust.Shared.Log;
using Robust.Shared.Exceptions;
@@ -177,12 +177,13 @@ namespace Robust.Shared.Timing
}
#endif
_timing.InSimulation = true;
var tickPeriod = CalcTickPeriod();
var tickPeriod = _timing.CalcAdjustedTickPeriod();
using (_prof.Group("Ticks"))
{
var countTicksRan = 0;
// run the simulation for every accumulated tick
while (accumulator >= tickPeriod)
{
accumulator -= tickPeriod;
@@ -192,6 +193,7 @@ namespace Robust.Shared.Timing
if (_timing.Paused)
continue;
_timing.TickRemainder = accumulator;
countTicksRan += 1;
// update the simulation
@@ -237,7 +239,7 @@ namespace Robust.Shared.Timing
}
#endif
_timing.CurTick = new GameTick(_timing.CurTick.Value + 1);
tickPeriod = CalcTickPeriod();
tickPeriod = _timing.CalcAdjustedTickPeriod();
if (SingleStep)
_timing.Paused = true;
@@ -304,14 +306,6 @@ namespace Robust.Shared.Timing
Thread.Sleep((int)SleepMode);
}
}
private TimeSpan CalcTickPeriod()
{
// ranges from -1 to 1, with 0 being 'default'
var ratio = MathHelper.Clamp(_timing.TickTimingAdjustment, -0.99f, 0.99f);
var diff = TimeSpan.FromTicks((long)(_timing.TickPeriod.Ticks * ratio));
return _timing.TickPeriod - diff;
}
}
/// <summary>

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Shared.Timing
@@ -153,6 +154,14 @@ namespace Robust.Shared.Timing
}
}
public TimeSpan CalcAdjustedTickPeriod()
{
// ranges from -1 to 1, with 0 being 'default'
var ratio = MathHelper.Clamp(TickTimingAdjustment, -0.99f, 0.99f);
var ticks = (1.0 / TickRate * TimeSpan.TicksPerSecond) - (TickPeriod.Ticks * ratio);
return TimeSpan.FromTicks((long) ticks);
}
/// <summary>
/// Current graphics frame since init OpenGL which is taken as frame 1, from swapbuffer to swapbuffer. Useful to set a
/// conditional breakpoint on specific frames, and synchronize with OGL debugging tools that capture frames.
@@ -251,14 +260,11 @@ namespace Robust.Shared.Timing
public bool IsFirstTimePredicted { get; protected set; } = true;
/// <inheritdoc />
public bool InPrediction => !ApplyingState && CurTick > LastRealTick;
public virtual bool InPrediction => false;
/// <inheritdoc />
public bool ApplyingState {get; protected set; }
/// <inheritdoc />
public GameTick LastRealTick { get; set; }
/// <summary>
/// Calculates the average FPS of the last 50 real frame times.
/// </summary>

View File

@@ -102,6 +102,8 @@ namespace Robust.Shared.Timing
/// </summary>
TimeSpan TickRemainder { get; set; }
TimeSpan CalcAdjustedTickPeriod();
/// <summary>
/// Fraction of how far into the tick we are. <c>0</c> is 0% and <see cref="ushort.MaxValue"/> is 100%.
/// </summary>
@@ -148,11 +150,6 @@ namespace Robust.Shared.Timing
/// </summary>
bool ApplyingState { get; }
/// <summary>
/// The last real non-predicted tick that was processed.
/// </summary>
GameTick LastRealTick { get; set; }
string TickStamp => $"{CurTick}, predFirst: {IsFirstTimePredicted}, tickRem: {TickRemainder.TotalSeconds}, sim: {InSimulation}";
static string TickStampStatic => IoCManager.Resolve<IGameTiming>().TickStamp;

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
namespace Robust.Shared.Utility
{
@@ -159,6 +158,19 @@ namespace Robust.Shared.Utility
return element != null;
}
/// <summary>
/// Just like <see cref="Enumerable.FirstOrDefault{TSource}(System.Collections.Generic.IEnumerable{TSource}, Func{TSource, bool})"/> but returns null for value types as well.
/// </summary>
/// <param name="source">An <see cref="T:System.Collections.Generic.IEnumerable`1" /> to return an element from.</param>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <returns>True if an element has been found.</returns>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="source" /> is <see langword="null" />.</exception>
public static bool TryFirstOrNull<TSource>(this IEnumerable<TSource> source, [NotNullWhen(true)] out TSource? element) where TSource : struct
{
return TryFirstOrNull(source, _ => true, out element);
}
/// <summary>
/// Wraps Linq's FirstOrDefault.
/// </summary>
@@ -174,6 +186,19 @@ namespace Robust.Shared.Utility
return element != null;
}
/// <summary>
/// Wraps Linq's FirstOrDefault.
/// </summary>
/// <param name="source">An <see cref="T:System.Collections.Generic.IEnumerable`1" /> to return an element from.</param>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <returns>True if an element has been found.</returns>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="source" /> is <see langword="null" />.</exception>
public static bool TryFirstOrDefault<TSource>(this IEnumerable<TSource> source, [NotNullWhen(true)] out TSource? element) where TSource : class
{
return TryFirstOrDefault(source, _ => true, out element);
}
public static TValue GetOrNew<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key) where TValue : new()
where TKey : notnull
{

View File

@@ -1,6 +1,7 @@
using Moq;
using Moq;
using NUnit.Framework;
using Robust.Client.GameStates;
using Robust.Client.Timing;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
@@ -12,27 +13,27 @@ namespace Robust.UnitTesting.Client.GameStates
[Test]
public void FillBufferBlocksProcessing()
{
var timingMock = new Mock<IGameTiming>();
var timingMock = new Mock<IClientGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
var timing = timingMock.Object;
var processor = new GameStateProcessor(timing);
processor.Interpolation = true;
processor.AddNewState(GameStateFactory(0, 1));
processor.AddNewState(GameStateFactory(1, 2)); // buffer is at 2/3, so processing should be blocked
// calculate states for first tick
timing.CurTick = new GameTick(3);
var result = processor.ProcessTickStates(new GameTick(1), out _, out _);
timing.LastProcessedTick = new GameTick(0);
var result = processor.TryGetServerState(out _, out _);
Assert.That(result, Is.False);
Assert.That(timing.CurTick.Value, Is.EqualTo(1));
}
[Test]
public void FillBufferAndCalculateFirstState()
{
var timingMock = new Mock<IGameTiming>();
var timingMock = new Mock<IClientGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
var timing = timingMock.Object;
@@ -43,12 +44,11 @@ namespace Robust.UnitTesting.Client.GameStates
processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states.
// calculate states for first tick
timing.CurTick = new GameTick(1);
var result = processor.ProcessTickStates(new GameTick(1), out var curState, out var nextState);
timing.LastProcessedTick = new GameTick(0);
var result = processor.TryGetServerState(out var curState, out var nextState);
Assert.That(result, Is.True);
Assert.That(curState, Is.Not.Null);
Assert.That(curState!.Extrapolated, Is.False);
Assert.That(curState.ToSequence.Value, Is.EqualTo(1));
Assert.That(nextState, Is.Null);
}
@@ -60,7 +60,7 @@ namespace Robust.UnitTesting.Client.GameStates
[Test]
public void FullStateResyncsCurTick()
{
var timingMock = new Mock<IGameTiming>();
var timingMock = new Mock<IClientGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
var timing = timingMock.Object;
@@ -71,10 +71,11 @@ namespace Robust.UnitTesting.Client.GameStates
processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states.
// calculate states for first tick
timing.CurTick = new GameTick(3);
processor.ProcessTickStates(timing.CurTick, out _, out _);
timing.LastProcessedTick = new GameTick(2);
processor.TryGetServerState(out var state, out _);
Assert.That(timing.CurTick.Value, Is.EqualTo(1));
Assert.NotNull(state);
Assert.That(state.ToSequence.Value, Is.EqualTo(1));
}
[Test]
@@ -82,12 +83,10 @@ namespace Robust.UnitTesting.Client.GameStates
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = false;
// a few moments later...
timing.CurTick = new GameTick(5); // current clock is ahead of server
timing.LastProcessedTick = new GameTick(4); // current clock is ahead of server
processor.AddNewState(GameStateFactory(3, 4)); // received a late state
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
var result = processor.TryGetServerState(out _, out _);
Assert.That(result, Is.False);
}
@@ -101,57 +100,13 @@ namespace Robust.UnitTesting.Client.GameStates
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = false;
// a few moments later...
timing.CurTick = new GameTick(5); // current clock is ahead of server
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
timing.LastProcessedTick = new GameTick(4); // current clock is ahead of server
var result = processor.TryGetServerState(out _, out _);
Assert.That(result, Is.False);
}
/// <summary>
/// When processing is blocked because the client is ahead of the server, reset CurTick to the last
/// received state.
/// </summary>
[Test]
public void ServerLagsWithoutExtrapolationSetsCurTick()
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = false;
// a few moments later...
timing.CurTick = new GameTick(4); // current clock is ahead of server (server=1, client=5)
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
Assert.That(result, Is.False);
Assert.That(timing.CurTick.Value, Is.EqualTo(1));
}
/// <summary>
/// The server fell behind the client, so the client clock is now ahead of the incoming states.
/// With extrapolation, processing returns a fake extrapolated state for the current tick.
/// </summary>
[Test]
public void ServerLagsWithExtrapolation()
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = true;
// a few moments later...
timing.CurTick = new GameTick(5); // current clock is ahead of server
var result = processor.ProcessTickStates(timing.CurTick, out var curState, out var nextState);
Assert.That(result, Is.True);
Assert.That(curState, Is.Not.Null);
Assert.That(curState!.Extrapolated, Is.True);
Assert.That(curState.ToSequence.Value, Is.EqualTo(5));
Assert.That(nextState, Is.Null);
}
/// <summary>
/// There is a hole in the state buffer, we have a future state but their FromSequence is too high!
/// In this case we stop and wait for the server to get us the missing link.
@@ -162,11 +117,10 @@ namespace Robust.UnitTesting.Client.GameStates
var (timing, processor) = SetupProcessorFactory();
processor.AddNewState(GameStateFactory(4, 5));
processor.LastProcessedRealState = new GameTick(3);
timing.LastRealTick = new GameTick(3);
timing.LastProcessedTick = new GameTick(3);
timing.CurTick = new GameTick(4);
var result = processor.ProcessTickStates(timing.CurTick, out _, out _);
var result = processor.TryGetServerState(out _, out _);
Assert.That(result, Is.False);
}
@@ -182,52 +136,24 @@ namespace Robust.UnitTesting.Client.GameStates
processor.Interpolation = true;
timing.CurTick = new GameTick(4);
timing.LastProcessedTick = new GameTick(3);
processor.LastProcessedRealState = new GameTick(3);
timing.LastRealTick = new GameTick(3);
processor.AddNewState(GameStateFactory(3, 5));
// We're missing the state for this tick so go into extrap.
var result = processor.ProcessTickStates(timing.CurTick, out var curState, out _);
var result = processor.TryGetServerState(out var curState, out _);
Assert.That(result, Is.True);
Assert.That(curState, Is.Not.Null);
Assert.That(curState!.Extrapolated, Is.True);
Assert.That(curState, Is.Null);
timing.CurTick = new GameTick(5);
timing.LastProcessedTick = new GameTick(4);
// But we DO have the state for the tick after so apply away!
result = processor.ProcessTickStates(timing.CurTick, out curState, out _);
result = processor.TryGetServerState(out curState, out _);
Assert.That(result, Is.True);
Assert.That(curState, Is.Not.Null);
Assert.That(curState!.Extrapolated, Is.False);
}
/// <summary>
/// The client started extrapolating and now received the state it needs to "continue as normal".
/// In this scenario the CurTick passed to the game state processor
/// is higher than the real next tick to apply, IF it went into extrapolation.
/// The processor needs to go back to the next REAL tick.
/// </summary>
[Test, Ignore("Extrapolation is currently non functional anyways")]
public void UndoExtrapolation()
{
var (timing, processor) = SetupProcessorFactory();
processor.Extrapolation = true;
processor.AddNewState(GameStateFactory(4, 5));
processor.AddNewState(GameStateFactory(3, 4));
processor.LastProcessedRealState = new GameTick(3);
timing.CurTick = new GameTick(5);
var result = processor.ProcessTickStates(timing.CurTick, out var curState, out _);
Assert.That(result, Is.True);
Assert.That(curState, Is.Not.Null);
Assert.That(curState!.ToSequence, Is.EqualTo(new GameTick(4)));
}
/// <summary>
@@ -242,10 +168,12 @@ namespace Robust.UnitTesting.Client.GameStates
/// Creates a new GameTiming and GameStateProcessor, fills the processor with enough states, and calculate the first tick.
/// CurTick = 1, states 1 - 3 are in the buffer.
/// </summary>
private static (IGameTiming timing, GameStateProcessor processor) SetupProcessorFactory()
private static (IClientGameTiming timing, GameStateProcessor processor) SetupProcessorFactory()
{
var timingMock = new Mock<IGameTiming>();
var timingMock = new Mock<IClientGameTiming>();
timingMock.SetupProperty(p => p.CurTick);
timingMock.SetupProperty(p => p.LastProcessedTick);
timingMock.SetupProperty(p => p.LastRealTick);
timingMock.SetupProperty(p => p.TickTimingAdjustment);
var timing = timingMock.Object;
@@ -255,9 +183,8 @@ namespace Robust.UnitTesting.Client.GameStates
processor.AddNewState(GameStateFactory(1, 2));
processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states.
// calculate states for first tick
timing.CurTick = new GameTick(1);
processor.ProcessTickStates(timing.CurTick, out _, out _);
processor.LastFullStateRequested = null;
timing.LastProcessedTick = timing.LastRealTick = new GameTick(1);
return (timing, processor);
}

View File

@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -45,6 +46,8 @@ namespace Robust.UnitTesting
private readonly Dictionary<Type, ProcessMessage> _callbacks = new();
private readonly HashSet<Type> _registeredMessages = new();
private readonly Dictionary<string, Guid> _userGuids = new Dictionary<string, Guid>();
/// <summary>
/// The channel we will connect to when <see cref="ClientConnect"/> is called.
/// </summary>
@@ -111,10 +114,14 @@ namespace Robust.UnitTesting
async Task DoConnect()
{
var writer = connect.ChannelWriter;
var uid = _genConnectionUid();
var sessionId = new NetUserId(Guid.NewGuid());
var userName = $"integration_{uid}";
var userName = connect.Username ?? $"integration_{uid}";
if (!_userGuids.TryGetValue(userName, out var userId))
{
userId = Guid.NewGuid();
_userGuids.Add(userName, userId);
}
var sessionId = new NetUserId(userId);
var userData = new NetUserData(sessionId, userName)
{
HWId = ImmutableArray<byte>.Empty
@@ -252,6 +259,10 @@ namespace Robust.UnitTesting
{
DebugTools.Assert(IsServer);
// MsgState sending method depends on the size of the possible compressed buffer. But tests bypass buffer read/write.
if (message is MsgState stateMsg)
stateMsg._hasWritten = true;
var channel = (IntegrationNetChannel) recipient;
channel.OtherChannel.TryWrite(new DataMessage(message, channel.RemoteUid));
}
@@ -353,7 +364,7 @@ namespace Robust.UnitTesting
_clientConnectingUid = _genConnectionUid();
NextConnectChannel.TryWrite(new ConnectMessage(MessageChannelWriter, _clientConnectingUid));
NextConnectChannel.TryWrite(new ConnectMessage(MessageChannelWriter, _clientConnectingUid, userNameRequest));
}
public void ClientDisconnect(string reason)
@@ -457,14 +468,16 @@ namespace Robust.UnitTesting
private sealed class ConnectMessage
{
public ConnectMessage(ChannelWriter<object> channelWriter, int uid)
public ConnectMessage(ChannelWriter<object> channelWriter, int uid, string? username)
{
ChannelWriter = channelWriter;
Uid = uid;
Username = username;
}
public ChannelWriter<object> ChannelWriter { get; }
public int Uid { get; }
public string? Username { get; }
}
private sealed class ConfirmConnectMessage

View File

@@ -99,7 +99,7 @@ entities:
entMan.EnsureComponent<BroadphaseComponent>(mapUid);
var mapLoad = IoCManager.Resolve<IMapLoader>();
var geid = mapLoad.LoadBlueprint(mapId, "/TestMap.yml").gridId;
var geid = mapLoad.LoadGrid(mapId, "/TestMap.yml").gridId;
Assert.That(geid, NUnit.Framework.Is.Not.Null);

View File

@@ -2,12 +2,14 @@ using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Client.GameObjects;
using Robust.Server.Maps;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
// ReSharper disable AccessToStaticMemberViaDerivedType
namespace Robust.UnitTesting.Shared.GameObjects
{
@@ -261,5 +263,83 @@ namespace Robust.UnitTesting.Shared.GameObjects
});
}
/// <summary>
/// Sets up a new container, initializes map, saves the map, then loads it again on another map. The contained entity should still
/// be inside the container.
/// </summary>
[Test]
public async Task Container_DeserializeGrid_IsStillContained()
{
var server = StartServer();
await Task.WhenAll(server.WaitIdleAsync());
await server.WaitAssertion(() =>
{
var entMan = IoCManager.Resolve<IEntityManager>();
var containerSys = entMan.EntitySysManager.GetEntitySystem<Robust.Server.Containers.ContainerSystem>();
// build the map
var mapIdOne = new MapId(1);
var mapManager = IoCManager.Resolve<IMapManager>();
mapManager.CreateMap(mapIdOne);
Assert.That(mapManager.IsMapInitialized(mapIdOne), Is.True);
var containerEnt = entMan.SpawnEntity(null, new MapCoordinates(1, 1, mapIdOne));
entMan.GetComponent<MetaDataComponent>(containerEnt).EntityName = "ContainerEnt";
var containeeEnt = entMan.SpawnEntity(null, new MapCoordinates(2, 2, mapIdOne));
entMan.GetComponent<MetaDataComponent>(containeeEnt).EntityName = "ContaineeEnt";
var container = containerSys.MakeContainer<Container>(containerEnt, "testContainer");
container.OccludesLight = true;
container.ShowContents = true;
container.Insert(containeeEnt);
// save the map
var mapLoader = IoCManager.Resolve<IMapLoader>();
mapLoader.SaveMap(mapIdOne, "container_test.yml");
mapManager.DeleteMap(mapIdOne);
});
// A few moments later...
await server.WaitRunTicks(10);
await server.WaitAssertion(() =>
{
var mapIdTwo = new MapId(2);
var mapManager = IoCManager.Resolve<IMapManager>();
var mapLoader = IoCManager.Resolve<IMapLoader>();
// load the map
mapLoader.LoadMap(mapIdTwo, "container_test.yml");
Assert.That(mapManager.IsMapInitialized(mapIdTwo), Is.True); // Map Initialize-ness is saved in the map file.
});
await server.WaitRunTicks(1);
await server.WaitAssertion(() =>
{
var entMan = IoCManager.Resolve<IEntityManager>();
// verify container
var containerQuery = entMan.EntityQuery<ContainerManagerComponent>();
var containerComp = containerQuery.First();
var containerEnt = containerComp.Owner;
Assert.That(entMan.GetComponent<MetaDataComponent>(containerEnt).EntityName, Is.EqualTo("ContainerEnt"));
Assert.That(containerComp.Containers.ContainsKey("testContainer"));
var iContainer = containerComp.GetContainer("testContainer");
Assert.That(iContainer.ContainedEntities.Count, Is.EqualTo(1));
var containeeEnt = iContainer.ContainedEntities[0];
Assert.That(entMan.GetComponent<MetaDataComponent>(containeeEnt).EntityName, Is.EqualTo("ContaineeEnt"));
});
}
}
}

View File

@@ -62,8 +62,8 @@ namespace Robust.UnitTesting.Shared.GameObjects
new EntityUid(512),
new []
{
new ComponentChange(0, true, false, new MapGridComponentState(new GridId(0), 16))
});
new ComponentChange(0, true, false, new MapGridComponentState(new GridId(0), 16), default)
}, default);
serializer.Serialize(stream, payload);
array = stream.ToArray();

View File

@@ -1,5 +1,7 @@
using System.Linq;
using NUnit.Framework;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -10,10 +12,20 @@ namespace Robust.UnitTesting.Shared.Map;
[TestFixture]
public sealed class GridSplit_Tests
{
private ISimulation GetSim()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var config = sim.Resolve<IConfigurationManager>();
config.SetCVar(CVars.GridSplitting, true);
return sim;
}
[Test]
public void SimpleSplit()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var sim =GetSim();
var mapManager = sim.Resolve<IMapManager>();
var mapId = mapManager.CreateMap();
var grid = mapManager.CreateGrid(mapId);
@@ -34,7 +46,7 @@ public sealed class GridSplit_Tests
[Test]
public void DonutSplit()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var sim =GetSim();
var mapManager = sim.Resolve<IMapManager>();
var mapId = mapManager.CreateMap();
var grid = mapManager.CreateGrid(mapId);
@@ -64,7 +76,7 @@ public sealed class GridSplit_Tests
[Test]
public void TriSplit()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var sim =GetSim();
var mapManager = sim.Resolve<IMapManager>();
var mapId = mapManager.CreateMap();
var grid = mapManager.CreateGrid(mapId);
@@ -90,7 +102,7 @@ public sealed class GridSplit_Tests
[Test]
public void ReparentSplit()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var sim =GetSim();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var mapId = mapManager.CreateMap();

View File

@@ -11,6 +11,39 @@ namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
public sealed class Broadphase_Test
{
/// <summary>
/// If we change a grid's map does it still remain not on the general broadphase.
/// </summary>
/// <remarks>
/// Grids are stored on their own broadphase because moving them is costly.
/// </remarks>
[Test]
public void GridMapUpdate()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var mapId1 = mapManager.CreateMap();
var mapId2 = mapManager.CreateMap();
var grid = mapManager.CreateGrid(mapId1);
var xform = entManager.GetComponent<TransformComponent>(grid.GridEntityId);
grid.SetTile(Vector2i.Zero, new Tile(1));
var mapBroadphase1 = entManager.GetComponent<BroadphaseComponent>(mapManager.GetMapEntityId(mapId1));
var mapBroadphase2 = entManager.GetComponent<BroadphaseComponent>(mapManager.GetMapEntityId(mapId2));
entManager.TickUpdate(0.016f, false);
#pragma warning disable NUnit2046
Assert.That(mapBroadphase1.Tree.Count, Is.EqualTo(0));
#pragma warning restore NUnit2046
xform.Coordinates = new EntityCoordinates(mapManager.GetMapEntityId(mapId2), Vector2.Zero);
entManager.TickUpdate(0.016f, false);
#pragma warning disable NUnit2046
Assert.That(mapBroadphase2.Tree.Count, Is.EqualTo(0));
#pragma warning restore NUnit2046
}
/// <summary>
/// If an entity's broadphase is changed are its children's broadphases recursively changed.
/// </summary>
@@ -26,7 +59,7 @@ public sealed class Broadphase_Test
grid.SetTile(Vector2i.Zero, new Tile(1));
var gridBroadphase = entManager.GetComponent<BroadphaseComponent>(grid.GridEntityId);
var mapBroapdhase = entManager.GetComponent<BroadphaseComponent>(mapManager.GetMapEntityId(mapId));
var mapBroadphase = entManager.GetComponent<BroadphaseComponent>(mapManager.GetMapEntityId(mapId));
Assert.That(entManager.EntityQuery<BroadphaseComponent>(true).Count(), Is.EqualTo(2));
@@ -53,8 +86,8 @@ public sealed class Broadphase_Test
// They should get deparented to the map and updated to the map's broadphase instead.
grid.SetTile(Vector2i.Zero, Tile.Empty);
Assert.That(parentBody.Broadphase, Is.EqualTo(mapBroapdhase));
Assert.That(child1Body.Broadphase, Is.EqualTo(mapBroapdhase));
Assert.That(parentBody.Broadphase, Is.EqualTo(mapBroadphase));
Assert.That(child1Body.Broadphase, Is.EqualTo(mapBroadphase));
Assert.That(child2Body.Broadphase, Is.EqualTo(null));
}

View File

@@ -1,60 +1,77 @@
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Client.Physics;
using Robust.Server.Physics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Dynamics.Joints;
using Robust.Shared.Reflection;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics
{
[TestFixture, TestOf(typeof(JointSystem))]
public sealed class Joints_Test : RobustIntegrationTest
public sealed class Joints_Test
{
/// <summary>
/// Simple test that just adds and removes each joint.
/// Assert that if a joint exists between 2 bodies they can collide or not collide correctly.
/// </summary>
/// <returns></returns>
[Test]
public async Task TestJoints()
public void JointsCollidableTest()
{
var server = StartServer();
await server.WaitIdleAsync();
var factory = RobustServerSimulation.NewSimulation();
var server = factory.InitializeInstance();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var fixtureSystem = entManager.EntitySysManager.GetEntitySystem<FixtureSystem>();
var jointSystem = entManager.EntitySysManager.GetEntitySystem<JointSystem>();
var broadphaseSystem = entManager.EntitySysManager.GetEntitySystem<SharedBroadphaseSystem>();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var reflectionManager = server.ResolveDependency<IReflectionManager>();
var typeFactory = server.ResolveDependency<IDynamicTypeFactory>();
var jointSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SharedJointSystem>();
var mapId = mapManager.CreateMap();
/*
await server.WaitAssertion(() =>
var ent1 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var ent2 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var body1 = entManager.AddComponent<PhysicsComponent>(ent1);
var body2 = entManager.AddComponent<PhysicsComponent>(ent2);
body1.BodyType = BodyType.Dynamic;
body2.BodyType = BodyType.Dynamic;
fixtureSystem.TryCreateFixture(body1, new Fixture(new PhysShapeCircle()
{
var mapId = mapManager.CreateMap();
var entA = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var entB = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var bodyA = entA.EnsureComponent<PhysicsComponent>();
var bodyB = entB.EnsureComponent<PhysicsComponent>();
Radius = 0.1f,
}, 1, 1, false));
foreach (var jType in new Joint[]
{
new DistanceJoint(),
new FrictionJoint(),
new RevoluteJoint()
})
{
if (jType.IsAbstract) continue;
var joint = (Joint) typeFactory.CreateInstance(jType);
jointSystem.AddJointDeferred(bodyA, bodyB, joint);
jointSystem.Update(0.016f);
jointSystem.RemoveJointDeferred(joint);
}
});
*/
fixtureSystem.TryCreateFixture(body2, new Fixture(new PhysShapeCircle()
{
Radius = 0.1f,
}, 1, 1, false));
var joint = jointSystem.CreateDistanceJoint(ent1, ent2);
Assert.That(joint.CollideConnected, Is.EqualTo(true));
// Joints are deferred because I hate them so need to make sure it exists
jointSystem.Update(0.016f);
Assert.That(entManager.HasComponent<JointComponent>(ent1), Is.EqualTo(true));
// We should have a contact in both situations.
broadphaseSystem.FindNewContacts(mapId);
Assert.That(body1.Contacts, Has.Count.EqualTo(1));
// Alright now try the other way
jointSystem.RemoveJoint(joint);
joint = jointSystem.CreateDistanceJoint(ent2, ent1);
Assert.That(joint.CollideConnected, Is.EqualTo(true));
jointSystem.Update(0.016f);
Assert.That(entManager.HasComponent<JointComponent>(ent1));
broadphaseSystem.FindNewContacts(mapId);
Assert.That(body1.Contacts, Has.Count.EqualTo(1));
mapManager.DeleteMap(mapId);
}
}
}

View File

@@ -5,11 +5,7 @@ meta:
author: str()
postmapinit: bool()
tilemap: map(str(), key=int())
grids:
- settings:
chunksize: int()
tilesize: int()
chunks: list(include('chunk'), min=1)
grids: list(include('grid'), min=1)
entities: list(include('entity'), min=1)
---
chunk:
@@ -19,6 +15,12 @@ entity:
uid: int()
type: str(required=False)
components: list(comp())
grid:
settings:
chunksize: int()
tilesize: int()
snapsize: int(required=False)
chunks: list(include('chunk'), min=1)
# Example
# meta:

View File

@@ -5,7 +5,4 @@ class Component(Validator):
tag = "comp"
def _is_valid(self, value):
data = yaml.safe_load(value)
if data["type"]:
return True
return False
return 'type' in value