Compare commits

...

22 Commits

Author SHA1 Message Date
Acruid
e16732eb7b Network View Bubble (#1629)
* Adds barbones culling.

* Visibility culling and recursive parent ent additions.
DebugEntityNetView improvements.
Visibility moved from session to eyecomponent.

* Multiple viewport support.

* Perf improvements.

* Removed old netbubble system from ServerEntityManager.
Supports old NaN system for entities leaving view.
Supports old SendFullMap optimization for anchored, non-updating Entities.

* Fixes size of netView box.

* Remove empty EntityManager.Update method.
Switching ViewCulling back to PLINQ.
2021-03-29 16:17:34 -07:00
Acruid
91f61bb9de Reverts component NetId storage in ComponentManager back to the way Acruid originally designed it.
Removes NetId methods from IEntity, content does not need to be messing with them.
Fixes bug in DeleteComponent where the ComponentDeleted event was not being raised if a component did not have a NetId.
2021-03-29 03:40:48 -07:00
Pieter-Jan Briers
ddc91d05ec Some work towards multi-monitor support in Clyde.
Most of this was me experimenting with GLFW, but I figured I'd still commit it.
2021-03-28 21:23:38 +02:00
Acruid
ef22842b90 Fixes bug where FirstTimePredicted was not being set properly for the first predicted frame. 2021-03-27 20:16:04 -07:00
Pieter-Jan Briers
303e2152d2 UIScale now updates dynamically.
So if you move the window between different monitors with different scaling, the game updates.
2021-03-28 01:55:35 +01:00
Vera Aguilera Puerto
37fc0d0d2a Set correct class constrains for prototype id list serializers 2021-03-27 22:47:24 +01:00
Vera Aguilera Puerto
53987e1e5d Adds prototype "Variant" helper methods to IPrototypeManager (#1662) 2021-03-27 22:40:07 +01:00
metalgearsloth
3216d7770b Fix net.rate cvar warning (#1659)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-27 02:14:45 -07:00
Acruid
3203ca2ff4 Removed Control.Update from the UI system. UI Controls have no business running code in simulation updates.
Refactored the client update loop so that the GameStateManager is in full control of the simulation update.
2021-03-26 17:46:34 -07:00
metalgearsloth
e22254cd51 Clear velocities on container insertion (#1653)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-26 22:39:11 +01:00
Acruid
7ed722f669 Visibility moved from session to EyeComponent (#1657) 2021-03-26 22:38:45 +01:00
DrSmugleaf
4864096b2a Add prototype id list serializer and tests (#1658)
* Add prototype id list serializer and tests

* Bring old .Value code back

* Paul made me do this
2021-03-26 20:52:22 +01:00
Acruid
5161385de4 Removed unused Update and Resize code from GameStates. Presenters can get resize events from the interface manager (hint: you won't ever need to), and there is no reason for a UI Presenter to do anything in simulation ticks (UI should be event driven, not polling data every frame). 2021-03-25 14:01:51 -07:00
Acruid
98e009b38f Removed the GameController dependency from Clyde.
Removed the ConfigurationManager dependency from FontManager.
2021-03-25 11:36:57 -07:00
Vera Aguilera Puerto
3863ab8f62 Adds PrototypeIdHashSetSerializer for HashSet<string> prototype ID validation (#1656)
* Adds PrototypeIdHashSetSerializer for HashSet<string> prototype ID validation

* Paul changes

* cleanup, better stuff
2021-03-25 14:26:14 +01:00
Metal Gear Sloth
f576eb5125 Optimise showbb 2021-03-25 23:32:06 +11:00
Acruid
314742ccd8 NullableHelper tests now properly set up their required DI container instead of reusing the container from whatever test was ran before it. Service Locator anti-pattern :( 2021-03-25 02:02:09 -07:00
Acruid
f9074811f9 Adds constructor injection to the IoCManager & DependencyCollection. 2021-03-25 01:16:08 -07:00
Pieter-Jan Briers
5f3e1eb378 Frame graph now shows when GCs occur. 2021-03-25 02:24:38 +01:00
Pieter-Jan Briers
3c1ee20ca1 A 2021-03-25 02:05:28 +01:00
Pieter-Jan Briers
3768f5e68e Remove allocs from ContainerSlot.ContainedEntities. 2021-03-25 01:56:06 +01:00
Pieter-Jan Briers
765a560380 Fix integer overflow breaking Lidgren metrics. 2021-03-25 01:47:45 +01:00
89 changed files with 2124 additions and 1523 deletions

View File

@@ -27,8 +27,6 @@ using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
namespace Robust.Client
{
@@ -66,8 +64,6 @@ namespace Robust.Client
IoCManager.Register<IDiscordRichPresence, DiscordRichPresence>();
IoCManager.Register<IClientConsoleHost, ClientConsoleHost>();
IoCManager.Register<IConsoleHost, ClientConsoleHost>();
IoCManager.Register<IFontManager, FontManager>();
IoCManager.Register<IFontManagerInternal, FontManager>();
IoCManager.Register<IMidiManager, MidiManager>();
IoCManager.Register<IAuthManager, AuthManager>();
switch (mode)
@@ -94,8 +90,9 @@ namespace Robust.Client
throw new ArgumentOutOfRangeException();
}
IoCManager.Register<IFontManager, FontManager>();
IoCManager.Register<IFontManagerInternal, FontManager>();
IoCManager.Register<IEyeManager, EyeManager>();
IoCManager.Register<IPlacementManager, PlacementManager>();
IoCManager.Register<IOverlayManager, OverlayManager>();
IoCManager.Register<IOverlayManagerInternal, OverlayManager>();

View File

@@ -0,0 +1,44 @@
using System.Linq;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
[UsedImplicitly]
public sealed class LsMonitorCommand : IConsoleCommand
{
public string Command => "lsmonitor";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var clyde = IoCManager.Resolve<IClyde>();
foreach (var monitor in clyde.EnumerateMonitors())
{
shell.WriteLine(
$"[{monitor.Id}] {monitor.Name}: {monitor.Size.X}x{monitor.Size.Y}@{monitor.RefreshRate}Hz");
}
}
}
[UsedImplicitly]
public sealed class SetMonitorCommand : IConsoleCommand
{
public string Command => "setmonitor";
public string Description => "";
public string Help => "Usage: setmonitor <id>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var clyde = IoCManager.Resolve<IClyde>();
var id = int.Parse(args[0]);
var monitor = clyde.EnumerateMonitors().Single(m => m.Id == id);
clyde.SetWindowMonitor(monitor);
}
}
}

View File

@@ -161,6 +161,8 @@ namespace Robust.Client.Debugging
if (viewport.IsEmpty()) return;
var mapId = _eyeManager.CurrentMap;
var sleepThreshold = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.TimeToSleep);
var colorEdge = Color.Red.WithAlpha(0.33f);
foreach (var physBody in EntitySystem.Get<SharedBroadPhaseSystem>().GetCollidingEntities(mapId, viewport))
{
@@ -170,9 +172,6 @@ namespace Robust.Client.Debugging
var worldBox = physBody.GetWorldAABB();
if (worldBox.IsEmpty()) continue;
var colorEdge = Color.Red.WithAlpha(0.33f);
var sleepThreshold = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.TimeToSleep);
foreach (var fixture in physBody.Fixtures)
{
var shape = fixture.Shape;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Management;
using System.Net;
@@ -82,65 +82,8 @@ namespace Robust.Client
public bool Startup(Func<ILogHandler>? logHandlerFactory = null)
{
ReadInitialLaunchState();
SetupLogging(_logManager, logHandlerFactory ?? (() => new ConsoleLogHandler()));
_taskManager.Initialize();
// Figure out user data directory.
var userDataDir = GetUserDataDir();
_configurationManager.Initialize(false);
// MUST load cvars before loading from config file so the cfg manager is aware of secure cvars.
// So SECURE CVars are blacklisted from config.
_configurationManager.LoadCVarsFromAssembly(typeof(GameController).Assembly); // Client
_configurationManager.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Shared
if (LoadConfigAndUserData)
{
var configFile = Path.Combine(userDataDir, "client_config.toml");
if (File.Exists(configFile))
{
// Load config from user data if available.
_configurationManager.LoadFromFile(configFile);
}
else
{
// Else we just use code-defined defaults and let it save to file when the user changes things.
_configurationManager.SetSaveFile(configFile);
}
}
_configurationManager.OverrideConVars(EnvironmentVariables.GetEnvironmentCVars());
if (_commandLineArgs != null)
{
_configurationManager.OverrideConVars(_commandLineArgs.CVars);
}
ProfileOptSetup.Setup(_configurationManager);
_resourceCache.Initialize(LoadConfigAndUserData ? userDataDir : null);
ProgramShared.DoMounts(_resourceCache, _commandLineArgs?.MountOptions, "Content.Client", _loaderArgs != null);
if (_loaderArgs != null)
{
_stringSerializer.EnableCaching = false;
_resourceCache.MountLoaderApi(_loaderArgs.FileApi, "Resources/");
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
// Bring display up as soon as resources are mounted.
if (!_clyde.Initialize())
{
if (!StartupSystemSplash(logHandlerFactory))
return false;
}
_clyde.SetWindowTitle("Space Station 14");
_fontManager.Initialize();
// Disable load context usage on content start.
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
@@ -205,6 +148,77 @@ namespace Robust.Client
return true;
}
private bool StartupSystemSplash(Func<ILogHandler>? logHandlerFactory)
{
ReadInitialLaunchState();
SetupLogging(_logManager, logHandlerFactory ?? (() => new ConsoleLogHandler()));
_taskManager.Initialize();
// Figure out user data directory.
var userDataDir = GetUserDataDir();
_configurationManager.Initialize(false);
// MUST load cvars before loading from config file so the cfg manager is aware of secure cvars.
// So SECURE CVars are blacklisted from config.
_configurationManager.LoadCVarsFromAssembly(typeof(GameController).Assembly); // Client
_configurationManager.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Shared
if (LoadConfigAndUserData)
{
var configFile = Path.Combine(userDataDir, "client_config.toml");
if (File.Exists(configFile))
{
// Load config from user data if available.
_configurationManager.LoadFromFile(configFile);
}
else
{
// Else we just use code-defined defaults and let it save to file when the user changes things.
_configurationManager.SetSaveFile(configFile);
}
}
_configurationManager.OverrideConVars(EnvironmentVariables.GetEnvironmentCVars());
if (_commandLineArgs != null)
{
_configurationManager.OverrideConVars(_commandLineArgs.CVars);
}
ProfileOptSetup.Setup(_configurationManager);
_resourceCache.Initialize(LoadConfigAndUserData ? userDataDir : null);
ProgramShared.DoMounts(_resourceCache, _commandLineArgs?.MountOptions, "Content.Client", _loaderArgs != null);
if (_loaderArgs != null)
{
_stringSerializer.EnableCaching = false;
_resourceCache.MountLoaderApi(_loaderArgs.FileApi, "Resources/");
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
_clyde.TextEntered += TextEntered;
_clyde.MouseMove += MouseMove;
_clyde.KeyUp += KeyUp;
_clyde.KeyDown += KeyDown;
_clyde.MouseWheel += MouseWheel;
_clyde.CloseWindow += Shutdown;
// Bring display up as soon as resources are mounted.
if (!_clyde.Initialize())
{
return false;
}
_clyde.SetWindowTitle("Space Station 14");
_fontManager.SetFontDpi((uint) _configurationManager.GetCVar(CVars.DisplayFontDpi));
return true;
}
private Stream? VerifierExtraLoadHandler(string arg)
{
DebugTools.AssertNotNull(_loaderArgs);
@@ -278,17 +292,13 @@ namespace Robust.Client
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
_timerManager.UpdateTimers(frameEventArgs);
_taskManager.ProcessPendingTasks();
_userInterfaceManager.Update(frameEventArgs);
// GameStateManager is in full control of the simulation update.
if (_client.RunLevel >= ClientRunLevel.Connected)
{
_componentManager.CullRemovedComponents();
_gameStateManager.ApplyGameState();
_entityManager.Update(frameEventArgs.DeltaSeconds);
_playerManager.Update(frameEventArgs.DeltaSeconds);
}
_stateManager.Update(frameEventArgs);
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
}

View File

@@ -34,7 +34,7 @@ namespace Robust.Client.GameObjects
_networkManager.RegisterNetMessage<MsgEntity>(MsgEntity.NAME, HandleEntityNetworkMessage);
}
public void Update()
public void TickUpdate()
{
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
{

View File

@@ -1,4 +1,4 @@
using Robust.Client.Graphics;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -152,6 +152,7 @@ namespace Robust.Client.GameObjects
Zoom = state.Zoom;
Offset = state.Offset;
Rotation = state.Rotation;
VisibilityMask = state.VisibilityMask;
}
public override void OnRemove()

View File

@@ -2150,11 +2150,6 @@ namespace Robust.Client.GameObjects
return null!;
}
public IComponent GetComponent(uint netID)
{
return null!;
}
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component) where T : class
{
component = null;
@@ -2181,17 +2176,6 @@ namespace Robust.Client.GameObjects
return null;
}
public bool TryGetComponent(uint netId, [NotNullWhen(true)] out IComponent? component)
{
component = null;
return false;
}
public IComponent? GetComponentOrNull(uint netId)
{
return null;
}
public void Delete()
{
}

View File

@@ -242,65 +242,67 @@ namespace Robust.Client.GameStates
if (!Predicting) return;
using var _ = _timing.StartPastPredictionArea();
if (_pendingInputs.Count > 0)
using(var _ = _timing.StartPastPredictionArea())
{
Logger.DebugS(CVars.NetPredict.Name, "CL> Predicted:");
if (_pendingInputs.Count > 0)
{
Logger.DebugS(CVars.NetPredict.Name, "CL> Predicted:");
}
var pendingInputEnumerator = _pendingInputs.GetEnumerator();
var pendingMessagesEnumerator = _pendingSystemMessages.GetEnumerator();
var hasPendingInput = pendingInputEnumerator.MoveNext();
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;
// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");
for (var t = _lastProcessedTick.Value + 1; t <= targetTick; t++)
{
var tick = new GameTick(t);
_timing.CurTick = tick;
while (hasPendingInput && pendingInputEnumerator.Current.Tick <= tick)
{
var inputCmd = pendingInputEnumerator.Current;
_inputManager.NetworkBindMap.TryGetKeyFunction(inputCmd.InputFunctionId, out var boundFunc);
Logger.DebugS(CVars.NetPredict.Name,
$" seq={inputCmd.InputSequence}, sub={inputCmd.SubTick}, dTick={tick}, func={boundFunc.FunctionName}, " +
$"state={inputCmd.State}");
input.PredictInputCommand(inputCmd);
hasPendingInput = pendingInputEnumerator.MoveNext();
}
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= tick)
{
var msg = pendingMessagesEnumerator.Current.msg;
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
}
if (t != targetTick)
{
// Don't run EntitySystemManager.TickUpdate if this is the target tick,
// because the rest of the main loop will call into it with the target tick later,
// and it won't be a past prediction.
_entitySystemManager.TickUpdate((float) _timing.TickPeriod.TotalSeconds);
((IBroadcastEventBusInternal) _entities.EventBus).ProcessEventQueue();
}
}
}
var pendingInputEnumerator = _pendingInputs.GetEnumerator();
var pendingMessagesEnumerator = _pendingSystemMessages.GetEnumerator();
var hasPendingInput = pendingInputEnumerator.MoveNext();
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;
// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");
for (var t = _lastProcessedTick.Value + 1; t <= targetTick; t++)
{
var tick = new GameTick(t);
_timing.CurTick = tick;
while (hasPendingInput && pendingInputEnumerator.Current.Tick <= tick)
{
var inputCmd = pendingInputEnumerator.Current;
_inputManager.NetworkBindMap.TryGetKeyFunction(inputCmd.InputFunctionId, out var boundFunc);
Logger.DebugS(CVars.NetPredict.Name,
$" seq={inputCmd.InputSequence}, sub={inputCmd.SubTick}, dTick={tick}, func={boundFunc.FunctionName}, " +
$"state={inputCmd.State}");
input.PredictInputCommand(inputCmd);
hasPendingInput = pendingInputEnumerator.MoveNext();
}
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= tick)
{
var msg = pendingMessagesEnumerator.Current.msg;
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
}
if (t != targetTick)
{
// Don't run EntitySystemManager.Update if this is the target tick,
// because the rest of the main loop will call into it with the target tick later,
// and it won't be a past prediction.
_entitySystemManager.Update((float) _timing.TickPeriod.TotalSeconds);
((IBroadcastEventBusInternal) _entities.EventBus).ProcessEventQueue();
}
}
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds);
}
private void ResetPredictedEntities(GameTick curTick)

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
@@ -9,6 +10,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
@@ -28,7 +30,7 @@ namespace Robust.Client.GameStates
private const int TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
/// <inheritdoc />
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
private readonly Font _font;
private readonly int _lineHeight;
@@ -95,9 +97,9 @@ namespace Robust.Client.GameStates
}
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize*2, pvsSize*2));
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++)
@@ -130,16 +132,52 @@ namespace Robust.Client.GameStates
if (!_netManager.IsConnected)
return;
switch (currentSpace)
{
case OverlaySpace.ScreenSpace:
DrawScreen(handle);
break;
case OverlaySpace.WorldSpace:
DrawWorld(handle);
break;
}
}
private void DrawWorld(DrawingHandleBase handle)
{
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
if(!pvsEnabled)
return;
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize, pvsSize));
var worldHandle = (DrawingHandleWorld)handle;
worldHandle.DrawRect(pvsBox, Color.Red, false);
}
private void DrawScreen(DrawingHandleBase handle)
{
// remember, 0,0 is top left of ui with +X right and +Y down
var screenHandle = (DrawingHandleScreen)handle;
var screenHandle = (DrawingHandleScreen) handle;
for (int i = 0; i < _netEnts.Count; i++)
{
var netEnt = _netEnts[i];
if (!_entityManager.TryGetEntity(netEnt.Id, out var ent))
{
_netEnts.RemoveSwap(i);
i--;
continue;
}
var xPos = 100;
var yPos = 10 + _lineHeight * i;
var name = $"({netEnt.Id}) {_entityManager.GetEntity(netEnt.Id).Prototype?.ID}";
var name = $"({netEnt.Id}) {ent.Prototype?.ID}";
var color = CalcTextColor(ref netEnt);
DrawString(screenHandle, _font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
DrawTrafficBox(screenHandle, ref netEnt, xPos, yPos);
@@ -225,7 +263,7 @@ namespace Robust.Client.GameStates
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
shell.WriteError("Invalid argument amount. Expected 1 arguments.");
return;
}

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
@@ -36,6 +38,10 @@ namespace Robust.Client.GameStates
private readonly List<(GameTick Tick, int Payload, int lag, int interp)> _history = new(HistorySize+10);
private int _totalHistoryPayload; // sum of all data point sizes in bytes
public EntityUid WatchEntId { get; set; }
public NetGraphOverlay()
{
IoCManager.InjectDependencies(this);
@@ -60,7 +66,73 @@ namespace Robust.Client.GameStates
// calc interp info
var interpBuff = _gameStateManager.CurrentBufferSize - _gameStateManager.MinBufferSize;
_totalHistoryPayload += sz;
_history.Add((toSeq, sz, lag, interpBuff));
// not watching an ent
if(!WatchEntId.IsValid() || WatchEntId.IsClientSide())
return;
string? entStateString = null;
string? entDelString = null;
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
var entStates = args.AppliedState.EntityStates;
if (entStates is not null)
{
var sb = new StringBuilder();
foreach (var entState in entStates)
{
if (entState.Uid == WatchEntId)
{
if(entState.ComponentChanges is not null)
{
sb.Append($"\n Changes:");
foreach (var compChange in entState.ComponentChanges)
{
var del = compChange.Deleted ? 'D' : 'C';
sb.Append($"\n [{del}]{compChange.NetID}:{compChange.ComponentName}");
}
}
if (entState.ComponentStates is not null)
{
sb.Append($"\n States:");
foreach (var compState in entState.ComponentStates)
{
sb.Append($"\n {compState.NetID}:{compState.GetType().Name}");
}
}
}
}
entStateString = sb.ToString();
}
var entDeletes = args.AppliedState.EntityDeletions;
if (entDeletes is not null)
{
var sb = new StringBuilder();
foreach (var entDelete in entDeletes)
{
if (entDelete == WatchEntId)
{
entDelString = "\n Deleted";
}
}
}
if (!string.IsNullOrWhiteSpace(entStateString) || !string.IsNullOrWhiteSpace(entDelString))
{
var fullString = $"watchEnt: from={args.AppliedState.FromSequence}, to={args.AppliedState.ToSequence}, eid={WatchEntId}";
if (!string.IsNullOrWhiteSpace(entStateString))
fullString += entStateString;
if (!string.IsNullOrWhiteSpace(entDelString))
fullString += entDelString;
conShell.WriteLine(fullString + "\n");
}
}
/// <inheritdoc />
@@ -69,10 +141,16 @@ namespace Robust.Client.GameStates
base.FrameUpdate(args);
var over = _history.Count - HistorySize;
if (over > 0)
if (over <= 0)
return;
for (int i = 0; i < over; i++)
{
_history.RemoveRange(0, over);
var point = _history[i];
_totalHistoryPayload -= point.Payload;
}
_history.RemoveRange(0, over);
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
@@ -82,6 +160,7 @@ namespace Robust.Client.GameStates
var leftMargin = 300;
var width = HistorySize;
var height = 500;
var drawSizeThreshold = Math.Min(_totalHistoryPayload / HistorySize, 300);
// bottom payload line
handle.DrawLine(new Vector2(leftMargin, height), new Vector2(leftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
@@ -101,6 +180,12 @@ namespace Robust.Client.GameStates
var yoff = height - state.Payload / BytesPerPixel;
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LightGreen.WithAlpha(0.8f));
// Draw size if above average
if (drawSizeThreshold * 1.5 < state.Payload)
{
DrawString((DrawingHandleScreen) handle, _font, new Vector2(xOff, yoff - _font.GetLineHeight(1)), state.Payload.ToString());
}
// second tick marks
if (state.Tick.Value % _gameTiming.TickRate == 0)
{
@@ -125,6 +210,10 @@ namespace Robust.Client.GameStates
handle.DrawLine(new Vector2(xOff, height + LowerGraphOffset), new Vector2(xOff, height + LowerGraphOffset + state.interp * 6), interpColor.WithAlpha(0.8f));
}
// average payload line
var avgyoff = height - drawSizeThreshold / BytesPerPixel;
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));
@@ -197,5 +286,36 @@ namespace Robust.Client.GameStates
}
}
}
private class NetWatchEntCommand : IConsoleCommand
{
public string Command => "net_watchent";
public string Help => "net_watchent <0|EntityUid>";
public string Description => "Dumps all network updates for an EntityId to the console.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 1 argument.");
return;
}
if (!EntityUid.TryParse(args[0], out var eValue))
{
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
return;
}
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
var netOverlay = overlayMan.GetOverlay<NetGraphOverlay>();
netOverlay.WatchEntId = eValue;
}
}
}
}
}

