Skip to content
Open
Show file tree
Hide file tree
Changes from all 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<IHotReloadManager, 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 IHotReloadManager _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 IHotReloadManager _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<IHotReloadManager, 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