Skip to content

Commit

Permalink
Resource Hot Reloading (#1780)
Browse files Browse the repository at this point in the history
Co-authored-by: amylizzle <[email protected]>
  • Loading branch information
amylizzle and amylizzle authored Jun 30, 2024
1 parent 534ebdf commit 4d22478
Show file tree
Hide file tree
Showing 13 changed files with 126 additions and 21 deletions.
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
4 changes: 3 additions & 1 deletion OpenDreamClient/Rendering/DreamIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal sealed class DreamIcon(IGameTiming gameTiming, IClyde clyde, ClientAppe
public DMIResource? DMI {
get => _dmi;
private set {
_dmi?.OnUpdateCallbacks.Remove(DirtyTexture);
_dmi = value;
CheckSizeChange();
}
Expand Down Expand Up @@ -62,6 +63,7 @@ public DreamIcon(IGameTiming gameTiming, IClyde clyde, ClientAppearanceSystem ap
public void Dispose() {
_ping?.Dispose();
_pong?.Dispose();
DMI = null; //triggers the removal of the onUpdateCallback
}

public Texture? GetTexture(DreamViewOverlay viewOverlay, DrawingHandleWorld handle, RendererMetaData iconMetaData, Texture? textureOverride = null) {
Expand Down Expand Up @@ -410,7 +412,7 @@ private void UpdateIcon() {
} else {
IoCManager.Resolve<IDreamResourceManager>().LoadResourceAsync<DMIResource>(Appearance.Icon.Value, dmi => {
if (dmi.Id != Appearance.Icon) return; //Icon changed while resource was loading

dmi.OnUpdateCallbacks.Add(DirtyTexture);
DMI = dmi;
_animationFrame = 0;
_animationFrameTime = gameTiming.CurTime;
Expand Down
27 changes: 22 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,19 @@ 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
_resourceCache[message.ResourceId].OnUpdateCallbacks.ForEach(cb => cb.Invoke());
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 +119,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
7 changes: 6 additions & 1 deletion OpenDreamClient/Resources/ResourceTypes/DreamResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ namespace OpenDreamClient.Resources.ResourceTypes;
[Virtual]
public class DreamResource {
public readonly int Id;
public List<Action> OnUpdateCallbacks = new();

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

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

public virtual void UpdateData(byte[] data) {
Data = data;
}
}
34 changes: 34 additions & 0 deletions OpenDreamRuntime/DreamManager.Connections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@ public void HotReloadInterface() {
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,27 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) {
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]]\""

8 changes: 7 additions & 1 deletion TestGame/code.dm
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,18 @@
set name = "wingettextverb"
world << "recieved: [rawtext]"

verb/test_hot_reload()
verb/test_hot_reload_interface()
set category = "Test"
src << "trying hot reload of interface..."
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!"

verb/manipulate_world_size()
var/x = input("New World X?", "World Size", 0) as num|null
var/y = input("New World Y?", "World Size", 0) as num|null
Expand Down

0 comments on commit 4d22478

Please sign in to comment.