View File

@@ -53,7 +53,7 @@ namespace Robust.Client.Graphics.Clyde
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_configurationManager.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
ConfigurationManager.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
}
private void _audioCreateContext()
@@ -81,7 +81,7 @@ namespace Robust.Client.Graphics.Clyde
private void _audioOpenDevice()
{
var preferredDevice = _configurationManager.GetCVar(CVars.AudioDevice);
var preferredDevice = ConfigurationManager.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Log;
@@ -115,7 +115,7 @@ namespace Robust.Client.Graphics.Clyde
var prev = cap;
var cVarName = $"display.ogl_block_{capName}";
var block = _configurationManager.GetCVar<bool>(cVarName);
var block = ConfigurationManager.GetCVar<bool>(cVarName);
if (block)
{
@@ -146,7 +146,7 @@ namespace Robust.Client.Graphics.Clyde
foreach (var cvar in cvars)
{
_configurationManager.RegisterCVar($"display.ogl_block_{cvar}", false);
ConfigurationManager.RegisterCVar($"display.ogl_block_{cvar}", false);
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using OpenToolkit;
using OpenToolkit.Graphics.OpenGL4;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Client.Utility;
@@ -14,6 +15,7 @@ using Robust.Shared;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using static Robust.Client.Utility.LiterallyJustMessageBox;
@@ -46,6 +48,7 @@ namespace Robust.Client.Graphics.Clyde
{
// Keep delegates around to prevent GC issues.
private GLFWCallbacks.ErrorCallback _errorCallback = default!;
private GLFWCallbacks.MonitorCallback _monitorCallback = default!;
private GLFWCallbacks.CharCallback _charCallback = default!;
private GLFWCallbacks.CursorPosCallback _cursorPosCallback = default!;
private GLFWCallbacks.KeyCallback _keyCallback = default!;
@@ -73,6 +76,18 @@ namespace Robust.Client.Graphics.Clyde
private Vector2 _lastMousePos;
// Can't use ClydeHandle because it's 64 bit.
private int _nextWindowId = 1;
private readonly Dictionary<int, MonitorReg> _monitors = new();
public event Action<TextEventArgs>? TextEntered;
public event Action<MouseMoveEventArgs>? MouseMove;
public event Action<KeyEventArgs>? KeyUp;
public event Action<KeyEventArgs>? KeyDown;
public event Action<MouseWheelEventArgs>? MouseWheel;
public event Action<string>? CloseWindow;
public event Action? OnWindowScaleChanged;
// NOTE: in engine we pretend the framebuffer size is the screen size..
// For practical reasons like UI rendering.
public override Vector2i ScreenSize => _framebufferSize;
@@ -148,24 +163,71 @@ namespace Robust.Client.Graphics.Clyde
return false;
}
InitMonitors();
InitCursors();
return InitWindow();
}
private void InitMonitors()
{
var monitors = GLFW.GetMonitorsRaw(out var count);
for (var i = 0; i < count; i++)
{
SetupMonitor(monitors[i]);
}
}
private void SetupMonitor(Monitor* monitor)
{
var handle = _nextWindowId++;
DebugTools.Assert(GLFW.GetMonitorUserPointer(monitor) == null, "GLFW window already has user pointer??");
var name = GLFW.GetMonitorName(monitor);
var videoMode = GLFW.GetVideoMode(monitor);
var impl = new ClydeMonitorImpl(handle, name, (videoMode->Width, videoMode->Height), videoMode->RefreshRate);
GLFW.SetMonitorUserPointer(monitor, (void*) handle);
_monitors[handle] = new MonitorReg
{
Id = handle,
Impl = impl,
Monitor = monitor
};
}
private void DestroyMonitor(Monitor* monitor)
{
var ptr = GLFW.GetMonitorUserPointer(monitor);
if (ptr == null)
{
var name = GLFW.GetMonitorName(monitor);
Logger.WarningS("clyde.win", $"Monitor '{name}' had no user pointer set??");
return;
}
_monitors.Remove((int) ptr);
GLFW.SetMonitorUserPointer(monitor, null);
}
private bool InitWindow()
{
var width = _configurationManager.GetCVar(CVars.DisplayWidth);
var height = _configurationManager.GetCVar(CVars.DisplayHeight);
var width = ConfigurationManager.GetCVar(CVars.DisplayWidth);
var height = ConfigurationManager.GetCVar(CVars.DisplayHeight);
Monitor* monitor = null;
if (WindowMode == WindowMode.Fullscreen)
{
monitor = GLFW.GetPrimaryMonitor();
monitor = GLFW.GetMonitors()[1];
var mode = GLFW.GetVideoMode(monitor);
width = mode->Width;
height = mode->Height;
GLFW.WindowHint(WindowHintInt.RefreshRate, mode->RefreshRate);
}
#if DEBUG
@@ -174,13 +236,16 @@ namespace Robust.Client.Graphics.Clyde
GLFW.WindowHint(WindowHintString.X11ClassName, "SS14");
GLFW.WindowHint(WindowHintString.X11InstanceName, "SS14");
var renderer = (Renderer) _configurationManager.GetCVar<int>(CVars.DisplayRenderer);
var renderer = (Renderer) ConfigurationManager.GetCVar<int>(CVars.DisplayRenderer);
Span<Renderer> renderers = (renderer == Renderer.Default) ? stackalloc Renderer[] {
Renderer.OpenGL33,
Renderer.OpenGL31,
Renderer.OpenGLES2
} : stackalloc Renderer[] {renderer};
Span<Renderer> renderers = (renderer == Renderer.Default)
? stackalloc Renderer[]
{
Renderer.OpenGL33,
Renderer.OpenGL31,
Renderer.OpenGLES2
}
: stackalloc Renderer[] {renderer};
foreach (Renderer r in renderers)
{
@@ -193,6 +258,7 @@ namespace Robust.Client.Graphics.Clyde
_isCore = renderer == Renderer.OpenGL33;
break;
}
// Window failed to init due to error.
// Try not to treat the error code seriously.
var code = GLFW.GetError(out string desc);
@@ -206,9 +272,9 @@ namespace Robust.Client.Graphics.Clyde
var code = GLFW.GetError(out string desc);
var errorContent = "Failed to create the game window. " +
"This probably means your GPU is too old to play the game. " +
"That or update your graphic drivers\n" +
$"The exact error is: [{code}]\n {desc}";
"This probably means your GPU is too old to play the game. " +
"That or update your graphic drivers\n" +
$"The exact error is: [{code}]\n {desc}";
MessageBoxW(null,
errorContent,
@@ -301,6 +367,7 @@ namespace Robust.Client.Graphics.Clyde
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
GLFW.WindowHint(WindowHintBool.SrgbCapable, false);
}
_glfwWindow = GLFW.CreateWindow(width, height, string.Empty, monitor, null);
}
}
@@ -393,11 +460,30 @@ namespace Robust.Client.Graphics.Clyde
Logger.ErrorS("clyde.win.glfw", "GLFW Error: [{0}] {1}", code, description);
}
private void OnGlfwMonitor(Monitor* monitor, ConnectedState state)
{
try
{
if (state == ConnectedState.Connected)
{
SetupMonitor(monitor);
}
else
{
DestroyMonitor(monitor);
}
}
catch (Exception e)
{
CatchCallbackException(e);
}
}
private void OnGlfwChar(Window* window, uint codepoint)
{
try
{
_gameController.TextEntered(new TextEventArgs(codepoint));
TextEntered?.Invoke(new TextEventArgs(codepoint));
}
catch (Exception e)
{
@@ -413,8 +499,7 @@ namespace Robust.Client.Graphics.Clyde
var delta = newPos - _lastMousePos;
_lastMousePos = newPos;
var ev = new MouseMoveEventArgs(delta, newPos);
_gameController.MouseMove(ev);
MouseMove?.Invoke(new MouseMoveEventArgs(delta, newPos));
}
catch (Exception e)
{
@@ -461,11 +546,11 @@ namespace Robust.Client.Graphics.Clyde
switch (action)
{
case InputAction.Release:
_gameController.KeyUp(ev);
KeyUp?.Invoke(ev);
break;
case InputAction.Press:
case InputAction.Repeat:
_gameController.KeyDown(ev);
KeyDown?.Invoke(ev);
break;
default:
throw new ArgumentOutOfRangeException(nameof(action), action, null);
@@ -477,7 +562,7 @@ namespace Robust.Client.Graphics.Clyde
try
{
var ev = new MouseWheelEventArgs(((float) offsetX, (float) offsetY), _lastMousePos);
_gameController.MouseWheel(ev);
MouseWheel?.Invoke(ev);
}
catch (Exception e)
{
@@ -489,7 +574,7 @@ namespace Robust.Client.Graphics.Clyde
{
try
{
_gameController.Shutdown("Window closed");
CloseWindow?.Invoke("Window closed");
}
catch (Exception e)
{
@@ -533,6 +618,7 @@ namespace Robust.Client.Graphics.Clyde
try
{
_windowScale = (xScale, yScale);
OnWindowScaleChanged?.Invoke();
}
catch (Exception e)
{
@@ -568,6 +654,7 @@ namespace Robust.Client.Graphics.Clyde
private void StoreCallbacks()
{
_errorCallback = OnGlfwError;
_monitorCallback = OnGlfwMonitor;
_charCallback = OnGlfwChar;
_cursorPosCallback = OnGlfwCursorPos;
_keyCallback = OnGlfwKey;
@@ -590,6 +677,19 @@ namespace Robust.Client.Graphics.Clyde
GLFW.SetWindowTitle(_glfwWindow, title);
}
public void SetWindowMonitor(IClydeMonitor monitor)
{
var monitorImpl = (ClydeMonitorImpl) monitor;
var reg = _monitors[monitorImpl.Id];
GLFW.SetWindowMonitor(
_glfwWindow,
reg.Monitor,
0, 0,
monitorImpl.Size.X, monitorImpl.Size.Y,
monitorImpl.RefreshRate);
}
public void RequestWindowAttention()
{
GLFW.RequestWindowAttention(_glfwWindow);
@@ -654,18 +754,40 @@ namespace Robust.Client.Graphics.Clyde
GLFW.GetWindowPos(_glfwWindow, out var x, out var y);
_prevWindowPos = (x, y);
var monitor = GLFW.GetPrimaryMonitor();
var monitor = MonitorForWindow(_glfwWindow);
var mode = GLFW.GetVideoMode(monitor);
GLFW.SetWindowMonitor(_glfwWindow, GLFW.GetPrimaryMonitor(), 0, 0, mode->Width, mode->Height,
GLFW.SetWindowMonitor(_glfwWindow, monitor, 0, 0, mode->Width, mode->Height,
mode->RefreshRate);
}
else
{
GLFW.SetWindowMonitor(_glfwWindow, null, _prevWindowPos.X, _prevWindowPos.Y, _prevWindowSize.X, _prevWindowSize.Y, 0);
GLFW.SetWindowMonitor(_glfwWindow, null, _prevWindowPos.X, _prevWindowPos.Y, _prevWindowSize.X,
_prevWindowSize.Y, 0);
}
}
// glfwGetWindowMonitor only works for fullscreen windows.
// Picks the monitor with the top-left corner of the window.
private Monitor* MonitorForWindow(Window* window)
{
GLFW.GetWindowPos(window, out var winPosX, out var winPosY);
var monitors = GLFW.GetMonitorsRaw(out var count);
for (var i = 0; i < count; i++)
{
var monitor = monitors[i];
GLFW.GetMonitorPos(monitor, out var monPosX, out var monPosY);
var videoMode = GLFW.GetVideoMode(monitor);
var box = Box2i.FromDimensions(monPosX, monPosY, videoMode->Width, videoMode->Height);
if (box.Contains(winPosX, winPosY))
return monitor;
}
// Fallback
return GLFW.GetPrimaryMonitor();
}
string IClipboardManager.GetText()
{
return GLFW.GetClipboardString(_glfwWindow);
@@ -676,6 +798,11 @@ namespace Robust.Client.Graphics.Clyde
GLFW.SetClipboardString(_glfwWindow, text);
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
{
return _monitors.Values.Select(c => c.Impl);
}
// We can't let exceptions unwind into GLFW, as that can cause the CLR to crash.
// And it probably messes up GLFW too.
// So all the callbacks are passed to this method.
@@ -689,5 +816,28 @@ namespace Robust.Client.Graphics.Clyde
_glfwExceptionList.Add(e);
}
private sealed class MonitorReg
{
public int Id;
public Monitor* Monitor;
public ClydeMonitorImpl Impl = default!;
}
private sealed class ClydeMonitorImpl : IClydeMonitor
{
public ClydeMonitorImpl(int id, string name, Vector2i size, int refreshRate)
{
Id = id;
Name = name;
Size = size;
RefreshRate = refreshRate;
}
public int Id { get; }
public string Name { get; }
public Vector2i Size { get; }
public int RefreshRate { get; }
}
}
}

View File

@@ -87,7 +87,7 @@ namespace Robust.Client.Graphics.Clyde
{
base.Initialize();
_configurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
ConfigurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
if (!InitWindowing())
{
@@ -124,9 +124,9 @@ namespace Robust.Client.Graphics.Clyde
protected override void ReadConfig()
{
base.ReadConfig();
_lightmapDivider = _configurationManager.GetCVar(CVars.DisplayLightMapDivider);
_maxLightsPerScene = _configurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
_enableSoftShadows = _configurationManager.GetCVar(CVars.DisplaySoftShadows);
_lightmapDivider = ConfigurationManager.GetCVar(CVars.DisplayLightMapDivider);
_maxLightsPerScene = ConfigurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
_enableSoftShadows = ConfigurationManager.GetCVar(CVars.DisplaySoftShadows);
}
protected override void ReloadConfig()
@@ -238,7 +238,7 @@ namespace Robust.Client.Graphics.Clyde
private (int major, int minor)? ParseGLOverrideVersion()
{
var overrideGLVersion = _configurationManager.GetCVar(CVars.DisplayOGLOverrideVersion);
var overrideGLVersion = ConfigurationManager.GetCVar(CVars.DisplayOGLOverrideVersion);
if (string.IsNullOrEmpty(overrideGLVersion))
{
return null;

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using JetBrains.Annotations;
using Robust.Client.Audio;
@@ -37,6 +38,13 @@ namespace Robust.Client.Graphics.Clyde
public IClydeDebugInfo DebugInfo { get; } = new DummyDebugInfo();
public IClydeDebugStats DebugStats { get; } = new DummyDebugStats();
public event Action<TextEventArgs>? TextEntered;
public event Action<MouseMoveEventArgs>? MouseMove;
public event Action<KeyEventArgs>? KeyUp;
public event Action<KeyEventArgs>? KeyDown;
public event Action<MouseWheelEventArgs>? MouseWheel;
public event Action<string>? CloseWindow;
public Texture GetStockTexture(ClydeStockTexture stockTexture)
{
return new DummyTexture((1, 1));
@@ -63,6 +71,11 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public void SetWindowMonitor(IClydeMonitor monitor)
{
// Nada.
}
public void RequestWindowAttention()
{
// Nada.
@@ -86,6 +99,12 @@ namespace Robust.Client.Graphics.Clyde
remove { }
}
public event Action OnWindowScaleChanged
{
add { }
remove { }
}
public void Render()
{
// Nada.
@@ -156,6 +175,12 @@ namespace Robust.Client.Graphics.Clyde
return new Viewport();
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
{
// TODO: Actually return something.
yield break;
}
public ClydeHandle LoadShader(ParsedShader shader, string? name = null)
{
return default;

View File

@@ -18,8 +18,7 @@ namespace Robust.Client.Graphics
/// </summary>
internal abstract class ClydeBase
{
[Dependency] protected readonly IConfigurationManager _configurationManager = default!;
[Dependency] protected readonly IGameControllerInternal _gameController = default!;
[Dependency] protected readonly IConfigurationManager ConfigurationManager = default!;
protected WindowMode WindowMode { get; private set; } = WindowMode.Windowed;
protected bool VSync { get; private set; } = true;
@@ -31,11 +30,11 @@ namespace Robust.Client.Graphics
public virtual bool Initialize()
{
_configurationManager.OnValueChanged(CVars.DisplayVSync, _vSyncChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayWindowMode, _windowModeChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
_configurationManager.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayVSync, _vSyncChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayWindowMode, _windowModeChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
return true;
}
@@ -51,8 +50,8 @@ namespace Robust.Client.Graphics
protected virtual void ReadConfig()
{
WindowMode = (WindowMode) _configurationManager.GetCVar(CVars.DisplayWindowMode);
VSync = _configurationManager.GetCVar(CVars.DisplayVSync);
WindowMode = (WindowMode) ConfigurationManager.GetCVar(CVars.DisplayWindowMode);
VSync = ConfigurationManager.GetCVar(CVars.DisplayVSync);
}
private void _vSyncChanged(bool newValue)

View File

@@ -4,9 +4,6 @@ using System.IO;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SharpFont;
@@ -20,18 +17,18 @@ namespace Robust.Client.Graphics
private const int SheetWidth = 256;
private const int SheetHeight = 256;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IClyde _clyde = default!;
private readonly IClyde _clyde;
private uint BaseFontDPI;
private uint _baseFontDpi = 96;
private readonly Library _library;
private readonly Dictionary<(FontFaceHandle, int fontSize), FontInstanceHandle> _loadedInstances =
new();
public FontManager()
public FontManager(IClyde clyde)
{
_clyde = clyde;
_library = new Library();
}
@@ -42,9 +39,9 @@ namespace Robust.Client.Graphics
return handle;
}
void IFontManagerInternal.Initialize()
void IFontManagerInternal.SetFontDpi(uint fontDpi)
{
BaseFontDPI = (uint) _configuration.GetCVar(CVars.DisplayFontDpi);
_baseFontDpi = fontDpi;
}
public IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size)
@@ -64,7 +61,7 @@ namespace Robust.Client.Graphics
private ScaledFontData _generateScaledDatum(FontInstanceHandle instance, float scale)
{
var ftFace = instance.FaceHandle.Face;
ftFace.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
ftFace.SetCharSize(0, instance.Size, 0, (uint) (_baseFontDpi * scale));
var ascent = ftFace.Size.Metrics.Ascender.ToInt32();
var descent = -ftFace.Size.Metrics.Descender.ToInt32();
@@ -83,7 +80,7 @@ namespace Robust.Client.Graphics
return;
var face = instance.FaceHandle.Face;
face.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
face.SetCharSize(0, instance.Size, 0, (uint) (_baseFontDpi * scale));
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
face.Glyph.RenderGlyph(RenderMode.Normal);
@@ -189,7 +186,7 @@ namespace Robust.Client.Graphics
OwnedTexture GenSheet()
{
var sheet = _clyde.CreateBlankTexture<A8>((SheetWidth, SheetHeight),
$"font-{face.FamilyName}-{instance.Size}-{(uint) (BaseFontDPI * scale)}-sheet{scaled.AtlasTextures.Count}");
$"font-{face.FamilyName}-{instance.Size}-{(uint) (_baseFontDpi * scale)}-sheet{scaled.AtlasTextures.Count}");
scaled.AtlasTextures.Add(sheet);
return sheet;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Robust.Shared.Maths;
@@ -21,6 +22,7 @@ namespace Robust.Client.Graphics
Vector2 DefaultWindowScale { get; }
void SetWindowTitle(string title);
void SetWindowMonitor(IClydeMonitor monitor);
/// <summary>
/// This is the magic method to make the game window ping you in the task bar.
@@ -31,6 +33,8 @@ namespace Robust.Client.Graphics
event Action<WindowFocusedEventArgs> OnWindowFocused;
event Action OnWindowScaleChanged;
OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParams = null);
@@ -104,6 +108,8 @@ namespace Robust.Client.Graphics
}
IClydeViewport CreateViewport(Vector2i size, string? name = null);
IEnumerable<IClydeMonitor> EnumerateMonitors();
}
// TODO: Maybe implement IDisposable for render targets. I got lazy and didn't.

View File

@@ -1,3 +1,4 @@
using System;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Shared.Maths;
@@ -16,6 +17,13 @@ namespace Robust.Client.Graphics
bool Initialize();
void Ready();
event Action<TextEventArgs> TextEntered;
event Action<MouseMoveEventArgs> MouseMove;
event Action<KeyEventArgs> KeyUp;
event Action<KeyEventArgs> KeyDown;
event Action<MouseWheelEventArgs> MouseWheel;
event Action<string> CloseWindow;
ClydeHandle LoadShader(ParsedShader shader, string? name = null);
void ReloadShader(ClydeHandle handle, ParsedShader newShader);

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{
/// <summary>
/// Represents a connected monitor on the user's system.
/// </summary>
public interface IClydeMonitor
{
/// <summary>
/// This ID is not consistent between startups of the game.
/// </summary>
int Id { get; }
string Name { get; }
Vector2i Size { get; }
int RefreshRate { get; }
}
}

View File

@@ -12,7 +12,7 @@ namespace Robust.Client.Graphics
{
IFontFaceHandle Load(Stream stream);
IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size);
void Initialize();
void SetFontDpi(uint fontDpi);
}
internal interface IFontFaceHandle

View File

@@ -87,11 +87,11 @@ namespace Robust.Client.Graphics
public bool HasOverlay(Type overlayClass) {
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
Logger.Error("HasOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
return _overlays.Remove(overlayClass);
return _overlays.ContainsKey(overlayClass);
}
public bool HasOverlay<T>() where T : Overlay {
return _overlays.Remove(typeof(T));
return _overlays.ContainsKey(typeof(T));
}
}

View File

@@ -21,7 +21,6 @@ namespace Robust.Client.Player
void Initialize();
void Startup(INetChannel channel);
void Update(float frameTime);
void Shutdown();
void ApplyPlayerStates(IEnumerable<PlayerState>? list);

View File

@@ -95,12 +95,6 @@ namespace Robust.Client.Player
_network.ClientSendMessage(msgList);
}
/// <inheritdoc />
public void Update(float frameTime)
{
// Uh, nothing anymore I guess.
}
/// <inheritdoc />
public void Shutdown()
{

View File

@@ -9,9 +9,7 @@ namespace Robust.Client.State
State CurrentState { get; }
void RequestStateChange<T>() where T : State, new();
void Update(FrameEventArgs e);
void FrameUpdate(FrameEventArgs e);
void FormResize();
void RequestStateChange(Type type);
}
}

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Timing;
using Robust.Shared.Timing;
namespace Robust.Client.State
{
@@ -14,16 +14,6 @@ namespace Robust.Client.State
/// </summary>
public abstract void Shutdown();
/// <summary>
/// Update the contents of this screen.
/// </summary>
public virtual void Update(FrameEventArgs e) { }
public virtual void FrameUpdate(FrameEventArgs e) { }
/// <summary>
/// The screen has changed size, usually from resizing window. This is called automatically right after Startup.
/// </summary>
public virtual void FormResize() { }
}
}

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Log;
using Robust.Shared.Log;
using System;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
@@ -17,21 +17,11 @@ namespace Robust.Client.State
CurrentState = new DefaultState();
}
public void Update(FrameEventArgs e)
{
CurrentState?.Update(e);
}
public void FrameUpdate(FrameEventArgs e)
{
CurrentState?.FrameUpdate(e);
}
public void FormResize()
{
CurrentState?.FormResize();
}
public void RequestStateChange<T>() where T : State, new()
{
RequestStateChange(typeof(T));

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -817,25 +817,7 @@ namespace Robust.Client.UserInterface
/// <summary>
/// Called when the size of the control changes.
/// </summary>
protected virtual void Resized()
{
}
internal void DoUpdate(FrameEventArgs args)
{
Update(args);
foreach (var child in Children)
{
child.DoUpdate(args);
}
}
/// <summary>
/// This is called every process frame.
/// </summary>
protected virtual void Update(FrameEventArgs args)
{
}
protected virtual void Resized() { }
internal void DoFrameUpdate(FrameEventArgs args)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -45,9 +45,9 @@ namespace Robust.Client.UserInterface.CustomControls
Visible = false;
}
protected override void Update(FrameEventArgs args)
protected override void FrameUpdate(FrameEventArgs args)
{
base.Update(args);
base.FrameUpdate(args);
if ((_gameTiming.RealTime - _lastUpdate).Seconds < 1 || !VisibleInTree)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
@@ -51,9 +51,9 @@ namespace Robust.Client.UserInterface.CustomControls
MouseFilter = contents.MouseFilter = MouseFilterMode.Ignore;
}
protected override void Update(FrameEventArgs args)
protected override void FrameUpdate(FrameEventArgs args)
{
base.Update(args);
base.FrameUpdate(args);
if ((GameTiming.RealTime - LastUpdate).Seconds < 1 || !VisibleInTree)
{

View File

@@ -1,4 +1,4 @@
using Robust.Client.GameStates;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
@@ -36,9 +36,9 @@ namespace Robust.Client.UserInterface.CustomControls
HorizontalAlignment = HAlignment.Left;
}
protected override void Update(FrameEventArgs args)
protected override void FrameUpdate(FrameEventArgs args)
{
base.Update(args);
base.FrameUpdate(args);
if (!VisibleInTree)
{

View File

@@ -1,4 +1,4 @@
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
@@ -17,7 +17,7 @@ namespace Robust.Client.UserInterface.CustomControls
ShadowOffsetYOverride = 1;
}
protected override void Update(FrameEventArgs args)
protected override void FrameUpdate(FrameEventArgs args)
{
if (!VisibleInTree)
{

View File

@@ -1,4 +1,6 @@
using Robust.Client.Graphics;
using System;
using System.Collections;
using Robust.Client.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
@@ -31,9 +33,11 @@ namespace Robust.Client.UserInterface.CustomControls
// We keep track of frame times in a ring buffer.
private readonly float[] _frameTimes = new float[TrackedFrames];
private readonly BitArray _gcMarkers = new(TrackedFrames);
// Position of the last frame in the ring buffer.
private int _frameIndex;
private int _lastGCCount;
public FrameGraph(IGameTiming gameTiming)
{
@@ -49,14 +53,21 @@ namespace Robust.Client.UserInterface.CustomControls
protected override void FrameUpdate(FrameEventArgs args)
{
var gcCount = GC.CollectionCount(0);
_frameTimes[_frameIndex] = (float)_gameTiming.RealFrameTime.TotalSeconds;
_gcMarkers[_frameIndex] = gcCount > _lastGCCount;
_frameIndex = (_frameIndex + 1) % TrackedFrames;
_lastGCCount = gcCount;
}
protected internal override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
Span<Vector2> triangle = stackalloc Vector2[3];
float maxHeight = 0;
for (var i = 0; i < _frameTimes.Length; i++)
{
@@ -88,6 +99,16 @@ namespace Robust.Client.UserInterface.CustomControls
color = Color.Lime;
}
handle.DrawRect(rect, color);
var gc = _gcMarkers[currentFrameIndex];
if (gc)
{
triangle[0] = (rect.Left, 0);
triangle[1] = (rect.Right, 0);
triangle[2] = (rect.Center.X, 5);
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, triangle, Color.LightBlue);
}
}
}
}

View File

@@ -91,7 +91,7 @@ namespace Robust.Client.UserInterface.CustomControls
// Prevent window headers from getting off screen due to game window resizes.
protected override void Update(FrameEventArgs args)
protected override void FrameUpdate(FrameEventArgs args)
{
var (spaceX, spaceY) = Parent!.Size;
if (Position.Y > spaceY)

View File

@@ -17,8 +17,6 @@ namespace Robust.Client.UserInterface
void Initialize();
void InitializeTesting();
void Update(FrameEventArgs args);
void FrameUpdate(FrameEventArgs args);
/// <returns>True if a UI control was hit and the key event should not pass through past UI.</returns>

View File

@@ -133,6 +133,7 @@ namespace Robust.Client.UserInterface
QueueMeasureUpdate(RootControl);
_displayManager.OnWindowResized += args => _updateRootSize();
_displayManager.OnWindowScaleChanged += UpdateUIScale;
StateRoot = new LayoutContainer
{
@@ -174,11 +175,7 @@ namespace Robust.Client.UserInterface
_initializeCommon();
}
public void Update(FrameEventArgs args)
{
RootControl.DoUpdate(args);
}
/// <inheritdoc />
public void FrameUpdate(FrameEventArgs args)
{
// Process queued style & layout updates.
@@ -833,7 +830,13 @@ namespace Robust.Client.UserInterface
private void _uiScaleChanged(float newValue)
{
UIScale = newValue == 0f ? DefaultUIScale : newValue;
UpdateUIScale();
}
private void UpdateUIScale()
{
var newVal = _configurationManager.GetCVar(CVars.DisplayUIScale);
UIScale = newVal == 0f ? DefaultUIScale : newVal;
if (RootControl == null)
{

View File

@@ -94,8 +94,8 @@ namespace Robust.Server
private IGameLoop _mainLoop = default!;
private TimeSpan _lastTitleUpdate;
private int _lastReceivedBytes;
private int _lastSentBytes;
private long _lastReceivedBytes;
private long _lastSentBytes;
private string? _shutdownReason;
@@ -573,7 +573,7 @@ namespace Robust.Server
}
// Pass Histogram into the IEntityManager.Update so it can do more granular measuring.
_entities.Update(frameEventArgs.DeltaSeconds, TickUsage);
_entities.TickUpdate(frameEventArgs.DeltaSeconds, TickUsage);
using (TickUsage.WithLabels("PostEngine").NewTimer())
{

View File

@@ -16,6 +16,7 @@ namespace Robust.Server.GameObjects
private Vector2 _zoom = Vector2.One/2f;
private Vector2 _offset;
private Angle _rotation;
private uint _visibilityMask;
public override bool DrawFov
{
@@ -69,9 +70,22 @@ namespace Robust.Server.GameObjects
}
}
public override uint VisibilityMask
{
get => _visibilityMask;
set
{
if(_visibilityMask == value)
return;
_visibilityMask = value;
Dirty();
}
}
public override ComponentState GetComponentState(ICommonSession player)
{
return new EyeComponentState(DrawFov, Zoom, Offset, Rotation);
return new EyeComponentState(DrawFov, Zoom, Offset, Rotation, VisibilityMask);
}
}
}

View File

@@ -1,41 +1,9 @@
using System.Collections.Generic;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
namespace Robust.Server.GameObjects
{
public interface IServerEntityManager : IEntityManager
{
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
List<EntityState>? GetEntityStates(GameTick fromTick, IPlayerSession player);
/// <summary>
/// Gets all entity states within an AABB that have been modified after and including the provided tick.
/// </summary>
List<EntityState>? UpdatePlayerSeenEntityStates(GameTick fromTick, IPlayerSession player, float range);
// Keep track of deleted entities so we can sync deletions with the client.
/// <summary>
/// Gets a list of all entity UIDs that were deleted between <paramref name="fromTick" /> and now.
/// </summary>
List<EntityUid>? GetDeletedEntities(GameTick fromTick);
/// <summary>
/// Remove deletion history.
/// </summary>
/// <param name="toTick">The last tick to delete the history for. Inclusive.</param>
void CullDeletionHistory(GameTick toTick);
/// <summary>
/// Removes entity state persistence information from the entity manager for a player.
/// </summary>
/// <param name="player"></param>
void DropPlayerState(IPlayerSession player);
float MaxUpdateRange { get; }
}
/// <summary>
/// Server side version of the <see cref="IEntityManager"/>.
/// </summary>
public interface IServerEntityManager : IEntityManager { }
}

View File

@@ -1,55 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using Prometheus;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Shared.GameObjects.TransformComponent;
namespace Robust.Server.GameObjects
{
/// <summary>
/// Manager for entities -- controls things like template loading and instantiation
/// </summary>
[UsedImplicitly] // DI Container
public sealed class ServerEntityManager : EntityManager, IServerEntityManagerInternal
{
private static readonly Gauge EntitiesCount = Metrics.CreateGauge(
"robust_entities_count",
"Amount of alive entities.");
private const float MinimumMotionForMovers = 1 / 128f;
#region IEntityManager Members
[Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
[Shared.IoC.Dependency] private readonly IPauseManager _pauseManager = default!;
[Shared.IoC.Dependency] private readonly IConfigurationManager _configurationManager = default!;
private float? _maxUpdateRangeCache;
public float MaxUpdateRange => _maxUpdateRangeCache
??= _configurationManager.GetCVar(CVars.NetMaxUpdateRange);
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPauseManager _pauseManager = default!;
private int _nextServerEntityUid = (int) EntityUid.FirstUid;
private readonly List<(GameTick tick, EntityUid uid)> _deletionHistory = new();
public override void Update()
{
base.Update();
_maxUpdateRangeCache = null;
}
/// <inheritdoc />
public override IEntity CreateEntityUninitialized(string? prototypeName)
{
@@ -79,33 +53,6 @@ namespace Robust.Server.GameObjects
return newEntity;
}
private Entity CreateEntityServer(string? prototypeName)
{
var entity = CreateEntity(prototypeName);
if (prototypeName != null)
{
var prototype = PrototypeManager.Index<EntityPrototype>(prototypeName);
// At this point in time, all data configure on the entity *should* be purely from the prototype.
// As such, we can reset the modified ticks to Zero,
// which indicates "not different from client's own deserialization".
// So the initial data for the component or even the creation doesn't have to be sent over the wire.
foreach (var component in ComponentManager.GetNetComponents(entity.Uid))
{
// Make sure to ONLY get components that are defined in the prototype.
// Others could be instantiated directly by AddComponent (e.g. ContainerManager).
// And those aren't guaranteed to exist on the client, so don't clear them.
if (prototype.Components.ContainsKey(component.Name))
{
((Component) component).ClearTicks();
}
}
}
return entity;
}
/// <inheritdoc />
public override IEntity SpawnEntity(string? protoName, EntityCoordinates coordinates)
{
@@ -116,10 +63,7 @@ namespace Robust.Server.GameObjects
InitializeAndStartEntity((Entity) entity);
if (_pauseManager.IsMapInitialized(coordinates.GetMapId(this)))
{
entity.RunMapInit();
}
if (_pauseManager.IsMapInitialized(coordinates.GetMapId(this))) entity.RunMapInit();
return entity;
}
@@ -133,720 +77,26 @@ namespace Robust.Server.GameObjects
}
/// <inheritdoc />
public List<EntityState>? GetEntityStates(GameTick fromTick, IPlayerSession player)
public override void Startup()
{
var stateEntities = new List<EntityState>();
foreach (var entity in AllEntities)
{
if (entity.Deleted)
{
continue;
}
DebugTools.Assert(entity.Initialized);
if (entity.LastModifiedTick <= fromTick)
continue;
stateEntities.Add(GetEntityState(ComponentManager, entity.Uid, fromTick, player));
}
// no point sending an empty collection
return stateEntities.Count == 0 ? default : stateEntities;
base.Startup();
EntitySystemManager.Initialize();
Started = true;
}
private readonly Dictionary<IPlayerSession, SortedSet<EntityUid>> _seenMovers
= new();
// Is thread safe.
private SortedSet<EntityUid> GetSeenMovers(IPlayerSession player)
{
lock (_seenMovers)
{
return GetSeenMoversUnlocked(player);
}
}
private SortedSet<EntityUid> GetSeenMoversUnlocked(IPlayerSession player)
{
if (!_seenMovers.TryGetValue(player, out var movers))
{
movers = new SortedSet<EntityUid>();
_seenMovers.Add(player, movers);
}
return movers;
}
private void AddToSeenMovers(IPlayerSession player, EntityUid entityUid)
{
var movers = GetSeenMoversUnlocked(player);
movers.Add(entityUid);
}
private readonly Dictionary<IPlayerSession, Dictionary<EntityUid, GameTick>> _playerLastSeen
= new();
private static readonly Vector2 Vector2NaN = new(float.NaN, float.NaN);
private Dictionary<EntityUid, GameTick> GetLastSeen(IPlayerSession player)
{
lock (_playerLastSeen)
{
if (!_playerLastSeen.TryGetValue(player, out var lastSeen))
{
lastSeen = new Dictionary<EntityUid, GameTick>();
_playerLastSeen.Add(player, lastSeen);
}
return lastSeen;
}
}
private static GameTick GetLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid)
{
if (!lastSeen.TryGetValue(uid, out var tick))
{
tick = GameTick.First;
}
return tick;
}
private static GameTick UpdateLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid, GameTick newTick)
{
if (!lastSeen.TryGetValue(uid, out var oldTick))
{
oldTick = GameTick.First;
}
lastSeen[uid] = newTick;
return oldTick;
}
private static IEnumerable<EntityUid> GetLastSeenAfter(Dictionary<EntityUid, GameTick> lastSeen, GameTick fromTick)
{
foreach (var (uid, tick) in lastSeen)
{
if (tick > fromTick)
{
yield return uid;
}
}
}
private IEnumerable<EntityUid> GetLastSeenOn(Dictionary<EntityUid, GameTick> lastSeen, GameTick fromTick)
{
foreach (var (uid, tick) in lastSeen)
{
if (tick == fromTick)
{
yield return uid;
}
}
}
private static void SetLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid, GameTick tick)
{
lastSeen[uid] = tick;
}
private static void ClearLastSeenTick(Dictionary<EntityUid, GameTick> lastSeen, EntityUid uid)
{
lastSeen.Remove(uid);
}
public void DropPlayerState(IPlayerSession player)
{
lock (_playerLastSeen)
{
_playerLastSeen.Remove(player);
}
}
private void IncludeRelatives(IEnumerable<IEntity> children, HashSet<IEntity> set)
{
foreach (var child in children)
{
var ent = child!;
while (ent != null && !ent.Deleted)
{
if (set.Add(ent))
{
AddContainedRecursive(ent, set);
ent = ent.Transform.Parent?.Owner!;
}
else
{
// Already processed this entity once.
break;
}
}
}
}
private static void AddContainedRecursive(IEntity ent, HashSet<IEntity> set)
{
if (!ent.TryGetComponent(out ContainerManagerComponent? contMgr))
{
return;
}
foreach (var container in contMgr.GetAllContainers())
{
// Manual for loop to cut out allocations.
// ReSharper disable once ForCanBeConvertedToForeach
for (var i = 0; i < container.ContainedEntities.Count; i++)
{
var contEnt = container.ContainedEntities[i];
set.Add(contEnt);
AddContainedRecursive(contEnt, set);
}
}
}
private class PlayerSeenEntityStatesResources
{
public readonly HashSet<EntityUid> IncludedEnts = new();
public readonly List<EntityState> EntityStates = new();
public readonly HashSet<EntityUid> NeededEnts = new();
public readonly HashSet<IEntity> Relatives = new();
}
private readonly ThreadLocal<PlayerSeenEntityStatesResources> _playerSeenEntityStatesResources
= new(() => new PlayerSeenEntityStatesResources());
/// <inheritdoc />
public List<EntityState>? UpdatePlayerSeenEntityStates(GameTick fromTick, IPlayerSession player, float range)
public override void TickUpdate(float frameTime, Histogram? histogram)
{
var playerEnt = player.AttachedEntity;
if (playerEnt == null)
{
// super-observer?
return GetEntityStates(fromTick, player);
}
base.TickUpdate(frameTime, histogram);
var playerUid = playerEnt.Uid;
var transform = playerEnt.Transform;
var position = transform.WorldPosition;
var mapId = transform.MapID;
var viewbox = new Box2(position, position).Enlarged(MaxUpdateRange);
var seenMovers = GetSeenMovers(player);
var lSeen = GetLastSeen(player);
var pseStateRes = _playerSeenEntityStatesResources.Value!;
var checkedEnts = pseStateRes.IncludedEnts;
var entityStates = pseStateRes.EntityStates;
var neededEnts = pseStateRes.NeededEnts;
var relatives = pseStateRes.Relatives;
checkedEnts.Clear();
entityStates.Clear();
neededEnts.Clear();
relatives.Clear();
foreach (var uid in seenMovers.ToList())
{
if (!TryGetEntity(uid, out var entity) || entity.Deleted)
{
seenMovers.Remove(uid);
continue;
}
if (entity.TryGetComponent(out IPhysBody? body))
{
if (body.LinearVelocity.EqualsApprox(Vector2.Zero, MinimumMotionForMovers))
{
if (AnyParentInSet(uid, seenMovers))
{
// parent is moving
continue;
}
if (MathF.Abs(body.AngularVelocity) > 0)
{
if (entity.TryGetComponent(out TransformComponent? txf) && txf.ChildCount > 0)
{
// has children spinning
continue;
}
}
seenMovers.Remove(uid);
}
}
var state = GetEntityState(ComponentManager, uid, fromTick, player);
if (checkedEnts.Add(uid))
{
entityStates.Add(state);
// mover did not change
if (state.ComponentStates != null)
{
// mover can be seen
if (!viewbox.Intersects(GetWorldAabbFromEntity(entity)))
{
// mover changed and can't be seen
var idx = Array.FindIndex(state.ComponentStates,
x => x is TransformComponentState);
if (idx != -1)
{
// mover changed positional data and can't be seen
var oldState =
(TransformComponentState) state.ComponentStates[idx];
var newState = new TransformComponentState(Vector2NaN,
oldState.Rotation, oldState.ParentID, oldState.NoLocalRotation);
state.ComponentStates[idx] = newState;
seenMovers.Remove(uid);
ClearLastSeenTick(lSeen, uid);
checkedEnts.Add(uid);
var needed = oldState.ParentID;
if (!needed.IsValid() || checkedEnts.Contains(needed))
{
// either no parent attached or parent already included
continue;
}
if (GetLastSeenTick(lSeen, needed) == GameTick.Zero)
{
neededEnts.Add(needed);
}
}
}
}
}
else
{
// mover already added?
if (!viewbox.Intersects(GetWorldAabbFromEntity(entity)))
{
// mover can't be seen
var oldState =
(TransformComponentState) entity.Transform.GetComponentState(player);
entityStates.Add(new EntityState(uid,
new ComponentChanged[]
{
new(false, NetIDs.TRANSFORM, "Transform")
},
new ComponentState[]
{
new TransformComponentState(Vector2NaN, oldState.Rotation,
oldState.ParentID, oldState.NoLocalRotation)
}));
seenMovers.Remove(uid);
ClearLastSeenTick(lSeen, uid);
checkedEnts.Add(uid);
var needed = oldState.ParentID;
if (!needed.IsValid() || checkedEnts.Contains(needed))
{
// either no parent attached or parent already included
continue;
}
if (GetLastSeenTick(lSeen, needed) == GameTick.Zero)
{
neededEnts.Add(needed);
}
}
}
}
var currentTick = CurrentTick;
// scan pvs box and include children and parents recursively
IncludeRelatives(GetEntitiesInRange(mapId, position, range, true), relatives);
// Exclude any entities that are currently invisible to the player.
ExcludeInvisible(relatives, player.VisibilityMask);
// Always send updates for all grid and map entities.
// If we don't, the client-side game state manager WILL blow up.
// TODO: Make map manager netcode aware of PVS to avoid the need for this workaround.
IncludeMapCriticalEntities(relatives);
foreach (var entity in relatives)
{
DebugTools.Assert(entity.Initialized && !entity.Deleted);
var lastChange = entity.LastModifiedTick;
var uid = entity.Uid;
var lastSeen = UpdateLastSeenTick(lSeen, uid, currentTick);
DebugTools.Assert(lastSeen != currentTick);
/*
if (uid != playerUid && entity.Prototype == playerEnt.Prototype && lastSeen < fromTick)
{
Logger.DebugS("pvs", $"Player {playerUid} is seeing player {uid}.");
}
*/
if (checkedEnts.Contains(uid))
{
// already have it
continue;
}
if (lastChange <= lastSeen)
{
// hasn't changed since last seen
continue;
}
// should this be lastSeen or fromTick?
var entityState = GetEntityState(ComponentManager, uid, lastSeen, player);
checkedEnts.Add(uid);
if (entityState.ComponentStates == null)
{
// no changes
continue;
}
entityStates.Add(entityState);
if (uid == playerUid)
{
continue;
}
if (!entity.TryGetComponent(out IPhysBody? body))
{
// can't be a mover w/o physics
continue;
}
if (!body.LinearVelocity.EqualsApprox(Vector2.Zero, MinimumMotionForMovers))
{
// has motion
seenMovers.Add(uid);
}
else
{
// not moving
seenMovers.Remove(uid);
}
}
var priorTick = new GameTick(fromTick.Value - 1);
foreach (var uid in GetLastSeenOn(lSeen, priorTick))
{
if (checkedEnts.Contains(uid))
{
continue;
}
if (uid == playerUid)
{
continue;
}
if (!TryGetEntity(uid, out var entity) || entity.Deleted)
{
// TODO: remove from states list being sent?
continue;
}
if (viewbox.Intersects(GetWorldAabbFromEntity(entity)))
{
// can be seen
continue;
}
var state = GetEntityState(ComponentManager, uid, fromTick, player);
if (state.ComponentStates == null)
{
// nothing changed
continue;
}
checkedEnts.Add(uid);
entityStates.Add(state);
seenMovers.Remove(uid);
ClearLastSeenTick(lSeen, uid);
var idx = Array.FindIndex(state.ComponentStates, x => x is TransformComponentState);
if (idx == -1)
{
// no transform changes
continue;
}
var oldState = (TransformComponentState) state.ComponentStates[idx];
var newState =
new TransformComponentState(Vector2NaN, oldState.Rotation, oldState.ParentID, oldState.NoLocalRotation);
state.ComponentStates[idx] = newState;
var needed = oldState.ParentID;
if (!needed.IsValid() || checkedEnts.Contains(needed))
{
// don't need to include parent or already included
continue;
}
if (GetLastSeenTick(lSeen, needed) == GameTick.First)
{
neededEnts.Add(needed);
}
}
do
{
var moreNeededEnts = new HashSet<EntityUid>();
foreach (var uid in moreNeededEnts)
{
if (checkedEnts.Contains(uid))
{
continue;
}
var entity = GetEntity(uid);
var state = GetEntityState(ComponentManager, uid, fromTick, player);
if (state.ComponentStates == null || viewbox.Intersects(GetWorldAabbFromEntity(entity)))
{
// no states or should already be seen
continue;
}
checkedEnts.Add(uid);
entityStates.Add(state);
var idx = Array.FindIndex(state.ComponentStates,
x => x is TransformComponentState);
if (idx == -1)
{
// no transform state
continue;
}
var oldState = (TransformComponentState) state.ComponentStates[idx];
var newState =
new TransformComponentState(Vector2NaN, oldState.Rotation,
oldState.ParentID, oldState.NoLocalRotation);
state.ComponentStates[idx] = newState;
seenMovers.Remove(uid);
ClearLastSeenTick(lSeen, uid);
var needed = oldState.ParentID;
if (!needed.IsValid() || checkedEnts.Contains(needed))
{
// done here
continue;
}
// check if further needed
if (!checkedEnts.Contains(uid) && GetLastSeenTick(lSeen, needed) == GameTick.Zero)
{
moreNeededEnts.Add(needed);
}
}
neededEnts = moreNeededEnts;
} while (neededEnts.Count > 0);
// help the client out
entityStates.Sort((a, b) => a.Uid.CompareTo(b.Uid));
#if DEBUG_NULL_ENTITY_STATES
foreach ( var state in entityStates ) {
if (state.ComponentStates == null)
{
throw new NotImplementedException("Shouldn't send null states.");
}
}
#endif
// no point sending an empty collection
return entityStates.Count == 0 ? default : entityStates;
EntitiesCount.Set(AllEntities.Count);
}
public override void DeleteEntity(IEntity e)
{
base.DeleteEntity(e);
EventBus.RaiseEvent(EventSource.Local, new EntityDeletedMessage(e));
_deletionHistory.Add((CurrentTick, e.Uid));
}
public List<EntityUid>? GetDeletedEntities(GameTick fromTick)
{
var list = new List<EntityUid>();
foreach (var (tick, id) in _deletionHistory)
{
if (tick >= fromTick)
{
list.Add(id);
}
}
// no point sending an empty collection
return list.Count == 0 ? default : list;
}
public void CullDeletionHistory(GameTick toTick)
{
_deletionHistory.RemoveAll(hist => hist.tick <= toTick);
}
public override bool UpdateEntityTree(IEntity entity, Box2? worldAABB = null)
{
var currentTick = CurrentTick;
var updated = base.UpdateEntityTree(entity, worldAABB);
if (entity.Deleted
|| !entity.Initialized
|| !Entities.ContainsKey(entity.Uid))
{
return updated;
}
DebugTools.Assert(entity.Transform.Initialized);
// note: updated can be false even if something moved a bit
worldAABB ??= GetWorldAabbFromEntity(entity);
foreach (var (player, lastSeen) in _playerLastSeen)
{
var playerEnt = player.AttachedEntity;
if (playerEnt == null)
{
// player has no entity, gaf?
continue;
}
var playerUid = playerEnt.Uid;
var entityUid = entity.Uid;
if (entityUid == playerUid)
{
continue;
}
if (!lastSeen.TryGetValue(playerUid, out var playerTick))
{
// player can't "see" itself, gaf?
continue;
}
var playerPos = playerEnt.Transform.WorldPosition;
var viewbox = new Box2(playerPos, playerPos).Enlarged(MaxUpdateRange);
if (!lastSeen.TryGetValue(entityUid, out var tick))
{
// never saw it other than first tick or was cleared
if (!AnyParentMoving(player, entityUid))
{
continue;
}
}
if (tick >= currentTick)
{
// currently seeing it
continue;
}
// saw it previously
// player can't see it now
if (!viewbox.Intersects(worldAABB.Value))
{
var addToMovers = false;
if (entity.Transform.LastModifiedTick >= currentTick)
{
addToMovers = true;
}
else if (entity.TryGetComponent(out IPhysBody? physics)
&& physics.LastModifiedTick >= currentTick)
{
addToMovers = true;
}
if (addToMovers)
{
AddToSeenMovers(player, entityUid);
}
}
}
return updated;
}
private bool AnyParentMoving(IPlayerSession player, EntityUid entityUid)
{
var seenMovers = GetSeenMoversUnlocked(player);
if (seenMovers == null)
{
return false;
}
return AnyParentInSet(entityUid, seenMovers);
}
private bool AnyParentInSet(EntityUid entityUid, SortedSet<EntityUid> set)
{
for (;;)
{
if (!TryGetEntity(entityUid, out var ent))
{
return false;
}
var txf = ent.Transform;
entityUid = txf.ParentUid;
if (entityUid == EntityUid.Invalid)
{
return false;
}
if (set.Contains(entityUid))
{
return true;
}
}
}
#endregion IEntityManager Members
IEntity IServerEntityManagerInternal.AllocEntity(string? prototypeName, EntityUid? uid)
{
return AllocEntity(prototypeName, uid);
}
protected override EntityUid GenerateEntityUid()
{
return new(_nextServerEntityUid++);
}
void IServerEntityManagerInternal.FinishEntityLoad(IEntity entity, IEntityLoadContext? context)
{
LoadEntity((Entity) entity, context);
@@ -863,95 +113,33 @@ namespace Robust.Server.GameObjects
}
/// <inheritdoc />
public override void Startup()
protected override EntityUid GenerateEntityUid()
{
base.Startup();
EntitySystemManager.Initialize();
Started = true;
return new(_nextServerEntityUid++);
}
/// <summary>
/// Generates a network entity state for the given entity.
/// </summary>
/// <param name="compMan">ComponentManager that contains the components for the entity.</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="player">The player to generate this state for.</param>
/// <returns>New entity State for the given entity.</returns>
private static EntityState GetEntityState(IComponentManager compMan, EntityUid entityUid, GameTick fromTick, IPlayerSession player)
private Entity CreateEntityServer(string? prototypeName)
{
var compStates = new List<ComponentState>();
var changed = new List<ComponentChanged>();
var entity = CreateEntity(prototypeName);
foreach (var comp in compMan.GetNetComponents(entityUid))
if (prototypeName != null)
{
DebugTools.Assert(comp.Initialized);
var prototype = PrototypeManager.Index<EntityPrototype>(prototypeName);
// NOTE: When LastModifiedTick or CreationTick are 0 it means that the relevant data is
// "not different from entity creation".
// i.e. when the client spawns the entity and loads the entity prototype,
// the data it deserializes from the prototype SHOULD be equal
// to what the component state / ComponentChanged would send.
// As such, we can avoid sending this data in this case since the client "already has it".
if (comp.NetSyncEnabled && comp.LastModifiedTick != GameTick.Zero && comp.LastModifiedTick >= fromTick)
compStates.Add(comp.GetComponentState(player));
if (comp.CreationTick != GameTick.Zero && comp.CreationTick >= fromTick && !comp.Deleted)
// At this point in time, all data configure on the entity *should* be purely from the prototype.
// As such, we can reset the modified ticks to Zero,
// which indicates "not different from client's own deserialization".
// So the initial data for the component or even the creation doesn't have to be sent over the wire.
foreach (var component in ComponentManager.GetNetComponents(entity.Uid))
{
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChanged.Added(comp.NetID!.Value, comp.Name));
}
else if (comp.Deleted && comp.LastModifiedTick >= fromTick)
{
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChanged.Removed(comp.NetID!.Value));
// Make sure to ONLY get components that are defined in the prototype.
// Others could be instantiated directly by AddComponent (e.g. ContainerManager).
// And those aren't guaranteed to exist on the client, so don't clear them.
if (prototype.Components.ContainsKey(component.Name)) ((Component) component).ClearTicks();
}
}
return new EntityState(entityUid, changed.ToArray(), compStates.ToArray());
}
private void IncludeMapCriticalEntities(HashSet<IEntity> set)
{
foreach (var mapId in _mapManager.GetAllMapIds())
{
if (_mapManager.HasMapEntity(mapId))
{
set.Add(_mapManager.GetMapEntity(mapId));
}
}
foreach (var grid in _mapManager.GetAllGrids())
{
if (grid.GridEntityId != EntityUid.Invalid)
{
set.Add(GetEntity(grid.GridEntityId));
}
}
}
private void ExcludeInvisible(HashSet<IEntity> set, int visibilityMask)
{
set.RemoveWhere(e =>
{
if (!e.TryGetComponent(out VisibilityComponent? visibility))
{
return false;
}
return (visibilityMask & visibility.Layer) == 0;
});
}
public override void Update(float frameTime, Histogram? histogram)
{
base.Update(frameTime, histogram);
EntitiesCount.Set(AllEntities.Count);
return entity;
}
}
}

View File

@@ -47,7 +47,7 @@ namespace Robust.Server.GameObjects
_configurationManager.OnValueChanged(CVars.NetLogLateMsg, b => _logLateMsgs = b, true);
}
public void Update()
public void TickUpdate()
{
while (_queue.Count != 0 && _queue.Peek().SourceTick <= _gameTiming.CurTick)
{

View File

@@ -0,0 +1,324 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.ObjectPool;
using Robust.Server.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates
{
internal class EntityViewCulling
{
private const int ViewSetCapacity = 128; // starting number of entities that are in view
private const int PlayerSetSize = 64; // Starting number of players
private const int MaxVisPoolSize = 1024; // Maximum number of pooled objects
private static readonly Vector2 Vector2NaN = new(float.NaN, float.NaN);
private readonly IServerEntityManager _entMan;
private readonly IComponentManager _compMan;
private readonly IMapManager _mapManager;
private readonly Dictionary<ICommonSession, HashSet<EntityUid>> _playerVisibleSets = new(PlayerSetSize);
private readonly ConcurrentDictionary<ICommonSession, GameTick> _playerLastFullMap = new();
private readonly List<(GameTick tick, EntityUid uid)> _deletionHistory = new();
private readonly ObjectPool<HashSet<EntityUid>> _visSetPool
= new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<EntityUid>> _viewerEntsPool
= new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), MaxVisPoolSize);
/// <summary>
/// Is view culling enabled, or will we send the whole map?
/// </summary>
public bool CullingEnabled { get; set; }
/// <summary>
/// Size of the side of the view bounds square.
/// </summary>
public float ViewSize { get; set; }
public EntityViewCulling(IServerEntityManager entMan, IMapManager mapManager)
{
_entMan = entMan;
_compMan = entMan.ComponentManager;
_mapManager = mapManager;
_compMan = _entMan.ComponentManager;
}
// Not thread safe
public void EntityDeleted(EntityUid e)
{
// Not aware of prediction
_deletionHistory.Add((_entMan.CurrentTick, e));
}
// Not thread safe
public void CullDeletionHistory(GameTick oldestAck)
{
_deletionHistory.RemoveAll(hist => hist.tick < oldestAck);
}
private List<EntityUid> GetDeletedEntities(GameTick fromTick)
{
var list = new List<EntityUid>();
foreach (var (tick, id) in _deletionHistory)
{
if (tick >= fromTick) list.Add(id);
}
return list;
}
// Not thread safe
public void AddPlayer(ICommonSession session)
{
_playerVisibleSets.Add(session, new HashSet<EntityUid>(ViewSetCapacity));
}
// Not thread safe
public void RemovePlayer(ICommonSession session)
{
_playerVisibleSets.Remove(session);
_playerLastFullMap.Remove(session, out _);
}
// thread safe
public bool IsPointVisible(ICommonSession session, in MapCoordinates position)
{
var viewables = GetSessionViewers(session);
bool CheckInView(MapCoordinates mapCoordinates, HashSet<EntityUid> entityUids)
{
foreach (var euid in entityUids)
{
var (viewBox, mapId) = CalcViewBounds(in euid);
if (mapId != mapCoordinates.MapId)
continue;
if (!CullingEnabled)
return true;
if (viewBox.Contains(mapCoordinates.Position))
return true;
}
return false;
}
bool result = CheckInView(position, viewables);
viewables.Clear();
_viewerEntsPool.Return(viewables);
return result;
}
private HashSet<EntityUid> GetSessionViewers(ICommonSession session)
{
var viewers = _viewerEntsPool.Get();
if (session.Status != SessionStatus.InGame || session.AttachedEntityUid is null)
return viewers;
var query = _compMan.EntityQuery<BasicActorComponent>();
foreach (var actorComp in query)
{
if (actorComp.playerSession == session)
viewers.Add(actorComp.Owner.Uid);
}
return viewers;
}
// thread safe
public (List<EntityState>? updates, List<EntityUid>? deletions) CalculateEntityStates(ICommonSession session, GameTick fromTick, GameTick toTick)
{
DebugTools.Assert(session.Status == SessionStatus.InGame);
//TODO: Stop sending all entities to every player first tick
List<EntityUid>? deletions;
if (!CullingEnabled || fromTick == GameTick.Zero)
{
var allStates = ServerGameStateManager.GetAllEntityStates(_entMan, session, fromTick);
deletions = GetDeletedEntities(fromTick);
_playerLastFullMap.AddOrUpdate(session, toTick, (_, _) => toTick);
return (allStates, deletions);
}
var lastMapUpdate = _playerLastFullMap.GetValueOrDefault(session);
var currentSet = CalcCurrentViewSet(session);
// If they don't have a usable eye, nothing to send, and map remove will deal with ent removal
if (currentSet is null)
return (null, null);
deletions = GetDeletedEntities(fromTick);
// pretty big allocations :(
List<EntityState> entityStates = new(currentSet.Count);
var previousSet = _playerVisibleSets[session];
// complement set
foreach (var entityUid in previousSet)
{
if (!currentSet.Contains(entityUid) && !deletions.Contains(entityUid))
{
if(_compMan.HasComponent<SnapGridComponent>(entityUid))
continue;
// PVS leave message
//TODO: Remove NaN as the signal to leave PVS
var xform = _compMan.GetComponent<ITransformComponent>(entityUid);
var oldState = (TransformComponent.TransformComponentState)xform.GetComponentState(session);
entityStates.Add(new EntityState(entityUid,
new ComponentChanged[]
{
new(false, NetIDs.TRANSFORM, "Transform")
},
new ComponentState[]
{
new TransformComponent.TransformComponentState(Vector2NaN, oldState.Rotation,
oldState.ParentID, oldState.NoLocalRotation)
}));
}
}
foreach (var entityUid in currentSet)
{
if (previousSet.Contains(entityUid))
{
//Still Visible
// only send new changes
var newState = ServerGameStateManager.GetEntityState(_entMan.ComponentManager, session, entityUid, fromTick);
if (!newState.Empty)
entityStates.Add(newState);
}
else
{
// PVS enter message
// skip sending anchored entities (walls)
if (_compMan.HasComponent<SnapGridComponent>(entityUid) && _entMan.GetEntity(entityUid).LastModifiedTick <= lastMapUpdate)
continue;
// don't assume the client knows anything about us
var newState = ServerGameStateManager.GetEntityState(_entMan.ComponentManager, session, entityUid, GameTick.Zero);
entityStates.Add(newState);
}
}
// swap out vis sets
_playerVisibleSets[session] = currentSet;
previousSet.Clear();
_visSetPool.Return(previousSet);
// no point sending an empty collection
deletions = deletions?.Count == 0 ? default : deletions;
return (entityStates, deletions);
}
private HashSet<EntityUid>? CalcCurrentViewSet(ICommonSession session)
{
if (!CullingEnabled)
return null;
// if you don't have an attached entity, you don't see the world.
if (session.AttachedEntityUid is null)
return null;
var visibleEnts = _visSetPool.Get();
var viewers = GetSessionViewers(session);
foreach (var eyeEuid in viewers)
{
var (viewBox, mapId) = CalcViewBounds(in eyeEuid);
uint visMask = 0;
if (_compMan.TryGetComponent<EyeComponent>(eyeEuid, out var eyeComp))
visMask = eyeComp.VisibilityMask;
//Always include the map entity
visibleEnts.Add(_mapManager.GetMapEntityId(mapId));
//Always include viewable ent itself
visibleEnts.Add(eyeEuid);
// grid entity should be added through this
// assume there are no deleted ents in here, cull them first in ent/comp manager
_entMan.FastEntitiesIntersecting(in mapId, ref viewBox, entity => RecursiveAdd((TransformComponent)entity.Transform, visibleEnts, visMask));
}
viewers.Clear();
_viewerEntsPool.Return(viewers);
return visibleEnts;
}
// Read Safe
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private bool RecursiveAdd(TransformComponent xform, HashSet<EntityUid> visSet, uint visMask)
{
var xformUid = xform.Owner.Uid;
// we are done, this ent has already been checked and is visible
if (visSet.Contains(xformUid))
return true;
// if we are invisible, we are not going into the visSet, so don't worry about parents, and children are not going in
if (_compMan.TryGetComponent<VisibilityComponent>(xformUid, out var visComp))
{
if ((visMask & visComp.Layer) == 0)
return false;
}
var xformParentUid = xform.ParentUid;
// this is the world entity, it is always visible
if (!xformParentUid.IsValid())
{
visSet.Add(xformUid);
return true;
}
// parent is already in the set
if (visSet.Contains(xformParentUid))
{
visSet.Add(xformUid);
return true;
}
// parent was not added, so we are not either
var xformParent = _compMan.GetComponent<TransformComponent>(xformParentUid);
if (!RecursiveAdd(xformParent, visSet, visMask))
return false;
// add us
visSet.Add(xformUid);
return true;
}
// Read Safe
private (Box2 view, MapId mapId) CalcViewBounds(in EntityUid euid)
{
var xform = _compMan.GetComponent<ITransformComponent>(euid);
var view = Box2.UnitCentered.Scale(ViewSize).Translated(xform.WorldPosition);
var map = xform.MapID;
return (view, map);
}
}
}

View File

@@ -1,18 +1,21 @@
namespace Robust.Server.GameStates
{
/// <summary>
/// Engine service that provides creating and dispatching of game states.
/// Engine service that provides creating and dispatching of game states.
/// </summary>
public interface IServerGameStateManager
{
/// <summary>
/// One time initialization of the service.
/// One time initialization of the service.
/// </summary>
void Initialize();
/// <summary>
/// Create and dispatch game states to all connected sessions.
/// Create and dispatch game states to all connected sessions.
/// </summary>
void SendGameStateUpdate();
bool PvsEnabled { get; }
float PvsRange { get; }
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared;
@@ -9,22 +10,24 @@ using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates
{
/// <inheritdoc />
public class ServerGameStateManager : IServerGameStateManager
/// <inheritdoc cref="IServerGameStateManager"/>
public class ServerGameStateManager : IServerGameStateManager, IPostInjectInit
{
// Mapping of net UID of clients -> last known acked state.
private readonly Dictionary<long, GameTick> _ackedStates = new();
private GameTick _lastOldestAck = GameTick.Zero;
private EntityViewCulling _entityView = null!;
[Dependency] private readonly IServerEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IServerNetManager _networkManager = default!;
@@ -35,6 +38,12 @@ namespace Robust.Server.GameStates
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
public bool PvsEnabled => _configurationManager.GetCVar(CVars.NetPVS);
public float PvsRange => _configurationManager.GetCVar(CVars.NetMaxUpdateRange);
public void PostInject()
{
_entityView = new EntityViewCulling(_entityManager, _mapManager);
}
/// <inheritdoc />
public void Initialize()
@@ -44,6 +53,27 @@ namespace Robust.Server.GameStates
_networkManager.Connected += HandleClientConnected;
_networkManager.Disconnect += HandleClientDisconnect;
_playerManager.PlayerStatusChanged += HandlePlayerStatusChanged;
_entityManager.EntityDeleted += HandleEntityDeleted;
}
private void HandleEntityDeleted(object? sender, EntityUid e)
{
_entityView.EntityDeleted(e);
}
private void HandlePlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.InGame)
{
_entityView.AddPlayer(e.Session);
}
else if(e.OldStatus == SessionStatus.InGame)
{
_entityView.RemovePlayer(e.Session);
}
}
private void HandleClientConnected(object? sender, NetChannelArgs e)
@@ -57,13 +87,6 @@ namespace Robust.Server.GameStates
private void HandleClientDisconnect(object? sender, NetChannelArgs e)
{
_ackedStates.Remove(e.Channel.ConnectionId);
if (!_playerManager.TryGetSessionByChannel(e.Channel, out var session))
{
return;
}
_entityManager.DropPlayerState(session);
}
private void HandleStateAck(MsgStateAck msg)
@@ -98,12 +121,13 @@ namespace Robust.Server.GameStates
{
DebugTools.Assert(_networkManager.IsServer);
_entityManager.Update();
_entityView.ViewSize = PvsRange * 2;
_entityView.CullingEnabled = PvsEnabled;
if (!_networkManager.IsConnected)
{
// Prevent deletions piling up if we have no clients.
_entityManager.CullDeletionHistory(GameTick.MaxValue);
_entityView.CullDeletionHistory(GameTick.MaxValue);
_mapManager.CullDeletionHistory(GameTick.MaxValue);
return;
}
@@ -112,16 +136,14 @@ namespace Robust.Server.GameStates
var oldestAck = GameTick.MaxValue;
var oldDeps = IoCManager.Resolve<IDependencyCollection>();
var deps = new DependencyCollection();
deps.RegisterInstance<ILogManager>(new ProxyLogManager(IoCManager.Resolve<ILogManager>()));
deps.BuildGraph();
var mainThread = Thread.CurrentThread;
(MsgState, INetChannel) GenerateMail(IPlayerSession session)
{
IoCManager.InitThread(deps, true);
// KILL IT WITH FIRE
if(mainThread != Thread.CurrentThread)
IoCManager.InitThread(new DependencyCollection(), true);
// people not in the game don't get states
if (session.Status != SessionStatus.InGame)
{
return default;
@@ -134,11 +156,8 @@ namespace Robust.Server.GameStates
DebugTools.Assert("Why does this channel not have an entry?");
}
var entStates = lastAck == GameTick.Zero || !PvsEnabled
? _entityManager.GetEntityStates(lastAck, session)
: _entityManager.UpdatePlayerSeenEntityStates(lastAck, session, _entityManager.MaxUpdateRange);
var (entStates, deletions) = _entityView.CalculateEntityStates(session, lastAck, _gameTiming.CurTick);
var playerStates = _playerManager.GetPlayerStates(lastAck);
var deletions = _entityManager.GetDeletedEntities(lastAck);
var mapData = _mapManager.GetStateData(lastAck);
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
@@ -167,15 +186,9 @@ namespace Robust.Server.GameStates
}
var mailBag = _playerManager.GetAllPlayers()
.AsParallel().Select(GenerateMail).ToList();
// TODO: oh god oh fuck kill it with fire.
// PLINQ *seems* to be scheduling to the main thread partially (I guess that makes sense?)
// Which causes that IoC "hack" up there to override IoC in the main thread, nuking the game.
// At least, that's our running theory. I reproduced it once locally and can't reproduce it again.
// Throwing shit at the wall to hope it fixes it.
IoCManager.InitThread(oldDeps, true);
.AsParallel()
.Where(s=>s.Status == SessionStatus.InGame).Select(GenerateMail);
foreach (var (msg, chan) in mailBag)
{
// see session.Status != SessionStatus.InGame above
@@ -187,9 +200,78 @@ namespace Robust.Server.GameStates
if (oldestAck > _lastOldestAck)
{
_lastOldestAck = oldestAck;
_entityManager.CullDeletionHistory(oldestAck);
_entityView.CullDeletionHistory(oldestAck);
_mapManager.CullDeletionHistory(oldestAck);
}
}
/// <summary>
/// Generates a network entity state for the given entity.
/// </summary>
/// <param name="compMan">ComponentManager that contains the components for the entity.</param>
/// <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>
/// <returns>New entity State for the given entity.</returns>
internal static EntityState GetEntityState(IComponentManager compMan, ICommonSession player, EntityUid entityUid, GameTick fromTick)
{
var compStates = new List<ComponentState>();
var changed = new List<ComponentChanged>();
foreach (var comp in compMan.GetNetComponents(entityUid))
{
DebugTools.Assert(comp.Initialized);
// NOTE: When LastModifiedTick or CreationTick are 0 it means that the relevant data is
// "not different from entity creation".
// i.e. when the client spawns the entity and loads the entity prototype,
// the data it deserializes from the prototype SHOULD be equal
// to what the component state / ComponentChanged would send.
// As such, we can avoid sending this data in this case since the client "already has it".
if (comp.NetSyncEnabled && comp.LastModifiedTick != GameTick.Zero && comp.LastModifiedTick >= fromTick)
compStates.Add(comp.GetComponentState(player));
if (comp.CreationTick != GameTick.Zero && comp.CreationTick >= fromTick && !comp.Deleted)
{
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChanged.Added(comp.NetID!.Value, comp.Name));
}
else if (comp.Deleted && comp.LastModifiedTick >= fromTick)
{
// Can't be null since it's returned by GetNetComponents
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChanged.Removed(comp.NetID!.Value));
}
}
return new EntityState(entityUid, changed.ToArray(), compStates.ToArray());
}
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
internal static List<EntityState>? GetAllEntityStates(IEntityManager entityMan, ICommonSession player, GameTick fromTick)
{
var stateEntities = new List<EntityState>();
foreach (var entity in entityMan.GetEntities())
{
if (entity.Deleted)
{
continue;
}
DebugTools.Assert(entity.Initialized);
if (entity.LastModifiedTick <= fromTick)
continue;
stateEntities.Add(GetEntityState(entityMan.ComponentManager, player, entity.Uid, fromTick));
}
// no point sending an empty collection
return stateEntities.Count == 0 ? default : stateEntities;
}
}
}

