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 16 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: 3 additions & 1 deletion DMCompiler/DMStandard/Types/World.dm
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,6 @@
set opendream_unimplemented = TRUE
return 0

proc/ODHotReloadInterface()
proc/ODHotReloadInterface()

proc/ODHotReloadResource(var/file_name)
7 changes: 3 additions & 4 deletions OpenDreamClient/Interface/DreamInterfaceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -794,22 +794,21 @@ 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();
}
Expand Down
26 changes: 21 additions & 5 deletions OpenDreamClient/Resources/DreamResourceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public void Initialize() {
_netManager.RegisterNetMessage<MsgBrowseResourceResponse>(RxBrowseResourceResponse);
_netManager.RegisterNetMessage<MsgRequestResource>();
_netManager.RegisterNetMessage<MsgResource>(RxResource);
_netManager.RegisterNetMessage<MsgNotifyResourceUpdate>(RxResourceUpdateNotification);
}

public void Shutdown() {
Expand Down Expand Up @@ -91,12 +92,18 @@ private void RxBrowseResourceResponse(MsgBrowseResourceResponse 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 @@ -111,6 +118,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
22 changes: 16 additions & 6 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 All @@ -45,10 +55,10 @@ public DMIResource(int id, byte[] data) : base(id, data) {
}

private bool IsValidPNG() {
if (Data.Length < _pngHeader.Length) return false;
if (Data.Length < PngHeader.Length) return false;

for (int i=0; i<_pngHeader.Length; i++) {
if (Data[i] != _pngHeader[i]) return false;
for (int i=0; i<PngHeader.Length; i++) {
if (Data[i] != PngHeader[i]) return false;
}

return true;
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;
}
}
35 changes: 35 additions & 0 deletions OpenDreamRuntime/DreamManager.Connections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@
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);
}
}
}
}

Expand All @@ -322,3 +332,28 @@
shell.WriteLine("Reloading interface");
}
}

public sealed class HotReloadResourceCommand : IConsoleCommand {
// ReSharper disable once StringLiteralTypo
public string Command => "hotreloadresource";
public string Description => "Reload a specified resource and send the update to all clients who have the old version already";
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 != 1) {
shell.WriteError("This command requires a file path to reload as an argument! Example: hotreloadresource ./path/to/resource.dmi");
return;
}

DreamManager dreamManager = IoCManager.Resolve<DreamManager>();
shell.WriteLine($"Reloading {args[0]}");
dreamManager.HotReloadResource(args[0]);
}

}
1 change: 1 addition & 0 deletions OpenDreamRuntime/Procs/Native/DreamProcNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) {
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
10 changes: 10 additions & 0 deletions OpenDreamRuntime/Procs/Native/DreamProcNativeWorld.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ public static DreamValue NativeProc_ODHotReloadInterface(NativeProc.Bundle bundl
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
2 changes: 1 addition & 1 deletion 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 @@ -75,7 +76,6 @@ DreamResource GetResource() {
resource = new DreamResource(resourceId, resourcePath, resourcePath);
break;
}

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 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);
}
}

1 change: 0 additions & 1 deletion TestGame/TestInterface.dmf
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,3 @@ window "wingetwindow"
size = 0,20
text = "as raw"
command = "wingettextverb \"raw: [[wingetinput.text as raw]]\""

6 changes: 6 additions & 0 deletions TestGame/code.dm
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@
world.ODHotReloadInterface()
src << "done hot reload of interface!"

verb/test_hot_reload_icon()
set category = "Test"
src << "trying hot reload of icon..."
world.ODHotReloadResource("icons/turf.dmi")
src << "done hot reload of icon!"

/mob/Stat()
if (statpanel("Status"))
stat("tick_usage", world.tick_usage)
Expand Down
Loading