Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Robust.Client/ClientIoC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Robust.Client.Audio.Midi;
using Robust.Client.Configuration;
using Robust.Client.Console;
using Robust.Client.ContentPack;
using Robust.Client.Debugging;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
Expand Down Expand Up @@ -174,6 +175,7 @@ public static void RegisterIoC(GameController.DisplayMode mode, IDependencyColle
deps.Register<IXamlProxyHelper, XamlProxyHelper>();
deps.Register<MarkupTagManager>();
deps.Register<IHWId, BasicHWId>();
deps.Register<HotReloadManager, ClientHotReloadManager>(); // DevaStation - hot-reload
}
}
}
31 changes: 31 additions & 0 deletions Robust.Client/ContentPack/ClientHotReloadManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;

namespace Robust.Client.ContentPack;

internal sealed class ClientHotReloadManager : HotReloadManager
{
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;

public override void Initialize()
{
base.Initialize();

_netManager.RegisterNetMessage<MsgHotReload>(HandleHotReloadMessage);
}

private void HandleHotReloadMessage(MsgHotReload msg)
{
if (!_cfg.GetCVar(CVars.HotReload))
{
return;
}

TriggerReload(msg.AssemblyName);
}
}
9 changes: 9 additions & 0 deletions Robust.Client/GameController/GameController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ internal sealed partial class GameController : IGameControllerInternal
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly ISystemFontManagerInternal _systemFontManager = default!;
[Dependency] private readonly LoadingScreenManager _loadscr = default!;
[Dependency] private readonly HotReloadManager _hotReloadManager = default!; // DevaStation - hot-reload

private IWebViewManagerHook? _webViewHook;

Expand Down Expand Up @@ -244,6 +245,9 @@ internal bool StartupContinue(DisplayMode displayMode)

_loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.PostInit), "Content PostInit");

// DevaStation start - hot-reload
_hotReloadManager.Initialize();

_loadscr.Finish();

if (_commandLineArgs?.Username != null)
Expand Down Expand Up @@ -347,6 +351,11 @@ private bool LoadModules()
return false;
}

// DevaStation start - hot-reload
_hotReloadManager.AssemblyDirectory = assemblyDir;
_hotReloadManager.FilterPrefix = assemblyPrefix;
// DevaStation end

return true;
}

Expand Down
8 changes: 8 additions & 0 deletions Robust.Server/BaseServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ internal sealed class BaseServer : IBaseServerInternal, IPostInjectInit
[Dependency] private readonly UploadedContentManager _uploadedContMan = default!;
[Dependency] private readonly NetworkResourceManager _netResMan = default!;
[Dependency] private readonly IReflectionManager _refMan = default!;
[Dependency] private readonly HotReloadManager _hotReloadManager = default!; // DevaStation - hot-reload

private readonly Stopwatch _uptimeStopwatch = new();

Expand Down Expand Up @@ -326,6 +327,11 @@ public bool Start(ServerOptions options, Func<ILogHandler>? logHandlerFactory =
return true;
}

// DevaStation start - hot-reload
_hotReloadManager.AssemblyDirectory = Options.AssemblyDirectory;
_hotReloadManager.FilterPrefix = resourceManifest.AssemblyPrefix ?? Options.ContentModulePrefix;
// DevaStation end