View File

@@ -9,12 +9,6 @@ namespace Robust.Server.Player
{
DateTime ConnectedTime { get; }
/// <summary>
/// The visibility mask for this player.
/// The player will be able to get updates for entities whose layers match the mask.
/// </summary>
int VisibilityMask { get; set; }
event EventHandler<SessionStatusEventArgs> PlayerStatusChanged;
void JoinGame();

View File

@@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.3" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.4" />
<PackageReference Include="prometheus-net" Version="4.1.1" />
<PackageReference Include="Serilog.Sinks.Loki" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Primitives" Version="5.0.0" />

View File

@@ -40,7 +40,7 @@ namespace Robust.Shared
CVarDef.Create("net.cmdrate", 30, CVar.ARCHIVE | CVar.CLIENTONLY);
public static readonly CVarDef<int> NetRate =
CVarDef.Create("net.rate", 10240, CVar.ARCHIVE | CVar.REPLICATED | CVar.CLIENTONLY);
CVarDef.Create("net.rate", 10240, CVar.ARCHIVE | CVar.CLIENTONLY);
// That's comma-separated, btw.
public static readonly CVarDef<string> NetBindTo =

View File

@@ -19,15 +19,29 @@ namespace Robust.Shared.Containers
{
get
{
if (ContainedEntity == null) return Array.Empty<IEntity>();
if (ContainedEntity == null)
return Array.Empty<IEntity>();
return new List<IEntity> {ContainedEntity};
// Cast to handle nullability.
return (IEntity[]) _containedEntityArray!;
}
}
[ViewVariables]
[field: DataField("ent")]
public IEntity? ContainedEntity { get; private set; }
[DataField("ent")]
public IEntity? ContainedEntity
{
get => _containedEntity;
private set
{
_containedEntity = value;
_containedEntityArray[0] = value;
}
}
private IEntity? _containedEntity;
// Used by ContainedEntities to avoid allocating.
private readonly IEntity?[] _containedEntityArray = new IEntity[1];
/// <inheritdoc />
public override string ContainerType => ClassName;

View File

@@ -23,9 +23,11 @@ namespace Robust.Shared.GameObjects
private const int TypeCapacity = 32;
private const int ComponentCollectionCapacity = 1024;
private const int EntityCapacity = 1024;
private const int NetComponentCapacity = 8;
private readonly Dictionary<uint, Dictionary<EntityUid, Component>> _entNetIdDict
= new();
private readonly Dictionary<EntityUid, Dictionary<uint, Component>> _netComponents
= new(EntityCapacity);
private readonly Dictionary<Type, Dictionary<EntityUid, Component>> _entTraitDict
= new();
@@ -60,7 +62,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public void Clear()
{
_entNetIdDict.Clear();
_netComponents.Clear();
_entTraitDict.Clear();
_entCompIndex.Clear();
_deleteSet.Clear();
@@ -76,10 +78,6 @@ namespace Robust.Shared.GameObjects
private void OnComponentAdded(IComponentRegistration obj)
{
_entTraitDict.Add(obj.Type, new Dictionary<EntityUid, Component>());
var netID = obj.NetID;
if (netID.HasValue)
_entNetIdDict.Add(netID.Value, new Dictionary<EntityUid, Component>());
}
private void OnComponentReferenceAdded((IComponentRegistration, Type) obj)
@@ -148,7 +146,13 @@ namespace Robust.Shared.GameObjects
{
// the main comp grid keeps this in sync
var netId = component.NetID.Value;
_entNetIdDict[netId].Add(uid, component);
if (!_netComponents.TryGetValue(uid, out var netSet))
{
netSet = new Dictionary<uint, Component>(NetComponentCapacity);
_netComponents.Add(uid, netSet);
}
netSet.Add(netId, component);
// mark the component as dirty for networking
component.Dirty();
@@ -325,15 +329,19 @@ namespace Robust.Shared.GameObjects
_entTraitDict[refType].Remove(entityUid);
}
if (component.NetID == null) return;
// ReSharper disable once InvertIf
if (component.NetID != null)
{
var netSet = _netComponents[entityUid];
if (netSet.Count == 1)
_netComponents.Remove(entityUid);
else
netSet.Remove(component.NetID.Value);
component.Owner.Dirty();
}
var netId = component.NetID.Value;
_entNetIdDict[netId].Remove(entityUid);
_entCompIndex.Remove(entityUid, component);
// mark the owning entity as dirty for networking
component.Owner.Dirty();
ComponentDeleted?.Invoke(this, new DeletedComponentEventArgs(component, entityUid));
}
@@ -356,8 +364,8 @@ namespace Robust.Shared.GameObjects
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasComponent(EntityUid uid, uint netId)
{
var dict = _entNetIdDict[netId];
return dict.TryGetValue(uid, out var comp) && !comp.Deleted;
return _netComponents.TryGetValue(uid, out var netSet)
&& netSet.ContainsKey(netId);
}
/// <inheritdoc />
@@ -386,17 +394,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public IComponent GetComponent(EntityUid uid, uint netId)
{
// ReSharper disable once InvertIf
var dict = _entNetIdDict[netId];
if (dict.TryGetValue(uid, out var comp))
{
if (!comp.Deleted)
{
return comp;
}
}
throw new KeyNotFoundException($"Entity {uid} does not have a component of NetID {netId}");
return _netComponents[uid][netId];
}
/// <inheritdoc />
@@ -433,19 +431,16 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
public bool TryGetComponent(EntityUid uid, uint netId, [NotNullWhen(true)] out IComponent? component)
public bool TryGetComponent(EntityUid uid, uint netId, [MaybeNullWhen(false)] out IComponent component)
{
var dict = _entNetIdDict[netId];
if (dict.TryGetValue(uid, out var comp))
if (_netComponents.TryGetValue(uid, out var netSet)
&& netSet.TryGetValue(netId, out var comp))
{
if (!comp.Deleted)
{
component = comp;
return true;
}
component = comp;
return true;
}
component = null;
component = default;
return false;
}
@@ -476,13 +471,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public IEnumerable<IComponent> GetNetComponents(EntityUid uid)
{
var comps = _entCompIndex[uid];
foreach (var comp in comps)
{
if (comp.Deleted || comp.NetID == null) continue;
yield return comp;
}
return _netComponents[uid].Values;
}
#region Join Functions
@@ -601,11 +590,6 @@ namespace Robust.Shared.GameObjects
{
_entTraitDict.Add(refType, new Dictionary<EntityUid, Component>());
}
foreach (var netId in _componentFactory.GetAllNetIds())
{
_entNetIdDict.Add(netId, new Dictionary<EntityUid, Component>());
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -21,6 +21,13 @@ namespace Robust.Shared.GameObjects
[ViewVariables(VVAccess.ReadWrite)]
public virtual Angle Rotation { get; set; }
/// <summary>
/// The visibility mask for this eye.
/// The player will be able to get updates for entities whose layers match the mask.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public virtual uint VisibilityMask { get; set; }
}
[NetSerializable, Serializable]
@@ -30,13 +37,15 @@ namespace Robust.Shared.GameObjects
public Vector2 Zoom { get; }
public Vector2 Offset { get; }
public Angle Rotation { get; }
public uint VisibilityMask { get; }
public EyeComponentState(bool drawFov, Vector2 zoom, Vector2 offset, Angle rotation) : base(NetIDs.EYE)
public EyeComponentState(bool drawFov, Vector2 zoom, Vector2 offset, Angle rotation, uint visibilityMask) : base(NetIDs.EYE)
{
DrawFov = drawFov;
Zoom = zoom;
Offset = offset;
Rotation = rotation;
VisibilityMask = visibilityMask;
}
}
}

