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)