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 9c1993aa14..f3a0b0253f 100644 --- a/OpenDreamClient/Interface/Controls/ControlWindow.cs +++ b/OpenDreamClient/Interface/Controls/ControlWindow.cs @@ -53,6 +53,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 d0eb540f83..7b0510e9ab 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -293,6 +293,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) { @@ -793,10 +795,23 @@ 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 ceed48dbfa..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; @@ -223,7 +225,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 void RxBrowseResourceRequest(MsgBrowseResourceRequest message) { @@ -278,5 +281,43 @@ 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); + } + } + } +} + +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"); } } 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..bc8aa1762d 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs @@ -185,6 +185,13 @@ 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..a8402fb862 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": @@ -79,8 +76,20 @@ public DreamResource LoadResource(string 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/TestGame/TestInterface.dmf b/TestGame/TestInterface.dmf index 6144f4b568..5fa3064637 100644 --- a/TestGame/TestInterface.dmf +++ b/TestGame/TestInterface.dmf @@ -56,6 +56,7 @@ menu "menu" command = ".winset \"wingetwindow.is-visible=false?wingetwindow.is-visible=true:wingetwindow.is-visible=false\"" category = "Menu" + window "mapwindow" elem "mapwindow" type = MAIN @@ -98,7 +99,8 @@ window "outputwindow" size = 0x0 anchor1 = 0,0 anchor2 = 100,100 - is-default = true + is-default = true + elem "input" type = INPUT pos = 0,460 @@ -146,6 +148,9 @@ window "testwindow" size = 200x100 title = "popup" is-visible = false + elem "testwindowlabel" + type = LABEL + text = "I am a test" font-size = 6pt elem "testwindowlabel" type = LABEL @@ -203,4 +208,5 @@ window "wingetwindow" pos = 0,160 size = 0,20 text = "as raw" - command = "wingettextverb \"raw: [[wingetinput.text as raw]]\"" \ No newline at end of file + command = "wingettextverb \"raw: [[wingetinput.text as raw]]\"" + diff --git a/TestGame/code.dm b/TestGame/code.dm index 82f38a6970..4f4e22560b 100644 --- a/TestGame/code.dm +++ b/TestGame/code.dm @@ -201,6 +201,12 @@ set name = "wingettextverb" world << "recieved: [rawtext]" + verb/test_hot_reload() + set category = "Test" + src << "trying hot reload of interface..." + world.ODHotReloadInterface() + src << "done hot reload of interface!" + /mob/Stat() if (statpanel("Status")) stat("tick_usage", world.tick_usage)