View File

@@ -269,14 +269,6 @@ namespace Robust.Shared.GameObjects
return EntityManager.ComponentManager.GetComponent(Uid, type);
}
/// <inheritdoc />
public IComponent GetComponent(uint netId)
{
DebugTools.Assert(!Deleted, "Tried to get component on a deleted entity.");
return EntityManager.ComponentManager.GetComponent(Uid, netId);
}
/// <inheritdoc />
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component) where T : class
{
@@ -303,19 +295,6 @@ namespace Robust.Shared.GameObjects
return TryGetComponent(type, out var component) ? component : null;
}
/// <inheritdoc />
public bool TryGetComponent(uint netId, [NotNullWhen(true)] out IComponent? component)
{
DebugTools.Assert(!Deleted, "Tried to get component on a deleted entity.");
return EntityManager.ComponentManager.TryGetComponent(Uid, netId, out component);
}
public IComponent? GetComponentOrNull(uint netId)
{
return TryGetComponent(netId, out var component) ? component : null;
}
/// <inheritdoc />
public void Delete()
{

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Prometheus;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -14,18 +15,20 @@ using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects
{
public delegate void EntityQueryCallback(IEntity entity);
/// <inheritdoc />
public abstract class EntityManager : IEntityManager
{
#region Dependencies
[Dependency] private readonly IEntityNetworkManager EntityNetworkManager = default!;
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[Dependency] protected readonly IEntitySystemManager EntitySystemManager = default!;
[Dependency] private readonly IComponentFactory ComponentFactory = default!;
[Dependency] private readonly IComponentManager _componentManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[IoC.Dependency] private readonly IEntityNetworkManager EntityNetworkManager = default!;
[IoC.Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[IoC.Dependency] protected readonly IEntitySystemManager EntitySystemManager = default!;
[IoC.Dependency] private readonly IComponentFactory ComponentFactory = default!;
[IoC.Dependency] private readonly IComponentManager _componentManager = default!;
[IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
[IoC.Dependency] private readonly IMapManager _mapManager = default!;
#endregion Dependencies
@@ -91,16 +94,16 @@ namespace Robust.Shared.GameObjects
_componentManager.Clear();
}
public virtual void Update(float frameTime, Histogram? histogram)
public virtual void TickUpdate(float frameTime, Histogram? histogram)
{
using (histogram?.WithLabels("EntityNet").NewTimer())
{
EntityNetworkManager.Update();
EntityNetworkManager.TickUpdate();
}
using (histogram?.WithLabels("EntitySystems").NewTimer())
{
EntitySystemManager.Update(frameTime);
EntitySystemManager.TickUpdate(frameTime);
}
using (histogram?.WithLabels("EntityEventBus").NewTimer())
@@ -108,6 +111,11 @@ namespace Robust.Shared.GameObjects
_eventBus.ProcessEventQueue();
}
using (histogram?.WithLabels("ComponentCull").NewTimer())
{
_componentManager.CullRemovedComponents();
}
using (histogram?.WithLabels("EntityCull").NewTimer())
{
CullDeletedEntities();
@@ -272,6 +280,7 @@ namespace Robust.Shared.GameObjects
entity.LifeStage = EntityLifeStage.Deleted;
EntityDeleted?.Invoke(this, entity.Uid);
EventBus.RaiseEvent(EventSource.Local, new EntityDeletedMessage(entity));
}
public void DeleteEntity(EntityUid uid)
@@ -451,6 +460,9 @@ namespace Robust.Shared.GameObjects
}
}
/// <summary>
/// Factory for generating a new EntityUid for an entity currently being created.
/// </summary>
protected abstract EntityUid GenerateEntityUid();
#region Spatial Queries
@@ -472,6 +484,17 @@ namespace Robust.Shared.GameObjects
return found;
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public void FastEntitiesIntersecting(in MapId mapId, ref Box2 position, EntityQueryCallback callback)
{
if (mapId == MapId.Nullspace)
return;
_entityTreesPerMap[mapId]._b2Tree
.FastQuery(ref position, (ref IEntity data) => callback(data));
}
/// <inheritdoc />
public IEnumerable<IEntity> GetEntitiesIntersecting(MapId mapId, Box2 position, bool approximate = false)
{
@@ -712,18 +735,8 @@ namespace Robust.Shared.GameObjects
}
#endregion
public virtual void Update()
{
}
}
/// <summary>
/// The children of this entity are about to be deleted.
/// </summary>
public class EntityTerminatingEvent : EntityEventArgs { }
public enum EntityMessageType : byte
{
Error = 0,

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Serialization;
using Robust.Shared.Serialization;
using System;
namespace Robust.Shared.GameObjects
@@ -10,6 +10,8 @@ namespace Robust.Shared.GameObjects
public ComponentChanged[]? ComponentChanges { get; }
public ComponentState[]? ComponentStates { get; }
public bool Empty => ComponentChanges is null && ComponentStates is null;
public EntityState(EntityUid uid, ComponentChanged[]? changedComponents, ComponentState[]? componentStates)
{
Uid = uid;

View File

@@ -251,7 +251,7 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
public void Update(float frameTime)
public void TickUpdate(float frameTime)
{
foreach (var updReg in _updateOrder)
{

View File

@@ -1,6 +1,4 @@
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects
namespace Robust.Shared.GameObjects
{
public sealed class EntityDeletedMessage : EntityEventArgs
{

View File

@@ -0,0 +1,7 @@
namespace Robust.Shared.GameObjects
{
/// <summary>
/// The children of this entity are about to be deleted.
/// </summary>
public class EntityTerminatingEvent : EntityEventArgs { }
}

View File

@@ -112,7 +112,8 @@ namespace Robust.Shared.GameObjects
bool HasComponent(EntityUid uid, Type type);
/// <summary>
/// Checks if the entity has a component with a given network ID.
/// Checks if the entity has a component with a given network ID. This does not check
/// if the component is deleted.
/// </summary>
/// <param name="uid">Entity UID to check.</param>
/// <param name="netId">Network ID to check for.</param>
@@ -136,7 +137,8 @@ namespace Robust.Shared.GameObjects
IComponent GetComponent(EntityUid uid, Type type);
/// <summary>
/// Returns the component with a specific network ID.
/// Returns the component with a specific network ID. This does not check
/// if the component is deleted.
/// </summary>
/// <param name="uid">Entity UID to look on.</param>
/// <param name="netId">Network ID of the component to retrieve.</param>
@@ -162,7 +164,8 @@ namespace Robust.Shared.GameObjects
bool TryGetComponent(EntityUid uid, Type type, [NotNullWhen(true)] out IComponent? component);
/// <summary>
/// Returns the component with a specified network ID.
/// Returns the component with a specified network ID. This does not check
/// if the component is deleted.
/// </summary>
/// <param name="uid">Entity UID to check.</param>
/// <param name="netId">Component Network ID to check for.</param>

View File

@@ -127,17 +127,6 @@ namespace Robust.Shared.GameObjects
/// </exception>
IComponent GetComponent(Type type);
/// <summary>
/// Retrieves the component with the specified network ID.
/// </summary>
/// <param name="netID">The net ID of the component to retrieve.</param>
/// <returns>The component with the provided net ID.</returns>
/// <seealso cref="IComponent.NetID" />
/// <exception cref="Shared.GameObjects.UnknownComponentException">
/// Thrown if there is no component with the specified net ID.
/// </exception>
IComponent GetComponent(uint netID);
/// <summary>
/// Attempt to retrieve the component with specified type,
/// writing it to the <paramref name="component" /> out parameter if it was found.
@@ -172,23 +161,6 @@ namespace Robust.Shared.GameObjects
/// <returns>The component, if it was found. Null otherwise.</returns>
IComponent? GetComponentOrNull(Type type);
/// <summary>
/// Attempt to retrieve the component with specified network ID,
/// writing it to the <paramref name="component" /> out parameter if it was found.
/// </summary>
/// <param name="netId">The component net ID to attempt to fetch.</param>
/// <param name="component">The component, if it was found. Null otherwise.</param>
/// <returns>True if a component with specified net ID was found.</returns>
bool TryGetComponent(uint netId, [NotNullWhen(true)] out IComponent? component);
/// <summary>
/// Attempt to retrieve the component with specified network ID,
/// returning it if it was found.
/// </summary>
/// <param name="netId">The component net ID to attempt to fetch.</param>
/// <returns>The component, if it was found. Null otherwise.</returns>
IComponent? GetComponentOrNull(uint netId);
/// <summary>
/// Deletes this entity.
/// </summary>
@@ -222,6 +194,9 @@ namespace Robust.Shared.GameObjects
/// <param name="message">Message to send.</param>
void SendNetworkMessage(IComponent owner, ComponentMessage message, INetChannel? channel = null);
/// <summary>
/// Marks this entity as dirty so that it will be updated over the network.
/// </summary>
void Dirty();
}
}

View File

@@ -18,7 +18,7 @@ namespace Robust.Shared.GameObjects
void Initialize();
void Startup();
void Shutdown();
void Update(float frameTime, Histogram? histogram=null);
void TickUpdate(float frameTime, Histogram? histogram=null);
/// <summary>
/// Client-specific per-render frame updating.
@@ -126,6 +126,8 @@ namespace Robust.Shared.GameObjects
/// <param name="box"></param>
/// <param name="approximate">If true, will not recalculate precise entity AABBs, resulting in a perf increase. </param>
bool AnyEntitiesIntersecting(MapId mapId, Box2 box, bool approximate = false);
void FastEntitiesIntersecting(in MapId mapId, ref Box2 position, EntityQueryCallback callback);
/// <summary>
/// Gets entities with a bounding box that intersects this box
@@ -216,8 +218,5 @@ namespace Robust.Shared.GameObjects
bool RemoveFromEntityTree(IEntity entity, MapId mapId);
#endregion
void Update();
}
}

View File

@@ -65,6 +65,6 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Sends out queued messages based on current tick.
/// </summary>
void Update();
void TickUpdate();
}
}

View File

@@ -65,7 +65,7 @@ namespace Robust.Shared.GameObjects
/// </summary>
/// <param name="frameTime">Time since the last frame was rendered.</param>
/// <seealso cref="IEntitySystem.Update(float)"/>
void Update(float frameTime);
void TickUpdate(float frameTime);
void FrameUpdate(float frameTime);
/// <summary>

View File

@@ -7,6 +7,7 @@ using Robust.Shared.Containers;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Controllers;
@@ -260,6 +261,8 @@ namespace Robust.Shared.GameObjects
var mapId = message.Container.Owner.Transform.MapID;
physicsComponent.LinearVelocity = Vector2.Zero;
physicsComponent.AngularVelocity = 0.0f;
physicsComponent.ClearJoints();
_maps[mapId].RemoveBody(physicsComponent);
}