foreach (var loadedModule in _modLoader.LoadedModules)
{
_config.LoadCVarsFromAssembly(loadedModule);
Expand Down Expand Up @@ -411,6 +417,8 @@ public bool Start(ServerOptions options, Func<ILogHandler>? logHandlerFactory =

_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);

_hotReloadManager.Initialize(); // DevaStation - hot-reload

_statusHost.Start();
_hubManager.Start();

Expand Down
40 changes: 40 additions & 0 deletions Robust.Server/ContentPack/ServerHotReloadManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;

namespace Robust.Server.ContentPack;

internal sealed class ServerHotReloadManager : HotReloadManager
{
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;

private AssemblyFileWatcher? _fileWatcher;

public override void Initialize()
{
base.Initialize();

_netManager.RegisterNetMessage<MsgHotReload>();

if (!_cfg.GetCVar(CVars.HotReload))
return;

_fileWatcher = new AssemblyFileWatcher();
IoCManager.InjectDependencies(_fileWatcher);
_fileWatcher.Initialize(AssemblyDirectory, FilterPrefix, TriggerReloadForAssembly);
}

private void TriggerReloadForAssembly(string assemblyName)
{
if (!_cfg.GetCVar(CVars.HotReload))
return;

_netManager.ServerSendToAll(new MsgHotReload { AssemblyName = assemblyName });

TriggerReload(assemblyName);
}
}
2 changes: 1 addition & 1 deletion Robust.Server/GameObjects/ServerEntityManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
switch (args.NewStatus)
{
case SessionStatus.Connected:
_lastProcessedSequencesCmd.Add(args.Session, 0);
_lastProcessedSequencesCmd[args.Session] = 0; // DevaStation - hot-reload: use indexing
break;

case SessionStatus.Disconnected:
Expand Down
28 changes: 28 additions & 0 deletions Robust.Server/GameStates/PvsSystem.Dirty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ private void InitializeDirty()
_addEntities[i] = new HashSet<EntityUid>(32);
_dirtyEntities[i] = new HashSet<EntityUid>(32);
}
// DevaStation start - hot-reload
// Sync _currentIndex to current tick so the assertion in OnEntityAdd holds after reinit
_currentIndex = (int)(_gameTiming.CurTick.Value % DirtyBufferSize);
// DevaStation end
EntityManager.EntityAdded += OnEntityAdd;
EntityManager.EntityDirtied += OnEntityDirty;
}
Expand All @@ -42,6 +46,19 @@ private void ShutdownDirty()

private void OnEntityAdd(Entity<MetaDataComponent> e)
{
// DevaStation start - hot-reload
// Auto-correct _currentIndex when stale. CleanupDirty() only runs from
// AfterSerializeStates() which requires connected players, so after a
// hot-reload with no clients _currentIndex drifts from the actual tick.
var expectedIndex = (int)(_gameTiming.CurTick.Value % DirtyBufferSize);
if (_currentIndex != expectedIndex
&& _gameTiming.GetType().Name != "IGameTimingProxy")
{
_currentIndex = expectedIndex;
_addEntities[_currentIndex].Clear();
_dirtyEntities[_currentIndex].Clear();
}
// DevaStation end
DebugTools.Assert(_currentIndex == _gameTiming.CurTick.Value % DirtyBufferSize ||
_gameTiming.GetType().Name == "IGameTimingProxy");// Look I have NFI how best to excuse this assert if the game timing isn't real (a Mock<IGameTiming>).
_addEntities[_currentIndex].Add(e);
Expand All @@ -55,6 +72,17 @@ private void OnEntityDirty(Entity<MetaDataComponent> uid)
meta.LastModifiedTick = uid.Comp.EntityLastModifiedTick;
}

// DevaStation start - hot-reload
// OH MY STALE _currentIndex!
var expectedIndex = (int)(_gameTiming.CurTick.Value % DirtyBufferSize);
if (_currentIndex != expectedIndex)
{
_currentIndex = expectedIndex;
_addEntities[_currentIndex].Clear();
_dirtyEntities[_currentIndex].Clear();
}
// DevaStation end

if (!_addEntities[_currentIndex].Contains(uid))
_dirtyEntities[_currentIndex].Add(uid);
}
Expand Down
2 changes: 2 additions & 0 deletions Robust.Server/ServerIoC.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.Metrics;
using Robust.Server.Configuration;
using Robust.Server.Console;
using Robust.Server.ContentPack;
using Robust.Server.DataMetrics;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
Expand Down Expand Up @@ -102,6 +103,7 @@ internal static void RegisterIoC(IDependencyCollection deps)
deps.Register<IHWId, DummyHWId>();
deps.Register<ILocalizationManager, ServerLocalizationManager>();
deps.Register<ILocalizationManagerInternal, ServerLocalizationManager>();
deps.Register<HotReloadManager, ServerHotReloadManager>(); // DevaStation - hot-reload
}
}
}
7 changes: 7 additions & 0 deletions Robust.Shared.Testing/TestingModLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ public void AddEngineModuleDirectory(string dir)
{
// Only used for ILVerify, not necessary.
}
// DevaStation - hot-reload
public (Assembly oldAssembly, Assembly newAssembly)? ReloadSingleAssembly(string assemblyName, ResPath assemblyDirectory, string filterPrefix)
{
// nop
return null;
}

