From 3d606f43169d51bc57646242b54f42c147841ec8 Mon Sep 17 00:00:00 2001 From: 20kdc Date: Wed, 4 May 2022 17:55:21 +0100 Subject: [PATCH] Parallax refactors (#7654) --- Content.Client/Entry/EntryPoint.cs | 2 +- .../EscapeMenu/UI/Tabs/GraphicsTab.xaml | 1 + .../EscapeMenu/UI/Tabs/GraphicsTab.xaml.cs | 5 + .../Data/GeneratedParallaxTextureSource.cs | 148 ++++++++++++++ .../Parallax/Data/IParallaxTextureSource.cs | 19 ++ .../Data/ImageParallaxTextureSource.cs | 29 +++ .../Parallax/Data/ParallaxLayerConfig.cs | 66 ++++++ .../Parallax/Data/ParallaxPrototype.cs | 37 ++++ .../Parallax/Managers/IParallaxManager.cs | 37 +++- .../Parallax/Managers/ParallaxManager.cs | 192 ++++++++++-------- Content.Client/Parallax/ParallaxControl.cs | 68 ++++--- Content.Client/Parallax/ParallaxGenerator.cs | 62 +++++- .../Parallax/ParallaxLayerPrepared.cs | 22 ++ Content.Client/Parallax/ParallaxOverlay.cs | 125 +++++++----- .../DummyParallaxManager.cs | 17 +- Content.Server/Entry/EntryPoint.cs | 4 + Content.Shared/CCVar/CCVars.cs | 3 + .../en-US/escape-menu/ui/options-menu.ftl | 1 + Resources/Prototypes/Parallaxes/default.yml | 26 +++ .../Parallaxes}/parallax_config.toml | 5 + .../Parallaxes/parallax_config_clouds.toml | 31 +++ .../Parallaxes/parallax_config_stars.toml | 57 ++++++ .../Parallaxes/parallax_config_stars_dim.toml | 25 +++ Resources/Prototypes/Parallaxes/test.yml | 54 +++++ 24 files changed, 857 insertions(+), 179 deletions(-) create mode 100644 Content.Client/Parallax/Data/GeneratedParallaxTextureSource.cs create mode 100644 Content.Client/Parallax/Data/IParallaxTextureSource.cs create mode 100644 Content.Client/Parallax/Data/ImageParallaxTextureSource.cs create mode 100644 Content.Client/Parallax/Data/ParallaxLayerConfig.cs create mode 100644 Content.Client/Parallax/Data/ParallaxPrototype.cs create mode 100644 Content.Client/Parallax/ParallaxLayerPrepared.cs create mode 100644 Resources/Prototypes/Parallaxes/default.yml rename Resources/{ => Prototypes/Parallaxes}/parallax_config.toml (96%) create mode 100644 Resources/Prototypes/Parallaxes/parallax_config_clouds.toml create mode 100644 Resources/Prototypes/Parallaxes/parallax_config_stars.toml create mode 100644 Resources/Prototypes/Parallaxes/parallax_config_stars_dim.toml create mode 100644 Resources/Prototypes/Parallaxes/test.yml diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 0c76a94800..34747ef28f 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -118,7 +118,6 @@ namespace Content.Client.Entry factory.GenerateNetIds(); IoCManager.Resolve().Initialize(); - IoCManager.Resolve().LoadParallax(); IoCManager.Resolve().PlayerJoinedServer += SubscribePlayerAttachmentEvents; IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); @@ -180,6 +179,7 @@ namespace Content.Client.Entry ContentContexts.SetupContexts(inputMan.Contexts); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().LoadParallax(); // Have to do this later because prototypes are needed. var overlayMgr = IoCManager.Resolve(); overlayMgr.AddOverlay(new ParallaxOverlay()); diff --git a/Content.Client/EscapeMenu/UI/Tabs/GraphicsTab.xaml b/Content.Client/EscapeMenu/UI/Tabs/GraphicsTab.xaml index 5011839ffa..7790a7da92 100644 --- a/Content.Client/EscapeMenu/UI/Tabs/GraphicsTab.xaml +++ b/Content.Client/EscapeMenu/UI/Tabs/GraphicsTab.xaml @@ -35,6 +35,7 @@ Text="{Loc 'ui-options-vp-integer-scaling'}" ToolTip="{Loc 'ui-options-vp-integer-scaling-tooltip'}" /> + diff --git a/Content.Client/EscapeMenu/UI/Tabs/GraphicsTab.xaml.cs b/Content.Client/EscapeMenu/UI/Tabs/GraphicsTab.xaml.cs index 9b790f4251..4d8540b8a9 100644 --- a/Content.Client/EscapeMenu/UI/Tabs/GraphicsTab.xaml.cs +++ b/Content.Client/EscapeMenu/UI/Tabs/GraphicsTab.xaml.cs @@ -76,6 +76,7 @@ namespace Content.Client.EscapeMenu.UI.Tabs ShowHeldItemCheckBox.OnToggled += OnCheckBoxToggled; IntegerScalingCheckBox.OnToggled += OnCheckBoxToggled; ViewportLowResCheckBox.OnToggled += OnCheckBoxToggled; + ParallaxLowQualityCheckBox.OnToggled += OnCheckBoxToggled; FpsCounterCheckBox.OnToggled += OnCheckBoxToggled; ApplyButton.OnPressed += OnApplyButtonPressed; VSyncCheckBox.Pressed = _cfg.GetCVar(CVars.DisplayVSync); @@ -87,6 +88,7 @@ namespace Content.Client.EscapeMenu.UI.Tabs ViewportStretchCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportStretch); IntegerScalingCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0; ViewportLowResCheckBox.Pressed = !_cfg.GetCVar(CCVars.ViewportScaleRender); + ParallaxLowQualityCheckBox.Pressed = _cfg.GetCVar(CCVars.ParallaxLowQuality); FpsCounterCheckBox.Pressed = _cfg.GetCVar(CCVars.HudFpsCounterVisible); ShowHeldItemCheckBox.Pressed = _cfg.GetCVar(CCVars.HudHeldItemShow); @@ -123,6 +125,7 @@ namespace Content.Client.EscapeMenu.UI.Tabs _cfg.SetCVar(CCVars.ViewportSnapToleranceMargin, IntegerScalingCheckBox.Pressed ? CCVars.ViewportSnapToleranceMargin.DefaultValue : 0); _cfg.SetCVar(CCVars.ViewportScaleRender, !ViewportLowResCheckBox.Pressed); + _cfg.SetCVar(CCVars.ParallaxLowQuality, ParallaxLowQualityCheckBox.Pressed); _cfg.SetCVar(CCVars.HudHeldItemShow, ShowHeldItemCheckBox.Pressed); _cfg.SetCVar(CCVars.HudFpsCounterVisible, FpsCounterCheckBox.Pressed); _cfg.SaveToFile(); @@ -151,6 +154,7 @@ namespace Content.Client.EscapeMenu.UI.Tabs var isVPScaleSame = (int) ViewportScaleSlider.Value == _cfg.GetCVar(CCVars.ViewportFixedScaleFactor); var isIntegerScalingSame = IntegerScalingCheckBox.Pressed == (_cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0); var isVPResSame = ViewportLowResCheckBox.Pressed == !_cfg.GetCVar(CCVars.ViewportScaleRender); + var isPLQSame = ParallaxLowQualityCheckBox.Pressed == _cfg.GetCVar(CCVars.ParallaxLowQuality); var isShowHeldItemSame = ShowHeldItemCheckBox.Pressed == _cfg.GetCVar(CCVars.HudHeldItemShow); var isFpsCounterVisibleSame = FpsCounterCheckBox.Pressed == _cfg.GetCVar(CCVars.HudFpsCounterVisible); @@ -162,6 +166,7 @@ namespace Content.Client.EscapeMenu.UI.Tabs isVPScaleSame && isIntegerScalingSame && isVPResSame && + isPLQSame && isHudThemeSame && isShowHeldItemSame && isFpsCounterVisibleSame; diff --git a/Content.Client/Parallax/Data/GeneratedParallaxTextureSource.cs b/Content.Client/Parallax/Data/GeneratedParallaxTextureSource.cs new file mode 100644 index 0000000000..19a7d1142d --- /dev/null +++ b/Content.Client/Parallax/Data/GeneratedParallaxTextureSource.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using JetBrains.Annotations; +using Nett; +using Content.Shared; +using Content.Shared.CCVar; +using Content.Client.Resources; +using Content.Client.IoC; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Utility; +using Robust.Shared.Configuration; +using Robust.Shared.ContentPack; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Utility; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Content.Client.Parallax.Data; + +[UsedImplicitly] +[DataDefinition] +public sealed class GeneratedParallaxTextureSource : IParallaxTextureSource +{ + /// + /// Parallax config path (the TOML file). + /// In client resources. + /// + [DataField("configPath")] + public ResourcePath ParallaxConfigPath { get; } = new("/parallax_config.toml"); + + /// + /// ID for debugging, caching, and so forth. + /// The empty string here is reserved for the original parallax. + /// It is advisible to provide a roughly unique ID for any unique config contents. + /// + [DataField("id")] + public string Identifier { get; } = "other"; + + /// + /// Cached path. + /// In user directory. + /// + private ResourcePath ParallaxCachedImagePath => new($"/parallax_{Identifier}cache.png"); + + /// + /// Old parallax config path (for checking for parallax updates). + /// In user directory. + /// + private ResourcePath PreviousParallaxConfigPath => new($"/parallax_{Identifier}config_old"); + + async Task IParallaxTextureSource.GenerateTexture(CancellationToken cancel = default) + { + var parallaxConfig = GetParallaxConfig(); + if (parallaxConfig == null) + { + Logger.ErrorS("parallax", $"Parallax config not found or unreadable: {ParallaxConfigPath}"); + // The show must go on. + return Texture.Transparent; + } + + var debugParallax = IoCManager.Resolve().GetCVar(CCVars.ParallaxDebug); + + if (debugParallax + || !StaticIoC.ResC.UserData.TryReadAllText(PreviousParallaxConfigPath, out var previousParallaxConfig) + || previousParallaxConfig != parallaxConfig) + { + var table = Toml.ReadString(parallaxConfig); + await UpdateCachedTexture(table, debugParallax, cancel); + + //Update the previous config + using var writer = StaticIoC.ResC.UserData.OpenWriteText(PreviousParallaxConfigPath); + writer.Write(parallaxConfig); + } + + try + { + return GetCachedTexture(); + } + catch (Exception ex) + { + Logger.ErrorS("parallax", $"Couldn't retrieve parallax cached texture: {ex}"); + // The show must go on. + try + { + // Also try to at least sort of fix this if we've been fooled by a config backup + StaticIoC.ResC.UserData.Delete(PreviousParallaxConfigPath); + } + catch (Exception) + { + } + return Texture.Transparent; + } + } + + private async Task UpdateCachedTexture(TomlTable config, bool saveDebugLayers, CancellationToken cancel = default) + { + var debugImages = saveDebugLayers ? new List>() : null; + + var sawmill = IoCManager.Resolve().GetSawmill("parallax"); + + // Generate the parallax in the thread pool. + using var newParallexImage = await Task.Run(() => + ParallaxGenerator.GenerateParallax(config, new Size(1920, 1080), sawmill, debugImages, cancel), cancel); + + // And load it in the main thread for safety reasons. + // But before spending time saving it, make sure to exit out early if it's not wanted. + cancel.ThrowIfCancellationRequested(); + + // Store it and CRC so further game starts don't need to regenerate it. + using var imageStream = StaticIoC.ResC.UserData.OpenWrite(ParallaxCachedImagePath); + newParallexImage.SaveAsPng(imageStream); + + if (saveDebugLayers) + { + for (var i = 0; i < debugImages!.Count; i++) + { + var debugImage = debugImages[i]; + using var debugImageStream = StaticIoC.ResC.UserData.OpenWrite(new ResourcePath($"/parallax_{Identifier}debug_{i}.png")); + debugImage.SaveAsPng(debugImageStream); + } + } + } + + private Texture GetCachedTexture() + { + using var imageStream = StaticIoC.ResC.UserData.OpenRead(ParallaxCachedImagePath); + return Texture.LoadFromPNGStream(imageStream, "Parallax"); + } + + private string? GetParallaxConfig() + { + if (!StaticIoC.ResC.TryContentFileRead(ParallaxConfigPath, out var configStream)) + { + return null; + } + + using var configReader = new StreamReader(configStream, EncodingHelpers.UTF8); + return configReader.ReadToEnd().Replace(Environment.NewLine, "\n"); + } +} + diff --git a/Content.Client/Parallax/Data/IParallaxTextureSource.cs b/Content.Client/Parallax/Data/IParallaxTextureSource.cs new file mode 100644 index 0000000000..e40f924c74 --- /dev/null +++ b/Content.Client/Parallax/Data/IParallaxTextureSource.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Client.Graphics; + +namespace Content.Client.Parallax.Data +{ + [ImplicitDataDefinitionForInheritors] + public interface IParallaxTextureSource + { + /// + /// Generates or loads the texture. + /// Note that this should be cached, but not necessarily *here*. + /// + Task GenerateTexture(CancellationToken cancel = default); + } +} + diff --git a/Content.Client/Parallax/Data/ImageParallaxTextureSource.cs b/Content.Client/Parallax/Data/ImageParallaxTextureSource.cs new file mode 100644 index 0000000000..359e2ee797 --- /dev/null +++ b/Content.Client/Parallax/Data/ImageParallaxTextureSource.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using JetBrains.Annotations; +using Content.Client.Resources; +using Content.Client.IoC; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Utility; + +namespace Content.Client.Parallax.Data; + +[UsedImplicitly] +[DataDefinition] +public sealed class ImageParallaxTextureSource : IParallaxTextureSource +{ + /// + /// Texture path. + /// + [DataField("path", required: true)] + public ResourcePath Path { get; } = default!; + + async Task IParallaxTextureSource.GenerateTexture(CancellationToken cancel = default) + { + return StaticIoC.ResC.GetTexture(Path); + } +} + diff --git a/Content.Client/Parallax/Data/ParallaxLayerConfig.cs b/Content.Client/Parallax/Data/ParallaxLayerConfig.cs new file mode 100644 index 0000000000..4c6884781a --- /dev/null +++ b/Content.Client/Parallax/Data/ParallaxLayerConfig.cs @@ -0,0 +1,66 @@ +using System; +using Robust.Client.Graphics; +using Content.Client.Parallax.Data; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Client.Parallax.Data; + +/// +/// The configuration for a parallax layer. +/// +[DataDefinition] +public sealed class ParallaxLayerConfig +{ + /// + /// The texture source for this layer. + /// + [DataField("texture", required: true)] + public IParallaxTextureSource Texture { get; set; } = default!; + + /// + /// A scaling factor for the texture. + /// In the interest of simplifying maths, this is rounded down to integer for ParallaxControl, so be careful. + /// + [DataField("scale")] + public Vector2 Scale { get; set; } = Vector2.One; + + /// + /// If true, this layer is tiled as the camera scrolls around. + /// If false, this layer only shows up around it's home position. + /// + [DataField("tiled")] + public bool Tiled { get; set; } = true; + + /// + /// A position relative to the centre of a ParallaxControl that this parallax should be drawn at, in pixels. + /// Used for menus. + /// Note that this is ignored if the parallax layer is tiled - in that event a random pixel offset is used and slowness is applied. + /// + [DataField("controlHomePosition")] + public Vector2 ControlHomePosition { get; set; } + + /// + /// The "relative to ParallaxAnchor" starting world position for this layer. + /// Essentially, an unclamped lerp occurs between here and the eye position, with Slowness as the factor. + /// Used for in-game. + /// + [DataField("worldHomePosition")] + public Vector2 WorldHomePosition { get; set; } + + /// + /// An adjustment performed to the world position of this layer after parallax shifting. + /// Used for in-game. + /// Useful for moving around Slowness = 1.0 objects (which can't otherwise be moved from screen centre). + /// + [DataField("worldAdjustPosition")] + public Vector2 WorldAdjustPosition { get; set; } + + /// + /// Multiplier of parallax shift. + /// A slowness of 0.0f anchors this layer to the world. + /// A slowness of 1.0f anchors this layer to the camera. + /// + [DataField("slowness")] + public float Slowness { get; set; } = 0.5f; +} + diff --git a/Content.Client/Parallax/Data/ParallaxPrototype.cs b/Content.Client/Parallax/Data/ParallaxPrototype.cs new file mode 100644 index 0000000000..e57b431823 --- /dev/null +++ b/Content.Client/Parallax/Data/ParallaxPrototype.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; +using Robust.Shared.Utility; + +namespace Content.Client.Parallax.Data; + +/// +/// Prototype data for a parallax. +/// +[Prototype("parallax")] +public sealed class ParallaxPrototype : IPrototype +{ + /// + [IdDataFieldAttribute] + public string ID { get; } = default!; + + /// + /// Parallax layers. + /// + [DataField("layers")] + public List Layers { get; } = new(); + + /// + /// Parallax layers, low-quality. + /// + [DataField("layersLQ")] + public List LayersLQ { get; } = new(); + + /// + /// If low-quality layers don't exist for this parallax and high-quality should be used instead. + /// + [DataField("layersLQUseHQ")] + public bool LayersLQUseHQ { get; } = true; +} diff --git a/Content.Client/Parallax/Managers/IParallaxManager.cs b/Content.Client/Parallax/Managers/IParallaxManager.cs index 6a2aaf192e..74606112b5 100644 --- a/Content.Client/Parallax/Managers/IParallaxManager.cs +++ b/Content.Client/Parallax/Managers/IParallaxManager.cs @@ -1,12 +1,35 @@ using System; using Robust.Client.Graphics; +using Content.Client.Parallax; -namespace Content.Client.Parallax.Managers +namespace Content.Client.Parallax.Managers; + +public interface IParallaxManager { - public interface IParallaxManager - { - event Action? OnTextureLoaded; - Texture? ParallaxTexture { get; } - void LoadParallax(); - } + /// + /// The current parallax. + /// Changing this causes a new parallax to be loaded (eventually). + /// Do not alter until prototype manager is available. + /// Useful "csi" input for testing new parallaxes: + /// using Content.Client.Parallax.Managers; IoCManager.Resolve().ParallaxName = "test"; + /// + string ParallaxName { get; set; } + + /// + /// All WorldHomePosition values are offset by this. + /// + Vector2 ParallaxAnchor { get; set; } + + /// + /// The layers of the currently loaded parallax. + /// This will change on a whim without notification. + /// + ParallaxLayerPrepared[] ParallaxLayers { get; } + + /// + /// Used to initialize the manager. + /// Do not call until prototype manager is available. + /// + void LoadParallax(); } + diff --git a/Content.Client/Parallax/Managers/ParallaxManager.cs b/Content.Client/Parallax/Managers/ParallaxManager.cs index d3d7d07966..ffa2d0237e 100644 --- a/Content.Client/Parallax/Managers/ParallaxManager.cs +++ b/Content.Client/Parallax/Managers/ParallaxManager.cs @@ -1,12 +1,16 @@ using System; +using System.Linq; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; +using Content.Client.Parallax.Data; using Content.Shared; using Content.Shared.CCVar; using Nett; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; +using Robust.Shared.Prototypes; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.IoC; @@ -15,91 +19,115 @@ using Robust.Shared.Utility; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; -namespace Content.Client.Parallax.Managers +namespace Content.Client.Parallax.Managers; + +internal sealed class ParallaxManager : IParallaxManager { - internal sealed class ParallaxManager : IParallaxManager + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + + private string _parallaxName = ""; + public string ParallaxName { - [Dependency] private readonly IResourceCache _resourceCache = default!; - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly IConfigurationManager _configurationManager = default!; - - private static readonly ResourcePath ParallaxConfigPath = new("/parallax_config.toml"); - - // Both of these below are in the user directory. - private static readonly ResourcePath ParallaxCachedImagePath = new("/parallax_cache.png"); - private static readonly ResourcePath PreviousParallaxConfigPath = new("/parallax_config_old"); - - public event Action? OnTextureLoaded; - public Texture? ParallaxTexture { get; private set; } - - public async void LoadParallax() + get => _parallaxName; + set { - if (!_configurationManager.GetCVar(CCVars.ParallaxEnabled)) - return; - - var parallaxConfig = GetParallaxConfig(); - if (parallaxConfig == null) - return; - - var debugParallax = _configurationManager.GetCVar(CCVars.ParallaxDebug); - - if (debugParallax - || !_resourceCache.UserData.TryReadAllText(PreviousParallaxConfigPath, out var previousParallaxConfig) - || previousParallaxConfig != parallaxConfig) - { - var table = Toml.ReadString(parallaxConfig); - await UpdateCachedTexture(table, debugParallax); - - //Update the previous config - using var writer = _resourceCache.UserData.OpenWriteText(PreviousParallaxConfigPath); - writer.Write(parallaxConfig); - } - - ParallaxTexture = GetCachedTexture(); - OnTextureLoaded?.Invoke(ParallaxTexture); - } - - private async Task UpdateCachedTexture(TomlTable config, bool saveDebugLayers) - { - var debugImages = saveDebugLayers ? new List>() : null; - - var sawmill = _logManager.GetSawmill("parallax"); - // Generate the parallax in the thread pool. - using var newParallexImage = await Task.Run(() => - ParallaxGenerator.GenerateParallax(config, new Size(1920, 1080), sawmill, debugImages)); - // And load it in the main thread for safety reasons. - - // Store it and CRC so further game starts don't need to regenerate it. - using var imageStream = _resourceCache.UserData.OpenWrite(ParallaxCachedImagePath); - newParallexImage.SaveAsPng(imageStream); - - if (saveDebugLayers) - { - for (var i = 0; i < debugImages!.Count; i++) - { - var debugImage = debugImages[i]; - using var debugImageStream = _resourceCache.UserData.OpenWrite(new ResourcePath($"/parallax_debug_{i}.png")); - debugImage.SaveAsPng(debugImageStream); - } - } - } - - private Texture GetCachedTexture() - { - using var imageStream = _resourceCache.UserData.OpenRead(ParallaxCachedImagePath); - return Texture.LoadFromPNGStream(imageStream, "Parallax"); - } - - private string? GetParallaxConfig() - { - if (!_resourceCache.TryContentFileRead(ParallaxConfigPath, out var configStream)) - { - Logger.ErrorS("parallax", "Parallax config not found."); - return null; - } - - using var configReader = new StreamReader(configStream, EncodingHelpers.UTF8); - return configReader.ReadToEnd().Replace(Environment.NewLine, "\n"); + LoadParallaxByName(value); } } + + public Vector2 ParallaxAnchor { get; set; } + + private CancellationTokenSource? _presentParallaxLoadCancel; + + private ParallaxLayerPrepared[] _parallaxLayersHQ = {}; + private ParallaxLayerPrepared[] _parallaxLayersLQ = {}; + + public ParallaxLayerPrepared[] ParallaxLayers => _configurationManager.GetCVar(CCVars.ParallaxLowQuality) ? _parallaxLayersLQ : _parallaxLayersHQ; + + public async void LoadParallax() + { + await LoadParallaxByName("default"); + } + + private async Task LoadParallaxByName(string name) + { + // Update _parallaxName + if (_parallaxName == name) + { + return; + } + _parallaxName = name; + + // Cancel any existing load and setup the new cancellation token + _presentParallaxLoadCancel?.Cancel(); + _presentParallaxLoadCancel = new CancellationTokenSource(); + var cancel = _presentParallaxLoadCancel.Token; + + // Empty parallax name = no layers (this is so that the initial "" parallax name is consistent) + if (_parallaxName == "") + { + _parallaxLayersHQ = _parallaxLayersLQ = new ParallaxLayerPrepared[] {}; + return; + } + + // Begin (for real) + Logger.InfoS("parallax", $"Loading parallax {name}"); + + try + { + var parallaxPrototype = _prototypeManager.Index(name); + + ParallaxLayerPrepared[] hq; + ParallaxLayerPrepared[] lq; + + if (parallaxPrototype.LayersLQUseHQ) + { + lq = hq = await LoadParallaxLayers(parallaxPrototype.Layers, cancel); + } + else + { + var results = await Task.WhenAll( + LoadParallaxLayers(parallaxPrototype.Layers, cancel), + LoadParallaxLayers(parallaxPrototype.LayersLQ, cancel) + ); + hq = results[0]; + lq = results[1]; + } + + // Still keeping this check just in case. + if (_parallaxName == name) + { + _parallaxLayersHQ = hq; + _parallaxLayersLQ = lq; + Logger.InfoS("parallax", $"Loaded parallax {name}"); + } + } + catch (Exception ex) + { + Logger.ErrorS("parallax", $"Failed to loaded parallax {name}: {ex}"); + } + } + + private async Task LoadParallaxLayers(List layersIn, CancellationToken cancel = default) + { + // Because this is async, make sure it doesn't change (prototype reloads could muck this up) + // Since the tasks aren't awaited until the end, this should be fine + var tasks = new Task[layersIn.Count]; + for (var i = 0; i < layersIn.Count; i++) + { + tasks[i] = LoadParallaxLayer(layersIn[i], cancel); + } + return await Task.WhenAll(tasks); + } + + private async Task LoadParallaxLayer(ParallaxLayerConfig config, CancellationToken cancel = default) + { + return new ParallaxLayerPrepared() + { + Texture = await config.Texture.GenerateTexture(cancel), + Config = config + }; + } } + diff --git a/Content.Client/Parallax/ParallaxControl.cs b/Content.Client/Parallax/ParallaxControl.cs index b02cb661af..8833dfb58c 100644 --- a/Content.Client/Parallax/ParallaxControl.cs +++ b/Content.Client/Parallax/ParallaxControl.cs @@ -6,42 +6,60 @@ using Robust.Shared.Maths; using Robust.Shared.Random; using Robust.Shared.ViewVariables; -namespace Content.Client.Parallax +namespace Content.Client.Parallax; + +/// +/// Renders the parallax background as a UI control. +/// +public sealed class ParallaxControl : Control { - /// - /// Renders the parallax background as a UI control. - /// - public sealed class ParallaxControl : Control + [Dependency] private readonly IParallaxManager _parallaxManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + [ViewVariables(VVAccess.ReadWrite)] public Vector2i Offset { get; set; } + + public ParallaxControl() { - [Dependency] private readonly IParallaxManager _parallaxManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; + IoCManager.InjectDependencies(this); - [ViewVariables(VVAccess.ReadWrite)] public Vector2i Offset { get; set; } + Offset = (_random.Next(0, 1000), _random.Next(0, 1000)); + RectClipContent = true; + } - public ParallaxControl() + protected override void Draw(DrawingHandleScreen handle) + { + foreach (var layer in _parallaxManager.ParallaxLayers) { - IoCManager.InjectDependencies(this); - - Offset = (_random.Next(0, 1000), _random.Next(0, 1000)); - RectClipContent = true; - } - - protected override void Draw(DrawingHandleScreen handle) - { - var tex = _parallaxManager.ParallaxTexture; - if (tex == null) - return; - - var size = tex.Size; + var tex = layer.Texture; + var texSize = tex.Size * layer.Config.Scale.Floored(); var ourSize = PixelSize; - for (var x = -size.X + Offset.X; x < ourSize.X; x += size.X) + if (layer.Config.Tiled) { - for (var y = -size.Y + Offset.Y; y < ourSize.Y; y += size.Y) + // 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) { - handle.DrawTexture(tex, (x, y)); + for (var y = -scaledOffset.Y; y < ourSize.Y; y += texSize.Y) + { + handle.DrawTextureRect(tex, UIBox2.FromDimensions((x, y), texSize)); + } } } + else + { + var origin = ((ourSize - texSize) / 2) + layer.Config.ControlHomePosition; + handle.DrawTextureRect(tex, UIBox2.FromDimensions(origin, texSize)); + } } } } + diff --git a/Content.Client/Parallax/ParallaxGenerator.cs b/Content.Client/Parallax/ParallaxGenerator.cs index ddf82f7115..1998ff9f4e 100644 --- a/Content.Client/Parallax/ParallaxGenerator.cs +++ b/Content.Client/Parallax/ParallaxGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -18,7 +19,7 @@ namespace Content.Client.Parallax { private readonly List Layers = new(); - public static Image GenerateParallax(TomlTable config, Size size, ISawmill sawmill, List>? debugLayerDump) + public static Image GenerateParallax(TomlTable config, Size size, ISawmill sawmill, List>? debugLayerDump, CancellationToken cancel = default) { sawmill.Debug("Generating parallax!"); var generator = new ParallaxGenerator(); @@ -27,10 +28,11 @@ namespace Content.Client.Parallax sawmill.Debug("Timing start!"); var sw = new Stopwatch(); sw.Start(); - var image = new Image(Configuration.Default, size.Width, size.Height, new Rgba32(0, 0, 0, 255)); + var image = new Image(Configuration.Default, size.Width, size.Height, new Rgba32(0, 0, 0, 0)); var count = 0; foreach (var layer in generator.Layers) { + cancel.ThrowIfCancellationRequested(); layer.Apply(image); debugLayerDump?.Add(image.Clone()); sawmill.Debug("Layer {0} done!", count++); @@ -48,6 +50,16 @@ namespace Content.Client.Parallax { switch (((TomlValue) layerArray.Get("type")).Value) { + case "clear": + var layerClear = new LayerClear(layerArray); + Layers.Add(layerClear); + break; + + case "toalpha": + var layerToAlpha = new LayerToAlpha(layerArray); + Layers.Add(layerToAlpha); + break; + case "noise": var layerNoise = new LayerNoise(layerArray); Layers.Add(layerNoise); @@ -69,6 +81,52 @@ namespace Content.Client.Parallax public abstract void Apply(Image bitmap); } + private abstract class LayerConversion : Layer + { + public abstract Color ConvertColor(Color input); + + public override void Apply(Image bitmap) + { + var span = bitmap.GetPixelSpan(); + + for (var y = 0; y < bitmap.Height; y++) + { + for (var x = 0; x < bitmap.Width; x++) + { + var i = y * bitmap.Width + x; + span[i] = ConvertColor(span[i].ConvertImgSharp()).ConvertImgSharp(); + } + } + } + } + + private sealed class LayerClear : LayerConversion + { + private readonly Color Color = Color.Black; + + public LayerClear(TomlTable table) + { + if (table.TryGetValue("color", out var tomlObject)) + { + Color = Color.FromHex(((TomlValue) tomlObject).Value); + } + } + + public override Color ConvertColor(Color input) => Color; + } + + private sealed class LayerToAlpha : LayerConversion + { + public LayerToAlpha(TomlTable table) + { + } + + public override Color ConvertColor(Color input) + { + return new Color(input.R, input.G, input.B, MathF.Min(input.R + input.G + input.B, 1.0f)); + } + } + private sealed class LayerNoise : Layer { private readonly Color InnerColor = Color.White; diff --git a/Content.Client/Parallax/ParallaxLayerPrepared.cs b/Content.Client/Parallax/ParallaxLayerPrepared.cs new file mode 100644 index 0000000000..4bd186033a --- /dev/null +++ b/Content.Client/Parallax/ParallaxLayerPrepared.cs @@ -0,0 +1,22 @@ +using System; +using Robust.Client.Graphics; +using Content.Client.Parallax.Data; + +namespace Content.Client.Parallax; + +/// +/// A 'prepared' (i.e. texture loaded and ready to use) parallax layer. +/// +public struct ParallaxLayerPrepared +{ + /// + /// The loaded texture for this layer. + /// + public Texture Texture { get; set; } + + /// + /// The configuration for this layer. + /// + public ParallaxLayerConfig Config { get; set; } +} + diff --git a/Content.Client/Parallax/ParallaxOverlay.cs b/Content.Client/Parallax/ParallaxOverlay.cs index 93d5e4e2db..89b2719426 100644 --- a/Content.Client/Parallax/ParallaxOverlay.cs +++ b/Content.Client/Parallax/ParallaxOverlay.cs @@ -1,72 +1,95 @@ using System; using Content.Client.Parallax.Managers; +using Content.Shared.CCVar; using Robust.Client.Graphics; +using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Prototypes; -namespace Content.Client.Parallax +namespace Content.Client.Parallax; + +public sealed class ParallaxOverlay : Overlay { - public sealed class ParallaxOverlay : Overlay + [Dependency] private readonly IParallaxManager _parallaxManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld; + private readonly ShaderInstance _shader; + + public ParallaxOverlay() { - [Dependency] private readonly IParallaxManager _parallaxManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + IoCManager.InjectDependencies(this); + _shader = _prototypeManager.Index("unshaded").Instance(); + } - private const float Slowness = 0.5f; - - private Texture? _parallaxTexture; - - public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld; - private readonly ShaderInstance _shader; - - public ParallaxOverlay() + protected override void Draw(in OverlayDrawArgs args) + { + if (args.Viewport.Eye == null) { - IoCManager.InjectDependencies(this); - _shader = _prototypeManager.Index("unshaded").Instance(); + return; + } - if (_parallaxManager.ParallaxTexture == null) + if (!_configurationManager.GetCVar(CCVars.ParallaxEnabled)) + { + return; + } + + var screenHandle = args.WorldHandle; + screenHandle.UseShader(_shader); + + foreach (var layer in _parallaxManager.ParallaxLayers) + { + var tex = layer.Texture; + + // Size of the texture in world units. + var size = (tex.Size / (float) EyeManager.PixelsPerMeter) * layer.Config.Scale; + + // The "home" position is the effective origin of this layer. + // Parallax shifting is relative to the home, and shifts away from the home and towards the Eye centre. + // The effects of this are such that a slowness of 1 anchors the layer to the centre of the screen, while a slowness of 0 anchors the layer to the world. + // (For values 0.0 to 1.0 this is in effect a lerp, but it's deliberately unclamped.) + // The ParallaxAnchor adapts the parallax for station positioning and possibly map-specific tweaks. + var home = layer.Config.WorldHomePosition + _parallaxManager.ParallaxAnchor; + + // Origin - start with the parallax shift itself. + var originBL = (args.Viewport.Eye.Position.Position - home) * layer.Config.Slowness; + + // Place at the home. + originBL += home; + + // Adjust. + originBL += layer.Config.WorldAdjustPosition; + + // Centre the image. + originBL -= size / 2; + + if (layer.Config.Tiled) { - _parallaxManager.OnTextureLoaded += texture => _parallaxTexture = texture; + // Remove offset so we can floor. + var flooredBL = args.WorldAABB.BottomLeft - originBL; + + // Floor to background size. + flooredBL = (flooredBL / size).Floored() * size; + + // Re-offset. + flooredBL += originBL; + + for (var x = flooredBL.X; x < args.WorldAABB.Right; x += size.X) + { + for (var y = flooredBL.Y; y < args.WorldAABB.Top; y += size.Y) + { + screenHandle.DrawTextureRect(tex, Box2.FromDimensions((x, y), size)); + } + } } else { - _parallaxTexture = _parallaxManager.ParallaxTexture; - } - } - - protected override void Draw(in OverlayDrawArgs args) - { - if (_parallaxTexture == null || args.Viewport.Eye == null) - { - return; - } - - var screenHandle = args.WorldHandle; - screenHandle.UseShader(_shader); - - var (sizeX, sizeY) = _parallaxTexture.Size / (float) EyeManager.PixelsPerMeter; - var (posX, posY) = args.Viewport.Eye.Position; - var o = new Vector2(posX * Slowness, posY * Slowness); - - // Remove offset so we can floor. - var (l, b) = args.WorldAABB.BottomLeft - o; - - // Floor to background size. - l = sizeX * MathF.Floor(l / sizeX); - b = sizeY * MathF.Floor(b / sizeY); - - // Re-offset. - l += o.X; - b += o.Y; - - for (var x = l; x < args.WorldAABB.Right; x += sizeX) - { - for (var y = b; y < args.WorldAABB.Top; y += sizeY) - { - screenHandle.DrawTexture(_parallaxTexture, (x, y)); - } + screenHandle.DrawTextureRect(tex, Box2.FromDimensions(originBL, size)); } } } } + diff --git a/Content.IntegrationTests/DummyParallaxManager.cs b/Content.IntegrationTests/DummyParallaxManager.cs index d4312dd706..e972a04130 100644 --- a/Content.IntegrationTests/DummyParallaxManager.cs +++ b/Content.IntegrationTests/DummyParallaxManager.cs @@ -1,25 +1,20 @@ using System; using Content.Client.Parallax.Managers; +using Content.Client.Parallax; using Robust.Client.Graphics; +using Robust.Shared.Maths; namespace Content.IntegrationTests { public sealed class DummyParallaxManager : IParallaxManager { - public event Action OnTextureLoaded - { - add - { - } - remove - { - } - } - - public Texture ParallaxTexture => null; + public string ParallaxName { get; set; } = ""; + public Vector2 ParallaxAnchor { get; set; } + public ParallaxLayerPrepared[] ParallaxLayers { get; } = {}; public void LoadParallax() { + ParallaxName = "default"; } } } diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 56be6cf456..c648ce67a6 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -32,6 +32,7 @@ using Robust.Shared.ContentPack; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -51,6 +52,7 @@ namespace Content.Server.Entry new[] { "Content.Client", "Content.Shared", "Content.Shared.Database" }); var factory = IoCManager.Resolve(); + var prototypes = IoCManager.Resolve(); factory.DoAutoRegistrations(); @@ -59,6 +61,8 @@ namespace Content.Server.Entry factory.RegisterIgnore(ignoreName); } + prototypes.RegisterIgnore("parallax"); + ServerContentIoC.Register(); foreach (var callback in TestingCallbacks) diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 6c63009531..82c2d5d803 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -328,6 +328,9 @@ namespace Content.Shared.CCVar public static readonly CVarDef ParallaxDebug = CVarDef.Create("parallax.debug", false, CVar.CLIENTONLY); + public static readonly CVarDef ParallaxLowQuality = + CVarDef.Create("parallax.low_quality", false, CVar.ARCHIVE | CVar.CLIENTONLY); + /* * Physics */ diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index 11a5f5e2fc..67cdf0bcc3 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -49,6 +49,7 @@ ui-options-vp-integer-scaling-tooltip = If this option is enabled, the viewport means that black bars appear at the top/bottom of the screen or that part of the viewport is not visible. ui-options-vp-low-res = Low-resolution viewport +ui-options-parallax-low-quality = Low-quality Parallax (background) ui-options-fps-counter = Show FPS counter ## Controls menu diff --git a/Resources/Prototypes/Parallaxes/default.yml b/Resources/Prototypes/Parallaxes/default.yml new file mode 100644 index 0000000000..513ec10847 --- /dev/null +++ b/Resources/Prototypes/Parallaxes/default.yml @@ -0,0 +1,26 @@ +- type: parallax + id: default + layers: + - texture: + !type:GeneratedParallaxTextureSource + id: "hq_wizard_clouds" + configPath: "/Prototypes/Parallaxes/parallax_config_clouds.toml" + slowness: 0.5 + - texture: + !type:GeneratedParallaxTextureSource + id: "hq_wizard_stars_dim" + configPath: "/Prototypes/Parallaxes/parallax_config_stars_dim.toml" + slowness: 0.375 + - texture: + !type:GeneratedParallaxTextureSource + id: "hq_wizard_stars" + configPath: "/Prototypes/Parallaxes/parallax_config_stars.toml" + slowness: 0.25 + layersLQ: + - texture: + !type:GeneratedParallaxTextureSource + id: "" + configPath: "/Prototypes/Parallaxes/parallax_config.toml" + slowness: 0.5 + layersLQUseHQ: false + diff --git a/Resources/parallax_config.toml b/Resources/Prototypes/Parallaxes/parallax_config.toml similarity index 96% rename from Resources/parallax_config.toml rename to Resources/Prototypes/Parallaxes/parallax_config.toml index dbb74f9448..8df37315b2 100644 --- a/Resources/parallax_config.toml +++ b/Resources/Prototypes/Parallaxes/parallax_config.toml @@ -1,3 +1,8 @@ +# Clear to black. +[[layers]] +type = "clear" +color = "#000000" + # Background nebula detail. [[layers]] type = "noise" diff --git a/Resources/Prototypes/Parallaxes/parallax_config_clouds.toml b/Resources/Prototypes/Parallaxes/parallax_config_clouds.toml new file mode 100644 index 0000000000..690e1ed19f --- /dev/null +++ b/Resources/Prototypes/Parallaxes/parallax_config_clouds.toml @@ -0,0 +1,31 @@ +# Clear to black. +[[layers]] +type = "clear" +color = "#000000" + +# Background nebula detail. +[[layers]] +type = "noise" +seed = 7832 +innercolor = "#5d1fe1" +outercolor = "#230070" +noise_type = "ridged" +frequency = "4" +octaves = 8 +power = "0.25" +threshold = "0.40" + +# Mask background nebula. +[[layers]] +type = "noise" +noise_type = "fbm" +innercolor = "#000000" +outercolor = "#000000" +destfactor = "SrcAlpha" +seed = 3551 +octaves = 4 +power = "0.35" +lacunarity = "1.5" +frequency = "3" +threshold = "0.0" + diff --git a/Resources/Prototypes/Parallaxes/parallax_config_stars.toml b/Resources/Prototypes/Parallaxes/parallax_config_stars.toml new file mode 100644 index 0000000000..998c73a5f9 --- /dev/null +++ b/Resources/Prototypes/Parallaxes/parallax_config_stars.toml @@ -0,0 +1,57 @@ +# Clear to black. +[[layers]] +type = "clear" +color = "#000000" + +# Bright background nebula stars. +[[layers]] +type = "points" +closecolor = "#7E86BF" +count = 1000 +seed = 3472 +mask = true +masknoise_type = "fbm" +maskoctaves = 4 +maskpersistence = "0.5" +maskpower = "0.35" +masklacunarity = "1.5" +maskfrequency = "3" +maskthreshold = "0.37" +maskseed = 3551 + +# Bright background nebula stars, dim edge. +[[layers]] +type = "points" +closecolor = "#3D415C" +pointsize = 2 +count = 1000 +seed = 3472 +mask = true +masknoise_type = "fbm" +maskoctaves = 4 +maskpersistence = "0.5" +maskpower = "0.35" +masklacunarity = "1.5" +maskfrequency = "3" +maskthreshold = "0.37" +maskseed = 3551 + +# Couple of odd bright yellow-ish stars. +[[layers]] +type = "points" +closecolor = "#FFD363" +count = 30 +seed = 6454 + +# And their dim edge. +[[layers]] +type = "points" +closecolor = "#43371A" +pointsize = 2 +count = 30 +seed = 6454 + +# Colour-to-alpha. +[[layers]] +type = "toalpha" + diff --git a/Resources/Prototypes/Parallaxes/parallax_config_stars_dim.toml b/Resources/Prototypes/Parallaxes/parallax_config_stars_dim.toml new file mode 100644 index 0000000000..b8dc226778 --- /dev/null +++ b/Resources/Prototypes/Parallaxes/parallax_config_stars_dim.toml @@ -0,0 +1,25 @@ +# Clear to black. +[[layers]] +type = "clear" +color = "#000000" + +# Dim background nebula stars. +[[layers]] +type = "points" +seed = 3909 +closecolor = "#4B5072" +count = 1500 +mask = true +masknoise_type = "fbm" +maskoctaves = 4 +maskpersistence = "0.5" +maskpower = "0.35" +masklacunarity = "1.5" +maskfrequency = "3" +maskthreshold = "0.0" +maskseed = 3551 + +# Colour-to-alpha. +[[layers]] +type = "toalpha" + diff --git a/Resources/Prototypes/Parallaxes/test.yml b/Resources/Prototypes/Parallaxes/test.yml new file mode 100644 index 0000000000..b38182849f --- /dev/null +++ b/Resources/Prototypes/Parallaxes/test.yml @@ -0,0 +1,54 @@ +# looks like the AME became the universe again. +# someone call that guy who went and became the universe last time. +# using Content.Client.Parallax.Managers; IoCManager.Resolve().ParallaxName = "test"; + +- type: parallax + id: test + layers: + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Decals/dirty.rsi/damaged.png" + slowness: 0.5 + scale: "2, 2" + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Decals/dirty.rsi/rust.png" + slowness: 3.0 + scale: "3, 3" + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Objects/Power/AME/ame_part.rsi/box.png" + slowness: 0.995 + tiled: false + controlHomePosition: "-128, -128" + worldHomePosition: "0, 0" + worldAdjustPosition: "4, 4" + scale: "8, 8" + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Objects/Power/AME/ame_jar.rsi/jar.png" + slowness: 0.0 + tiled: false + scale: "2, 2" + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Objects/Power/AME/ame_jar.rsi/jar.png" + slowness: 0.125 + tiled: false + scale: "1, 1" + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Objects/Power/AME/ame_part.rsi/box.png" + slowness: 0.0 + tiled: false + controlHomePosition: "0, 32" + worldHomePosition: "0, 1" + scale: "2, 2" + layersLQ: + - texture: + !type:ImageParallaxTextureSource + path: "/Textures/Decals/dirty.rsi/rust.png" + slowness: 3.0 + scale: "3, 3" + layersLQUseHQ: false +