View File

@@ -31,6 +31,8 @@ namespace Robust.Shared.IoC
private readonly Dictionary<Type, DependencyFactoryDelegate<object>> _resolveFactories = new();
private readonly Queue<Type> _pendingResolves = new();
// To do injection of common types like components, we make DynamicMethods to do the actual injecting.
// This is way faster than reflection and should be allocation free outside setup.
private readonly Dictionary<Type, (InjectorDelegate? @delegate, object[]? services)> _injectorCache =
@@ -38,9 +40,38 @@ namespace Robust.Shared.IoC
/// <inheritdoc />
public void Register<TInterface, TImplementation>(bool overwrite = false)
where TImplementation : class, TInterface, new()
where TImplementation : class, TInterface
{
Register<TInterface, TImplementation>(() => new TImplementation(), overwrite);
Register<TInterface, TImplementation>(() =>
{
var objectType = typeof(TImplementation);
var constructors = objectType.GetConstructors();
if (constructors.Length != 1)
throw new InvalidOperationException($"Dependency '{typeof(TImplementation).FullName}' requires exactly one constructor.");
var constructorParams = constructors[0].GetParameters();
var parameters = new object[constructorParams.Length];
for (var index = 0; index < constructorParams.Length; index++)
{
var param = constructorParams[index];
if (_services.TryGetValue(param.ParameterType, out var instance))
parameters[index] = instance;
else
{
if (_resolveTypes.ContainsKey(param.ParameterType))
{
throw new InvalidOperationException($"Dependency '{typeof(TImplementation).FullName}' ctor requires {param.ParameterType.FullName} registered before it.");
}
throw new InvalidOperationException($"Dependency '{typeof(TImplementation).FullName}' ctor has unknown dependency {param.ParameterType.FullName}");
}
}
return (TImplementation) Activator.CreateInstance(objectType, parameters)!;
}, overwrite);
}
public void Register<TInterface, TImplementation>(DependencyFactoryDelegate<TImplementation> factory, bool overwrite = false)
@@ -51,6 +82,7 @@ namespace Robust.Shared.IoC
_resolveTypes[interfaceType] = typeof(TImplementation);
_resolveFactories[typeof(TImplementation)] = factory;
_pendingResolves.Enqueue(interfaceType);
}
[AssertionMethod]
@@ -151,8 +183,11 @@ namespace Robust.Shared.IoC
// First we build every type we have registered but isn't yet built.
// This allows us to run this after the content assembly has been loaded.
foreach (var (key, value) in _resolveTypes.Where(p => !_services.ContainsKey(p.Key)))
while(_pendingResolves.Count > 0)
{
Type key = _pendingResolves.Dequeue();
var value = _resolveTypes[key];
// Find a potential dupe by checking other registered types that have already been instantiated that have the same instance type.
// Can't catch ourselves because we're not instantiated.
// Ones that aren't yet instantiated are about to be and will find us instead.

View File

@@ -42,7 +42,7 @@ namespace Robust.Shared.IoC
/// or if an already instantiated interface (by <see cref="DependencyCollection.BuildGraph"/>) is attempting to be overwritten.
/// </exception>
void Register<TInterface, TImplementation>(bool overwrite = false)
where TImplementation : class, TInterface, new();
where TImplementation : class, TInterface;
/// <summary>
/// Registers an interface to an implementation, to make it accessible to <see cref="DependencyCollection.Resolve{T}"/>

View File

@@ -97,7 +97,7 @@ namespace Robust.Shared.IoC
/// or if an already instantiated interface (by <see cref="BuildGraph"/>) is attempting to be overwritten.
/// </exception>
public static void Register<TInterface, TImplementation>(bool overwrite = false)
where TImplementation : class, TInterface, new()
where TImplementation : class, TInterface
{
DebugTools.Assert(_container.IsValueCreated, NoContextAssert);

View File

@@ -1,10 +1,10 @@
using System;
using System;
using Robust.Shared.Serialization;
namespace Robust.Shared.Map
{
[Serializable, NetSerializable]
public struct MapId : IEquatable<MapId>
public readonly struct MapId : IEquatable<MapId>
{
public static readonly MapId Nullspace = new(0);

View File

@@ -177,10 +177,10 @@ namespace Robust.Shared.Network
{
get
{
var sentPackets = 0;
var sentBytes = 0;
var recvPackets = 0;
var recvBytes = 0;
var sentPackets = 0L;
var sentBytes = 0L;
var recvPackets = 0L;
var recvBytes = 0L;
foreach (var peer in _netPeers)
{
@@ -1155,24 +1155,24 @@ namespace Robust.Shared.Network
/// <summary>
/// Total sent bytes.
/// </summary>
public readonly int SentBytes;
public readonly long SentBytes;
/// <summary>
/// Total received bytes.
/// </summary>
public readonly int ReceivedBytes;
public readonly long ReceivedBytes;
/// <summary>
/// Total sent packets.
/// </summary>
public readonly int SentPackets;
public readonly long SentPackets;
/// <summary>
/// Total received packets.
/// </summary>
public readonly int ReceivedPackets;
public readonly long ReceivedPackets;
public NetworkStats(int sentBytes, int receivedBytes, int sentPackets, int receivedPackets)
public NetworkStats(long sentBytes, long receivedBytes, long sentPackets, long receivedPackets)
{
SentBytes = sentBytes;
ReceivedBytes = receivedBytes;

View File

@@ -915,6 +915,41 @@ namespace Robust.Shared.Physics
}
}
public delegate void FastQueryCallback(ref T userData);
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public void FastQuery(ref Box2 aabb, FastQueryCallback callback)
{
var stack = new GrowableStack<Proxy>(stackalloc Proxy[256]);
stack.Push(_root);
ref var baseRef = ref _nodes[0];
while (stack.GetCount() != 0)
{
var nodeId = stack.Pop();
if (nodeId == Proxy.Free)
{
continue;
}
// Skip bounds check with Unsafe.Add().
ref var node = ref Unsafe.Add(ref baseRef, nodeId);
ref var nodeAabb = ref node.Aabb;
if (nodeAabb.Intersects(aabb))
{
if (node.IsLeaf)
{
callback(ref node.UserData);
}
else
{
stack.Push(node.Child1);
stack.Push(node.Child2);
}
}
}
}
private static readonly RayQueryCallback<RayQueryCallback> EasyRayQueryCallback =
(ref RayQueryCallback callback, Proxy proxy, in Vector2 hitPos, float distance) => callback(proxy, hitPos, distance);

View File

@@ -1,4 +1,4 @@
/*
/*
* Initially based on Box2D by Erin Catto, license follows;
*
* Copyright (c) 2009 Erin Catto http://www.box2d.org
@@ -72,7 +72,7 @@ namespace Robust.Shared.Physics
// avoids "Collection was modified; enumeration operation may not execute."
private Dictionary<T, Proxy> _nodeLookup;
private readonly B2DynamicTree<T> _b2Tree;
public readonly B2DynamicTree<T> _b2Tree;
public DynamicTree(ExtractAabbDelegate extractAabbFunc, IEqualityComparer<T>? comparer = null, float aabbExtendSize = 1f / 32, int capacity = 256, Func<int, int>? growthFunc = null)
{

View File

@@ -12,12 +12,5 @@ namespace Robust.Shared.Physics.Dynamics.Joints
bodyA.AddJoint(joint);
return joint;
}
public static SlothJoint CreateSlothJoint(this PhysicsComponent bodyA, PhysicsComponent bodyB)
{
var joint = new SlothJoint(bodyA, bodyB);
bodyA.AddJoint(joint);
return joint;
}
}
}

View File

@@ -1,126 +0,0 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Physics.Dynamics.Joints
{
[Serializable, NetSerializable]
public class SlothJoint : Joint
{
// Solver temp
[NonSerialized] private int _indexA;
[NonSerialized] private int _indexB;
[NonSerialized] private Vector2 _rA;
[NonSerialized] private Vector2 _rB;
[NonSerialized] private Vector2 _localCenterA;
[NonSerialized] private Vector2 _localCenterB;
[NonSerialized] private float _invMassA;
[NonSerialized] private float _invMassB;
[NonSerialized] private float _invIA;
[NonSerialized] private float _invIB;
[NonSerialized] private float _mass;
[NonSerialized] private float _currentLength;
[NonSerialized] private float _softMass;
public SlothJoint(PhysicsComponent bodyA, PhysicsComponent bodyB) : base(bodyA, bodyB)
{
}
public override JointType JointType => JointType.Distance;
[field:NonSerialized]
public override Vector2 WorldAnchorA { get; set; }
[field:NonSerialized]
public override Vector2 WorldAnchorB { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public float MaxLength
{
get => _maxLength;
set
{
if (MathHelper.CloseTo(value, _maxLength)) return;
_maxLength = value;
Dirty();
}
}
private float _maxLength;
public override Vector2 GetReactionForce(float invDt)
{
// TODO: Need break force
return Vector2.Zero;
}
public override float GetReactionTorque(float invDt)
{
return 0f;
}
internal override void InitVelocityConstraints(SolverData data)
{
_indexA = BodyA.IslandIndex;
_indexB = BodyB.IslandIndex;
_localCenterA = Vector2.Zero; //BodyA->m_sweep.localCenter;
_localCenterB = Vector2.Zero; //BodyB->m_sweep.localCenter;
_invMassA = BodyA.InvMass;
_invMassB = BodyB.InvMass;
_invIA = BodyA.InvI;
_invIB = BodyB.InvI;
_currentLength = (data.Positions[_indexA] - data.Positions[_indexB]).Length;
_softMass = _mass;
}
internal override void SolveVelocityConstraints(SolverData data)
{
if (_currentLength < _maxLength) return;
var posA = data.Positions[_indexA];
var posB = data.Positions[_indexB];
var vA = data.LinearVelocities[_indexA];
float wA = data.AngularVelocities[_indexA];
var vB = data.LinearVelocities[_indexB];
float wB = data.AngularVelocities[_indexB];
var correctionDistance = _maxLength - _currentLength;
//var P = _u * impulse;
//vA -= P * _invMassA;
//wA -= _invIA * Vector2.Cross(_rA, P);
//vB += P * _invMassB;
//wB += _invIB * Vector2.Cross(_rB, P);
}
internal override bool SolvePositionConstraints(SolverData data)
{
if (_currentLength < _maxLength) return true;
var posA = data.Positions[_indexA];
var posB = data.Positions[_indexB];
var vA = data.LinearVelocities[_indexA];
float wA = data.AngularVelocities[_indexA];
var vB = data.LinearVelocities[_indexB];
float wB = data.AngularVelocities[_indexB];
var correctionDistance = _maxLength - _currentLength;
data.Positions[_indexB] -= correctionDistance;
//var P = _u * impulse;
//vA -= P * _invMassA;
//wA -= _invIA * Vector2.Cross(_rA, P);
//vB += P * _invMassB;
//wB += _invIB * Vector2.Cross(_rB, P);
return true;
}
}
}

View File

@@ -48,6 +48,14 @@ namespace Robust.Shared.Prototypes
/// </exception>
IEnumerable<IPrototype> EnumeratePrototypes(Type type);
/// <summary>
/// Return an IEnumerable to iterate all prototypes of a certain variant.
/// </summary>
/// <exception cref="KeyNotFoundException">
/// Thrown if the variant of prototype is not registered.
/// </exception>
IEnumerable<IPrototype> EnumeratePrototypes(string variant);
/// <summary>
/// Index for a <see cref="IPrototype"/> by ID.
/// </summary>
@@ -64,8 +72,61 @@ namespace Robust.Shared.Prototypes
/// </exception>
IPrototype Index(Type type, string id);
bool HasIndex<T>(string id) where T : IPrototype;
bool TryIndex<T>(string id, [NotNullWhen(true)] out T? prototype) where T : IPrototype;
/// <summary>
/// Returns whether a prototype of type <typeparamref name="T"/> with the specified <param name="id"/> exists.
/// </summary>
bool HasIndex<T>(string id) where T : class, IPrototype;
bool TryIndex<T>(string id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype;
bool TryIndex(Type type, string id, [NotNullWhen(true)] out IPrototype? prototype);
/// <summary>
/// Returns whether a prototype variant <param name="variant"/> exists.
/// </summary>
/// <param name="variant">Identifier for the prototype variant.</param>
/// <returns>Whether the prototype variant exists.</returns>
bool HasVariant(string variant);
/// <summary>
/// Returns the Type for a prototype variant.
/// </summary>
/// <param name="variant">Identifier for the prototype variant.</param>
/// <returns>The specified prototype Type.</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown when the specified prototype variant isn't registered or doesn't exist.
/// </exception>
Type GetVariantType(string variant);
/// <summary>
/// Attempts to get the Type for a prototype variant.
/// </summary>
/// <param name="variant">Identifier for the prototype variant.</param>
/// <param name="prototype">The specified prototype Type, or null.</param>
/// <returns>Whether the prototype type was found and <see cref="prototype"/> isn't null.</returns>
bool TryGetVariantType(string variant, [NotNullWhen(true)] out Type? prototype);
/// <summary>
/// Attempts to get a prototype's variant.
/// </summary>
/// <param name="type"></param>
/// <param name="variant"></param>
/// <returns></returns>
bool TryGetVariantFrom(Type type, [NotNullWhen(true)] out string? variant);
/// <summary>
/// Attempts to get a prototype's variant.
/// </summary>
/// <param name="prototype">The prototype in question.</param>
/// <param name="variant">Identifier for the prototype variant, or null.</param>
/// <returns>Whether the prototype variant was successfully retrieved.</returns>
bool TryGetVariantFrom(IPrototype prototype, [NotNullWhen(true)] out string? variant);
/// <summary>
/// Attempts to get a prototype's variant.
/// </summary>
/// <param name="variant">Identifier for the prototype variant, or null.</param>
/// <typeparam name="T">The prototype in question.</typeparam>
/// <returns>Whether the prototype variant was successfully retrieved.</returns>
bool TryGetVariantFrom<T>([NotNullWhen(true)] out string? variant) where T : class, IPrototype;
/// <summary>
/// Load prototypes from files in a directory, recursively.
@@ -187,6 +248,11 @@ namespace Robust.Shared.Prototypes
return prototypes[type].Values;
}
public IEnumerable<IPrototype> EnumeratePrototypes(string variant)
{
return EnumeratePrototypes(GetVariantType(variant));
}
public T Index<T>(string id) where T : class, IPrototype
{
if (!_hasEverBeenReloaded)
@@ -586,7 +652,7 @@ namespace Robust.Shared.Prototypes
return changedPrototypes;
}
public bool HasIndex<T>(string id) where T : IPrototype
public bool HasIndex<T>(string id) where T : class, IPrototype
{
if (!prototypes.TryGetValue(typeof(T), out var index))
{
@@ -596,16 +662,74 @@ namespace Robust.Shared.Prototypes
return index.ContainsKey(id);
}
public bool TryIndex<T>(string id, [NotNullWhen(true)] out T? prototype) where T : IPrototype
public bool TryIndex<T>(string id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype
{
if (!prototypes.TryGetValue(typeof(T), out var index))
var returned = TryIndex(typeof(T), id, out var proto);
prototype = (proto ?? null) as T;
return returned;
}
public bool TryIndex(Type type, string id, [NotNullWhen(true)] out IPrototype? prototype)
{
if (!prototypes.TryGetValue(type, out var index))
{
throw new UnknownPrototypeException(id);
}
var returned = index.TryGetValue(id, out var uncast);
prototype = (T) uncast!;
return returned;
return index.TryGetValue(id, out prototype);
}
/// <inheritdoc />
public bool HasVariant(string variant)
{
return prototypeTypes.ContainsKey(variant);
}
/// <inheritdoc />
public Type GetVariantType(string variant)
{
return prototypeTypes[variant];
}
/// <inheritdoc />
public bool TryGetVariantType(string variant, [NotNullWhen(true)] out Type? prototype)
{
return prototypeTypes.TryGetValue(variant, out prototype);
}
/// <inheritdoc />
public bool TryGetVariantFrom(Type type, [NotNullWhen(true)] out string? variant)
{
variant = null;
// If the type doesn't implement IPrototype, this fails.
if (!(typeof(IPrototype).IsAssignableFrom(type)))
return false;
var attribute = (PrototypeAttribute?) Attribute.GetCustomAttribute(type, typeof(PrototypeAttribute));
// If the prototype type doesn't have the attribute, this fails.
if (attribute == null)
return false;
// If the variant isn't registered, this fails.
if (!HasVariant(attribute.Type))
return false;
variant = attribute.Type;
return true;
}
/// <inheritdoc />
public bool TryGetVariantFrom<T>([NotNullWhen(true)] out string? variant) where T : class, IPrototype
{
return TryGetVariantFrom(typeof(T), out variant);
}
/// <inheritdoc />
public bool TryGetVariantFrom(IPrototype prototype, [NotNullWhen(true)] out string? variant)
{
return TryGetVariantFrom(prototype.GetType(), out variant);
}
public void RegisterIgnore(string name)

View File

@@ -46,7 +46,11 @@ namespace Robust.Shared.Serialization.Manager
/// </returns>
ValidationNode ValidateNode<T>(DataNode node, ISerializationContext? context = null);
ValidationNode ValidateNodeWithCustomTypeSerializer(Type type, Type typeSerializer, DataNode node, ISerializationContext? context = null);
ValidationNode ValidateNodeWith(Type type, Type typeSerializer, DataNode node, ISerializationContext? context = null);
ValidationNode ValidateNodeWith<TType, TSerializer, TNode>(TNode node, ISerializationContext? context = null)
where TSerializer : ITypeValidator<TType, TNode>
where TNode : DataNode;
#endregion

View File

@@ -1,5 +1,4 @@
using System;
using Robust.Shared.IoC;
namespace Robust.Shared.Serialization.Manager.Result
{

View File

@@ -156,7 +156,7 @@ namespace Robust.Shared.Serialization.Manager
var keyValidated = serializationManager.ValidateNode(typeof(string), key, context);
ValidationNode valValidated = field.Attribute.CustomTypeSerializer != null
? serializationManager.ValidateNodeWithCustomTypeSerializer(field.FieldType,
? serializationManager.ValidateNodeWith(field.FieldType,
field.Attribute.CustomTypeSerializer, val, context)
: serializationManager.ValidateNode(field.FieldType, val, context);

View File

@@ -14,6 +14,7 @@ using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
using Robust.Shared.Utility;
namespace Robust.Shared.Serialization.Manager
@@ -200,7 +201,7 @@ namespace Robust.Shared.Serialization.Manager
return ValidateNode(typeof(T), node, context);
}
public ValidationNode ValidateNodeWithCustomTypeSerializer(Type type, Type typeSerializer, DataNode node,
public ValidationNode ValidateNodeWith(Type type, Type typeSerializer, DataNode node,
ISerializationContext? context = null)
{
var method =
@@ -209,6 +210,14 @@ namespace Robust.Shared.Serialization.Manager
return (ValidationNode)method.Invoke(this, new object?[] {node, context})!;
}
public ValidationNode ValidateNodeWith<TType, TSerializer, TNode>(TNode node,
ISerializationContext? context = null)
where TSerializer : ITypeValidator<TType, TNode>
where TNode: DataNode
{
return ValidateNodeWith(typeof(TType), typeof(TSerializer), node, context);
}
public DeserializationResult CreateDataDefinition<T>(DeserializedFieldEntry[] fields, bool skipHook = false)
where T : notnull, new()
{

View File

@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List
{
public partial class PrototypeIdListSerializer<T> :
ITypeSerializer<ImmutableList<string>, SequenceDataNode>
where T : class, IPrototype
{
public ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
{
return ValidateInternal(serializationManager, node, dependencies, context);
}
public DeserializationResult Read(ISerializationManager serializationManager, SequenceDataNode node,
IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null)
{
var builder = ImmutableList.CreateBuilder<string>();
var mappings = new List<DeserializationResult>();
foreach (var dataNode in node.Sequence)
{
var result = _prototypeSerializer.Read(
serializationManager,
(ValueDataNode) dataNode,
dependencies,
skipHook,
context);
builder.Add((string) result.RawValue!);
mappings.Add(result);
}
return new DeserializedCollection<ImmutableList<string>, string>(builder.ToImmutable(), mappings,
ImmutableList.CreateRange);
}
public DataNode Write(ISerializationManager serializationManager, ImmutableList<string> value, bool alwaysWrite = false,
ISerializationContext? context = null)
{
return WriteInternal(serializationManager, value, alwaysWrite, context);
}
public ImmutableList<string> Copy(ISerializationManager serializationManager, ImmutableList<string> source, ImmutableList<string> target,
bool skipHook, ISerializationContext? context = null)
{
return ImmutableList.CreateRange(source);
}
}
}

View File

@@ -0,0 +1,71 @@
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List
{
public partial class PrototypeIdListSerializer<T> :
ITypeSerializer<IReadOnlyCollection<string>, SequenceDataNode>
where T : class, IPrototype
{
ValidationNode ITypeValidator<IReadOnlyCollection<string>, SequenceDataNode>.Validate(
ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context)
{
return ValidateInternal(serializationManager, node, dependencies, context);
}
DeserializationResult ITypeReader<IReadOnlyCollection<string>, SequenceDataNode>.Read(
ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
bool skipHook,
ISerializationContext? context)
{
var list = new List<string>();
var mappings = new List<DeserializationResult>();
foreach (var dataNode in node.Sequence)
{
var result = _prototypeSerializer.Read(
serializationManager,
(ValueDataNode) dataNode,
dependencies,
skipHook,
context);
list.Add((string) result.RawValue!);
mappings.Add(result);
}
return new DeserializedCollection<List<string>, string>(list, mappings,
elements => new List<string>(elements));
}
DataNode ITypeWriter<IReadOnlyCollection<string>>.Write(
ISerializationManager serializationManager,
IReadOnlyCollection<string> value,
bool alwaysWrite,
ISerializationContext? context)
{
return WriteInternal(serializationManager, value, alwaysWrite, context);
}
IReadOnlyCollection<string> ITypeCopier<IReadOnlyCollection<string>>.Copy(
ISerializationManager serializationManager,
IReadOnlyCollection<string> source,
IReadOnlyCollection<string> target,
bool skipHook,
ISerializationContext? context)
{
return new List<string>(source);
}
}
}

View File

@@ -0,0 +1,85 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List
{
public partial class PrototypeIdListSerializer<T> : ITypeSerializer<IReadOnlyList<string>, SequenceDataNode>
where T : class, IPrototype
{
DataNode ITypeWriter<IReadOnlyList<string>>.Write(
ISerializationManager serializationManager,
IReadOnlyList<string> value,
bool alwaysWrite,
ISerializationContext? context)
{
return WriteInternal(serializationManager, value, alwaysWrite, context);
}
[MustUseReturnValue]
IReadOnlyList<string> ITypeCopier<IReadOnlyList<string>>.Copy(
ISerializationManager serializationManager,
IReadOnlyList<string> source,
IReadOnlyList<string> target,
bool skipHook,
ISerializationContext? context)
{
return new List<string>(source);
}
DeserializationResult ITypeReader<IReadOnlyList<string>, SequenceDataNode>.Read(
ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
bool skipHook,
ISerializationContext? context)
{
var list = new List<string>();
var mappings = new List<DeserializationResult>();
foreach (var dataNode in node.Sequence)
{
var result = _prototypeSerializer.Read(
serializationManager,
(ValueDataNode) dataNode,
dependencies,
skipHook,
context);
list.Add((string) result.RawValue!);
mappings.Add(result);
}
return new DeserializedCollection<IReadOnlyList<string>, string>(list, mappings,
elements => new List<string>(elements));
}
ValidationNode ITypeValidator<IReadOnlyList<string>, SequenceDataNode>.Validate(
ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context)
{
var list = new List<ValidationNode>();
foreach (var dataNode in node.Sequence)
{
if (dataNode is not ValueDataNode value)
{
list.Add(new ErrorNode(dataNode, $"Cannot cast node {dataNode} to ValueDataNode."));
continue;
}
list.Add(_prototypeSerializer.Validate(serializationManager, value, dependencies, context));
}
return new ValidatedSequenceNode(list);
}
}
}

View File

@@ -0,0 +1,109 @@
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List
{
public partial class PrototypeIdListSerializer<T> : ITypeSerializer<List<string>, SequenceDataNode> where T : class, IPrototype
{
private readonly PrototypeIdSerializer<T> _prototypeSerializer = new();
private ValidationNode ValidateInternal(
ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context)
{
var list = new List<ValidationNode>();
foreach (var dataNode in node.Sequence)
{
if (dataNode is not ValueDataNode value)
{
list.Add(new ErrorNode(dataNode, $"Cannot cast node {dataNode} to ValueDataNode."));
continue;
}
list.Add(_prototypeSerializer.Validate(serializationManager, value, dependencies, context));
}
return new ValidatedSequenceNode(list);
}
private DataNode WriteInternal(
ISerializationManager serializationManager,
IEnumerable<string> value,
bool alwaysWrite,
ISerializationContext? context)
{
var list = new List<DataNode>();
foreach (var str in value)
{
list.Add(_prototypeSerializer.Write(serializationManager, str, alwaysWrite, context));
}
return new SequenceDataNode(list);
}
ValidationNode ITypeValidator<List<string>, SequenceDataNode>.Validate(
ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context)
{
return ValidateInternal(serializationManager, node, dependencies, context);
}
DeserializationResult ITypeReader<List<string>, SequenceDataNode>.Read(
ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
bool skipHook,
ISerializationContext? context)
{
var list = new List<string>();
var mappings = new List<DeserializationResult>();
foreach (var dataNode in node.Sequence)
{
var result = _prototypeSerializer.Read(
serializationManager,
(ValueDataNode) dataNode,
dependencies,
skipHook,
context);
list.Add((string) result.RawValue!);
mappings.Add(result);
}
return new DeserializedCollection<List<string>, string>(list, mappings,
elements => new List<string>(elements));
}
DataNode ITypeWriter<List<string>>.Write(
ISerializationManager serializationManager,
List<string> value,
bool alwaysWrite,
ISerializationContext? context)
{
return WriteInternal(serializationManager, value, alwaysWrite, context);
}
List<string> ITypeCopier<List<string>>.Copy(
ISerializationManager serializationManager,
List<string> source,
List<string> target,
bool skipHook,
ISerializationContext? context)
{
return new();
}
}
}

View File

@@ -6,14 +6,14 @@ using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype
{
public class PrototypeIdSerializer<TPrototype> : ITypeSerializer<string, ValueDataNode> where TPrototype : IPrototype
public class PrototypeIdSerializer<TPrototype> : ITypeSerializer<string, ValueDataNode> where TPrototype : class, IPrototype
{
public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
{
return IoCManager.Resolve<IPrototypeManager>().HasIndex<TPrototype>(node.Value)
return dependencies.Resolve<IPrototypeManager>().HasIndex<TPrototype>(node.Value)
? new ValidatedValueNode(node)
: new ErrorNode(node, $"PrototypeID {node.Value} for type {typeof(TPrototype)} not found");
}

View File

@@ -0,0 +1,73 @@
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set
{
public class PrototypeIdHashSetSerializer<TPrototype> : ITypeSerializer<HashSet<string>, SequenceDataNode> where TPrototype : class, IPrototype
{
private readonly PrototypeIdSerializer<TPrototype> _prototypeSerializer = new();
public ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode node, IDependencyCollection dependencies, ISerializationContext? context = null)
{
var list = new List<ValidationNode>();
foreach (var dataNode in node.Sequence)
{
if (dataNode is not ValueDataNode value)
{
list.Add(new ErrorNode(dataNode, $"Cannot cast node {dataNode} to ValueDataNode."));
continue;
}
list.Add(_prototypeSerializer.Validate(serializationManager, value, dependencies, context));
}
return new ValidatedSequenceNode(list);
}
public DeserializationResult Read(ISerializationManager serializationManager, SequenceDataNode node, IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null)
{
var set = new HashSet<string>();
var mappings = new List<DeserializationResult>();
foreach (var dataNode in node.Sequence)
{
var result = _prototypeSerializer.Read(
serializationManager,
(ValueDataNode) dataNode,
dependencies,
skipHook,
context);
set.Add((string) result.RawValue!);
mappings.Add(result);
}
return new DeserializedCollection<HashSet<string>, string>(set, mappings,
elements => new HashSet<string>(elements));
}
public DataNode Write(ISerializationManager serializationManager, HashSet<string> value, bool alwaysWrite = false, ISerializationContext? context = null)
{
var list = new List<DataNode>();
foreach (var str in value)
{
list.Add(_prototypeSerializer.Write(serializationManager, str, alwaysWrite, context));
}
return new SequenceDataNode(list);
}
public HashSet<string> Copy(ISerializationManager serializationManager, HashSet<string> source, HashSet<string> target, bool skipHook, ISerializationContext? context = null)
{
return new(source);
}
}
}

View File

@@ -232,6 +232,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic
return list;
}
[MustUseReturnValue]
public ImmutableList<T> Copy(ISerializationManager serializationManager, ImmutableList<T> source,
ImmutableList<T> target, bool skipHook, ISerializationContext? context = null)
{

View File

@@ -97,7 +97,7 @@ namespace Robust.UnitTesting.Shared.GameObjects
systems.GetEntitySystem<TestSystemC>().Counter = counter;
systems.GetEntitySystem<TestSystemD>().Counter = counter;
systems.Update(1);
systems.TickUpdate(1);
Assert.That(counter.X, Is.EqualTo(4));

View File

@@ -36,6 +36,18 @@ namespace Robust.UnitTesting.Shared.IoC
tester.Test();
}
[Test]
public void IoCTestConstructorInjection()
{
IoCManager.Register<TestFieldInjection, TestFieldInjection>();
IoCManager.Register<TestConstructorInjection, TestConstructorInjection>();
IoCManager.BuildGraph();
var tester = IoCManager.Resolve<TestConstructorInjection>();
Assert.That(tester.FieldInjection, Is.Not.Null);
}
[Test]
public void IoCTestBasic()
{
@@ -192,6 +204,16 @@ namespace Robust.UnitTesting.Shared.IoC
}
}
public class TestConstructorInjection
{
public TestFieldInjection FieldInjection { get; }
public TestConstructorInjection(TestFieldInjection fieldInjection)
{
FieldInjection = fieldInjection;
}
}
public class TestUnregisteredInjection
{
[Dependency]

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using Moq;
using NUnit.Framework;
using Robust.Server.GameObjects;
@@ -8,7 +8,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Timing;
using MapGrid = Robust.Shared.Map.MapGrid;
namespace Robust.UnitTesting.Shared.Map
@@ -159,7 +158,6 @@ namespace Robust.UnitTesting.Shared.Map
private static IMapGridInternal MapGridFactory(GridId id)
{
var entMan = (ServerEntityManager)IoCManager.Resolve<IEntityManager>();
entMan.CullDeletionHistory(GameTick.MaxValue);
var mapId = new MapId(5);
var mapMan = IoCManager.Resolve<IMapManager>();

View File

@@ -0,0 +1,177 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using YamlDotNet.RepresentationModel;
// ReSharper disable AccessToStaticMemberViaDerivedType
namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers.Custom.Prototype
{
[TestFixture]
[TestOf(typeof(PrototypeIdListSerializer<>))]
public class PrototypeIdListSerializerTest : TypeSerializerTest
{
private static readonly string TestEntityId = $"{nameof(PrototypeIdListSerializerTest)}Dummy";
private static readonly string TestInvalidEntityId = $"{nameof(PrototypeIdListSerializerTest)}DummyInvalid";
private static readonly string Prototypes = $@"
- type: entity
id: {TestEntityId}";
private static readonly string DataString = $@"
entitiesList:
- {TestEntityId}
entitiesReadOnlyList:
- {TestEntityId}
entitiesReadOnlyCollection:
- {TestEntityId}
entitiesImmutableList:
- {TestEntityId}";
[OneTimeSetUp]
public void OneTimeSetUp()
{
IoCManager.Resolve<IPrototypeManager>().LoadString(Prototypes);
}
[Test]
public void SerializationTest()
{
var definition = new PrototypeIdListSerializerTestDataDefinition
{
EntitiesList = {TestEntityId},
EntitiesReadOnlyList = new List<string>() {TestEntityId},
EntitiesReadOnlyCollection = new List<string>() {TestEntityId},
EntitiesImmutableList = ImmutableList.Create(TestEntityId)
};
var node = Serialization.WriteValueAs<MappingDataNode>(definition);
Assert.That(node.Children.Count, Is.EqualTo(4));
var entities = node.Cast<SequenceDataNode>("entitiesList");
Assert.That(entities.Sequence.Count, Is.EqualTo(1));
Assert.That(entities.Cast<ValueDataNode>(0).Value, Is.EqualTo(TestEntityId));
var entitiesReadOnlyList = node.Cast<SequenceDataNode>("entitiesReadOnlyList");
Assert.That(entitiesReadOnlyList.Sequence.Count, Is.EqualTo(1));
Assert.That(entitiesReadOnlyList.Cast<ValueDataNode>(0).Value, Is.EqualTo(TestEntityId));
var entitiesReadOnlyCollection = node.Cast<SequenceDataNode>("entitiesReadOnlyCollection");
Assert.That(entitiesReadOnlyCollection.Sequence.Count, Is.EqualTo(1));
Assert.That(entitiesReadOnlyCollection.Cast<ValueDataNode>(0).Value, Is.EqualTo(TestEntityId));
var entitiesImmutableList = node.Cast<SequenceDataNode>("entitiesImmutableList");
Assert.That(entitiesImmutableList.Sequence.Count, Is.EqualTo(1));
Assert.That(entitiesImmutableList.Cast<ValueDataNode>(0).Value, Is.EqualTo(TestEntityId));
}
[Test]
public void DeserializationTest()
{
var stream = new YamlStream();
stream.Load(new StringReader(DataString));
var node = stream.Documents[0].RootNode.ToDataNode();
var definition = Serialization.ReadValue<PrototypeIdListSerializerTestDataDefinition>(node);
Assert.NotNull(definition);
Assert.That(definition!.EntitiesList.Count, Is.EqualTo(1));
Assert.That(definition.EntitiesList[0], Is.EqualTo(TestEntityId));
Assert.That(definition!.EntitiesReadOnlyList.Count, Is.EqualTo(1));
Assert.That(definition.EntitiesReadOnlyList[0], Is.EqualTo(TestEntityId));
Assert.That(definition!.EntitiesReadOnlyCollection.Count, Is.EqualTo(1));
Assert.That(definition.EntitiesReadOnlyCollection.Single(), Is.EqualTo(TestEntityId));
Assert.That(definition!.EntitiesImmutableList.Count, Is.EqualTo(1));
Assert.That(definition.EntitiesImmutableList[0], Is.EqualTo(TestEntityId));
}
[Test]
public void ValidationValidTest()
{
var validSequence = new SequenceDataNode(TestEntityId);
var validations = Serialization.ValidateNodeWith<
List<string>,
PrototypeIdListSerializer<EntityPrototype>,
SequenceDataNode>(validSequence);
Assert.True(validations.Valid);
validations = Serialization.ValidateNodeWith<
IReadOnlyList<string>,
PrototypeIdListSerializer<EntityPrototype>,
SequenceDataNode>(validSequence);
Assert.True(validations.Valid);
validations = Serialization.ValidateNodeWith<
IReadOnlyCollection<string>,
PrototypeIdListSerializer<EntityPrototype>,
SequenceDataNode>(validSequence);
Assert.True(validations.Valid);
validations = Serialization.ValidateNodeWith<
ImmutableList<string>,
PrototypeIdListSerializer<EntityPrototype>,
SequenceDataNode>(validSequence);
Assert.True(validations.Valid);
}
[Test]
public void ValidationInvalidTest()
{
var invalidSequence = new SequenceDataNode(TestInvalidEntityId);
var validations = Serialization.ValidateNodeWith<
List<string>,
PrototypeIdListSerializer<EntityPrototype>,
SequenceDataNode>(invalidSequence);
Assert.False(validations.Valid);
validations = Serialization.ValidateNodeWith<
IReadOnlyList<string>,
PrototypeIdListSerializer<EntityPrototype>,
SequenceDataNode>(invalidSequence);
Assert.False(validations.Valid);
validations = Serialization.ValidateNodeWith<
IReadOnlyCollection<string>,
PrototypeIdListSerializer<EntityPrototype>,
SequenceDataNode>(invalidSequence);
Assert.False(validations.Valid);
validations = Serialization.ValidateNodeWith<
ImmutableList<string>,
PrototypeIdListSerializer<EntityPrototype>,
SequenceDataNode>(invalidSequence);
Assert.False(validations.Valid);
}
}
[DataDefinition]
public class PrototypeIdListSerializerTestDataDefinition
{
[field: DataField("entitiesList", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> EntitiesList = new();
[field: DataField("entitiesReadOnlyList", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public IReadOnlyList<string> EntitiesReadOnlyList = new List<string>();
[field: DataField("entitiesReadOnlyCollection", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public IReadOnlyCollection<string> EntitiesReadOnlyCollection = new List<string>();
[field: DataField("entitiesImmutableList", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public ImmutableList<string> EntitiesImmutableList = ImmutableList<string>.Empty;
}
}

View File

@@ -1,4 +1,4 @@
using NUnit.Framework;
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
@@ -9,13 +9,14 @@ namespace Robust.UnitTesting.Shared.Utility
[TestOf(typeof(NullableHelper))]
public class NullableHelper_Test
{
[OneTimeSetUp]
public void OneTimeSetup()
[SetUp]
public void Setup()
{
//initializing logmanager so it wont error out if nullablehelper logs an error
IoCManager.InitThread();
IoCManager.Register<ILogManager, LogManager>();
IoCManager.BuildGraph();
var collection = new DependencyCollection();
collection.Register<ILogManager, LogManager>();
collection.BuildGraph();
IoCManager.InitThread(collection, true);
}
[Test]