#pragma warning disable CS0067 // Needed by interface
public event ExtraModuleLoad? ExtraModuleLoaders;
#pragma warning restore CS0067
Expand Down
10 changes: 10 additions & 0 deletions Robust.Shared/CVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,16 @@ protected CVars()
public static readonly CVarDef<int> DebugTargetFps =
CVarDef.Create("debug.target_fps", 60, CVar.CLIENTONLY | CVar.ARCHIVE);

// DevaStation start
/// <summary>
/// Whether to enable assembly hot-reloading for content assemblies.
/// When enabled, content DLLs marked with [assembly: AssemblyMetadata("HotReloadable", "true")]
/// will be watched for changes and reloaded at runtime
/// </summary>
public static readonly CVarDef<bool> HotReload =
CVarDef.Create("devaStation.hot_reload", true);
// DevaStation end

/*
* MIDI
*/
Expand Down
32 changes: 21 additions & 11 deletions Robust.Shared/Console/ConsoleHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,16 @@ public void LoadConsoleCommands()
continue;

var instance = (IConsoleCommand)_typeFactory.CreateInstanceUnchecked(type, true);
if (AvailableCommands.TryGetValue(instance.Command, out var duplicate))

// DevaStation start - hot-reload
if (AvailableCommands.TryGetValue(instance.Command, out var existing))
{
// Allow re-registration of the same command type during hot-reload
if (existing.GetType().FullName == instance.GetType().FullName)
continue;

throw new InvalidImplementationException(instance.GetType(), typeof(IConsoleCommand),
$"Command name already registered: {instance.Command}, previous: {duplicate.GetType()}");
$"Command name already registered: {instance.Command}, previous: {existing.GetType()}");
}

RegisteredCommands[instance.Command] = instance;
Expand Down Expand Up @@ -110,9 +116,6 @@ public void RegisterCommand(
ConCommandCallback callback,
bool requireServerOrSingleplayer = false)
{
if (RegisteredCommands.ContainsKey(command))
throw new InvalidOperationException($"Command already registered: {command}");

var newCmd = new RegisteredCommand(command, description, help, callback, requireServerOrSingleplayer);
RegisterCommand(newCmd);
}
Expand All @@ -125,9 +128,6 @@ public void RegisterCommand(
ConCommandCompletionCallback completionCallback,
bool requireServerOrSingleplayer = false)
{
if (RegisteredCommands.ContainsKey(command))
throw new InvalidOperationException($"Command already registered: {command}");

var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer);
RegisterCommand(newCmd);
}
Expand All @@ -140,8 +140,6 @@ public void RegisterCommand(
ConCommandCompletionAsyncCallback completionCallback,
bool requireServerOrSingleplayer = false)
{
if (RegisteredCommands.ContainsKey(command))
throw new InvalidOperationException($"Command already registered: {command}");

var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer);
RegisterCommand(newCmd);
Expand Down Expand Up @@ -179,14 +177,26 @@ public void RegisterCommand(

public void RegisterCommand(IConsoleCommand command)
{
RegisteredCommands.Add(command.Command, command);
// DevaStation start - hot-reload: use indexer for safe overwrite
RegisteredCommands[command.Command] = command;
// DevaStation end

if (!_isInRegistrationRegion)
UpdateAvailableCommands();
}

#endregion

/// <summary>
/// Clears all registered commands and auto-registration tracking.
/// Used during hot-reload teardown to remove commands from unloaded content assemblies.
/// </summary>
public void ClearAllCommands()
{
RegisteredCommands.Clear();
_autoRegisteredCommands.Clear();
}

/// <inheritdoc />
public void UnregisterCommand(string command)
{
Expand Down
7 changes: 7 additions & 0 deletions Robust.Shared/Console/IConsoleHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@ void RegisterCommand(
/// Removes all text from the local console.
/// </summary>
void ClearLocalConsole();

// DevaStation start - hot-reload
/// <summary>
/// Clears all registered commands and auto-registration tracking.
/// Used during hot-reload teardown to remove commands from unloaded content assemblies.
/// </summary>
void ClearAllCommands();
}

internal interface IConsoleHostInternal : IConsoleHost
Expand Down
Loading
Loading