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

Resource Hot Reloading #1780

Merged
merged 22 commits into from
Jun 30, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions DMCompiler/DMStandard/Types/World.dm
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,7 @@
proc/PayCredits(player, credits, note)
set opendream_unimplemented = TRUE
return 0

proc/ODHotReloadInterface()

proc/ODHotReloadResource(var/file_name)
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
13 changes: 12 additions & 1 deletion OpenDreamClient/Interface/DreamInterfaceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -769,11 +771,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
26 changes: 21 additions & 5 deletions OpenDreamClient/Resources/DreamResourceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public void Initialize() {
_netManager.RegisterNetMessage<MsgBrowseResource>(RxBrowseResource);
_netManager.RegisterNetMessage<MsgRequestResource>();
_netManager.RegisterNetMessage<MsgResource>(RxResource);
_netManager.RegisterNetMessage<MsgNotifyResourceUpdate>(RxResourceUpdateNotification);
}

public void Shutdown() {
Expand All @@ -70,12 +71,18 @@ private void RxBrowseResource(MsgBrowseResource message) {
private void RxResource(MsgResource message) {
if (_loadingResources.ContainsKey(message.ResourceId)) {
LoadingResourceEntry entry = _loadingResources[message.ResourceId];
DreamResource resource = LoadResourceFromData(
entry.ResourceType,
message.ResourceId,
message.ResourceData);
DreamResource resource;
if(_resourceCache.ContainsKey(message.ResourceId)){
_resourceCache[message.ResourceId].UpdateData(message.ResourceData); //we update instead of replacing so we don't have to replace the handle in everything that uses it
resource = _resourceCache[message.ResourceId];
} else {
resource = LoadResourceFromData(
entry.ResourceType,
message.ResourceId,
message.ResourceData);
_resourceCache[message.ResourceId] = resource;
}

_resourceCache[message.ResourceId] = resource;
foreach (Action<DreamResource> callback in entry.LoadCallbacks) {
try {
callback.Invoke(resource);
Expand All @@ -90,6 +97,15 @@ private void RxResource(MsgResource message) {
}
}

private void RxResourceUpdateNotification(MsgNotifyResourceUpdate message) {
if (!_loadingResources.ContainsKey(message.ResourceId) && _resourceCache.TryGetValue(message.ResourceId, out var cached)) { //either we're already requesting it, or we don't have it so don't need to update
_sawmill.Debug($"Resource id {message.ResourceId} was updated, reloading");
_loadingResources[message.ResourceId] = new LoadingResourceEntry(cached.GetType());
var msg = new MsgRequestResource() { ResourceId = message.ResourceId };
_netManager.ClientSendMessage(msg);
}
}

public void LoadResourceAsync<T>(int resourceId, Action<T> onLoadCallback) where T:DreamResource {
DreamResource? resource = GetCachedResource(resourceId);

Expand Down
16 changes: 13 additions & 3 deletions OpenDreamClient/Resources/ResourceTypes/DMIResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace OpenDreamClient.Resources.ResourceTypes;

public sealed class DMIResource : DreamResource {
private readonly byte[] _pngHeader = { 0x89, 0x50, 0x4E, 0x47, 0xD, 0xA, 0x1A, 0xA };
private static readonly byte[] _pngHeader = { 0x89, 0x50, 0x4E, 0x47, 0xD, 0xA, 0x1A, 0xA };

public Texture Texture;
public Vector2i IconSize;
Expand All @@ -17,9 +17,19 @@ public sealed class DMIResource : DreamResource {
private readonly Dictionary<string, State> _states;

public DMIResource(int id, byte[] data) : base(id, data) {
_states = new Dictionary<string, State>();
ProcessDMIData();
}

public override void UpdateData(byte[] data) {
base.UpdateData(data);
ProcessDMIData();
}

private void ProcessDMIData() {
if (!IsValidPNG()) throw new Exception("Attempted to create a DMI using an invalid PNG");

using Stream dmiStream = new MemoryStream(data);
using Stream dmiStream = new MemoryStream(Data);
DMIParser.ParsedDMIDescription description = DMIParser.ParseDMI(dmiStream);

dmiStream.Seek(0, SeekOrigin.Begin);
Expand All @@ -29,7 +39,7 @@ public DMIResource(int id, byte[] data) : base(id, data) {
IconSize = new Vector2i(description.Width, description.Height);
Description = description;

_states = new Dictionary<string, State>();
_states.Clear();
foreach (DMIParser.ParsedDMIState parsedState in description.States.Values) {
State state = new State(Texture, parsedState, description.Width, description.Height);

Expand Down
6 changes: 5 additions & 1 deletion OpenDreamClient/Resources/ResourceTypes/DreamResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace OpenDreamClient.Resources.ResourceTypes;
public class DreamResource {
public readonly int Id;

protected readonly byte[] Data;
protected byte[] Data;

[UsedImplicitly]
public DreamResource(int id, byte[] data) {
Expand All @@ -18,4 +18,8 @@ public DreamResource(int id, byte[] data) {
public void WriteTo(Stream stream) {
stream.Write(Data);
}

public virtual void UpdateData(byte[] data) {
Data = data;
}
}
26 changes: 25 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,28 @@ 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 void HotReloadResource(string fileName){
var resource = _dreamResourceManager.LoadResource(fileName, forceReload:true);
var msgBrowseResource = new MsgNotifyResourceUpdate() { //send a message that this resource id has been updated, let the clients handle re-requesting it
ResourceId = resource.Id
};
foreach (var connection in _connections.Values) {
connection.Session?.Channel.SendMessage(msgBrowseResource);
}
}
}

}
2 changes: 2 additions & 0 deletions OpenDreamRuntime/Procs/Native/DreamProcNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ 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);
objectTree.SetNativeProc(objectTree.World, DreamProcNativeWorld.NativeProc_ODHotReloadResource);

SetOverridableNativeProc(objectTree, objectTree.World, DreamProcNativeWorld.NativeProc_Reboot);
}
Expand Down
18 changes: 18 additions & 0 deletions OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,24 @@ 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;
}

[DreamProc("ODHotReloadResource")]
[DreamProcParameter("file_name", Type = DreamValue.DreamValueTypeFlag.String)]
public static DreamValue NativeProc_ODHotReloadResource(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) {
if(!bundle.GetArgument(0, "file_name").TryGetValueAsString(out var fileName))
throw new ArgumentException("file_name must be a string");
var dreamManager = IoCManager.Resolve<DreamManager>();
dreamManager.HotReloadResource(fileName);
return DreamValue.Null;
}

/// <summary>
/// Determines the specified configuration space and configuration set in a config_set argument
/// </summary>
Expand Down
21 changes: 15 additions & 6 deletions OpenDreamRuntime/Resources/DreamResourceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public void PreInitialize() {
_sawmill = Logger.GetSawmill("opendream.res");
_netManager.RegisterNetMessage<MsgRequestResource>(RxRequestResource);
_netManager.RegisterNetMessage<MsgResource>();
_netManager.RegisterNetMessage<MsgNotifyResourceUpdate>();
}

public void Initialize(string rootPath, string[] resources) {
Expand Down Expand Up @@ -53,14 +54,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 +76,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
20 changes: 20 additions & 0 deletions OpenDreamShared/Network/Messages/MsgNotifyResourceUpdate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;

namespace OpenDreamShared.Network.Messages;
public sealed class MsgNotifyResourceUpdate : NetMessage {
public override MsgGroups MsgGroup => MsgGroups.EntityEvent;

public int ResourceId;

public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) {
ResourceId = buffer.ReadInt32();
}

public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) {
buffer.Write(ResourceId);
}
}

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"

Loading
Loading