diff --git a/OpenDreamClient/Interface/Controls/ControlBrowser.cs b/OpenDreamClient/Interface/Controls/ControlBrowser.cs
index fb5f58fdb8..98c98e1800 100644
--- a/OpenDreamClient/Interface/Controls/ControlBrowser.cs
+++ b/OpenDreamClient/Interface/Controls/ControlBrowser.cs
@@ -113,16 +113,21 @@ private void RequestHandler(IRequestHandlerContext context) {
Stream stream;
HttpStatusCode status;
var path = new ResPath(newUri.AbsolutePath);
- try {
- stream = _resourceManager.UserData.OpenRead(_dreamResource.GetCacheFilePath(newUri.AbsolutePath));
- status = HttpStatusCode.OK;
- } catch (FileNotFoundException) {
+ if(!_dreamResource.EnsureCacheFile(newUri.AbsolutePath)) {
stream = Stream.Null;
status = HttpStatusCode.NotFound;
- } catch (Exception e) {
- _sawmill.Error($"Exception while loading file from {newUri}:\n{e}");
- stream = Stream.Null;
- status = HttpStatusCode.InternalServerError;
+ } else {
+ try {
+ stream = _resourceManager.UserData.OpenRead(_dreamResource.GetCacheFilePath(newUri.AbsolutePath));
+ status = HttpStatusCode.OK;
+ } catch (FileNotFoundException) {
+ stream = Stream.Null;
+ status = HttpStatusCode.NotFound;
+ } catch (Exception e) {
+ _sawmill.Error($"Exception while loading file from {newUri}:\n{e}");
+ stream = Stream.Null;
+ status = HttpStatusCode.InternalServerError;
+ }
}
if (!FileExtensionMimeTypes.TryGetValue(path.Extension, out var mimeType))
diff --git a/OpenDreamClient/Resources/DreamResourceManager.cs b/OpenDreamClient/Resources/DreamResourceManager.cs
index ef4eb7c7eb..0d71813f9c 100644
--- a/OpenDreamClient/Resources/DreamResourceManager.cs
+++ b/OpenDreamClient/Resources/DreamResourceManager.cs
@@ -4,7 +4,6 @@
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Network;
-using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace OpenDreamClient.Resources {
@@ -23,6 +22,7 @@ public interface IDreamResourceManager {
/// The type of resource to load as.
void LoadResourceAsync(int resourceId, Action onLoadCallback) where T : DreamResource;
ResPath GetCacheFilePath(string filename);
+ public bool EnsureCacheFile(string filename, int timeoutSeconds = 5);
}
internal sealed class DreamResourceManager : IDreamResourceManager {
@@ -38,11 +38,14 @@ internal sealed class DreamResourceManager : IDreamResourceManager {
private ISawmill _sawmill = default!;
+ private readonly HashSet _activeBrowseRscRequests = new();
+
public void Initialize() {
_sawmill = Logger.GetSawmill("opendream.res");
InitCacheDirectory();
_netManager.RegisterNetMessage(RxBrowseResource);
+ _netManager.RegisterNetMessage(RxBrowseResourceResponse);
_netManager.RegisterNetMessage();
_netManager.RegisterNetMessage(RxResource);
}
@@ -64,7 +67,23 @@ private void InitCacheDirectory() {
}
private void RxBrowseResource(MsgBrowseResource message) {
- CreateCacheFile(message.Filename, message.Data);
+ _sawmill.Debug($"Received cache check for {message.Filename}");
+ if(_resourceManager.UserData.Exists(GetCacheFilePath(message.Filename))){ //TODO CHECK HASH
+ _sawmill.Debug($"Cache hit for {message.Filename}");
+ } else {
+ _sawmill.Debug($"Cache miss for {message.Filename}, requesting from server.");
+ _activeBrowseRscRequests.Add(message.Filename);
+ _netManager.ServerChannel?.SendMessage(new MsgBrowseResourceRequest(){ Filename = message.Filename});
+ }
+ }
+
+ private void RxBrowseResourceResponse(MsgBrowseResourceResponse message) {
+ if(_activeBrowseRscRequests.Contains(message.Filename)) {
+ _activeBrowseRscRequests.Remove(message.Filename);
+ CreateCacheFile(message.Filename, message.Data);
+ } else {
+ _sawmill.Error($"Recieved a browse_rsc response for a file we didn't ask for: {message.Filename}");
+ }
}
private void RxResource(MsgResource message) {
@@ -121,7 +140,7 @@ public void LoadResourceAsync(int resourceId, Action onLoadCallback) where
_netManager.ClientSendMessage(msg);
var timeout = _cfg.GetCVar(OpenDreamCVars.DownloadTimeout);
- Timer.Spawn(TimeSpan.FromSeconds(timeout), () => {
+ Robust.Shared.Timing.Timer.Spawn(TimeSpan.FromSeconds(timeout), () => {
if (_loadingResources.ContainsKey(resourceId)) {
_sawmill.Warning(
$"Resource id {resourceId} was requested, but is still not received {timeout} seconds later.");
@@ -163,6 +182,31 @@ public ResPath CreateCacheFile(string filename, byte[] data)
return new ResPath(filename);
}
+ ///
+ /// Blocking check for the existence of a cached file from `browse_rsc()`. Returns true when the file is ready, or returns false if the file is not ready within timeoutSeconds.
+ ///
+ /// filepath of the cached resource (eg `./foo.png`)
+ /// how long to block for while waiting for the resource. Default 5 seconds.
+ ///
+ public bool EnsureCacheFile(string filename, int timeoutSeconds = 5) {
+ var actualPath = GetCacheFilePath(filename);
+ if(_resourceManager.UserData.Exists(actualPath)) {
+ return true;
+ } else {
+ if(_activeBrowseRscRequests.Contains(actualPath.Filename)) {
+ //block until the file arrives for like 5 seconds, then give up
+ DateTime thresholdTime = DateTime.Now.AddSeconds(timeoutSeconds);
+ while(!_resourceManager.UserData.Exists(actualPath) && DateTime.Now < thresholdTime) {
+ _netManager.ProcessPackets(); //todo this should be sleep
+ }
+ return _resourceManager.UserData.Exists(actualPath);
+ } else {
+ _sawmill.Error("Cache was ensured for a file that does not exist in cache and is not requested. Probably someobody called browse() without browse_rsc() first.");
+ return false;
+ }
+ }
+ }
+
private DreamResource? GetCachedResource(int resourceId) {
_resourceCache.TryGetValue(resourceId, out var cached);
diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs
index 071f3a91b9..cf4bc36376 100644
--- a/OpenDreamRuntime/DreamConnection.cs
+++ b/OpenDreamRuntime/DreamConnection.cs
@@ -77,7 +77,7 @@ public DreamObjectMovable? Eye {
[ViewVariables] private string _selectedStatPanel;
[ViewVariables] private readonly Dictionary> _promptEvents = new();
[ViewVariables] private int _nextPromptEvent = 1;
-
+ private readonly Dictionary _permittedBrowseRscFiles = new();
private DreamObjectMob? _mob;
private DreamObjectMovable? _eye;
@@ -399,10 +399,25 @@ public void BrowseResource(DreamResource resource, string filename) {
var msg = new MsgBrowseResource() {
Filename = filename,
- Data = resource.ResourceData
+ DataHash = resource.ResourceData.Length //TODO: make a quick hash that can work clientside too
};
+ _permittedBrowseRscFiles.Add(filename, resource);
+
+ Session?.Channel.SendMessage(msg);
+ }
+
+ public void HandleBrowseResourceRequest(string filename) {
+ if(_permittedBrowseRscFiles.TryGetValue(filename, out var dreamResource)) {
+ var msg = new MsgBrowseResourceResponse() {
+ Filename = filename,
+ Data = dreamResource.ResourceData! //honestly if this is null, something mega fucked up has happened and we should error hard
+ };
+ _permittedBrowseRscFiles.Remove(filename);
+ Session?.Channel.SendMessage(msg);
+ } else {
+ _sawmill.Error($"Client({Session}) requested a browse_rsc file they had not been permitted to request ({filename}).");
+ }
- Session?.ConnectedClient.SendMessage(msg);
}
public void Browse(string? body, string? options) {
diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs
index 7077a15e01..ceed48dbfa 100644
--- a/OpenDreamRuntime/DreamManager.Connections.cs
+++ b/OpenDreamRuntime/DreamManager.Connections.cs
@@ -54,6 +54,8 @@ private void InitializeConnectionManager() {
_netManager.RegisterNetMessage();
_netManager.RegisterNetMessage(RxPromptResponse);
_netManager.RegisterNetMessage();
+ _netManager.RegisterNetMessage(RxBrowseResourceRequest);
+ _netManager.RegisterNetMessage();
_netManager.RegisterNetMessage();
_netManager.RegisterNetMessage(RxTopic);
_netManager.RegisterNetMessage();
@@ -224,6 +226,11 @@ private void RxAckLoadInterface(MsgAckLoadInterface message) {
_playerManager.JoinGame(player);
}
+ private void RxBrowseResourceRequest(MsgBrowseResourceRequest message) {
+ var connection = ConnectionForChannel(message.MsgChannel);
+ connection.HandleBrowseResourceRequest(message.Filename);
+ }
+
private DreamConnection ConnectionForChannel(INetChannel channel) {
return _connections[_playerManager.GetSessionByChannel(channel).UserId];
}
diff --git a/OpenDreamShared/Network/Messages/MsgBrowseResource.cs b/OpenDreamShared/Network/Messages/MsgBrowseResource.cs
index d0c1983338..b2ca8e2870 100644
--- a/OpenDreamShared/Network/Messages/MsgBrowseResource.cs
+++ b/OpenDreamShared/Network/Messages/MsgBrowseResource.cs
@@ -1,26 +1,22 @@
-using System;
-using Lidgren.Network;
+using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
-namespace OpenDreamShared.Network.Messages {
- public sealed class MsgBrowseResource : NetMessage {
- // TODO: Browse should be on its own channel or something.
- public override MsgGroups MsgGroup => MsgGroups.EntityEvent;
+namespace OpenDreamShared.Network.Messages;
+public sealed class MsgBrowseResource : NetMessage {
+ // TODO: Browse should be on its own channel or something.
+ public override MsgGroups MsgGroup => MsgGroups.EntityEvent;
- public string Filename = String.Empty;
- public byte[] Data = Array.Empty();
+ public string Filename = string.Empty;
+ public int DataHash;
- public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) {
- Filename = buffer.ReadString();
- var bytes = buffer.ReadVariableInt32();
- Data = buffer.ReadBytes(bytes);
- }
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) {
+ Filename = buffer.ReadString();
+ DataHash = buffer.ReadVariableInt32();
+ }
- public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) {
- buffer.Write(Filename);
- buffer.WriteVariableInt32(Data.Length);
- buffer.Write(Data);
- }
+ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) {
+ buffer.Write(Filename);
+ buffer.WriteVariableInt32(DataHash);
}
}
diff --git a/OpenDreamShared/Network/Messages/MsgBrowseResourceRequest.cs b/OpenDreamShared/Network/Messages/MsgBrowseResourceRequest.cs
new file mode 100644
index 0000000000..f860e5724e
--- /dev/null
+++ b/OpenDreamShared/Network/Messages/MsgBrowseResourceRequest.cs
@@ -0,0 +1,19 @@
+using Lidgren.Network;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+
+namespace OpenDreamShared.Network.Messages;
+public sealed class MsgBrowseResourceRequest : NetMessage {
+ // TODO: Browse should be on its own channel or something.
+ public override MsgGroups MsgGroup => MsgGroups.EntityEvent;
+
+ public string Filename = string.Empty;
+
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) {
+ Filename = buffer.ReadString();
+ }
+
+ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) {
+ buffer.Write(Filename);
+ }
+}
diff --git a/OpenDreamShared/Network/Messages/MsgBrowseResourceResponse.cs b/OpenDreamShared/Network/Messages/MsgBrowseResourceResponse.cs
new file mode 100644
index 0000000000..4f954737a0
--- /dev/null
+++ b/OpenDreamShared/Network/Messages/MsgBrowseResourceResponse.cs
@@ -0,0 +1,24 @@
+using Lidgren.Network;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+
+namespace OpenDreamShared.Network.Messages;
+public sealed class MsgBrowseResourceResponse : NetMessage {
+ // TODO: Browse should be on its own channel or something.
+ public override MsgGroups MsgGroup => MsgGroups.EntityEvent;
+
+ public string Filename = string.Empty;
+ public byte[] Data = [];
+
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) {
+ Filename = buffer.ReadString();
+ var bytes = buffer.ReadVariableInt32();
+ Data = buffer.ReadBytes(bytes);
+ }
+
+ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) {
+ buffer.Write(Filename);
+ buffer.WriteVariableInt32(Data.Length);
+ buffer.Write(Data);
+ }
+}
diff --git a/TestGame/code.dm b/TestGame/code.dm
index c6f0d1c214..550c04c673 100644
--- a/TestGame/code.dm
+++ b/TestGame/code.dm
@@ -48,6 +48,9 @@
usr << "menus: [json_encode(winget(usr, null, "menus"))]"
usr << "macros: [json_encode(winget(usr, null, "macros"))]"
+ verb/browse_rsc_test()
+ usr << browse_rsc('icons/mob.dmi', "mobicon.png")
+ usr << browse("
Oh look, it's you!","window=honk")
verb/rotate()
for(var/i in 1 to 8)