Сетевая подгрузка артов, анимаций и паралаксов. (#3646)
|
|
@ -1,5 +1,6 @@
|
|||
using Content.Client._RMC14.Explosion;
|
||||
using Content.Client._RMC14.Xenonids.Screech;
|
||||
using Content.Client._Sunrise;
|
||||
using Content.Client._Sunrise.Contributors;
|
||||
using Content.Client._Sunrise.Entry;
|
||||
using Content.Client._Sunrise.PlayerCache;
|
||||
|
|
@ -88,6 +89,7 @@ namespace Content.Client.Entry
|
|||
[Dependency] private readonly ServersHubManager _serversHubManager = default!; // Sunrise-Hub
|
||||
[Dependency] private readonly ContributorsManager _contributorsManager = default!; // Sunrise-Edit
|
||||
[Dependency] private readonly PlayerCacheManager _playerCacheManager = default!; // Sunrise-Edit
|
||||
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!; // Sunrise-Edit
|
||||
|
||||
public override void PreInit()
|
||||
{
|
||||
|
|
@ -162,6 +164,7 @@ namespace Content.Client.Entry
|
|||
_serversHubManager.Initialize(); // Sunrise-Hub
|
||||
_contributorsManager.Initialize(); // Sunrise-Edit
|
||||
_playerCacheManager.Initialize(); // Sunrise-Edit
|
||||
_netTexturesManager.Initialize(); // Sunrise-Edit
|
||||
|
||||
// Sunrise-Sponsors-Start
|
||||
SunriseClientEntry.Init();
|
||||
|
|
@ -262,6 +265,7 @@ namespace Content.Client.Entry
|
|||
if (level == ModUpdateLevel.FramePreEngine)
|
||||
{
|
||||
_debugMonitorManager.FrameUpdate();
|
||||
_netTexturesManager.Update(frameEventArgs.DeltaSeconds); // Sunrise-Edit
|
||||
}
|
||||
|
||||
if (level == ModUpdateLevel.PreEngine)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Content.Client._Sunrise.Contributors;
|
||||
using Content.Client._Sunrise;
|
||||
using Content.Client._Sunrise.Contributors;
|
||||
using Content.Client._Sunrise.InteractionsPanel.Models;
|
||||
using Content.Client._Sunrise.IoC;
|
||||
using Content.Client._Sunrise.PlayerCache;
|
||||
|
|
@ -72,6 +73,7 @@ namespace Content.Client.IoC
|
|||
collection.Register<ServersHubManager>();
|
||||
collection.Register<ContributorsManager>();
|
||||
collection.Register<PlayerCacheManager>();
|
||||
collection.Register<NetTexturesManager>();
|
||||
SunriseClientContentIoC.Register();
|
||||
collection.Register<CustomInteractionService, CustomInteractionService>(true);
|
||||
// Sunrise-End
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Linq;
|
||||
using Content.Client._Sunrise;
|
||||
using Content.Client._Sunrise.Contributors;
|
||||
using Content.Client._Sunrise.Latejoin;
|
||||
using Content.Client._Sunrise.ServersHub;
|
||||
|
|
@ -14,9 +15,11 @@ using Content.Shared._Sunrise.Contributors;
|
|||
using Robust.Client;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Upload;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
|
@ -52,15 +55,24 @@ namespace Content.Client.Lobby
|
|||
[Dependency] private readonly ContributorsManager _contributorsManager = default!;
|
||||
[Dependency] private readonly ChangelogManager _changelogManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
private ClientGameTicker _gameTicker = default!;
|
||||
private ContentAudioSystem _contentAudioSystem = default!;
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
// Track loaded resources for unloading
|
||||
private ResPath? _currentAnimationPath;
|
||||
private ResPath? _currentArtPath;
|
||||
|
||||
protected override Type? LinkedScreenType { get; } = typeof(LobbyGui);
|
||||
public LobbyGui? Lobby;
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("lobby");
|
||||
|
||||
if (_userInterfaceManager.ActiveScreen == null)
|
||||
{
|
||||
return;
|
||||
|
|
@ -115,6 +127,9 @@ namespace Content.Client.Lobby
|
|||
_cfg.OnValueChanged(SunriseCCVars.LobbyArt, OnLobbyArtChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.LobbyAnimation, OnLobbyAnimationChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.LobbyParallax, OnLobbyParallaxChanged, true);
|
||||
|
||||
// Subscribe to resource loaded events
|
||||
_netTexturesManager.ResourceLoaded += OnNetworkResourceLoaded;
|
||||
// Sunrise-end
|
||||
|
||||
Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
|
||||
|
|
@ -146,10 +161,28 @@ namespace Content.Client.Lobby
|
|||
Lobby!.ReadyButton.OnPressed -= OnReadyPressed;
|
||||
Lobby!.ReadyButton.OnToggled -= OnReadyToggled;
|
||||
|
||||
// Unload lobby resources if CVar is enabled
|
||||
if (_cfg.GetCVar(SunriseCCVars.LobbyUnloadResources))
|
||||
{
|
||||
if (_currentAnimationPath.HasValue)
|
||||
{
|
||||
UnloadResource(_currentAnimationPath.Value);
|
||||
_currentAnimationPath = null;
|
||||
}
|
||||
if (_currentArtPath.HasValue)
|
||||
{
|
||||
UnloadResource(_currentArtPath.Value);
|
||||
_currentArtPath = null;
|
||||
}
|
||||
}
|
||||
|
||||
Lobby = null;
|
||||
|
||||
_serversHubManager.ServersDataListChanged -= RefreshServersHubHeader;
|
||||
_contributorsManager.ContributorsDataListChanged -= RefreshContributorsHeader;
|
||||
|
||||
// Unsubscribe from resource loaded events
|
||||
_netTexturesManager.ResourceLoaded -= OnNetworkResourceLoaded;
|
||||
}
|
||||
|
||||
private void RefreshServersHubHeader(List<ServerHubEntry> servers)
|
||||
|
|
@ -237,9 +270,30 @@ namespace Content.Client.Lobby
|
|||
{
|
||||
// Sunrise-Start
|
||||
UpdateLobbyType();
|
||||
UpdateLobbyParallax();
|
||||
UpdateLobbyAnimation();
|
||||
UpdateLobbyArt();
|
||||
// Only update the selected background type, not all of them
|
||||
var backgroundType = _cfg.GetCVar(SunriseCCVars.LobbyBackgroundType);
|
||||
if (backgroundType == "Random" && _gameTicker.LobbyType != null)
|
||||
{
|
||||
backgroundType = _gameTicker.LobbyType;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(backgroundType, out LobbyBackgroundType lobbyBackgroundType))
|
||||
{
|
||||
lobbyBackgroundType = LobbyBackgroundType.Parallax; // Default
|
||||
}
|
||||
|
||||
switch (lobbyBackgroundType)
|
||||
{
|
||||
case LobbyBackgroundType.Parallax:
|
||||
UpdateLobbyParallax();
|
||||
break;
|
||||
case LobbyBackgroundType.Art:
|
||||
UpdateLobbyArt();
|
||||
break;
|
||||
case LobbyBackgroundType.Animation:
|
||||
UpdateLobbyAnimation();
|
||||
break;
|
||||
}
|
||||
// Sunrise-End
|
||||
UpdateLobbyUi();
|
||||
}
|
||||
|
|
@ -346,7 +400,7 @@ namespace Content.Client.Lobby
|
|||
|
||||
if (Lobby == null)
|
||||
{
|
||||
Logger.Error("Error in SetLobbyBackgroundType. Lobby is null");
|
||||
_sawmill.Error("Error in SetLobbyBackgroundType. Lobby is null");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -402,41 +456,197 @@ namespace Content.Client.Lobby
|
|||
|
||||
private void SetLobbyAnimation(string lobbyAnimation)
|
||||
{
|
||||
// Check if animation background type is currently selected
|
||||
var backgroundType = _cfg.GetCVar(SunriseCCVars.LobbyBackgroundType);
|
||||
if (backgroundType == "Random" && _gameTicker.LobbyType != null)
|
||||
{
|
||||
backgroundType = _gameTicker.LobbyType;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(backgroundType, out LobbyBackgroundType lobbyBackgroundType) ||
|
||||
lobbyBackgroundType != LobbyBackgroundType.Animation)
|
||||
{
|
||||
// Animation is not the selected background type, don't load it
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_protoMan.TryIndex<LobbyAnimationPrototype>(lobbyAnimation, out var lobbyAnimationPrototype))
|
||||
return;
|
||||
|
||||
if (Lobby == null)
|
||||
{
|
||||
Logger.Error("Error in SetLobbyAnimation. Lobby is null");
|
||||
_sawmill.Error("Error in SetLobbyAnimation. Lobby is null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Unload previous animation if CVar is enabled
|
||||
if (_cfg.GetCVar(SunriseCCVars.LobbyUnloadResources) && _currentAnimationPath.HasValue)
|
||||
{
|
||||
UnloadResource(_currentAnimationPath.Value);
|
||||
}
|
||||
|
||||
Lobby!.LobbyAnimation.SetFromSpriteSpecifier(new SpriteSpecifier.Rsi(new ResPath(lobbyAnimationPrototype.Animation), lobbyAnimationPrototype.State));
|
||||
Lobby!.LobbyAnimation.DisplayRect.TextureScale = lobbyAnimationPrototype.Scale;
|
||||
var rsiPath = lobbyAnimationPrototype.Animation;
|
||||
|
||||
// Check if resource is available, request if not
|
||||
var isAvailable = _netTexturesManager.EnsureResource(rsiPath);
|
||||
|
||||
ResPath targetPath;
|
||||
if (isAvailable)
|
||||
{
|
||||
// Resource is available, use uploaded path
|
||||
targetPath = _netTexturesManager.GetUploadedPath(rsiPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Resource is being requested, try to use uploaded path first
|
||||
var uploadedPath = _netTexturesManager.GetUploadedPath(rsiPath);
|
||||
var metaPath = (uploadedPath / "meta.json").ToRootedPath();
|
||||
|
||||
// Check if uploaded resource exists
|
||||
if (_resource.ContentFileExists(metaPath))
|
||||
{
|
||||
targetPath = uploadedPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Resource not available yet, don't try to load it (will cause error)
|
||||
// The resource will be loaded when it arrives via NetworkResourceUploadMessage
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to set the animation, handle errors gracefully
|
||||
try
|
||||
{
|
||||
// Check if the file actually exists before trying to load it
|
||||
if (!_resource.ContentFileExists(targetPath))
|
||||
{
|
||||
var metaPath = (targetPath / "meta.json").ToRootedPath();
|
||||
if (!_resource.ContentFileExists(metaPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get the resource - this will load it if not cached
|
||||
if (_resourceCache.TryGetResource<RSIResource>(targetPath, out var rsiResource))
|
||||
{
|
||||
Lobby!.LobbyAnimation.SetFromSpriteSpecifier(new SpriteSpecifier.Rsi(targetPath, lobbyAnimationPrototype.State));
|
||||
Lobby!.LobbyAnimation.DisplayRect.TextureScale = lobbyAnimationPrototype.Scale;
|
||||
_currentAnimationPath = targetPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
_sawmill.Warning($"Failed to load lobby animation RSI: {targetPath}. Resource not found in cache.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Warning($"Exception while setting lobby animation {lobbyAnimation}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLobbyArt(string lobbyArt)
|
||||
{
|
||||
// Check if art background type is currently selected
|
||||
var backgroundType = _cfg.GetCVar(SunriseCCVars.LobbyBackgroundType);
|
||||
if (backgroundType == "Random" && _gameTicker.LobbyType != null)
|
||||
{
|
||||
backgroundType = _gameTicker.LobbyType;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(backgroundType, out LobbyBackgroundType lobbyBackgroundType) ||
|
||||
lobbyBackgroundType != LobbyBackgroundType.Art)
|
||||
{
|
||||
// Art is not the selected background type, don't load it
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_protoMan.TryIndex<LobbyBackgroundPrototype>(lobbyArt, out var lobbyArtPrototype))
|
||||
return;
|
||||
|
||||
if (Lobby == null)
|
||||
{
|
||||
Logger.Error("Error in SetLobbyArt. Lobby is null");
|
||||
_sawmill.Error("Error in SetLobbyArt. Lobby is null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Unload previous art if CVar is enabled
|
||||
if (_cfg.GetCVar(SunriseCCVars.LobbyUnloadResources) && _currentArtPath.HasValue)
|
||||
{
|
||||
UnloadResource(_currentArtPath.Value);
|
||||
}
|
||||
|
||||
Lobby!.LobbyArt.Texture = _resourceCache.GetResource<TextureResource>(lobbyArtPrototype.Background);
|
||||
var imagePath = lobbyArtPrototype.Background.ToString();
|
||||
|
||||
// Check if resource is available, request if not
|
||||
var isAvailable = _netTexturesManager.EnsureResource(imagePath);
|
||||
|
||||
ResPath targetPath;
|
||||
if (isAvailable)
|
||||
{
|
||||
// Resource is available, use uploaded path
|
||||
targetPath = _netTexturesManager.GetUploadedPath(imagePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Resource is being requested, try to use uploaded path first
|
||||
var uploadedPath = _netTexturesManager.GetUploadedPath(imagePath);
|
||||
|
||||
// Check if uploaded resource exists
|
||||
if (_resource.ContentFileExists(uploadedPath))
|
||||
{
|
||||
targetPath = uploadedPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Resource not available yet, don't try to load it (will cause error)
|
||||
// The resource will be loaded when it arrives via NetworkResourceUploadMessage
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to set the art, handle errors gracefully
|
||||
try
|
||||
{
|
||||
if (_resourceCache.TryGetResource<TextureResource>(targetPath, out var textureResource))
|
||||
{
|
||||
Lobby!.LobbyArt.Texture = textureResource.Texture;
|
||||
_currentArtPath = targetPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
_sawmill.Warning($"Failed to load lobby art texture: {targetPath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Warning($"Exception while setting lobby art {lobbyArt}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLobbyParallax(string lobbyParallax)
|
||||
{
|
||||
// Check if parallax background type is currently selected
|
||||
var backgroundType = _cfg.GetCVar(SunriseCCVars.LobbyBackgroundType);
|
||||
if (backgroundType == "Random" && _gameTicker.LobbyType != null)
|
||||
{
|
||||
backgroundType = _gameTicker.LobbyType;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(backgroundType, out LobbyBackgroundType lobbyBackgroundType) ||
|
||||
lobbyBackgroundType != LobbyBackgroundType.Parallax)
|
||||
{
|
||||
// Parallax is not the selected background type, don't load it
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_protoMan.TryIndex<LobbyParallaxPrototype>(lobbyParallax, out var lobbyParallaxPrototype))
|
||||
return;
|
||||
|
||||
if (Lobby == null)
|
||||
{
|
||||
Logger.Error("Error in SetLobbyParallax. Lobby is null");
|
||||
_sawmill.Error("Error in SetLobbyParallax. Lobby is null");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -476,6 +686,111 @@ namespace Content.Client.Lobby
|
|||
SetLobbyParallax(_gameTicker.LobbyParallax!);
|
||||
}
|
||||
|
||||
private void OnNetworkResourceLoaded(string resourcePath)
|
||||
{
|
||||
// Only update the resource that matches the current background type
|
||||
var backgroundType = _cfg.GetCVar(SunriseCCVars.LobbyBackgroundType);
|
||||
if (backgroundType == "Random" && _gameTicker.LobbyType != null)
|
||||
{
|
||||
backgroundType = _gameTicker.LobbyType;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(backgroundType, out LobbyBackgroundType lobbyBackgroundType))
|
||||
{
|
||||
lobbyBackgroundType = LobbyBackgroundType.Parallax; // Default
|
||||
}
|
||||
|
||||
// Only load the resource for the currently selected background type
|
||||
switch (lobbyBackgroundType)
|
||||
{
|
||||
case LobbyBackgroundType.Animation:
|
||||
var currentAnimation = _cfg.GetCVar(SunriseCCVars.LobbyAnimation);
|
||||
if (currentAnimation != null)
|
||||
{
|
||||
if (currentAnimation == "Random")
|
||||
{
|
||||
// For Random, use the game ticker's selected animation
|
||||
if (_gameTicker.LobbyAnimation != null)
|
||||
{
|
||||
SetLobbyAnimation(_gameTicker.LobbyAnimation);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For specific animation, always try to set it
|
||||
SetLobbyAnimation(currentAnimation);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case LobbyBackgroundType.Art:
|
||||
var currentArt = _cfg.GetCVar(SunriseCCVars.LobbyArt);
|
||||
if (currentArt != null)
|
||||
{
|
||||
var artToSet = currentArt == "Random" ? _gameTicker.LobbyArt : currentArt;
|
||||
if (artToSet != null)
|
||||
{
|
||||
SetLobbyArt(artToSet);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case LobbyBackgroundType.Parallax:
|
||||
// Parallax doesn't need network resources, it uses local resources
|
||||
// But we can update it if needed
|
||||
var currentParallax = _cfg.GetCVar(SunriseCCVars.LobbyParallax);
|
||||
if (currentParallax != null)
|
||||
{
|
||||
var parallaxToSet = currentParallax == "Random" ? _gameTicker.LobbyParallax : currentParallax;
|
||||
if (parallaxToSet != null)
|
||||
{
|
||||
SetLobbyParallax(parallaxToSet);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unloads a resource from video memory by disposing it.
|
||||
/// TODO: Full resource unloading from cache is not currently possible due to sandbox restrictions.
|
||||
/// The ResourceCache does not expose a public API to remove resources from its internal cache.
|
||||
/// Reflection cannot be used in the client sandbox environment.
|
||||
/// This method currently only disposes the resource, but it remains in the cache.
|
||||
/// When the engine provides a proper API for resource cache management, this should be updated.
|
||||
/// </summary>
|
||||
private void UnloadResource(ResPath resourcePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool unloaded = false;
|
||||
|
||||
// Try to unload RSI resource
|
||||
if (_resourceCache.TryGetResource<RSIResource>(resourcePath, out var rsiResource))
|
||||
{
|
||||
rsiResource.Dispose();
|
||||
unloaded = true;
|
||||
_sawmill.Debug($"Disposed RSI resource: {resourcePath} (still in cache due to sandbox limitations)");
|
||||
}
|
||||
// Try to unload texture resource
|
||||
else if (_resourceCache.TryGetResource<TextureResource>(resourcePath, out var textureResource))
|
||||
{
|
||||
textureResource.Dispose();
|
||||
unloaded = true;
|
||||
_sawmill.Debug($"Disposed texture resource: {resourcePath} (still in cache due to sandbox limitations)");
|
||||
}
|
||||
|
||||
if (!unloaded)
|
||||
{
|
||||
_sawmill.Debug($"Resource not found in cache: {resourcePath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Warning($"Failed to unload resource {resourcePath}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Sunrise-end
|
||||
|
||||
private void SetReady(bool newReady)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<ui:OptionDropDown Name="DropDownLobbyAnimation" Title="{Loc 'ui-options-lobby-animation'}" />
|
||||
<ui:OptionDropDown Name="DropDownLobbyParallax" Title="{Loc 'ui-options-lobby-parallax'}" />
|
||||
<ui:OptionSlider Name="LobbyOpacitySlider" Title="{Loc 'ui-options-lobby-opacity'}" />
|
||||
<CheckBox Name="LobbyUnloadResourcesCheckBox" Text="{Loc 'ui-options-lobby-unload-resources'}" />
|
||||
|
||||
<!-- Combat -->
|
||||
<Label Text="{Loc 'ui-options-sunrise-general-combat'}"
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ public sealed partial class ExtraTab : Control
|
|||
Control.AddOptionDropDown(SunriseCCVars.LobbyAnimation, DropDownLobbyAnimation, lobbyAnimations);
|
||||
Control.AddOptionDropDown(SunriseCCVars.LobbyParallax, DropDownLobbyParallax, lobbyParallaxes);
|
||||
Control.AddOptionPercentSlider(SunriseCCVars.LobbyOpacity, LobbyOpacitySlider);
|
||||
Control.AddOptionCheckBox(SunriseCCVars.LobbyUnloadResources, LobbyUnloadResourcesCheckBox);
|
||||
Control.AddOptionCheckBox(SunriseCCVars.DamageOverlayEnable, DamageOverlayEnableCheckBox);
|
||||
Control.AddOptionCheckBox(SunriseCCVars.DamageOverlaySelf, DamageOverlaySelfCheckBox);
|
||||
Control.AddOptionCheckBox(SunriseCCVars.DamageOverlayStructures, DamageOverlayStructuresCheckBox);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Content.Client._Sunrise;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.IoC;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Parallax.Data;
|
||||
|
|
@ -19,9 +22,102 @@ public sealed partial class ImageParallaxTextureSource : IParallaxTextureSource
|
|||
[DataField("path", required: true)]
|
||||
public ResPath Path { get; private set; } = default!;
|
||||
|
||||
Task<Texture> IParallaxTextureSource.GenerateTexture(CancellationToken cancel)
|
||||
async Task<Texture> IParallaxTextureSource.GenerateTexture(CancellationToken cancel)
|
||||
{
|
||||
return Task.FromResult(StaticIoC.ResC.GetTexture(Path));
|
||||
// Check if this is a network texture (starts with /NetTextures/)
|
||||
var pathStr = Path.ToString();
|
||||
if (pathStr.StartsWith("/NetTextures/", System.StringComparison.Ordinal))
|
||||
{
|
||||
// Use NetTexturesManager for dynamic loading
|
||||
var netTexturesManager = IoCManager.Resolve<NetTexturesManager>();
|
||||
var resourceCache = IoCManager.Resolve<IResourceCache>();
|
||||
var resourceManager = IoCManager.Resolve<IResourceManager>();
|
||||
var netManager = IoCManager.Resolve<IClientNetManager>();
|
||||
|
||||
// If client is not connected to server, fallback to local resources
|
||||
if (!netManager.IsConnected)
|
||||
{
|
||||
// Try to load from local resources as fallback
|
||||
// Convert /NetTextures/Parallaxes/... to /Textures/Parallaxes/... for local fallback
|
||||
var fallbackPath = pathStr.Replace("/NetTextures/", "/Textures/");
|
||||
var fallbackResPath = new ResPath(fallbackPath);
|
||||
if (resourceManager.ContentFileExists(fallbackResPath))
|
||||
{
|
||||
return StaticIoC.ResC.GetTexture(fallbackResPath);
|
||||
}
|
||||
// If fallback path doesn't exist, try to use a default texture
|
||||
// This ensures we always have something to display before connecting
|
||||
var defaultPath = new ResPath("/Textures/Parallaxes/layer1.png");
|
||||
if (resourceManager.ContentFileExists(defaultPath))
|
||||
{
|
||||
return StaticIoC.ResC.GetTexture(defaultPath);
|
||||
}
|
||||
// Last resort: try original path (might fail, but at least we tried)
|
||||
return StaticIoC.ResC.GetTexture(Path);
|
||||
}
|
||||
|
||||
// Ensure the resource is available
|
||||
var isAvailable = netTexturesManager.EnsureResource(pathStr);
|
||||
|
||||
ResPath targetPath = netTexturesManager.GetUploadedPath(pathStr);
|
||||
|
||||
if (!isAvailable)
|
||||
{
|
||||
// Resource is being requested, wait for it to load
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
void OnResourceLoaded(string loadedPath)
|
||||
{
|
||||
if (loadedPath == pathStr)
|
||||
{
|
||||
netTexturesManager.ResourceLoaded -= OnResourceLoaded;
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
netTexturesManager.ResourceLoaded += OnResourceLoaded;
|
||||
|
||||
try
|
||||
{
|
||||
// Also check immediately in case it loads very fast
|
||||
var relativePath = new ResPath(pathStr).ToRelativePath();
|
||||
var checkPath = new ResPath("/Uploaded") / relativePath;
|
||||
if (resourceManager.ContentFileExists(checkPath.ToRootedPath()))
|
||||
{
|
||||
netTexturesManager.ResourceLoaded -= OnResourceLoaded;
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wait for the resource to load (with cancellation support)
|
||||
using (cancel.Register(() =>
|
||||
{
|
||||
netTexturesManager.ResourceLoaded -= OnResourceLoaded;
|
||||
tcs.TrySetCanceled();
|
||||
}))
|
||||
{
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Cancellation requested, fallback to original path
|
||||
return StaticIoC.ResC.GetTexture(Path);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get the texture resource
|
||||
if (resourceCache.TryGetResource<TextureResource>(targetPath, out var textureResource))
|
||||
{
|
||||
return textureResource.Texture;
|
||||
}
|
||||
|
||||
// Fallback to original path if network texture loading failed
|
||||
return StaticIoC.ResC.GetTexture(Path);
|
||||
}
|
||||
|
||||
// For non-network textures, use the original method
|
||||
return StaticIoC.ResC.GetTexture(Path);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Numerics;
|
||||
using Content.Client.Parallax.Data;
|
||||
using System.Linq; // Sunrise-Edit
|
||||
using Content.Client.Parallax.Managers;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
|
|
@ -8,8 +7,6 @@ using Robust.Shared.Prototypes;
|
|||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Prototypes; // Sunrise-Edit
|
||||
using Content.Shared._Sunrise.Lobby; // Sunrise-Edit
|
||||
|
||||
namespace Content.Client.Parallax;
|
||||
|
||||
|
|
@ -21,7 +18,6 @@ public sealed class ParallaxControl : Control
|
|||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IParallaxManager _parallaxManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; // Sunrise-Edit
|
||||
|
||||
private string _parallaxPrototype = "FastSpace";
|
||||
|
||||
|
|
@ -39,9 +35,6 @@ public sealed class ParallaxControl : Control
|
|||
_parallaxManager.LoadParallaxByName(value);
|
||||
}
|
||||
}
|
||||
[ViewVariables(VVAccess.ReadWrite)] public string CurrentParallax { get; private set; } = "FastSpace"; // Sunrise-Edit
|
||||
|
||||
private readonly HashSet<string> _invalidParallaxes = new(); // Sunrise-Edit
|
||||
|
||||
public ParallaxControl()
|
||||
{
|
||||
|
|
@ -50,65 +43,21 @@ public sealed class ParallaxControl : Control
|
|||
Offset = new Vector2(_random.Next(0, 1000), _random.Next(0, 1000));
|
||||
|
||||
RectClipContent = true;
|
||||
// Sunrise-Edit-Start
|
||||
SelectRandomParallax();
|
||||
}
|
||||
|
||||
private void SelectRandomParallax()
|
||||
{
|
||||
var parallaxes = _prototypeManager.EnumeratePrototypes<LobbyParallaxPrototype>()
|
||||
.Where(p => !_invalidParallaxes.Contains(p.Parallax))
|
||||
.ToList();
|
||||
|
||||
if (parallaxes.Any())
|
||||
{
|
||||
var selectedParallax = _random.Pick(parallaxes);
|
||||
CurrentParallax = selectedParallax.Parallax;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentParallax = "FastSpace";
|
||||
}
|
||||
|
||||
_parallaxManager.LoadParallaxByName(CurrentParallax);
|
||||
// Sunrise-Edit-End
|
||||
_parallaxManager.LoadParallaxByName(_parallaxPrototype);
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
if (Size.X <= 0 || Size.Y <= 0)
|
||||
return;
|
||||
// Sunrise-Edit-Start
|
||||
var layers = _parallaxManager.GetParallaxLayers(CurrentParallax).ToList();
|
||||
if (!layers.Any())
|
||||
{
|
||||
_invalidParallaxes.Add(CurrentParallax);
|
||||
SelectRandomParallax();
|
||||
return;
|
||||
}
|
||||
|
||||
var currentTime = (float) _timing.RealTime.TotalSeconds;
|
||||
var offset = Offset + new Vector2(currentTime * SpeedX, currentTime * SpeedY);
|
||||
|
||||
var hasValidLayers = false;
|
||||
foreach (var layer in layers)
|
||||
foreach (var layer in _parallaxManager.GetParallaxLayers(_parallaxPrototype))
|
||||
{
|
||||
var tex = layer.Texture;
|
||||
if (tex.Size.X <= 0 || tex.Size.Y <= 0)
|
||||
continue;
|
||||
|
||||
var scale = layer.Config.Scale.Floored();
|
||||
if (scale.X <= 0 || scale.Y <= 0)
|
||||
continue;
|
||||
var texSize = new Vector2i(
|
||||
(int)(tex.Size.X * Size.X * layer.Config.Scale.X / 1920 * ScaleX),
|
||||
(int)(tex.Size.Y * Size.X * layer.Config.Scale.Y / 1920 * ScaleY)
|
||||
);
|
||||
|
||||
if (texSize.X <= 0 || texSize.Y <= 0)
|
||||
continue;
|
||||
|
||||
hasValidLayers = true;
|
||||
var ourSize = PixelSize;
|
||||
|
||||
//Protection from division by zero.
|
||||
|
|
@ -117,10 +66,16 @@ public sealed class ParallaxControl : Control
|
|||
|
||||
if (layer.Config.Tiled)
|
||||
{
|
||||
// Multiply offset by slowness to match normal parallax
|
||||
var scaledOffset = (offset * layer.Config.Slowness).Floored();
|
||||
|
||||
// Then modulo the scaled offset by the size to prevent drawing a bunch of offscreen tiles for really small images.
|
||||
scaledOffset.X %= texSize.X;
|
||||
scaledOffset.Y %= texSize.Y;
|
||||
|
||||
// Note: scaledOffset must never be below 0 or there will be visual issues.
|
||||
// It could be allowed to be >= texSize on a given axis but that would be wasteful.
|
||||
|
||||
for (var x = -scaledOffset.X; x < ourSize.X; x += texSize.X)
|
||||
{
|
||||
for (var y = -scaledOffset.Y; y < ourSize.Y; y += texSize.Y)
|
||||
|
|
@ -135,13 +90,6 @@ public sealed class ParallaxControl : Control
|
|||
handle.DrawTextureRect(tex, UIBox2.FromDimensions(origin, texSize));
|
||||
}
|
||||
}
|
||||
// Sunrise-Edit-Start
|
||||
if (!hasValidLayers)
|
||||
{
|
||||
_invalidParallaxes.Add(CurrentParallax);
|
||||
SelectRandomParallax();
|
||||
}
|
||||
// Sunrise-Edit-End
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
239
Content.Client/_Sunrise/NetTexturesManager.cs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
using Content.Shared._Sunrise.NetTextures;
|
||||
using Robust.Client.Upload;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._Sunrise;
|
||||
|
||||
/// <summary>
|
||||
/// Manager that handles dynamic loading of network textures from server.
|
||||
/// Textures are loaded into MemoryContentRoot on the client.
|
||||
/// </summary>
|
||||
public sealed class NetTexturesManager
|
||||
{
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly NetworkResourceManager _networkResourceManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private const string UploadedPrefix = "/Uploaded";
|
||||
private readonly HashSet<string> _requestedResources = new();
|
||||
private readonly Dictionary<string, ResPath> _pendingResources = new(); // resourcePath -> ResPath
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when a network texture becomes available.
|
||||
/// </summary>
|
||||
public event Action<string>? ResourceLoaded; // resourcePath
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("network.textures");
|
||||
// NetworkResourceUploadMessage is already registered by SharedNetworkResourceManager
|
||||
// We'll check for loaded resources in Update() method very frequently
|
||||
}
|
||||
|
||||
public void Update(float frameTime)
|
||||
{
|
||||
// If there are no pending resources, skip checking
|
||||
if (_pendingResources.Count == 0)
|
||||
return;
|
||||
|
||||
// Check for loaded resources every frame when there are pending resources
|
||||
// This ensures we catch resources as soon as they're loaded
|
||||
var completedResources = new List<string>();
|
||||
foreach (var (resourcePath, resPath) in _pendingResources)
|
||||
{
|
||||
// Check if resource is available
|
||||
// MemoryContentRoot stores paths relative to /Uploaded prefix
|
||||
var relativePath = resPath.ToRelativePath();
|
||||
|
||||
bool exists = false;
|
||||
ResPath checkPath;
|
||||
|
||||
// For RSI directories, check for meta.json file
|
||||
// Check if path ends with .rsi (more reliable than Extension property)
|
||||
var pathStr = relativePath.ToString();
|
||||
if (pathStr.EndsWith(".rsi") || pathStr.EndsWith(".rsi/"))
|
||||
{
|
||||
checkPath = relativePath / "meta.json";
|
||||
exists = _networkResourceManager.FileExists(checkPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single file
|
||||
checkPath = relativePath;
|
||||
exists = _networkResourceManager.FileExists(checkPath);
|
||||
}
|
||||
|
||||
if (exists)
|
||||
{
|
||||
_requestedResources.Add(resourcePath);
|
||||
completedResources.Add(resourcePath);
|
||||
ResourceLoaded?.Invoke(resourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var resourcePath in completedResources)
|
||||
{
|
||||
_pendingResources.Remove(resourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a network texture is available, and requests it if not.
|
||||
/// </summary>
|
||||
/// <param name="resourcePath">Path to the resource (as specified in prototype, e.g., "/NetTextures/Lobby/Animations/bar.rsi")</param>
|
||||
/// <returns>True if the resource is available, false if it's being requested</returns>
|
||||
public bool EnsureResource(string resourcePath)
|
||||
{
|
||||
// Normalize the path
|
||||
ResPath resPath;
|
||||
if (resourcePath.StartsWith("/"))
|
||||
{
|
||||
resPath = new ResPath(resourcePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rootPath = new ResPath("/");
|
||||
resPath = rootPath / resourcePath;
|
||||
}
|
||||
|
||||
// Check if the resource is actually available
|
||||
var relativePath = resPath.ToRelativePath();
|
||||
var uploadedPath = (new ResPath(UploadedPrefix) / relativePath).ToRootedPath();
|
||||
|
||||
bool isAvailable = false;
|
||||
|
||||
// For RSI directories, check for meta.json
|
||||
// Check if path ends with .rsi (more reliable than Extension property)
|
||||
var pathStr = relativePath.ToString();
|
||||
if (pathStr.EndsWith(".rsi") || pathStr.EndsWith(".rsi/"))
|
||||
{
|
||||
var metaPath = relativePath / "meta.json";
|
||||
var metaUploadedPath = (new ResPath(UploadedPrefix) / metaPath).ToRootedPath();
|
||||
isAvailable = _networkResourceManager.FileExists(metaPath) || _resourceManager.ContentFileExists(metaUploadedPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single file
|
||||
isAvailable = _networkResourceManager.FileExists(relativePath) || _resourceManager.ContentFileExists(uploadedPath);
|
||||
}
|
||||
|
||||
if (isAvailable)
|
||||
{
|
||||
// Resource is available
|
||||
if (!_requestedResources.Contains(resourcePath))
|
||||
_requestedResources.Add(resourcePath);
|
||||
// Remove from pending if it was there
|
||||
_pendingResources.Remove(resourcePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resource is not available yet
|
||||
// If it's already in pending, we're already tracking it
|
||||
if (_pendingResources.ContainsKey(resourcePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If it was requested before but not in pending, add it to pending to track it
|
||||
if (_requestedResources.Contains(resourcePath))
|
||||
{
|
||||
_pendingResources[resourcePath] = resPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Request the resource for the first time
|
||||
RequestResource(resourcePath);
|
||||
_pendingResources[resourcePath] = resPath;
|
||||
|
||||
// Immediately check if the resource is already available (might be cached or loaded very fast)
|
||||
// This helps catch resources that load synchronously
|
||||
CheckResourceImmediately(resourcePath, resPath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RequestResource(string resourcePath)
|
||||
{
|
||||
if (_requestedResources.Contains(resourcePath))
|
||||
return;
|
||||
|
||||
// Check if client is connected to server before trying to send message
|
||||
if (!_netManager.IsConnected)
|
||||
{
|
||||
_sawmill.Debug($"Cannot request resource {resourcePath}: client not connected to server");
|
||||
return;
|
||||
}
|
||||
|
||||
_requestedResources.Add(resourcePath);
|
||||
|
||||
var msg = new RequestNetworkResourceMessage
|
||||
{
|
||||
ResourcePath = resourcePath
|
||||
};
|
||||
|
||||
_netManager.ClientSendMessage(msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately checks if a resource is available and fires the event if it is.
|
||||
/// This helps catch resources that load very quickly.
|
||||
/// </summary>
|
||||
private void CheckResourceImmediately(string resourcePath, ResPath resPath)
|
||||
{
|
||||
var relativePath = resPath.ToRelativePath();
|
||||
bool exists = false;
|
||||
|
||||
// For RSI directories, check for meta.json file
|
||||
// Check if path ends with .rsi (more reliable than Extension property)
|
||||
var pathStr = relativePath.ToString();
|
||||
if (pathStr.EndsWith(".rsi") || pathStr.EndsWith(".rsi/"))
|
||||
{
|
||||
var metaRelativePath = relativePath / "meta.json";
|
||||
exists = _networkResourceManager.FileExists(metaRelativePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single file
|
||||
exists = _networkResourceManager.FileExists(relativePath);
|
||||
}
|
||||
|
||||
if (exists)
|
||||
{
|
||||
if (!_requestedResources.Contains(resourcePath))
|
||||
_requestedResources.Add(resourcePath);
|
||||
_pendingResources.Remove(resourcePath);
|
||||
ResourceLoaded?.Invoke(resourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the uploaded path for a network texture.
|
||||
/// </summary>
|
||||
/// <param name="resourcePath">Original resource path from prototype</param>
|
||||
/// <returns>Rooted path to the resource in MemoryContentRoot</returns>
|
||||
public ResPath GetUploadedPath(string resourcePath)
|
||||
{
|
||||
ResPath resPath;
|
||||
if (resourcePath.StartsWith("/"))
|
||||
{
|
||||
resPath = new ResPath(resourcePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
resPath = new ResPath("/") / resourcePath;
|
||||
}
|
||||
|
||||
var relativePath = resPath.ToRelativePath();
|
||||
var path = new ResPath(UploadedPrefix) / relativePath;
|
||||
return path.ToRootedPath(); // Ensure it's always rooted
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ public static class ClientPackaging
|
|||
await RobustClientPackaging.WriteClientResources(
|
||||
contentDir,
|
||||
inputPass,
|
||||
SharedPackaging.AdditionalIgnoredResources,
|
||||
SharedPackaging.AdditionalIgnoredResources.Union(SharedPackaging.ClientOnlyIgnoredResources).ToHashSet(),
|
||||
cancel);
|
||||
|
||||
inputPass.InjectFinished();
|
||||
|
|
|
|||
|
|
@ -7,4 +7,11 @@ public sealed class SharedPackaging
|
|||
// MapRenderer outputs into Resources. Avoid these getting included in packaging.
|
||||
"MapImages",
|
||||
};
|
||||
|
||||
// Sunrise-Start
|
||||
public static readonly IReadOnlySet<string> ClientOnlyIgnoredResources = new HashSet<string>
|
||||
{
|
||||
"NetTextures",
|
||||
};
|
||||
// Sunrise-End
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Content.Server._Sunrise;
|
||||
using Content.Server._Sunrise.Contributors;
|
||||
using Content.Server._Sunrise.Entry;
|
||||
using Content.Server._Sunrise.PlayerCache;
|
||||
|
|
@ -90,6 +91,7 @@ namespace Content.Server.Entry
|
|||
[Dependency] private readonly ContributorsManager _contributorsManager = default!; // Sunrise-Edit
|
||||
[Dependency] private readonly PlayerCacheManager _playerCacheManager = default!; // Sunrise-Edit
|
||||
[Dependency] private readonly TTSManager _ttsManager = default!; // Sunrise-Edit
|
||||
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!; // Sunrise-Edit
|
||||
[Dependency] private readonly DiscordWebhook _discord = default!; // Sunrise-Edit
|
||||
[Dependency] private readonly IIPBlockingSystem _ipBlockingSystem = default!;
|
||||
private ISharedSponsorsManager? _sponsorsManager; // Sunrise-Sponsors
|
||||
|
|
@ -146,6 +148,7 @@ namespace Content.Server.Entry
|
|||
|
||||
// Sunrise-Start
|
||||
_ttsManager.Initialize();
|
||||
_netTexturesManager.Initialize();
|
||||
_ipBlockingSystem.Initialize();
|
||||
SunriseServerEntry.Init();
|
||||
IoCManager.Instance!.TryResolveType(out _sponsorsManager);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Content.Server._Sunrise;
|
||||
using Content.Server._Sunrise.Contributors;
|
||||
using Content.Server._Sunrise.IoC;
|
||||
using Content.Server._Sunrise.PlayerCache;
|
||||
|
|
@ -93,6 +94,7 @@ internal static class ServerContentIoC
|
|||
deps.Register<ContributorsManager>();
|
||||
deps.Register<PlayerCacheManager>();
|
||||
deps.Register<TTSManager>();
|
||||
deps.Register<NetTexturesManager>();
|
||||
deps.Register<IIPBlockingSystem, IPBlockingSystem>();
|
||||
SunriseServerContentIoC.Register();
|
||||
// Sunrise-End
|
||||
|
|
|
|||
192
Content.Server/_Sunrise/NetTexturesManager.cs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
using System.Linq;
|
||||
using Content.Shared._Sunrise.NetTextures;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Upload;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server._Sunrise;
|
||||
|
||||
/// <summary>
|
||||
/// Manager that handles dynamic loading of network textures from server to client.
|
||||
/// Textures are loaded into MemoryContentRoot on the client.
|
||||
/// </summary>
|
||||
public sealed class NetTexturesManager
|
||||
{
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly IServerNetManager _netManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private const string AllowedPrefix = "/NetTextures/";
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("network.textures");
|
||||
_netManager.RegisterNetMessage<RequestNetworkResourceMessage>(OnRequestNetworkResource);
|
||||
}
|
||||
|
||||
private void OnRequestNetworkResource(RequestNetworkResourceMessage msg)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionByChannel(msg.MsgChannel, out var session))
|
||||
return;
|
||||
|
||||
// Normalize the path - ensure it's rooted
|
||||
var resourcePath = msg.ResourcePath;
|
||||
ResPath resPath;
|
||||
|
||||
if (resourcePath.StartsWith("/"))
|
||||
{
|
||||
resPath = new ResPath(resourcePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
resPath = new ResPath("/") / resourcePath;
|
||||
}
|
||||
|
||||
// Clean the path to remove any .. sequences
|
||||
resPath = resPath.Clean();
|
||||
|
||||
// Validate the path to prevent path traversal attacks
|
||||
if (!ValidateResourcePath(resPath, out var errorMessage))
|
||||
{
|
||||
_sawmill.Warning($"Rejected resource request from {session.Name}: {errorMessage} (path: {msg.ResourcePath})");
|
||||
return;
|
||||
}
|
||||
|
||||
SendResource(session, resPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a resource path is safe and within allowed directories.
|
||||
/// Prevents path traversal attacks by ensuring paths don't escape allowed directories.
|
||||
/// </summary>
|
||||
private bool ValidateResourcePath(ResPath path, out string? errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
|
||||
// Path must be rooted
|
||||
if (!path.IsRooted)
|
||||
{
|
||||
errorMessage = "Path must be rooted";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for dangerous path traversal sequences in the original string representation
|
||||
// Even after Clean(), we should verify the path doesn't contain .. in segments
|
||||
var pathStr = path.ToString();
|
||||
if (pathStr.Contains("../") || pathStr.Contains("..\\") || pathStr.StartsWith(".."))
|
||||
{
|
||||
errorMessage = "Path contains traversal sequences";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow paths that start with /NetTextures/
|
||||
// This ensures clients can only access resources from the NetTextures directory
|
||||
if (!pathStr.StartsWith(AllowedPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
errorMessage = $"Path must start with {AllowedPrefix}";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additional check: ensure the cleaned path doesn't escape the allowed directory
|
||||
// by checking that it still starts with the allowed prefix after cleaning
|
||||
var relativePath = path.ToRelativePath();
|
||||
var relativePathStr = relativePath.ToString();
|
||||
|
||||
if (!relativePathStr.StartsWith("NetTextures/", StringComparison.Ordinal))
|
||||
{
|
||||
errorMessage = "Path escapes allowed directory after normalization";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a resource (file or directory) to the client.
|
||||
/// If the path points to a directory (e.g., .rsi), all files in that directory are sent.
|
||||
/// If the path points to a file, only that file is sent.
|
||||
/// </summary>
|
||||
private void SendResource(ICommonSession session, ResPath resourcePath)
|
||||
{
|
||||
// Check if it's a directory (RSI files are directories)
|
||||
// Try to find files in the directory first
|
||||
var files = _resourceManager.ContentFindFiles(resourcePath).ToList();
|
||||
|
||||
if (files.Count == 0)
|
||||
{
|
||||
// No files found in directory, try as single file
|
||||
if (!_resourceManager.ContentFileExists(resourcePath))
|
||||
{
|
||||
_sawmill.Warning($"Resource not found: {resourcePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
SendSingleFile(session, resourcePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Directory - send all files
|
||||
foreach (var filePath in files)
|
||||
{
|
||||
if (!filePath.TryRelativeTo(resourcePath, out var relativePath))
|
||||
continue;
|
||||
|
||||
// relativePath is guaranteed to be non-null here because TryRelativeTo returned true
|
||||
var relativePathValue = relativePath.Value;
|
||||
|
||||
if (!_resourceManager.TryContentFileRead(filePath, out var stream))
|
||||
{
|
||||
_sawmill.Warning($"Failed to read file: {filePath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
using (stream)
|
||||
{
|
||||
var data = new byte[stream.Length];
|
||||
stream.Read(data, 0, data.Length);
|
||||
|
||||
// Calculate uploaded path: preserve the original path structure relative to Resources root
|
||||
// Remove leading / and use as relative path for MemoryContentRoot
|
||||
var relativeUploadPath = resourcePath.ToRelativePath();
|
||||
var uploadedPath = relativeUploadPath / relativePathValue;
|
||||
|
||||
var uploadMsg = new NetworkResourceUploadMessage(data, uploadedPath);
|
||||
session.Channel.SendMessage(uploadMsg);
|
||||
}
|
||||
}
|
||||
|
||||
_sawmill.Debug($"Sent resource directory {resourcePath} ({files.Count} files) to {session.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a single file resource to the client.
|
||||
/// </summary>
|
||||
private void SendSingleFile(ICommonSession session, ResPath filePath)
|
||||
{
|
||||
if (!_resourceManager.TryContentFileRead(filePath, out var stream))
|
||||
{
|
||||
_sawmill.Warning($"Failed to read file: {filePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
using (stream)
|
||||
{
|
||||
var data = new byte[stream.Length];
|
||||
stream.Read(data, 0, data.Length);
|
||||
|
||||
// Calculate uploaded path: preserve the original path structure relative to Resources root
|
||||
var relativeUploadPath = filePath.ToRelativePath();
|
||||
var uploadMsg = new NetworkResourceUploadMessage(data, relativeUploadPath);
|
||||
session.Channel.SendMessage(uploadMsg);
|
||||
}
|
||||
|
||||
_sawmill.Debug($"Sent resource file {filePath} to {session.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using Lidgren.Network;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.NetTextures;
|
||||
|
||||
/// <summary>
|
||||
/// Message sent from client to server to request a network resource by path.
|
||||
/// The resource will be loaded into MemoryContentRoot on the client.
|
||||
/// </summary>
|
||||
public sealed class RequestNetworkResourceMessage : NetMessage
|
||||
{
|
||||
public override MsgGroups MsgGroup => MsgGroups.String;
|
||||
|
||||
/// <summary>
|
||||
/// Path to the resource to request (e.g., "/NetTextures/Lobby/Animations/bar.rsi" or "NetTextures/Lobby/Arts/image.webp").
|
||||
/// Can be absolute (starting with /) or relative to Resources root.
|
||||
/// </summary>
|
||||
public string ResourcePath { get; set; } = string.Empty;
|
||||
|
||||
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
ResourcePath = buffer.ReadString();
|
||||
}
|
||||
|
||||
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
buffer.Write(ResourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -216,6 +216,12 @@ public sealed partial class SunriseCCVars : CVars
|
|||
public static readonly CVarDef<string> LobbyParallax =
|
||||
CVarDef.Create("lobby.parallax", "Random", CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Whether to unload lobby resources from video memory when switching backgrounds or entering round.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> LobbyUnloadResources =
|
||||
CVarDef.Create("lobby.unload_resources", true, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<float> LobbyOpacity =
|
||||
CVarDef.Create("lobby.lobby_opacity", 0.90f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ ui-options-lobby-background-type = Тип фона лобби
|
|||
ui-options-lobby-art = Арт лобби
|
||||
ui-options-lobby-animation = Анимация лобби
|
||||
ui-options-lobby-parallax = Паралакс лобби
|
||||
ui-options-lobby-unload-resources = Выгружать ресурсы лобби из видеопамяти
|
||||
ui-options-damage-overlay-enable = Оверлей урона
|
||||
ui-options-damage-overlay-structures = Показывать урон по структурам
|
||||
ui-options-damage-overlay-self = Показывать урон по себе
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 8.9 MiB After Width: | Height: | Size: 8.9 MiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 335 KiB After Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 404 KiB After Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 859 KiB After Width: | Height: | Size: 859 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 4.2 MiB After Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 351 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |