mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
55 Commits
feature/st
...
v0.45.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a636b3b87 | ||
|
|
98ce017b4a | ||
|
|
26b04f0d66 | ||
|
|
f4f2dea688 | ||
|
|
e9a0f9a4c1 | ||
|
|
de438ae94c | ||
|
|
9ec77f20ee | ||
|
|
da01040b52 | ||
|
|
dce2a5ddb2 | ||
|
|
5c99fbabf2 | ||
|
|
9d0846c0e9 | ||
|
|
035ecfb098 | ||
|
|
3693f5aee7 | ||
|
|
b859815b07 | ||
|
|
3701ca83e4 | ||
|
|
1473f1d34c | ||
|
|
889c140fb9 | ||
|
|
c8259915f8 | ||
|
|
a2a25fb296 | ||
|
|
938a9929ea | ||
|
|
7726075b9b | ||
|
|
03b3d1bbe7 | ||
|
|
17ec51b74c | ||
|
|
e92998d1ec | ||
|
|
6691512136 | ||
|
|
a80f4ad76c | ||
|
|
2c6f4cd80c | ||
|
|
51a4c6dcf2 | ||
|
|
1f402e581a | ||
|
|
17ea92bfda | ||
|
|
6a8266af7e | ||
|
|
f6b7606648 | ||
|
|
9cd8adae93 | ||
|
|
c5ba8b75c8 | ||
|
|
3d73cc7289 | ||
|
|
11aa062ee0 | ||
|
|
b4358a9e33 | ||
|
|
0cce4714a1 | ||
|
|
9c4e6a6595 | ||
|
|
1ddd541fe9 | ||
|
|
e45aa3f2fe | ||
|
|
99efdb6061 | ||
|
|
32f0ffdc79 | ||
|
|
cf166483c9 | ||
|
|
49631867f4 | ||
|
|
9f56eaec9a | ||
|
|
04f2b732a5 | ||
|
|
b8cfabc339 | ||
|
|
1cdd39202f | ||
|
|
3290720b4c | ||
|
|
49badb06cb | ||
|
|
2c6941e73b | ||
|
|
5e5883cb88 | ||
|
|
02c504445e | ||
|
|
4d5075a792 |
@@ -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>
|
||||
|
||||
16
Resources/Locale/en-US/client-state-commands.ftl
Normal file
16
Resources/Locale/en-US/client-state-commands.ftl
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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....
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using JetBrains.Annotations;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Robust.Server.GameStates;
|
||||
namespace Robust.Server.GameStates;
|
||||
|
||||
public enum PVSEntityVisiblity : byte
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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} ).");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
21
Robust.Shared/GameObjects/EntityManager.Systems.cs
Normal file
21
Robust.Shared/GameObjects/EntityManager.Systems.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
28
Robust.Shared/GameObjects/IEntityManager.Systems.cs
Normal file
28
Robust.Shared/GameObjects/IEntityManager.Systems.cs
Normal 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;
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
47
Robust.Shared/Network/Messages/MsgStateLeavePvs.cs
Normal file
47
Robust.Shared/Network/Messages/MsgStateLeavePvs.cs
Normal 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;
|
||||
}
|
||||
25
Robust.Shared/Network/Messages/MsgStateRequestFull.cs
Normal file
25
Robust.Shared/Network/Messages/MsgStateRequestFull.cs
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user