diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 71f6ea2fe7..9a23ea946b 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -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) diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index 5e89e0b6a2..917af78037 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -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(); collection.Register(); collection.Register(); + collection.Register(); SunriseClientContentIoC.Register(); collection.Register(true); // Sunrise-End diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs index 47cf93bb77..15b946d1e7 100644 --- a/Content.Client/Lobby/LobbyState.cs +++ b/Content.Client/Lobby/LobbyState.cs @@ -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 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(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(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(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(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(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(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; + } + } + + /// + /// 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. + /// + private void UnloadResource(ResPath resourcePath) + { + try + { + bool unloaded = false; + + // Try to unload RSI resource + if (_resourceCache.TryGetResource(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(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) diff --git a/Content.Client/Options/UI/Tabs/ExtraTab.xaml b/Content.Client/Options/UI/Tabs/ExtraTab.xaml index b5e2807794..a349a8382f 100644 --- a/Content.Client/Options/UI/Tabs/ExtraTab.xaml +++ b/Content.Client/Options/UI/Tabs/ExtraTab.xaml @@ -13,6 +13,7 @@ +