diff --git a/OpenDreamClient/ClientVerbSystem.cs b/OpenDreamClient/ClientVerbSystem.cs index 7927fdaf41..808cf3ce7e 100644 --- a/OpenDreamClient/ClientVerbSystem.cs +++ b/OpenDreamClient/ClientVerbSystem.cs @@ -214,6 +214,8 @@ is VerbAccessibility.InRange or VerbAccessibility.InORange } private void OnAllVerbsEvent(AllVerbsEvent e) { + _verbs.Clear(); + _clientVerbs?.Clear(); _verbs.EnsureCapacity(e.Verbs.Count); for (int i = 0; i < e.Verbs.Count; i++) { @@ -226,7 +228,7 @@ private void OnAllVerbsEvent(AllVerbsEvent e) { } private void OnRegisterVerbEvent(RegisterVerbEvent e) { - _verbs.Add(e.VerbId, e.VerbInfo); + _verbs[e.VerbId]= e.VerbInfo; } private void OnUpdateClientVerbsEvent(UpdateClientVerbsEvent e) { diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs index 4f619e0ea2..e9e26a414d 100644 --- a/OpenDreamRuntime/DreamManager.Connections.cs +++ b/OpenDreamRuntime/DreamManager.Connections.cs @@ -356,3 +356,27 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) { dreamManager.HotReloadResource(args[0]); } } + +public sealed class HotReloadCodeCommand : IConsoleCommand { + // ReSharper disable once StringLiteralTypo + public string Command => "hotreloadcode"; + public string Description => "Reload a specified compiled json and hot reload the bytecode"; + public string Help => ""; + public bool RequireServerOrSingleplayer => true; + + public void Execute(IConsoleShell shell, string argStr, string[] args) { + if(!shell.IsLocal) { + shell.WriteError("You cannot use this command as a client. Execute it on the server console."); + return; + } + + if (args.Length != 1) { + shell.WriteError("This command requires a file path to reload as an argument! Example: hotreloadcode ./path/to/compiled.json"); + return; + } + + DreamManager dreamManager = IoCManager.Resolve(); + shell.WriteLine($"Reloading {args[0]}"); + dreamManager.HotReloadJson(args[0]); + } +} diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index e323c0b42e..4252eb642c 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -141,6 +141,7 @@ public void Update() { Profiler.EmitFrameMark(); } + public void ProcessDelQueue() { while (DelQueue.TryTake(out var obj)) { obj.Delete(); @@ -151,6 +152,41 @@ public bool TryGetGlobalProc(string name, [NotNullWhen(true)] out DreamProc? pro return _objectTree.TryGetGlobalProc(name, out proc); } + public void HotReloadJson(string? jsonPath) { + if (string.IsNullOrEmpty(jsonPath) || !File.Exists(jsonPath)) + throw new FileNotFoundException("Could not find the specified json file"); + + string jsonSource = File.ReadAllText(jsonPath); + DreamCompiledJson? json = JsonSerializer.Deserialize(jsonSource); + if (json == null) + throw new Exception("Failed to deserialize the json file"); + + if (!json.Metadata.Version.Equals(OpcodeVerifier.GetOpcodesHash())) + throw new Exception("Compiler opcode version does not match the runtime version!"); + + var rootPath = Path.GetFullPath(Path.GetDirectoryName(jsonPath)!); + var resources = json.Resources ?? Array.Empty(); + _dreamResourceManager.Initialize(rootPath, resources, json.Interface); + if (_entitySystemManager.TryGetEntitySystem(out ServerVerbSystem? _verbSystem)) + _verbSystem.ClearAllVerbs(); + + _objectTree.LoadJson(json); + DreamProcNative.SetupNativeProcs(_objectTree); + + foreach (DreamObject dreamObject in IterateDatums()) { + dreamObject.ObjectDefinition = _objectTree.GetObjectDefinition(_objectTree.GetTreeEntry(dreamObject.ObjectDefinition.Type).Id); + } + + foreach (var client in Connections) { + if (client.Client is not null) { + client.Client.ClientVerbs.HotReloadAll(_objectTree); + _verbSystem!.UpdateClientVerbs(client.Client); + } + } + + + } + public bool LoadJson(string? jsonPath) { if (string.IsNullOrEmpty(jsonPath) || !File.Exists(jsonPath)) return false; diff --git a/OpenDreamRuntime/Objects/Types/DreamList.cs b/OpenDreamRuntime/Objects/Types/DreamList.cs index 9eff66d1d9..1d7316433a 100644 --- a/OpenDreamRuntime/Objects/Types/DreamList.cs +++ b/OpenDreamRuntime/Objects/Types/DreamList.cs @@ -613,6 +613,21 @@ public ClientVerbsList(DreamObjectTree objectTree, ServerVerbSystem? verbSystem, } } + public void HotReloadAll(DreamObjectTree objectTree) { + Verbs.Clear(); + + List? verbs = _client.ObjectDefinition.Verbs; + if (verbs == null) + return; + + Verbs.EnsureCapacity(verbs.Count); + foreach (int verbId in verbs) { + Verbs.Add(objectTree.Procs[verbId]); + } + + _verbSystem!.HotReloadAllVerbs(_client); + } + public override DreamValue GetValue(DreamValue key) { if (!key.TryGetValueAsInteger(out var index)) throw new Exception($"Invalid index into verbs list: {key}"); diff --git a/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs b/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs index 8f747f90bf..163cc7fbc0 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs @@ -6,6 +6,8 @@ using OpenDreamRuntime.Procs.DebugAdapter.Protocol; using OpenDreamRuntime.Resources; using Robust.Server; +using Robust.Shared.Configuration; +using OpenDreamShared; namespace OpenDreamRuntime.Procs.DebugAdapter; @@ -15,6 +17,7 @@ internal sealed class DreamDebugManager : IDreamDebugManager { [Dependency] private readonly DreamResourceManager _resourceManager = default!; [Dependency] private readonly ProcScheduler _procScheduler = default!; [Dependency] private readonly IBaseServer _server = default!; + [Dependency] private readonly IConfigurationManager _configManager = default!; private ISawmill _sawmill = default!; @@ -353,6 +356,9 @@ private void OnRequest(DebugAdapterClient client, Request req) { case RequestHotReloadResource requestHotReloadResource: HandleRequestHotReloadResource(client, requestHotReloadResource); break; + case RequestHotReloadBytecode requestHotReloadBytecode: + HandleRequestHotReloadBytecode(client, requestHotReloadBytecode); + break; default: req.RespondError(client, $"Unknown request \"{req.Command}\""); break; @@ -848,7 +854,7 @@ private void HandleRequestHotReloadResource(DebugAdapterClient client, RequestHo requestHotReloadResource.RespondError(client, "No file provided for a hot reload"); return; } - + _sawmill.Debug("Debug adapter triggered resource hot reload for "+requestHotReloadResource.Arguments.FilePath); try { _dreamManager.HotReloadResource(requestHotReloadResource.Arguments.FilePath); @@ -858,6 +864,21 @@ private void HandleRequestHotReloadResource(DebugAdapterClient client, RequestHo } } + private void HandleRequestHotReloadBytecode(DebugAdapterClient client, RequestHotReloadBytecode requestHotReloadBytecode) { + if (string.IsNullOrWhiteSpace(requestHotReloadBytecode.Arguments.FilePath)) { + _sawmill.Error("Debug adapter requested a bytecode hot reload but didn't provide a json file"); + requestHotReloadBytecode.RespondError(client, "No file provided for a hot reload"); + return; + } + _sawmill.Debug($"Debug adapter triggered bytecode hot reload for file {requestHotReloadBytecode.Arguments.FilePath}"); + try { + _dreamManager.HotReloadJson(_configManager.GetCVar(OpenDreamCVars.JsonPath)); + requestHotReloadBytecode.Respond(client); + } catch (Exception e) { + requestHotReloadBytecode.RespondError(client, e.Message); + } + } + private IEnumerable DisassemblySkipTake(List list, int midpoint, int offset, int count) { for (int i = midpoint + offset; i < midpoint + offset + count; ++i) { if (i < 0) { diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs index d27c2d725b..09bc8476e6 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs @@ -37,6 +37,7 @@ public Request() : base("request") { } "disassemble" => json.Deserialize(), "hotreloadinterface" => json.Deserialize(), "hotreloadresource" => json.Deserialize(), + "hotreloadbytecode" => json.Deserialize(), // Caller will fail to recognize it and can respond with `success: false`. _ => request, }; diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestHotReloadBytecode.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestHotReloadBytecode.cs new file mode 100644 index 0000000000..05ed0d1c16 --- /dev/null +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestHotReloadBytecode.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; + +[UsedImplicitly] +public sealed class RequestHotReloadBytecode : Request { + [JsonPropertyName("arguments")] public required RequestHotReloadBytecodeArguments Arguments { get; set; } + + [UsedImplicitly] + public sealed class RequestHotReloadBytecodeArguments { + [JsonPropertyName("file")] public string? FilePath { get; set; } + } + + public void Respond(DebugAdapterClient client) { + client.SendMessage(Response.NewSuccess(this)); + } +} diff --git a/OpenDreamRuntime/ServerVerbSystem.cs b/OpenDreamRuntime/ServerVerbSystem.cs index 6da5de48e6..82514c81e8 100644 --- a/OpenDreamRuntime/ServerVerbSystem.cs +++ b/OpenDreamRuntime/ServerVerbSystem.cs @@ -122,6 +122,10 @@ public void UpdateClientVerbs(DreamObjectClient client) { RaiseNetworkEvent(new UpdateClientVerbsEvent(verbIds), client.Connection.Session!); } + public void HotReloadAllVerbs(DreamObjectClient client) { + RaiseNetworkEvent(new AllVerbsEvent(_verbs), client.Connection.Session!); + } + private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { if (e.NewStatus != SessionStatus.InGame) return; @@ -201,4 +205,13 @@ private bool CanExecute(DreamConnection connection, DreamObject src, DreamProc v return true; } } + + /// + /// Clear all registered verbs. Does not update clients. Really don't use this unless you are immediately + /// repopulating the verb list. + /// + public void ClearAllVerbs() { + _verbs.Clear(); + _verbIdToProc.Clear(); + } }