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)