From f856ac0efa204300c25949674386ea5a9dbfa424 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Mon, 28 Nov 2022 14:11:44 +1300 Subject: [PATCH] Misc replay related engine changes (#3508) --- .../GameObjects/ClientEntityManager.cs | 10 +- Robust.Client/Player/LocalPlayer.cs | 5 +- .../Configuration/ConfigurationManager.cs | 188 ++++++++++-------- .../Configuration/IConfigurationManager.cs | 8 + .../IConfigurationManagerInternal.cs | 2 +- .../ContentPack/IWritableDirProvider.cs | 2 +- Robust.Shared/GameObjects/EntityManager.cs | 9 +- Robust.Shared/GameObjects/IEntityManager.cs | 5 + Robust.Shared/Network/IClientNetManager.cs | 5 + Robust.Shared/Network/Messages/MsgConVars.cs | 2 +- Robust.Shared/Network/NetManager.cs | 8 + Robust.Shared/Utility/ZStd.cs | 4 +- .../RobustIntegrationTest.NetManager.cs | 8 + 13 files changed, 161 insertions(+), 95 deletions(-) diff --git a/Robust.Client/GameObjects/ClientEntityManager.cs b/Robust.Client/GameObjects/ClientEntityManager.cs index db0d52d6c..c9c95ff07 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Prometheus; using Robust.Client.GameStates; using Robust.Client.Player; @@ -32,16 +33,11 @@ namespace Robust.Client.GameObjects base.Initialize(); } - public override void Shutdown() - { - using var _ = _gameTiming.StartStateApplicationArea(); - base.Shutdown(); - } - public override void Cleanup() + public override void FlushEntities() { using var _ = _gameTiming.StartStateApplicationArea(); - base.Cleanup(); + base.FlushEntities(); } EntityUid IClientEntityManagerInternal.CreateEntity(string? prototypeName, EntityUid uid) diff --git a/Robust.Client/Player/LocalPlayer.cs b/Robust.Client/Player/LocalPlayer.cs index 86b718924..3a01d0af6 100644 --- a/Robust.Client/Player/LocalPlayer.cs +++ b/Robust.Client/Player/LocalPlayer.cs @@ -59,6 +59,9 @@ namespace Robust.Client.Player /// Entity to attach the client to. public void AttachEntity(EntityUid entity) { + if (ControlledEntity == entity) + return; + // Detach and cleanup first DetachEntity(); @@ -74,8 +77,8 @@ namespace Robust.Client.Player if (IoCManager.Resolve().RunLevel != ClientRunLevel.SinglePlayerGame) { Logger.Warning($"Attaching local player to an entity {entMan.ToPrettyString(entity)} without an eye. This eye will not be netsynced and may cause issues."); - eye.NetSyncEnabled = false; } + eye.NetSyncEnabled = false; } eye.Current = true; diff --git a/Robust.Shared/Configuration/ConfigurationManager.cs b/Robust.Shared/Configuration/ConfigurationManager.cs index 5c94f6c8c..3d02bbb74 100644 --- a/Robust.Shared/Configuration/ConfigurationManager.cs +++ b/Robust.Shared/Configuration/ConfigurationManager.cs @@ -50,31 +50,50 @@ namespace Robust.Shared.Configuration } /// - public void LoadFromFile(string configFile) + public HashSet LoadFromTomlStream(Stream file) { + var loaded = new HashSet(); try { - var tblRoot = Toml.ReadFile(configFile); + var tblRoot = Toml.ReadStream(file); var callbackEvents = new ValueList(); // Ensure callbacks are raised OUTSIDE the write lock. using (Lock.WriteGuard()) { - ProcessTomlObject(tblRoot, ref callbackEvents); + ProcessTomlObject(tblRoot, ref callbackEvents, loaded); } foreach (var callback in callbackEvents) { InvokeValueChanged(callback); } + } + catch (Exception e) + { + loaded.Clear(); + Logger.WarningS("cfg", "Unable to load configuration from stream:\n{0}", e); + } + return loaded; + } + + /// + public HashSet LoadFromFile(string configFile) + { + try + { + using var file = File.OpenRead(configFile); + var result = LoadFromTomlStream(file); _configFile = configFile; Logger.InfoS("cfg", $"Configuration Loaded from '{Path.GetFullPath(configFile)}'"); + return result; } catch (Exception e) { Logger.WarningS("cfg", "Unable to load configuration file:\n{0}", e); + return new HashSet(0); } } @@ -92,6 +111,7 @@ namespace Robust.Shared.Configuration private void ProcessTomlObject( TomlObject obj, ref ValueList changedInvokes, + HashSet loadedCvars, string tablePath = "") { if (obj is TomlTable table) // this is a table @@ -105,7 +125,7 @@ namespace Robust.Shared.Configuration else newPath = tablePath + kvTml.Key; - ProcessTomlObject(kvTml.Value, ref changedInvokes, newPath); + ProcessTomlObject(kvTml.Value, ref changedInvokes, loadedCvars, newPath); } } else // this is a key, add CVar @@ -116,6 +136,7 @@ namespace Robust.Shared.Configuration { // overwrite the value with the saved one cfgVar.Value = tomlValue; + loadedCvars.Add(cfgVar.Name); if (SetupInvokeValueChanged(cfgVar, tomlValue) is { } invoke) changedInvokes.Add(invoke); } @@ -125,12 +146,89 @@ namespace Robust.Shared.Configuration //Note: the defaultValue is arbitrarily 0, it will get overwritten when the cvar is registered. cfgVar = new ConfigVar(tablePath, 0, CVar.NONE) { Value = tomlValue }; _configVars.Add(tablePath, cfgVar); + loadedCvars.Add(cfgVar.Name); } cfgVar.ConfigModified = true; } } + /// + public void SaveToTomlStream(Stream stream, IEnumerable cvars) + { + var tblRoot = Toml.Create(); + + using (Lock.ReadGuard()) + { + foreach (var name in cvars) + { + if (!_configVars.TryGetValue(name, out var cVar)) + continue; + + var value = cVar.Value; + if (value == null && cVar.Registered) + { + value = cVar.DefaultValue; + } + + if (value == null) + { + Logger.ErrorS("cfg", + $"CVar {name} has no value or default value, was the default value registered as null?"); + continue; + } + + var keyIndex = name.LastIndexOf(TABLE_DELIMITER); + var tblPath = name.Substring(0, keyIndex).Split(TABLE_DELIMITER); + var keyName = name.Substring(keyIndex + 1); + + // locate the Table in the config tree + var table = tblRoot; + foreach (var curTblName in tblPath) + { + if (!table.TryGetValue(curTblName, out TomlObject tblObject)) + { + tblObject = table.Add(curTblName, new Dictionary()).Added; + } + + table = tblObject as TomlTable ?? throw new InvalidConfigurationException( + $"[CFG] Object {curTblName} is being used like a table, but it is a {tblObject}. Are your CVar names formed properly?"); + } + + //runtime unboxing, either this or generic hell... ¯\_(ツ)_/¯ + switch (value) + { + case Enum val: + table.Add(keyName, (int)(object)val); // asserts Enum value != (ulong || long) + break; + case int val: + table.Add(keyName, val); + break; + case long val: + table.Add(keyName, val); + break; + case bool val: + table.Add(keyName, val); + break; + case string val: + table.Add(keyName, val); + break; + case float val: + table.Add(keyName, val); + break; + case double val: + table.Add(keyName, val); + break; + default: + Logger.WarningS("cfg", $"Cannot serialize '{name}', unsupported type."); + break; + } + } + } + + Toml.WriteStream(tblRoot, stream); + } + /// public void SaveToFile() { @@ -142,82 +240,14 @@ namespace Robust.Shared.Configuration try { - var tblRoot = Toml.Create(); + // Always write if it was present when reading from the config file, otherwise: + // Don't write if Archive flag is not set. + // Don't write if the cVar is the default value. + var cvars = _configVars.Where(x => x.Value.ConfigModified + || ((x.Value.Flags & CVar.ARCHIVE) == 0 && x.Value.Value != null && !x.Value.Value.Equals(x.Value.DefaultValue))).Select(x => x.Key); - using (Lock.ReadGuard()) - { - foreach (var (name, cVar) in _configVars) - { - var value = cVar.Value; - if (value == null && cVar.Registered) - { - value = cVar.DefaultValue; - } - - if (value == null) - { - Logger.ErrorS("cfg", - $"CVar {name} has no value or default value, was the default value registered as null?"); - continue; - } - - // Don't write if Archive flag is not set. - // Don't write if the cVar is the default value. - if (!cVar.ConfigModified && - (cVar.Flags & CVar.ARCHIVE) == 0 || value.Equals(cVar.DefaultValue)) - { - continue; - } - - var keyIndex = name.LastIndexOf(TABLE_DELIMITER); - var tblPath = name.Substring(0, keyIndex).Split(TABLE_DELIMITER); - var keyName = name.Substring(keyIndex + 1); - - // locate the Table in the config tree - var table = tblRoot; - foreach (var curTblName in tblPath) - { - if (!table.TryGetValue(curTblName, out TomlObject tblObject)) - { - tblObject = table.Add(curTblName, new Dictionary()).Added; - } - - table = tblObject as TomlTable ?? throw new InvalidConfigurationException( - $"[CFG] Object {curTblName} is being used like a table, but it is a {tblObject}. Are your CVar names formed properly?"); - } - - //runtime unboxing, either this or generic hell... ¯\_(ツ)_/¯ - switch (value) - { - case Enum val: - table.Add(keyName, (int)(object)val); // asserts Enum value != (ulong || long) - break; - case int val: - table.Add(keyName, val); - break; - case long val: - table.Add(keyName, val); - break; - case bool val: - table.Add(keyName, val); - break; - case string val: - table.Add(keyName, val); - break; - case float val: - table.Add(keyName, val); - break; - case double val: - table.Add(keyName, val); - break; - default: - Logger.WarningS("cfg", $"Cannot serialize '{name}', unsupported type."); - break; - } - } - } - - Toml.WriteFile(tblRoot, _configFile); + using var file = File.OpenWrite(_configFile); + SaveToTomlStream(file, cvars); Logger.InfoS("cfg", $"config saved to '{_configFile}'."); } catch (Exception e) diff --git a/Robust.Shared/Configuration/IConfigurationManager.cs b/Robust.Shared/Configuration/IConfigurationManager.cs index f25052ea4..4334282d1 100644 --- a/Robust.Shared/Configuration/IConfigurationManager.cs +++ b/Robust.Shared/Configuration/IConfigurationManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using Robust.Shared.Timing; namespace Robust.Shared.Configuration @@ -44,6 +45,13 @@ namespace Robust.Shared.Configuration /// void SaveToFile(); + /// + /// Serializes a list of cvars to a toml. + /// + void SaveToTomlStream(Stream stream, IEnumerable cvars); + + HashSet LoadFromTomlStream(Stream stream); + /// /// Register a CVar with the system. This must be done before the CVar is accessed. /// diff --git a/Robust.Shared/Configuration/IConfigurationManagerInternal.cs b/Robust.Shared/Configuration/IConfigurationManagerInternal.cs index 593b204c3..040dc1989 100644 --- a/Robust.Shared/Configuration/IConfigurationManagerInternal.cs +++ b/Robust.Shared/Configuration/IConfigurationManagerInternal.cs @@ -16,7 +16,7 @@ namespace Robust.Shared.Configuration /// Sets up the ConfigurationManager and loads a TOML configuration file. /// /// the full name of the config file. - void LoadFromFile(string configFile); + HashSet LoadFromFile(string configFile); /// /// Specifies the location where the config file should be saved, without trying to load from it. diff --git a/Robust.Shared/ContentPack/IWritableDirProvider.cs b/Robust.Shared/ContentPack/IWritableDirProvider.cs index dcb85f345..8fc3796cd 100644 --- a/Robust.Shared/ContentPack/IWritableDirProvider.cs +++ b/Robust.Shared/ContentPack/IWritableDirProvider.cs @@ -24,7 +24,7 @@ namespace Robust.Shared.ContentPack void CreateDir(ResourcePath path); /// - /// Deletes a file or empty directory. If the file or directory + /// Deletes a file or directory. If the file or directory /// does not exist, does nothing. /// /// Path of object to delete. diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 4e45e84ad..11e4bdd73 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -10,6 +10,7 @@ using Robust.Shared.Utility; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Robust.Shared.GameObjects { @@ -480,15 +481,17 @@ namespace Robust.Shared.GameObjects /// /// Disposes all entities and clears all lists. /// - public void FlushEntities() + public virtual void FlushEntities() { QueuedDeletions.Clear(); QueuedDeletionsSet.Clear(); - foreach (var e in GetEntities()) + foreach (var e in GetEntities().ToArray()) { DeleteEntity(e); } - DebugTools.Assert(Entities.Count == 0); + + if (Entities.Count != 0) + Logger.Error("Entities were spawned while flushing entities."); } /// diff --git a/Robust.Shared/GameObjects/IEntityManager.cs b/Robust.Shared/GameObjects/IEntityManager.cs index 766118b25..33306731e 100644 --- a/Robust.Shared/GameObjects/IEntityManager.cs +++ b/Robust.Shared/GameObjects/IEntityManager.cs @@ -28,6 +28,11 @@ namespace Robust.Shared.GameObjects /// void Cleanup(); + /// + /// Deletes all entities. + /// + void FlushEntities(); + /// /// Only run systems with set true. /// diff --git a/Robust.Shared/Network/IClientNetManager.cs b/Robust.Shared/Network/IClientNetManager.cs index ad5cd10c5..fc31c1ba5 100644 --- a/Robust.Shared/Network/IClientNetManager.cs +++ b/Robust.Shared/Network/IClientNetManager.cs @@ -42,5 +42,10 @@ namespace Robust.Shared.Network void ClientDisconnect(string reason); void Reset(string reason); + + /// + /// Dispatches a messages as if it had just been received over the network. + /// + void DispatchLocalNetMessage(NetMessage message); } } diff --git a/Robust.Shared/Network/Messages/MsgConVars.cs b/Robust.Shared/Network/Messages/MsgConVars.cs index b4940894c..4c272277c 100644 --- a/Robust.Shared/Network/Messages/MsgConVars.cs +++ b/Robust.Shared/Network/Messages/MsgConVars.cs @@ -8,7 +8,7 @@ using Robust.Shared.Timing; namespace Robust.Shared.Network.Messages { - internal sealed class MsgConVars : NetMessage + public sealed class MsgConVars : NetMessage { // Max buffer could potentially be 255 * 128 * 1024 = ~33MB, so if MaxMessageSize starts being a problem it can be increased. private const int MaxMessageSize = 0x4000; // Arbitrarily chosen as a 'sane' value as the maximum size of the entire message. diff --git a/Robust.Shared/Network/NetManager.cs b/Robust.Shared/Network/NetManager.cs index ee5387ca5..f9acc0db7 100644 --- a/Robust.Shared/Network/NetManager.cs +++ b/Robust.Shared/Network/NetManager.cs @@ -922,6 +922,14 @@ namespace Robust.Shared.Network return true; } + public void DispatchLocalNetMessage(NetMessage message) + { + if (!_messages.TryGetValue(message.MsgName, out var msgDat)) + return; + + msgDat.Callback!.Invoke(message); + } + private void CacheNetMsgIndex(int id, string name) { if (!_messages.TryGetValue(name, out var msgDat)) diff --git a/Robust.Shared/Utility/ZStd.cs b/Robust.Shared/Utility/ZStd.cs index 9cef68440..3030dcc0f 100644 --- a/Robust.Shared/Utility/ZStd.cs +++ b/Robust.Shared/Utility/ZStd.cs @@ -173,7 +173,7 @@ public sealed unsafe class ZStdCompressionContext : IDisposable } } -internal sealed class ZStdDecompressStream : Stream +public sealed class ZStdDecompressStream : Stream { private readonly Stream _baseStream; private readonly bool _ownStream; @@ -330,7 +330,7 @@ internal sealed class ZStdDecompressStream : Stream } } -internal sealed class ZStdCompressStream : Stream +public sealed class ZStdCompressStream : Stream { private readonly Stream _baseStream; private readonly bool _ownStream; diff --git a/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs b/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs index 665d8f182..0c0d8954f 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs @@ -392,6 +392,14 @@ namespace Robust.UnitTesting channel.OtherChannel.TryWrite(new DataMessage(message, channel.RemoteUid)); } + public void DispatchLocalNetMessage(NetMessage message) + { + if (_callbacks.TryGetValue(message.GetType(), out var callback)) + { + callback(message); + } + } + private sealed class IntegrationNetChannel : INetChannel { private readonly IntegrationNetManager _owner;