Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable hot reloading of interface files #1779

Merged
merged 10 commits into from
Jun 9, 2024
2 changes: 2 additions & 0 deletions DMCompiler/DMStandard/Types/World.dm
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,5 @@
proc/PayCredits(player, credits, note)
set opendream_unimplemented = TRUE
return 0

proc/ODHotReloadInterface()
8 changes: 8 additions & 0 deletions OpenDreamClient/Interface/Controls/ControlWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ protected override void UpdateElementDescriptor() {
}
}

/// <summary>
/// Closes the window if it is a child window. No effect if it is either a default window or a pane
/// </summary>
public void CloseChildWindow() {
if(_myWindow.osWindow is not null)
_myWindow.osWindow.Close();
}

public OSWindow CreateWindow() {
if(_myWindow.osWindow is not null)
return _myWindow.osWindow;
Expand Down
14 changes: 13 additions & 1 deletion OpenDreamClient/Interface/DreamInterfaceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using System.Linq;
using OpenToolkit.GraphicsLibraryFramework;

namespace OpenDreamClient.Interface;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 15 additions & 1 deletion OpenDreamRuntime/DreamManager.Connections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
}
}

}
1 change: 1 addition & 0 deletions OpenDreamRuntime/Procs/Native/DreamProcNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
8 changes: 8 additions & 0 deletions OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
dreamManager.HotReloadInterface();
return DreamValue.Null;
}

/// <summary>
/// Determines the specified configuration space and configuration set in a config_set argument
/// </summary>
Expand Down
20 changes: 14 additions & 6 deletions OpenDreamRuntime/Resources/DreamResourceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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;
Expand Down
21 changes: 0 additions & 21 deletions Resources/OpenDream/DefaultInterface.dmf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"

147 changes: 147 additions & 0 deletions TestGame/TestInterface.dmf
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 6 additions & 0 deletions TestGame/code.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions TestGame/environment.dme
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// BEGIN_PREFERENCES
// END_PREFERENCES
// BEGIN_INCLUDE
#include "TestInterface.dmf"
#include "code.dm"
#include "renderer_tests.dm"
#include "map_z1.dmm"
Expand Down
Loading