From d18305551a7c2c2d01a78f3f1a9c57d94067e3d8 Mon Sep 17 00:00:00 2001 From: amylizzle Date: Sun, 12 May 2024 14:37:02 +0100 Subject: [PATCH 1/5] Enable hot reloading of interface --- DMCompiler/DMStandard/Types/World.dm | 2 + .../Interface/Controls/ControlWindow.cs | 8 + .../Interface/DreamInterfaceManager.cs | 14 +- OpenDreamRuntime/DreamManager.Connections.cs | 16 +- .../Procs/Native/DreamProcNative.cs | 1 + .../Procs/Native/DreamProcNativeWorld.cs | 8 + .../Resources/DreamResourceManager.cs | 20 ++- Resources/OpenDream/DefaultInterface.dmf | 21 --- TestGame/TestInterface.dmf | 147 ++++++++++++++++++ TestGame/code.dm | 6 + TestGame/environment.dme | 1 + 11 files changed, 215 insertions(+), 29 deletions(-) create mode 100644 TestGame/TestInterface.dmf diff --git a/DMCompiler/DMStandard/Types/World.dm b/DMCompiler/DMStandard/Types/World.dm index ef75ed3043..f7ca1d1e6c 100644 --- a/DMCompiler/DMStandard/Types/World.dm +++ b/DMCompiler/DMStandard/Types/World.dm @@ -113,3 +113,5 @@ proc/PayCredits(player, credits, note) set opendream_unimplemented = TRUE return 0 + + proc/ODHotReloadInterface() \ No newline at end of file diff --git a/OpenDreamClient/Interface/Controls/ControlWindow.cs b/OpenDreamClient/Interface/Controls/ControlWindow.cs index 3f1890712a..3197313936 100644 --- a/OpenDreamClient/Interface/Controls/ControlWindow.cs +++ b/OpenDreamClient/Interface/Controls/ControlWindow.cs @@ -51,6 +51,14 @@ protected override void UpdateElementDescriptor() { } } + /// + /// Closes the window if it is a child window. No effect if it is either a default window or a pane + /// + public void CloseChildWindow() { + if(_myWindow.osWindow is not null) + _myWindow.osWindow.Close(); + } + public OSWindow CreateWindow() { if(_myWindow.osWindow is not null) return _myWindow.osWindow; diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index 77d82ae850..b123e1c46d 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -23,6 +23,7 @@ using Robust.Shared.Utility; using SixLabors.ImageSharp; using System.Linq; +using OpenToolkit.GraphicsLibraryFramework; namespace OpenDreamClient.Interface; @@ -293,6 +294,8 @@ private void RxLoadInterface(MsgLoadInterface message) { LoadInterfaceFromSource(interfaceText); _netManager.ClientSendMessage(new MsgAckLoadInterface()); + if (_entitySystemManager.TryGetEntitySystem(out ClientVerbSystem? verbSystem)) + DefaultInfo?.RefreshVerbs(verbSystem); } private void RxUpdateClientInfo(MsgUpdateClientInfo msg) { @@ -769,11 +772,20 @@ public void WinClone(string controlId, string cloneId) { private void Reset() { _userInterfaceManager.MainViewport.Visible = false; - + //close windows if they're open, and clear all child uielements + foreach (var window in Windows.Values){ + window.CloseChildWindow(); + window.UIElement.RemoveAllChildren(); + } Windows.Clear(); Menus.Clear(); MacroSets.Clear(); + //close popups if they're open + foreach (var popup in _popupWindows.Values) { + popup.Close(); + } _popupWindows.Clear(); + _inputManager.ResetAllBindings(); } private void LoadInterface(InterfaceDescriptor descriptor) { diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs index 7077a15e01..b51aabdd4a 100644 --- a/OpenDreamRuntime/DreamManager.Connections.cs +++ b/OpenDreamRuntime/DreamManager.Connections.cs @@ -221,7 +221,8 @@ private void RxTopic(MsgTopic message) { private void RxAckLoadInterface(MsgAckLoadInterface message) { // Once the client loaded the interface, move them to in-game. var player = _playerManager.GetSessionByChannel(message.MsgChannel); - _playerManager.JoinGame(player); + if(player.Status != SessionStatus.InGame) //Don't rejoin if this is a hot reload of interface + _playerManager.JoinGame(player); } private DreamConnection ConnectionForChannel(INetChannel channel) { @@ -271,5 +272,18 @@ private void UpdateStat() { public DreamConnection GetConnectionBySession(ICommonSession session) { return _connections[session.UserId]; } + + public void HotReloadInterface() { + string? interfaceText = null; + if (_compiledJson.Interface != null) + interfaceText = _dreamResourceManager.LoadResource(_compiledJson.Interface, forceReload:true).ReadAsString(); + var msgLoadInterface = new MsgLoadInterface() { + InterfaceText = interfaceText + }; + foreach (var connection in _connections.Values) { + connection.Session?.Channel.SendMessage(msgLoadInterface); + } + } } + } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index 118905dffb..5c3a3e84ae 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -147,6 +147,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_GetConfig); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_Profile); objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_SetConfig); + objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_ODHotReloadInterface); SetOverridableNativeProc(objectTree, objectTree.World, DreamProcNativeWorld.NativeProc_Reboot); } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs index 860667aec7..95f2d1c411 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs @@ -185,6 +185,14 @@ public static DreamValue NativeProc_SetConfig(NativeProc.Bundle bundle, DreamObj return DreamValue.Null; } + + [DreamProc("ODHotReloadInterface")] + public static DreamValue NativeProc_ODHotReloadInterface(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var dreamManager = IoCManager.Resolve(); + dreamManager.HotReloadInterface(); + return DreamValue.Null; + } + /// /// Determines the specified configuration space and configuration set in a config_set argument /// diff --git a/OpenDreamRuntime/Resources/DreamResourceManager.cs b/OpenDreamRuntime/Resources/DreamResourceManager.cs index bc0faa2d8f..305e75742e 100644 --- a/OpenDreamRuntime/Resources/DreamResourceManager.cs +++ b/OpenDreamRuntime/Resources/DreamResourceManager.cs @@ -53,14 +53,11 @@ public bool DoesFileExist(string resourcePath) { return File.Exists(resourcePath); } - public DreamResource LoadResource(string resourcePath) { + public DreamResource LoadResource(string resourcePath, bool forceReload = false) { DreamResource resource; + int resourceId; - if (_resourcePathToId.TryGetValue(resourcePath, out int resourceId)) { - resource = _resourceCache[resourceId]; - } else { - resourceId = _resourceCache.Count; - + DreamResource GetResource() { // Create a new type of resource based on its extension switch (Path.GetExtension(resourcePath)) { case ".dmi": @@ -78,9 +75,20 @@ public DreamResource LoadResource(string resourcePath) { resource = new DreamResource(resourceId, resourcePath, resourcePath); break; } + return resource; + } + if (!forceReload && _resourcePathToId.TryGetValue(resourcePath, out resourceId)) { + resource = _resourceCache[resourceId]; + } else if(!forceReload) { + resourceId = _resourceCache.Count; + resource = GetResource(); _resourceCache.Add(resource); _resourcePathToId.Add(resourcePath, resourceId); + } else { + resourceId = _resourcePathToId[resourcePath]; + resource = GetResource(); + _resourceCache[resourceId] = resource; } return resource; diff --git a/Resources/OpenDream/DefaultInterface.dmf b/Resources/OpenDream/DefaultInterface.dmf index 53f02f0a52..9f81a5a80c 100644 --- a/Resources/OpenDream/DefaultInterface.dmf +++ b/Resources/OpenDream/DefaultInterface.dmf @@ -39,18 +39,6 @@ menu "menu" name = "Quit" command = ".quit" category = "Menu" - elem - name = "Show Popup" - command = ".winset \"testwindow.is-visible=true\"" - category = "Menu" - elem - name = "Hide Popup" - command = ".winset \"testwindow.is-visible=false\"" - category = "Menu" - elem - name = "Toggle Popup" - command = ".winset \"testwindow.is-visible=false?testwindow.is-visible=true:testwindow.is-visible=false\"" - category = "Menu" window "mapwindow" elem "mapwindow" @@ -136,13 +124,4 @@ window "mainwindow" right = "infowindow" is-vert = true -window "testwindow" - elem "testwindow" - type = MAIN - size = 200x100 - title = "popup" - is-visible = false - elem "testwindowlabel" - type = LABEL - text = "I am a test" diff --git a/TestGame/TestInterface.dmf b/TestGame/TestInterface.dmf new file mode 100644 index 0000000000..c408645489 --- /dev/null +++ b/TestGame/TestInterface.dmf @@ -0,0 +1,147 @@ +macro "macro" + elem + name = "North+REP" + command = ".north" + elem + name = "South+REP" + command = ".south" + elem + name = "East+REP" + command = ".east" + elem + name = "West+REP" + command = ".west" + elem + name = "Northeast+REP" + command = ".northeast" + elem + name = "Northwest+REP" + command = ".northwest" + elem + name = "Southeast+REP" + command = ".southeast" + elem + name = "Southwest+REP" + command = ".southwest" + elem + name = "Center+REP" + command = ".center" + +menu "menu" + elem + name = "Menu" + command = "" + elem + name = "Screenshot" + command = ".screenshot" + category = "Menu" + elem + name = "Quit" + command = ".quit" + category = "Menu" + elem + name = "Show Popup" + command = ".winset \"testwindow.is-visible=true\"" + category = "Menu" + elem + name = "Hide Popup" + command = ".winset \"testwindow.is-visible=false\"" + category = "Menu" + elem + name = "Toggle Popup" + command = ".winset \"testwindow.is-visible=false?testwindow.is-visible=true:testwindow.is-visible=false\"" + category = "Menu" + +window "mapwindow" + elem "mapwindow" + type = MAIN + pos = 0,0 + size = 640x480 + is-pane = true + elem "map" + type = MAP + pos = 0,0 + size = 640x480 + anchor1 = 0,0 + anchor2 = 100,100 + is-default = true + +window "infowindow" + elem "infowindow" + type = MAIN + pos = 0,0 + size = 640x480 + is-pane = true + elem "info" + type = CHILD + pos = 0,0 + size = 640x480 + anchor1 = 0,0 + anchor2 = 100,100 + left = "statwindow" + right = "outputwindow" + is-vert = false + +window "outputwindow" + elem "outputwindow" + type = MAIN + pos = 0,0 + size = 640x480 + is-pane = true + elem "output" + type = OUTPUT + pos = 0,0 + size = 0x0 + anchor1 = 0,0 + anchor2 = 100,100 + is-default = true + elem "input" + type = INPUT + pos = 0,460 + size = 640x20 + anchor1 = 0,100 + anchor2 = 100,100 + background-color = #d3b5b5 + is-default = true + +window "statwindow" + elem "statwindow" + type = MAIN + pos = 0,0 + size = 640x480 + is-pane = true + elem "stat" + type = INFO + pos = 0,0 + size = 0x0 + anchor1 = 0,0 + anchor2 = 100,100 + is-default = true + +window "mainwindow" + elem "mainwindow" + type = MAIN + size = 800x400 + is-default = true + menu = "menu" + macro = "macro" + icon = "icons/mob.dmi" + elem "split" + type = CHILD + pos = 3,0 + size = 0x0 + anchor1 = 0,0 + anchor2 = 100,100 + left = "mapwindow" + right = "infowindow" + is-vert = true + +window "testwindow" + elem "testwindow" + type = MAIN + size = 200x100 + title = "popup" + is-visible = false + elem "testwindowlabel" + type = LABEL + text = "I am a test" diff --git a/TestGame/code.dm b/TestGame/code.dm index c6f0d1c214..e77c0f1233 100644 --- a/TestGame/code.dm +++ b/TestGame/code.dm @@ -193,6 +193,12 @@ src << "showing main window" winset(src,"mainwindow","is-visible=true") + verb/test_hot_reload() + set category = "Test" + src << "tyring hot reload of interface..." + world.ODHotReloadInterface() + src << "done hot reload of interface!" + /mob/Stat() if (statpanel("Status")) stat("tick_usage", world.tick_usage) diff --git a/TestGame/environment.dme b/TestGame/environment.dme index 58693e87ae..8b6043085f 100644 --- a/TestGame/environment.dme +++ b/TestGame/environment.dme @@ -6,6 +6,7 @@ // BEGIN_PREFERENCES // END_PREFERENCES // BEGIN_INCLUDE +#include "TestInterface.dmf" #include "code.dm" #include "renderer_tests.dm" #include "map_z1.dmm" From 799180db6857f673c65ea52516e96317343e2203 Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Tue, 21 May 2024 11:50:08 +0100 Subject: [PATCH 2/5] Apply suggestions from code review --- OpenDreamClient/Interface/DreamInterfaceManager.cs | 1 - TestGame/code.dm | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index b123e1c46d..83feac5c76 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -23,7 +23,6 @@ using Robust.Shared.Utility; using SixLabors.ImageSharp; using System.Linq; -using OpenToolkit.GraphicsLibraryFramework; namespace OpenDreamClient.Interface; diff --git a/TestGame/code.dm b/TestGame/code.dm index e77c0f1233..da9641ddf4 100644 --- a/TestGame/code.dm +++ b/TestGame/code.dm @@ -195,7 +195,7 @@ verb/test_hot_reload() set category = "Test" - src << "tyring hot reload of interface..." + src << "trying hot reload of interface..." world.ODHotReloadInterface() src << "done hot reload of interface!" From 097bc072ddc15207bac402760bf1adb2b4dfd584 Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:55:09 +0100 Subject: [PATCH 3/5] lint --- OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs | 1 - OpenDreamRuntime/Resources/DreamResourceManager.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs index 95f2d1c411..bc8aa1762d 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs @@ -185,7 +185,6 @@ public static DreamValue NativeProc_SetConfig(NativeProc.Bundle bundle, DreamObj return DreamValue.Null; } - [DreamProc("ODHotReloadInterface")] public static DreamValue NativeProc_ODHotReloadInterface(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { var dreamManager = IoCManager.Resolve(); diff --git a/OpenDreamRuntime/Resources/DreamResourceManager.cs b/OpenDreamRuntime/Resources/DreamResourceManager.cs index 305e75742e..a8402fb862 100644 --- a/OpenDreamRuntime/Resources/DreamResourceManager.cs +++ b/OpenDreamRuntime/Resources/DreamResourceManager.cs @@ -75,6 +75,7 @@ DreamResource GetResource() { resource = new DreamResource(resourceId, resourcePath, resourcePath); break; } + return resource; } From ef25900751cc3f3a36a3dd81e5c666fb5708682e Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Sun, 9 Jun 2024 10:13:40 +0100 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: wixoa --- OpenDreamClient/Interface/DreamInterfaceManager.cs | 4 ++++ OpenDreamRuntime/DreamManager.Connections.cs | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index 7024c5b256..7b0510e9ab 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -794,18 +794,22 @@ public void WinClone(string controlId, string cloneId) { private void Reset() { _userInterfaceManager.MainViewport.Visible = false; + //close windows if they're open, and clear all child uielements foreach (var window in Windows.Values){ window.CloseChildWindow(); window.UIElement.RemoveAllChildren(); } + Windows.Clear(); Menus.Clear(); MacroSets.Clear(); + //close popups if they're open foreach (var popup in _popupWindows.Values) { popup.Close(); } + _popupWindows.Clear(); _inputManager.ResetAllBindings(); } diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs index 1086a9e3f7..66b2a94b2b 100644 --- a/OpenDreamRuntime/DreamManager.Connections.cs +++ b/OpenDreamRuntime/DreamManager.Connections.cs @@ -284,13 +284,14 @@ public void HotReloadInterface() { string? interfaceText = null; if (_compiledJson.Interface != null) interfaceText = _dreamResourceManager.LoadResource(_compiledJson.Interface, forceReload:true).ReadAsString(); + var msgLoadInterface = new MsgLoadInterface() { InterfaceText = interfaceText }; + foreach (var connection in _connections.Values) { connection.Session?.Channel.SendMessage(msgLoadInterface); } } } - } From e3d3e78c48af58628e24fca842d7e778ff57fbbc Mon Sep 17 00:00:00 2001 From: Amylizzle Date: Sun, 9 Jun 2024 20:09:12 +0100 Subject: [PATCH 5/5] add server command --- OpenDreamRuntime/DreamManager.Connections.cs | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs index 66b2a94b2b..537dd52842 100644 --- a/OpenDreamRuntime/DreamManager.Connections.cs +++ b/OpenDreamRuntime/DreamManager.Connections.cs @@ -4,9 +4,11 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using OpenDreamRuntime; using OpenDreamShared; using OpenDreamShared.Network.Messages; using Robust.Shared.Configuration; +using Robust.Shared.Console; using Robust.Shared.Enums; using Robust.Shared.Network; using Robust.Shared.Player; @@ -295,3 +297,27 @@ public void HotReloadInterface() { } } } + +public sealed class HotReloadInterfaceCommand : IConsoleCommand { + // ReSharper disable once StringLiteralTypo + public string Command => "hotreloadinterface"; + public string Description => "Reload the .dmf interface and send the update to all clients"; + 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 != 0) { + shell.WriteError("This command does not take any arguments!"); + return; + } + + DreamManager dreamManager = IoCManager.Resolve(); + dreamManager.HotReloadInterface(); + shell.WriteLine("Reloading interface"); + } +}