diff --git a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs index d7894265c8..ad26436946 100644 --- a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs @@ -19,7 +19,6 @@ namespace Content.Client.Atmos.EntitySystems [Dependency] private readonly SharedTransformSystem _xformSys = default!; private GasTileOverlay _overlay = default!; - private GasTileHeatOverlay _heatOverlay = default!; public override void Initialize() { @@ -29,16 +28,12 @@ namespace Content.Client.Atmos.EntitySystems _overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys); _overlayMan.AddOverlay(_overlay); - - _heatOverlay = new GasTileHeatOverlay(); - _overlayMan.AddOverlay(_heatOverlay); } public override void Shutdown() { base.Shutdown(); _overlayMan.RemoveOverlay(); - _overlayMan.RemoveOverlay(); } private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args) diff --git a/Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs b/Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs deleted file mode 100644 index 36f0a065c1..0000000000 --- a/Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System.Numerics; -using Content.Shared.Atmos; -using Content.Shared.Atmos.Components; -using Content.Client.Atmos.EntitySystems; -using Content.Shared.CCVar; -using Robust.Client.Graphics; -using Robust.Shared.Configuration; -using Robust.Shared.Enums; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Prototypes; - -namespace Content.Client.Atmos.Overlays; - -public sealed class GasTileHeatOverlay : Overlay -{ - public override bool RequestScreenTexture { get; set; } = true; - private static readonly ProtoId UnshadedShader = "unshaded"; - private static readonly ProtoId HeatOverlayShader = "Heat"; - - [Dependency] private readonly IEntityManager _entManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly IClyde _clyde = default!; - [Dependency] private readonly IConfigurationManager _configManager = default!; - // We can't resolve this immediately, because it's an entitysystem, but we will attempt to resolve and cache this - // once we begin to draw. - private GasTileOverlaySystem? _gasTileOverlay; - private readonly SharedTransformSystem _xformSys; - - private IRenderTexture? _heatTarget; - private IRenderTexture? _heatBlurTarget; - - public override OverlaySpace Space => OverlaySpace.WorldSpace; - private readonly ShaderInstance _shader; - - public GasTileHeatOverlay() - { - IoCManager.InjectDependencies(this); - _xformSys = _entManager.System(); - - _shader = _proto.Index(HeatOverlayShader).InstanceUnique(); - - _configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true); - - } - - private void SetReducedMotion(bool reducedMotion) - { - _shader.SetParameter("strength_scale", reducedMotion ? 0.5f : 1f); - _shader.SetParameter("speed_scale", reducedMotion ? 0.25f : 1f); - } - - protected override bool BeforeDraw(in OverlayDrawArgs args) - { - if (args.MapId == MapId.Nullspace) - return false; - - // If we haven't resolved this yet, give it a try or bail - _gasTileOverlay ??= _entManager.System(); - - if (_gasTileOverlay == null) - return false; - - var target = args.Viewport.RenderTarget; - - // Probably the resolution of the game window changed, remake the textures. - if (_heatTarget?.Texture.Size != target.Size) - { - _heatTarget?.Dispose(); - _heatTarget = _clyde.CreateRenderTarget( - target.Size, - new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), - name: nameof(GasTileHeatOverlay)); - } - if (_heatBlurTarget?.Texture.Size != target.Size) - { - _heatBlurTarget?.Dispose(); - _heatBlurTarget = _clyde.CreateRenderTarget( - target.Size, - new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), - name: $"{nameof(GasTileHeatOverlay)}-blur"); - } - - var overlayQuery = _entManager.GetEntityQuery(); - - args.WorldHandle.UseShader(_proto.Index(UnshadedShader).Instance()); - - var mapId = args.MapId; - var worldAABB = args.WorldAABB; - var worldBounds = args.WorldBounds; - var worldHandle = args.WorldHandle; - var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix(); - - // If there is no distortion after checking all visible tiles, we can bail early - var anyDistortion = false; - - // We're rendering in the context of the heat target texture, which will encode data as to where and how strong - // the heat distortion will be - args.WorldHandle.RenderInRenderTarget(_heatTarget, - () => - { - List> grids = new(); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref grids); - foreach (var grid in grids) - { - if (!overlayQuery.TryGetComponent(grid.Owner, out var comp)) - continue; - - var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner); - var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal; - - if (!Matrix3x2.Invert(gridEntToViewportLocal, out var viewportLocalToGridEnt)) - continue; - - var uvToUi = Matrix3Helpers.CreateScale(_heatTarget.Size.X, -_heatTarget.Size.Y); - var uvToGridEnt = uvToUi * viewportLocalToGridEnt; - - // Because we want the actual distortion to be calculated based on the grid coordinates*, we need - // to pass a matrix transformation to go from the viewport coordinates to grid coordinates. - // * (why? because otherwise the effect would shimmer like crazy as you moved around, think - // moving a piece of warped glass above a picture instead of placing the warped glass on the - // paper and moving them together) - _shader.SetParameter("grid_ent_from_viewport_local", uvToGridEnt); - - // Draw commands (like DrawRect) will be using grid coordinates from here - worldHandle.SetTransform(gridEntToViewportLocal); - - // We only care about tiles that fit in these bounds - var floatBounds = worldToViewportLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize); - var localBounds = new Box2i( - (int)MathF.Floor(floatBounds.Left), - (int)MathF.Floor(floatBounds.Bottom), - (int)MathF.Ceiling(floatBounds.Right), - (int)MathF.Ceiling(floatBounds.Top)); - - // for each tile and its gas ---> - foreach (var chunk in comp.Chunks.Values) - { - var enumerator = new GasChunkEnumerator(chunk); - - while (enumerator.MoveNext(out var tileGas)) - { - // ---> - // Check and make sure the tile is within the viewport/screen - var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y); - if (!localBounds.Contains(tilePosition)) - continue; - - // Get the distortion strength from the temperature and bail if it's not hot enough - var strength = _gasTileOverlay.GetHeatDistortionStrength(tileGas.Temperature); - if (strength <= 0f) - continue; - - anyDistortion = true; - // Encode the strength in the red channel, then 1.0 alpha if it's an active tile. - // BlurRenderTarget will then apply a blur around the edge, but we don't want it to bleed - // past the tile. - // So we use this alpha channel to chop the lower alpha values off in the shader to fit a - // fit mask back into the tile. - worldHandle.DrawRect( - Box2.CenteredAround(tilePosition + new Vector2(0.5f, 0.5f), grid.Comp.TileSizeVector), - new Color(strength,0f, 0f, strength > 0f ? 1.0f : 0f)); - } - } - } - }, - // This clears the buffer to all zero first... - new Color(0, 0, 0, 0)); - - // no distortion, no need to render - if (!anyDistortion) - { - // Return the draw handle to normal settings - args.WorldHandle.UseShader(null); - args.WorldHandle.SetTransform(Matrix3x2.Identity); - return false; - } - - // Clear to draw - return true; - } - - protected override void Draw(in OverlayDrawArgs args) - { - if (ScreenTexture is null || _heatTarget is null || _heatBlurTarget is null) - return; - - // Blur to soften the edges of the distortion. the lower parts of the alpha channel need to get cut off in the - // distortion shader to keep them in tile bounds. - _clyde.BlurRenderTarget(args.Viewport, _heatTarget, _heatBlurTarget, args.Viewport.Eye!, 14f); - - // Set up and render the distortion - _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); - args.WorldHandle.UseShader(_shader); - args.WorldHandle.DrawTextureRect(_heatTarget.Texture, args.WorldBounds); - - // Return the draw handle to normal settings - args.WorldHandle.UseShader(null); - args.WorldHandle.SetTransform(Matrix3x2.Identity); - } - - protected override void DisposeBehavior() - { - _heatTarget = null; - _heatBlurTarget = null; - _configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion); - base.DisposeBehavior(); - } -} diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index 52dba841d0..dfdfece979 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -1088,10 +1088,11 @@ namespace Content.Client.Lobby.UI if (Profile is null) return; var skin = _prototypeManager.Index(Profile.Species).SkinColoration; + var strategy = _prototypeManager.Index(skin).Strategy; - switch (skin) + switch (strategy.InputType) { - case HumanoidSkinColor.HumanToned: + case SkinColorationStrategyInput.Unary: { if (!Skin.Visible) { @@ -1099,39 +1100,14 @@ namespace Content.Client.Lobby.UI RgbSkinColorContainer.Visible = false; } - var color = SkinColor.HumanSkinTone((int) Skin.Value); - - Markings.CurrentSkinColor = color; - Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));// - break; - } - case HumanoidSkinColor.Hues: - { - if (!RgbSkinColorContainer.Visible) - { - Skin.Visible = false; - RgbSkinColorContainer.Visible = true; - } - - Markings.CurrentSkinColor = _rgbSkinColorSelector.Color; - Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color)); - break; - } - case HumanoidSkinColor.TintedHues: - { - if (!RgbSkinColorContainer.Visible) - { - Skin.Visible = false; - RgbSkinColorContainer.Visible = true; - } - - var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color); + var color = strategy.FromUnary(Skin.Value); Markings.CurrentSkinColor = color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); + break; } - case HumanoidSkinColor.VoxFeathers: + case SkinColorationStrategyInput.Color: { if (!RgbSkinColorContainer.Visible) { @@ -1139,10 +1115,11 @@ namespace Content.Client.Lobby.UI RgbSkinColorContainer.Visible = true; } - var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color); + var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color); Markings.CurrentSkinColor = color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); + break; } } @@ -1321,10 +1298,11 @@ namespace Content.Client.Lobby.UI return; var skin = _prototypeManager.Index(Profile.Species).SkinColoration; + var strategy = _prototypeManager.Index(skin).Strategy; - switch (skin) + switch (strategy.InputType) { - case HumanoidSkinColor.HumanToned: + case SkinColorationStrategyInput.Unary: { if (!Skin.Visible) { @@ -1332,11 +1310,11 @@ namespace Content.Client.Lobby.UI RgbSkinColorContainer.Visible = false; } - Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor); + Skin.Value = strategy.ToUnary(Profile.Appearance.SkinColor); break; } - case HumanoidSkinColor.Hues: + case SkinColorationStrategyInput.Color: { if (!RgbSkinColorContainer.Visible) { @@ -1344,36 +1322,11 @@ namespace Content.Client.Lobby.UI RgbSkinColorContainer.Visible = true; } - // set the RGB values to the direct values otherwise - _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor; - break; - } - case HumanoidSkinColor.TintedHues: - { - if (!RgbSkinColorContainer.Visible) - { - Skin.Visible = false; - RgbSkinColorContainer.Visible = true; - } - - // set the RGB values to the direct values otherwise - _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor; - break; - } - case HumanoidSkinColor.VoxFeathers: - { - if (!RgbSkinColorContainer.Visible) - { - Skin.Visible = false; - RgbSkinColorContainer.Visible = true; - } - - _rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor); + _rgbSkinColorSelector.Color = strategy.ClosestSkinColor(Profile.Appearance.SkinColor); break; } } - } public void UpdateSpeciesGuidebookIcon() diff --git a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs index e63a57c3b6..4882e93d23 100644 --- a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs @@ -1,4 +1,6 @@ +using System.Linq; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Content.Server.Atmos.Components; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; @@ -11,6 +13,7 @@ using JetBrains.Annotations; using Microsoft.Extensions.ObjectPool; using Robust.Server.Player; using Robust.Shared; +using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -29,6 +32,7 @@ namespace Content.Server.Atmos.EntitySystems [Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!; [Robust.Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!; [Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!; + [Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _confMan = default!; [Robust.Shared.IoC.Dependency] private readonly IParallelManager _parMan = default!; [Robust.Shared.IoC.Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; [Robust.Shared.IoC.Dependency] private readonly ChunkingSystem _chunkingSys = default!; @@ -60,12 +64,6 @@ namespace Content.Server.Atmos.EntitySystems private EntityQuery _gridQuery; private EntityQuery _query; - /// - /// How much the distortion strength should change for the temperature of a tile to be dirtied. - /// The strength goes from 0.0f to 1.0f, so 0.05f gives it essentially 20 "steps" - /// - private float _heatDistortionStrengthChangeTolerance; - public override void Initialize() { base.Initialize(); @@ -87,10 +85,9 @@ namespace Content.Server.Atmos.EntitySystems }; _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; - Subs.CVar(ConfMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true); - Subs.CVar(ConfMan, CCVars.GasOverlayThresholds, UpdateThresholds, true); - Subs.CVar(ConfMan, CVars.NetPVS, OnPvsToggle, true); - Subs.CVar(ConfMan, CCVars.GasOverlayHeatThreshold, UpdateHeatThresholds, true); + Subs.CVar(_confMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true); + Subs.CVar(_confMan, CCVars.GasOverlayThresholds, UpdateThresholds, true); + Subs.CVar(_confMan, CVars.NetPVS, OnPvsToggle, true); SubscribeLocalEvent(Reset); SubscribeLocalEvent(OnStartup); @@ -140,7 +137,6 @@ namespace Content.Server.Atmos.EntitySystems private void UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue; private void UpdateThresholds(int value) => _thresholds = value; - private void UpdateHeatThresholds(float v) => _heatDistortionStrengthChangeTolerance = MathHelper.Clamp01(v); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Invalidate(Entity grid, Vector2i index) @@ -179,9 +175,7 @@ namespace Content.Server.Atmos.EntitySystems public GasOverlayData GetOverlayData(GasMixture? mixture) { - var data = new GasOverlayData(0, - new byte[VisibleGasId.Length], - mixture?.Temperature ?? Atmospherics.TCMB); + var data = new GasOverlayData(0, new byte[VisibleGasId.Length]); for (var i = 0; i < VisibleGasId.Length; i++) { @@ -221,17 +215,15 @@ namespace Content.Server.Atmos.EntitySystems } var changed = false; - var temp = tile.Hotspot.Valid ? tile.Hotspot.Temperature : tile.Air?.Temperature ?? Atmospherics.TCMB; if (oldData.Equals(default)) { changed = true; - oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length], temp); + oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length]); } - else if (oldData.FireState != tile.Hotspot.State || - CheckTemperatureTolerance(oldData.Temperature, temp, _heatDistortionStrengthChangeTolerance)) + else if (oldData.FireState != tile.Hotspot.State) { changed = true; - oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity, temp); + oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity); } if (tile is {Air: not null, NoGridTile: false}) @@ -279,20 +271,6 @@ namespace Content.Server.Atmos.EntitySystems return true; } - /// - /// This function determines whether the change in temperature is significant enough to warrant dirtying the tile data. - /// - private bool CheckTemperatureTolerance(float tempA, float tempB, float tolerance) - { - var (strengthA, strengthB) = (GetHeatDistortionStrength(tempA), GetHeatDistortionStrength(tempB)); - - return (strengthA <= 0f && strengthB > 0f) || // change to or from 0 - (strengthB <= 0f && strengthA > 0f) || - (strengthA >= 1f && strengthB < 1f) || // change to or from 1 - (strengthB >= 1f && strengthA < 1f) || - Math.Abs(strengthA - strengthB) > tolerance; // other change within tolerance - } - private void UpdateOverlayData() { // TODO parallelize? diff --git a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs index ab6e1db494..39a3ecb7bb 100644 --- a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs @@ -102,19 +102,25 @@ public sealed class SharpSystem : EntitySystem component.Butchering.Remove(args.Args.Target.Value); - if (_containerSystem.IsEntityInContainer(args.Args.Target.Value)) - { - args.Handled = true; - return; - } - var spawnEntities = EntitySpawnCollection.GetSpawns(butcher.SpawnedEntities, _robustRandom); var coords = _transform.GetMapCoordinates(args.Args.Target.Value); EntityUid popupEnt = default!; - foreach (var proto in spawnEntities) + + if (_containerSystem.TryGetContainingContainer(args.Args.Target.Value, out var container)) { - // distribute the spawned items randomly in a small radius around the origin - popupEnt = Spawn(proto, coords.Offset(_robustRandom.NextVector2(0.25f))); + foreach (var proto in spawnEntities) + { + // distribute the spawned items randomly in a small radius around the origin + popupEnt = SpawnInContainerOrDrop(proto, container.Owner, container.ID); + } + } + else + { + foreach (var proto in spawnEntities) + { + // distribute the spawned items randomly in a small radius around the origin + popupEnt = Spawn(proto, coords.Offset(_robustRandom.NextVector2(0.25f))); + } } // only show a big popup when butchering living things. diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs index 1c7da938d4..8e7dfdedaf 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs @@ -1,7 +1,5 @@ using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Prototypes; -using Content.Shared.CCVar; -using Robust.Shared.Configuration; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -10,26 +8,11 @@ namespace Content.Shared.Atmos.EntitySystems { public abstract class SharedGasTileOverlaySystem : EntitySystem { - /// - /// The temperature at which the heat distortion effect starts to be applied. - /// - private float _tempAtMinHeatDistortion; - /// - /// The temperature at which the heat distortion effect is at maximum strength. - /// - private float _tempAtMaxHeatDistortion; - /// - /// Calculated linear slope and intercept to map temperature to a heat distortion strength from 0.0 to 1.0 - /// - private float _heatDistortionSlope; - private float _heatDistortionIntercept; - public const byte ChunkSize = 8; protected float AccumulatedFrameTime; protected bool PvsEnabled; [Dependency] protected readonly IPrototypeManager ProtoMan = default!; - [Dependency] protected readonly IConfigurationManager ConfMan = default!; /// /// array of the ids of all visible gases. @@ -39,11 +22,6 @@ namespace Content.Shared.Atmos.EntitySystems public override void Initialize() { base.Initialize(); - - // Make sure the heat distortion variables are updated if the CVars change - Subs.CVar(ConfMan, CCVars.GasOverlayHeatMinimum, UpdateMinHeat, true); - Subs.CVar(ConfMan, CCVars.GasOverlayHeatMaximum, UpdateMaxHeat, true); - SubscribeLocalEvent(OnGetState); List visibleGases = new(); @@ -58,29 +36,6 @@ namespace Content.Shared.Atmos.EntitySystems VisibleGasId = visibleGases.ToArray(); } - private void UpdateMaxHeat(float val) - { - _tempAtMaxHeatDistortion = val; - UpdateHeatSlopeAndIntercept(); - } - - private void UpdateMinHeat(float val) - { - _tempAtMinHeatDistortion = val; - UpdateHeatSlopeAndIntercept(); - } - - private void UpdateHeatSlopeAndIntercept() - { - // Make sure to avoid invalid settings (min == max or min > max) - // I'm not sure if CVars can have constraints or if CVar subscribers can reject changes. - var diff = _tempAtMinHeatDistortion < _tempAtMaxHeatDistortion - ? _tempAtMaxHeatDistortion - _tempAtMinHeatDistortion - : 0.001f; - _heatDistortionSlope = 1.0f / diff; - _heatDistortionIntercept = -_tempAtMinHeatDistortion * _heatDistortionSlope; - } - private void OnGetState(EntityUid uid, GasTileOverlayComponent component, ref ComponentGetState args) { if (PvsEnabled && !args.ReplayState) @@ -117,26 +72,14 @@ namespace Content.Shared.Atmos.EntitySystems [ViewVariables] public readonly byte[] Opacity; - /// - /// This temperature is currently only used by the GasTileHeatOverlay. - /// This value will only reflect the true temperature of the gas when the temperature is between - /// and as these are the only - /// values at which the heat distortion varies. - /// Additionally, it will only update when the heat distortion strength changes by - /// . By default, this is 5%, which corresponds to - /// 20 steps from to . - /// For 325K to 1000K with 5% tolerance, then this field will dirty only if it differs by 33.75K, or 20 steps. - /// - [ViewVariables] - public readonly float Temperature; - // TODO change fire color based on temps + // But also: dont dirty on a 0.01 kelvin change in temperatures. + // Either have a temp tolerance, or map temperature -> byte levels - public GasOverlayData(byte fireState, byte[] opacity, float temperature) + public GasOverlayData(byte fireState, byte[] opacity) { FireState = fireState; Opacity = opacity; - Temperature = temperature; } public bool Equals(GasOverlayData other) @@ -156,26 +99,10 @@ namespace Content.Shared.Atmos.EntitySystems } } - // This is only checking if two datas are equal -- a different routine is used to check if the - // temperature differs enough to dirty the chunk using a much wider tolerance. - if (!MathHelper.CloseToPercent(Temperature, other.Temperature)) - return false; - return true; } } - /// - /// Calculate the heat distortion from a temperature. - /// Returns 0.0f below TempAtMinHeatDistortion and 1.0f above TempAtMaxHeatDistortion. - /// - /// - /// - public float GetHeatDistortionStrength(float temp) - { - return MathHelper.Clamp01(temp * _heatDistortionSlope + _heatDistortionIntercept); - } - [Serializable, NetSerializable] public sealed class GasOverlayUpdateEvent : EntityEventArgs { diff --git a/Content.Shared/CCVar/CCVars.Net.cs b/Content.Shared/CCVar/CCVars.Net.cs index df8dc6932d..b7465def2e 100644 --- a/Content.Shared/CCVar/CCVars.Net.cs +++ b/Content.Shared/CCVar/CCVars.Net.cs @@ -12,23 +12,4 @@ public sealed partial class CCVars public static readonly CVarDef GasOverlayThresholds = CVarDef.Create("net.gasoverlaythresholds", 20); - - public static readonly CVarDef GasOverlayHeatThreshold = - CVarDef.Create("net.gasoverlayheatthreshold", - 0.05f, - CVar.SERVER | CVar.REPLICATED, - "Threshold for sending tile temperature updates to client in percent of distortion strength," + - "from 0.0 to 1.0. Example: 0.05 = 5%, which means heat distortion will appear in 20 'steps'."); - - public static readonly CVarDef GasOverlayHeatMinimum = - CVarDef.Create("net.gasoverlayheatminimum", - 325f, - CVar.SERVER | CVar.REPLICATED, - "Temperature at which heat distortion effect will begin to apply."); - - public static readonly CVarDef GasOverlayHeatMaximum = - CVarDef.Create("net.gasoverlayheatmaximum", - 1000f, - CVar.SERVER | CVar.REPLICATED, - "Temperature at which heat distortion effect will be at maximum strength."); } diff --git a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs index 66f9108365..583341c815 100644 --- a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Numerics; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; using Robust.Shared.Prototypes; @@ -27,7 +28,7 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, public Color EyeColor { get; set; } = Color.Black; [DataField] - public Color SkinColor { get; set; } = Humanoid.SkinColor.ValidHumanSkinTone; + public Color SkinColor { get; set; } = Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f)); [DataField] public List Markings { get; set; } = new(); @@ -92,14 +93,13 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, public static HumanoidCharacterAppearance DefaultWithSpecies(string species) { - var speciesPrototype = IoCManager.Resolve().Index(species); - var skinColor = speciesPrototype.SkinColoration switch + var protoMan = IoCManager.Resolve(); + var speciesPrototype = protoMan.Index(species); + var skinColoration = protoMan.Index(speciesPrototype.SkinColoration).Strategy; + var skinColor = skinColoration.InputType switch { - HumanoidSkinColor.HumanToned => Humanoid.SkinColor.HumanSkinTone(speciesPrototype.DefaultHumanSkinTone), - HumanoidSkinColor.Hues => speciesPrototype.DefaultSkinTone, - HumanoidSkinColor.TintedHues => Humanoid.SkinColor.TintedHues(speciesPrototype.DefaultSkinTone), - HumanoidSkinColor.VoxFeathers => Humanoid.SkinColor.ClosestVoxColor(speciesPrototype.DefaultSkinTone), - _ => Humanoid.SkinColor.ValidHumanSkinTone, + SkinColorationStrategyInput.Unary => skinColoration.FromUnary(speciesPrototype.DefaultHumanSkinTone), + SkinColorationStrategyInput.Color => skinColoration.ClosestSkinColor(speciesPrototype.DefaultSkinTone), }; return new( @@ -147,23 +147,15 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, var newEyeColor = random.Pick(RealisticEyeColors); - var skinType = IoCManager.Resolve().Index(species).SkinColoration; + var protoMan = IoCManager.Resolve(); + var skinType = protoMan.Index(species).SkinColoration; + var strategy = protoMan.Index(skinType).Strategy; - var newSkinColor = new Color(random.NextFloat(1), random.NextFloat(1), random.NextFloat(1), 1); - switch (skinType) + var newSkinColor = strategy.InputType switch { - case HumanoidSkinColor.HumanToned: - newSkinColor = Humanoid.SkinColor.HumanSkinTone(random.Next(0, 101)); - break; - case HumanoidSkinColor.Hues: - break; - case HumanoidSkinColor.TintedHues: - newSkinColor = Humanoid.SkinColor.ValidTintedHuesSkinTone(newSkinColor); - break; - case HumanoidSkinColor.VoxFeathers: - newSkinColor = Humanoid.SkinColor.ProportionalVoxColor(newSkinColor); - break; - } + SkinColorationStrategyInput.Unary => strategy.FromUnary(random.NextFloat(0f, 100f)), + SkinColorationStrategyInput.Color => strategy.ClosestSkinColor(new Color(random.NextFloat(1), random.NextFloat(1), random.NextFloat(1), 1)), + }; return new HumanoidCharacterAppearance(newHairStyle, newHairColor, newFacialHairStyle, newHairColor, newEyeColor, newSkinColor, new ()); @@ -207,10 +199,8 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, markingSet = new MarkingSet(appearance.Markings, speciesProto.MarkingPoints, markingManager, proto); markingSet.EnsureValid(markingManager); - if (!Humanoid.SkinColor.VerifySkinColor(speciesProto.SkinColoration, skinColor)) - { - skinColor = Humanoid.SkinColor.ValidSkinTone(speciesProto.SkinColoration, skinColor); - } + var strategy = proto.Index(speciesProto.SkinColoration).Strategy; + skinColor = strategy.EnsureVerified(skinColor); markingSet.EnsureSpecies(species, skinColor, markingManager); markingSet.EnsureSexes(sex, markingManager); diff --git a/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs b/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs index 0c63a88d5b..a23ecdfc53 100644 --- a/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs +++ b/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs @@ -81,7 +81,7 @@ public sealed partial class SpeciesPrototype : IPrototype /// Method of skin coloration used by the species. /// [DataField(required: true)] - public HumanoidSkinColor SkinColoration { get; private set; } + public ProtoId SkinColoration { get; private set; } [DataField] public ProtoId MaleFirstNames { get; private set; } = "NamesFirstMale"; diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index 7a22c0c29e..e88b99b593 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -297,9 +297,10 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem return; } - if (verify && !SkinColor.VerifySkinColor(species.SkinColoration, skinColor)) + if (verify && _proto.Resolve(species.SkinColoration, out var index)) { - skinColor = SkinColor.ValidSkinTone(species.SkinColoration, skinColor); + var strategy = index.Strategy; + skinColor = strategy.EnsureVerified(skinColor); } humanoid.SkinColor = skinColor; diff --git a/Content.Shared/Humanoid/SkinColor.cs b/Content.Shared/Humanoid/SkinColor.cs deleted file mode 100644 index d4d52682f3..0000000000 --- a/Content.Shared/Humanoid/SkinColor.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System.Numerics; -using System.Security.Cryptography; -using Microsoft.VisualBasic.CompilerServices; - -namespace Content.Shared.Humanoid; - -public static class SkinColor -{ - public const float MaxTintedHuesSaturation = 0.1f; - public const float MinTintedHuesLightness = 0.85f; - - public const float MinHuesLightness = 0.175f; - - public const float MinFeathersHue = 29f / 360; - public const float MaxFeathersHue = 174f / 360; - public const float MinFeathersSaturation = 20f / 100; - public const float MaxFeathersSaturation = 88f / 100; - public const float MinFeathersValue = 36f / 100; - public const float MaxFeathersValue = 55f / 100; - - public static Color ValidHumanSkinTone => Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f)); - - /// - /// Turn a color into a valid tinted hue skin tone. - /// - /// The color to validate - /// Validated tinted hue skin tone - public static Color ValidTintedHuesSkinTone(Color color) - { - return TintedHues(color); - } - - /// - /// Get a human skin tone based on a scale of 0 to 100. The value is clamped between 0 and 100. - /// - /// Skin tone. Valid range is 0 to 100, inclusive. 0 is gold/yellowish, 100 is dark brown. - /// A human skin tone. - public static Color HumanSkinTone(int tone) - { - // 0 - 100, 0 being gold/yellowish and 100 being dark - // HSV based - // - // 0 - 20 changes the hue - // 20 - 100 changes the value - // 0 is 45 - 20 - 100 - // 20 is 25 - 20 - 100 - // 100 is 25 - 100 - 20 - - tone = Math.Clamp(tone, 0, 100); - - var rangeOffset = tone - 20; - - float hue = 25; - float sat = 20; - float val = 100; - - if (rangeOffset <= 0) - { - hue += Math.Abs(rangeOffset); - } - else - { - sat += rangeOffset; - val -= rangeOffset; - } - - var color = Color.FromHsv(new Vector4(hue / 360, sat / 100, val / 100, 1.0f)); - - return color; - } - - /// - /// Gets a human skin tone from a given color. - /// - /// - /// - /// - /// Does not cause an exception if the color is not originally from the human color range. - /// Instead, it will return the approximation of the skin tone value. - /// - public static float HumanSkinToneFromColor(Color color) - { - var hsv = Color.ToHsv(color); - // check for hue/value first, if hue is lower than this percentage - // and value is 1.0 - // then it'll be hue - if (Math.Clamp(hsv.X, 25f / 360f, 1) > 25f / 360f - && hsv.Z == 1.0) - { - return Math.Abs(45 - (hsv.X * 360)); - } - // otherwise it'll directly be the saturation - else - { - return hsv.Y * 100; - } - } - - /// - /// Verify if a color is in the human skin tone range. - /// - /// The color to verify - /// True if valid, false otherwise. - public static bool VerifyHumanSkinTone(Color color) - { - var colorValues = Color.ToHsv(color); - - var hue = Math.Round(colorValues.X * 360f); - var sat = Math.Round(colorValues.Y * 100f); - var val = Math.Round(colorValues.Z * 100f); - // rangeOffset makes it so that this value - // is 25 <= hue <= 45 - if (hue < 25 || hue > 45) - { - return false; - } - - // rangeOffset makes it so that these two values - // are 20 <= sat <= 100 and 20 <= val <= 100 - // where saturation increases to 100 and value decreases to 20 - if (sat < 20 || val < 20) - { - return false; - } - - return true; - } - - /// - /// Convert a color to the 'tinted hues' skin tone type. - /// - /// Color to convert - /// Tinted hue color - public static Color TintedHues(Color color) - { - var newColor = Color.ToHsl(color); - newColor.Y *= MaxTintedHuesSaturation; - newColor.Z = MathHelper.Lerp(MinTintedHuesLightness, 1f, newColor.Z); - - return Color.FromHsv(newColor); - } - - /// - /// Verify if this color is a valid tinted hue color type, or not. - /// - /// The color to verify - /// True if valid, false otherwise - public static bool VerifyTintedHues(Color color) - { - // tinted hues just ensures saturation is always .1, or 10% saturation at all times - return Color.ToHsl(color).Y <= MaxTintedHuesSaturation && Color.ToHsl(color).Z >= MinTintedHuesLightness; - } - - /// - /// Converts a Color proportionally to the allowed vox color range. - /// Will NOT preserve the specific input color even if it is within the allowed vox color range. - /// - /// Color to convert - /// Vox feather coloration - public static Color ProportionalVoxColor(Color color) - { - var newColor = Color.ToHsv(color); - - newColor.X = newColor.X * (MaxFeathersHue - MinFeathersHue) + MinFeathersHue; - newColor.Y = newColor.Y * (MaxFeathersSaturation - MinFeathersSaturation) + MinFeathersSaturation; - newColor.Z = newColor.Z * (MaxFeathersValue - MinFeathersValue) + MinFeathersValue; - - return Color.FromHsv(newColor); - } - - // /// - // /// Ensures the input Color is within the allowed vox color range. - // /// - // /// Color to convert - // /// The same Color if it was within the allowed range, or the closest matching Color otherwise - public static Color ClosestVoxColor(Color color) - { - var hsv = Color.ToHsv(color); - - hsv.X = Math.Clamp(hsv.X, MinFeathersHue, MaxFeathersHue); - hsv.Y = Math.Clamp(hsv.Y, MinFeathersSaturation, MaxFeathersSaturation); - hsv.Z = Math.Clamp(hsv.Z, MinFeathersValue, MaxFeathersValue); - - return Color.FromHsv(hsv); - } - - /// - /// Verify if this color is a valid vox feather coloration, or not. - /// - /// The color to verify - /// True if valid, false otherwise - public static bool VerifyVoxFeathers(Color color) - { - var colorHsv = Color.ToHsv(color); - - if (colorHsv.X < MinFeathersHue || colorHsv.X > MaxFeathersHue) - return false; - - if (colorHsv.Y < MinFeathersSaturation || colorHsv.Y > MaxFeathersSaturation) - return false; - - if (colorHsv.Z < MinFeathersValue || colorHsv.Z > MaxFeathersValue) - return false; - - return true; - } - - /// - /// This takes in a color, and returns a color guaranteed to be above MinHuesLightness - /// - /// - /// Either the color as-is if it's above MinHuesLightness, or the color with luminosity increased above MinHuesLightness - public static Color MakeHueValid(Color color) - { - var manipulatedColor = Color.ToHsv(color); - manipulatedColor.Z = Math.Max(manipulatedColor.Z, MinHuesLightness); - return Color.FromHsv(manipulatedColor); - } - - /// - /// Verify if this color is above a minimum luminosity - /// - /// - /// True if valid, false if not - public static bool VerifyHues(Color color) - { - return Color.ToHsv(color).Z >= MinHuesLightness; - } - - public static bool VerifySkinColor(HumanoidSkinColor type, Color color) - { - return type switch - { - HumanoidSkinColor.HumanToned => VerifyHumanSkinTone(color), - HumanoidSkinColor.TintedHues => VerifyTintedHues(color), - HumanoidSkinColor.Hues => VerifyHues(color), - HumanoidSkinColor.VoxFeathers => VerifyVoxFeathers(color), - _ => false, - }; - } - - public static Color ValidSkinTone(HumanoidSkinColor type, Color color) - { - return type switch - { - HumanoidSkinColor.HumanToned => ValidHumanSkinTone, - HumanoidSkinColor.TintedHues => ValidTintedHuesSkinTone(color), - HumanoidSkinColor.Hues => MakeHueValid(color), - HumanoidSkinColor.VoxFeathers => ClosestVoxColor(color), - _ => color - }; - } -} - -public enum HumanoidSkinColor : byte -{ - HumanToned, - Hues, - VoxFeathers, // Vox feathers are limited to a specific color range - TintedHues, //This gives a color tint to a humanoid's skin (10% saturation with full hue range). -} diff --git a/Content.Shared/Humanoid/SkinColorationPrototype.cs b/Content.Shared/Humanoid/SkinColorationPrototype.cs new file mode 100644 index 0000000000..e37265cea1 --- /dev/null +++ b/Content.Shared/Humanoid/SkinColorationPrototype.cs @@ -0,0 +1,302 @@ +using System.Numerics; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Humanoid; + +/// +/// A prototype containing a SkinColorationStrategy +/// +[Prototype] +public sealed partial class SkinColorationPrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// The skin coloration strategy specified by this prototype + /// + [DataField(required: true)] + public ISkinColorationStrategy Strategy = default!; +} + +/// +/// The type of input taken by a +/// +[Serializable, NetSerializable] +public enum SkinColorationStrategyInput +{ + /// + /// A single floating point number from 0 to 100 (inclusive) + /// + Unary, + + /// + /// A + /// + Color, +} + +/// +/// Takes in the given and returns an adjusted Color +/// +public interface ISkinColorationStrategy +{ + /// + /// The type of input expected by the implementor; callers should consult InputType before calling the methods that require a given input + /// + SkinColorationStrategyInput InputType { get; } + + /// + /// Returns whether or not the provided is within bounds of this strategy + /// + bool VerifySkinColor(Color color); + + /// + /// Returns the closest skin color that this strategy would provide to the given + /// + Color ClosestSkinColor(Color color); + + /// + /// Returns the input if it passes , otherwise returns + /// + Color EnsureVerified(Color color) + { + if (VerifySkinColor(color)) + { + return color; + } + + return ClosestSkinColor(color); + } + + /// + /// Returns a colour representation of the given unary input + /// + Color FromUnary(float unary) + { + throw new InvalidOperationException("This coloration strategy does not support unary input"); + } + + /// + /// Returns a colour representation of the given unary input + /// + float ToUnary(Color color) + { + throw new InvalidOperationException("This coloration strategy does not support unary input"); + } +} + +/// +/// Unary coloration strategy that returns human skin tones, with 0 being lightest and 100 being darkest +/// +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class HumanTonedSkinColoration : ISkinColorationStrategy +{ + [DataField] + public Color ValidHumanSkinTone = Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f)); + + public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Unary; + + public bool VerifySkinColor(Color color) + { + var colorValues = Color.ToHsv(color); + + var hue = Math.Round(colorValues.X * 360f); + var sat = Math.Round(colorValues.Y * 100f); + var val = Math.Round(colorValues.Z * 100f); + // rangeOffset makes it so that this value + // is 25 <= hue <= 45 + if (hue < 25f || hue > 45f) + { + return false; + } + + // rangeOffset makes it so that these two values + // are 20 <= sat <= 100 and 20 <= val <= 100 + // where saturation increases to 100 and value decreases to 20 + if (sat < 20f || val < 20f) + { + return false; + } + + return true; + } + + public Color ClosestSkinColor(Color color) + { + return ValidHumanSkinTone; + } + + public Color FromUnary(float color) + { + // 0 - 100, 0 being gold/yellowish and 100 being dark + // HSV based + // + // 0 - 20 changes the hue + // 20 - 100 changes the value + // 0 is 45 - 20 - 100 + // 20 is 25 - 20 - 100 + // 100 is 25 - 100 - 20 + + var tone = Math.Clamp(color, 0f, 100f); + + var rangeOffset = tone - 20f; + + var hue = 25f; + var sat = 20f; + var val = 100f; + + if (rangeOffset <= 0) + { + hue += Math.Abs(rangeOffset); + } + else + { + sat += rangeOffset; + val -= rangeOffset; + } + + return Color.FromHsv(new Vector4(hue / 360f, sat / 100f, val / 100f, 1.0f)); + } + + public float ToUnary(Color color) + { + var hsv = Color.ToHsv(color); + // check for hue/value first, if hue is lower than this percentage + // and value is 1.0 + // then it'll be hue + if (Math.Clamp(hsv.X, 25f / 360f, 1) > 25f / 360f + && hsv.Z == 1.0) + { + return Math.Abs(45 - (hsv.X * 360)); + } + // otherwise it'll directly be the saturation + else + { + return hsv.Y * 100; + } + } +} + +/// +/// Unary coloration strategy that clamps the color within the HSV colorspace +/// +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class ClampedHsvColoration : ISkinColorationStrategy +{ + /// + /// The (min, max) of the hue channel. + /// + [DataField] + public (float, float)? Hue; + + /// + /// The (min, max) of the saturation channel. + /// + [DataField] + public (float, float)? Saturation; + + /// + /// The (min, max) of the value channel. + /// + [DataField] + public (float, float)? Value; + + public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color; + + public bool VerifySkinColor(Color color) + { + var hsv = Color.ToHsv(color); + + if (Hue is (var minHue, var maxHue) && (hsv.X < minHue || hsv.X > maxHue)) + return false; + + if (Saturation is (var minSaturation, var maxSaturation) && (hsv.Y < minSaturation || hsv.Y > maxSaturation)) + return false; + + if (Value is (var minValue, var maxValue) && (hsv.Z < minValue || hsv.Z > maxValue)) + return false; + + return true; + } + + public Color ClosestSkinColor(Color color) + { + var hsv = Color.ToHsv(color); + + if (Hue is (var minHue, var maxHue)) + hsv.X = Math.Clamp(hsv.X, minHue, maxHue); + + if (Saturation is (var minSaturation, var maxSaturation)) + hsv.Y = Math.Clamp(hsv.Y, minSaturation, maxSaturation); + + if (Value is (var minValue, var maxValue)) + hsv.Z = Math.Clamp(hsv.Z, minValue, maxValue); + + return Color.FromHsv(hsv); + } +} + +/// +/// Unary coloration strategy that clamps the color within the HSL colorspace +/// +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class ClampedHslColoration : ISkinColorationStrategy +{ + /// + /// The (min, max) of the hue channel. + /// + [DataField] + public (float, float)? Hue; + + /// + /// The (min, max) of the saturation channel. + /// + [DataField] + public (float, float)? Saturation; + + /// + /// The (min, max) of the lightness channel. + /// + [DataField] + public (float, float)? Lightness; + + public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color; + + public bool VerifySkinColor(Color color) + { + var hsl = Color.ToHsl(color); + + if (Hue is (var minHue, var maxHue) && (hsl.X < minHue || hsl.X > maxHue)) + return false; + + if (Saturation is (var minSaturation, var maxSaturation) && (hsl.Y < minSaturation || hsl.Y > maxSaturation)) + return false; + + if (Lightness is (var minValue, var maxValue) && (hsl.Z < minValue || hsl.Z > maxValue)) + return false; + + return true; + } + + public Color ClosestSkinColor(Color color) + { + var hsl = Color.ToHsl(color); + + if (Hue is (var minHue, var maxHue)) + hsl.X = Math.Clamp(hsl.X, minHue, maxHue); + + if (Saturation is (var minSaturation, var maxSaturation)) + hsl.Y = Math.Clamp(hsl.Y, minSaturation, maxSaturation); + + if (Lightness is (var minValue, var maxValue)) + hsl.Z = Math.Clamp(hsl.Z, minValue, maxValue); + + return Color.FromHsl(hsl); + } +} diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index d29c3436ac..c1bb855f36 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -1303,10 +1303,17 @@ namespace Content.Shared.Interaction var ev = new AccessibleOverrideEvent(user, target); RaiseLocalEvent(user, ref ev); + RaiseLocalEvent(target, ref ev); + // If either has handled it and neither has said we can't access it then we can access it. if (ev.Handled) return ev.Accessible; + return CanAccess(user, target); + } + + public bool CanAccess(EntityUid user, EntityUid target) + { if (_containerSystem.IsInSameOrParentContainer(user, target, out _, out var container)) return true; @@ -1511,16 +1518,16 @@ namespace Content.Shared.Interaction /// /// Override event raised directed on the user to say the target is accessible. /// - /// - /// + /// Entity we're targeting [ByRefEvent] public record struct AccessibleOverrideEvent(EntityUid User, EntityUid Target) { public readonly EntityUid User = User; public readonly EntityUid Target = Target; + // We set it to true by default for easier validation later. public bool Handled; - public bool Accessible = false; + public bool Accessible; } /// diff --git a/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs b/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs index c691a973f9..cdc831cd67 100644 --- a/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs +++ b/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs @@ -32,18 +32,19 @@ namespace Content.Shared.Kitchen; /// public sealed class SharedKitchenSpikeSystem : EntitySystem { - [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; - [Dependency] private readonly SharedContainerSystem _containerSystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly ISharedAdminLogManager _logger = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!; [Dependency] private readonly ExamineSystemShared _examineSystem = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly MetaDataSystem _metaDataSystem = default!; - [Dependency] private readonly ISharedAdminLogManager _logger = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly SharedBodySystem _bodySystem = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; public override void Initialize() { @@ -76,6 +77,9 @@ public sealed class SharedKitchenSpikeSystem : EntitySystem SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); + + // Container Jank + SubscribeLocalEvent(OnAccessibleOverride); } private void OnInit(Entity ent, ref ComponentInit args) @@ -382,6 +386,21 @@ public sealed class SharedKitchenSpikeSystem : EntitySystem args.Cancel(); } + private void OnAccessibleOverride(Entity ent, ref AccessibleOverrideEvent args) + { + // Check if the entity is the target to avoid giving the hooked entity access to everything. + // If we already have access we don't need to run more code. + if (args.Accessible || args.Target != ent.Owner) + return; + + var xform = Transform(ent); + if (!_interaction.CanAccess(args.User, xform.ParentUid)) + return; + + args.Accessible = true; + args.Handled = true; + } + public override void Update(float frameTime) { base.Update(frameTime); diff --git a/Content.Shared/Physics/CollisionGroup.cs b/Content.Shared/Physics/CollisionGroup.cs index 2878302e9a..1f19184b50 100644 --- a/Content.Shared/Physics/CollisionGroup.cs +++ b/Content.Shared/Physics/CollisionGroup.cs @@ -59,9 +59,6 @@ public enum CollisionGroup TableMask = Impassable | MidImpassable, TableLayer = MidImpassable, - // Tables that SmallMobs can't go under - CounterLayer = MidImpassable | LowImpassable, - // Tabletop machines, windoors, firelocks TabletopMachineMask = Impassable | HighImpassable, // Tabletop machines diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs index d76f16c446..1a3d4c788e 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs @@ -145,18 +145,17 @@ public abstract partial class SharedStationAiSystem : EntitySystem private void OnAiAccessible(Entity ent, ref AccessibleOverrideEvent args) { + // We don't want to allow entities to access the AI just because the eye is nearby. + // Only let the AI access entities through the eye. + if (args.Accessible || args.User != ent.Owner) + return; + args.Handled = true; // Hopefully AI never needs storage - if (_containers.TryGetContainingContainer(args.Target, out var targetContainer)) - { + if (_containers.TryGetContainingContainer(args.Target, out var targetContainer) || + !_containers.IsInSameOrTransparentContainer(ent.Owner, args.Target, otherContainer: targetContainer)) return; - } - - if (!_containers.IsInSameOrTransparentContainer(args.User, args.Target, otherContainer: targetContainer)) - { - return; - } args.Accessible = true; } diff --git a/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs b/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs index e13825ea28..63cefac812 100644 --- a/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs +++ b/Content.Tests/Shared/Preferences/Humanoid/SkinTonesTest.cs @@ -9,16 +9,20 @@ public sealed class SkinTonesTest [Test] public void TestHumanSkinToneValidity() { + var strategy = new HumanTonedSkinColoration(); + for (var i = 0; i <= 100; i++) { - var color = SkinColor.HumanSkinTone(i); - Assert.That(SkinColor.VerifyHumanSkinTone(color)); + var color = strategy.FromUnary(i); + Assert.That(strategy.VerifySkinColor(color)); } } [Test] public void TestDefaultSkinToneValid() { - Assert.That(SkinColor.VerifyHumanSkinTone(SkinColor.ValidHumanSkinTone)); + var strategy = new HumanTonedSkinColoration(); + + Assert.That(strategy.VerifySkinColor(strategy.ValidHumanSkinTone)); } } diff --git a/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml b/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml index 3cbbccd3ca..27cb4d8b68 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml @@ -57,4 +57,4 @@ mask: - TableMask layer: - - CounterLayer + - TableLayer diff --git a/Resources/Prototypes/Entities/Structures/Furniture/Tables/tables.yml b/Resources/Prototypes/Entities/Structures/Furniture/Tables/tables.yml index edf653d066..fda2ac5e18 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/Tables/tables.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/Tables/tables.yml @@ -77,7 +77,7 @@ mask: - TableMask layer: - - CounterLayer + - TableLayer - type: Damageable damageContainer: StructuralInorganic damageModifierSet: Wood @@ -201,7 +201,7 @@ - type: entity id: TableReinforced - parent: CounterBase + parent: TableBase name: reinforced table description: A square piece of metal standing on four metal legs. Extra robust. components: @@ -471,7 +471,7 @@ - type: entity id: TableBrass - parent: CounterBase + parent: TableBase name: brass table description: A shiny, corrosion resistant brass table. Steampunk! components: @@ -615,7 +615,7 @@ - type: entity id: TableStone - parent: CounterBase + parent: TableBase name: stone table description: Literally the sturdiest thing you have ever seen. components: @@ -675,7 +675,7 @@ collection: FootstepCarpet - type: entity - parent: CounterBase + parent: TableBase id: TableXeno name: xeno table description: I wouldn't put the silverware on it. diff --git a/Resources/Prototypes/Entities/Structures/Holographic/projections.yml b/Resources/Prototypes/Entities/Structures/Holographic/projections.yml index 1009374cd6..4867fc84d8 100644 --- a/Resources/Prototypes/Entities/Structures/Holographic/projections.yml +++ b/Resources/Prototypes/Entities/Structures/Holographic/projections.yml @@ -105,7 +105,7 @@ mask: - TableMask layer: - - CounterLayer + - TableLayer - type: TimedDespawn lifetime: 180 - type: PointLight diff --git a/Resources/Prototypes/Entities/Structures/conveyor.yml b/Resources/Prototypes/Entities/Structures/conveyor.yml index feec88eeaf..6722181b26 100644 --- a/Resources/Prototypes/Entities/Structures/conveyor.yml +++ b/Resources/Prototypes/Entities/Structures/conveyor.yml @@ -29,7 +29,10 @@ - 0.50,0.50 - -0.50,0.50 layer: - - ConveyorMask + - Impassable + - MidImpassable + - LowImpassable + - DoorPassable hard: False - type: Conveyor - type: DeviceNetwork diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index f7c704909e..057abf0ac2 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -115,13 +115,3 @@ id: Hologram kind: source path: "/Textures/Shaders/hologram.swsl" - -- type: shader - id: Heat - kind: source - path: "/Textures/Shaders/heat.swsl" - params: - spatial_scale: 1.0 - strength_scale: 1.0 - speed_scale: 1.0 - grid_ent_from_viewport_local: 1,0,0,1,0,1 diff --git a/Resources/Prototypes/Species/skin_colorations.yml b/Resources/Prototypes/Species/skin_colorations.yml new file mode 100644 index 0000000000..f5bc658c2b --- /dev/null +++ b/Resources/Prototypes/Species/skin_colorations.yml @@ -0,0 +1,27 @@ +- type: skinColoration + id: Hues + strategy: !type:ClampedHsvColoration + value: [0.175, 1] + +- type: skinColoration + id: TintedHues + strategy: !type:ClampedHslColoration + saturation: [0, 0.1] + lightness: [0.85, 1] + +- type: skinColoration + id: VoxFeathers + strategy: !type:ClampedHsvColoration + hue: [0.081, 0.48] + saturation: [0.2, 0.8] + value: [0.36, 0.55] + +- type: skinColoration + id: HumanToned + strategy: !type:HumanTonedSkinColoration {} + +- type: skinColoration + id: VulpkaninColors + strategy: !type:ClampedHslColoration + saturation: [0.0, 0.60] + lightness: [0.2, 0.9] diff --git a/Resources/Prototypes/Species/vulpkanin.yml b/Resources/Prototypes/Species/vulpkanin.yml index 5d2b4418c8..6f8acfa73e 100644 --- a/Resources/Prototypes/Species/vulpkanin.yml +++ b/Resources/Prototypes/Species/vulpkanin.yml @@ -7,7 +7,7 @@ defaultSkinTone: "#5a3f2d" markingLimits: MobVulpkaninMarkingLimits dollPrototype: MobVulpkaninDummy - skinColoration: Hues # TODO: Introduce clamping once #39175 or a similiar PR is merged. Ideally lower max saturation to around 80% and some minimum brightness. Same for markings. + skinColoration: VulpkaninColors maleFirstNames: names_vulpkanin_male femaleFirstNames: names_vulpkanin_female lastNames: names_vulpkanin_last diff --git a/Resources/Textures/Shaders/heat.swsl b/Resources/Textures/Shaders/heat.swsl deleted file mode 100644 index 8e478f471a..0000000000 --- a/Resources/Textures/Shaders/heat.swsl +++ /dev/null @@ -1,90 +0,0 @@ -uniform sampler2D SCREEN_TEXTURE; - -// Number of frequencies to combine, can't be a parameter/uniform else it causes problems in compatibility mode -// I have no idea why -const highp int N = 32; - -uniform highp float spatial_scale; // spatial scaling of modes, higher = fine turbulence, lower = coarse turbulence -uniform highp float strength_scale; // distortion strength -uniform highp float speed_scale; // scaling factor on the speed of the animation -// Matrix to convert screen coordinates into grid coordinates -// This is to "pin" the effect to the grid, so that it does not shimmer as you move -uniform highp mat3 grid_ent_from_viewport_local; - -const highp float TWO_PI = 6.28318530718; - // This is just the default target values so that the external parameters can be normalized to 1 -const highp float strength_factor = 0.0005; -const highp float spatial_factor = 22.0; - -// 1D pseudo-random function -highp float random_1d(highp float n) { - return fract(sin(n * 12.9898) * 43758.5453); -} - -// Kolmogorov amplitude, power spectrum goes as k^(–11/6) -highp float kolAmp(highp float k) { - return pow(k, -11.0 / 6.0); -} - -void fragment() { - - highp vec2 ps = vec2(1.0/SCREEN_PIXEL_SIZE.x, 1.0/SCREEN_PIXEL_SIZE.y); - highp float aspectratio = ps.x / ps.y; - - // scale the scale factor with the number of modes just cuz it works reasonably - highp float s_scale = spatial_scale * spatial_factor / sqrt(float(N)); - - // Coordinates to use to calculate the effects, convert to grid coordinates - highp vec2 uvW = (grid_ent_from_viewport_local * vec3(UV.x, UV.y, 1.0)).xy; - // Scale the coordinates - uvW *= s_scale; - - // accumulate phase gradienta - highp vec2 grad = vec2(0.0); - - for (lowp int i = 0; i < N; i++) { - // float cast of the index - highp float fi = float(i); - - // Pick a random direction - highp float ang = random_1d(fi + 1.0) * TWO_PI; - highp vec2 dir = vec2(cos(ang), sin(ang)); - - // Pick a random spatial frequency from 0.5 to 30 - highp float k = mix(0.5, 30.0, random_1d(fi + 17.0)); - - // Pick a random speed from 0.05 to 0.20 - highp float speed = mix(3., 8., random_1d(fi + 33.0)); - - // Pick a random phase offset - highp float phi_0 = random_1d(fi + 49.0) * TWO_PI; - - // phase argument - highp float t = dot(dir, uvW) * k + TIME * speed * speed_scale + phi_0; - - // analytical gradient: ∇[sin(t)] = cos(t) * ∇t - // ∇t = k * dir * scale (scale is factored out) - grad += kolAmp(k) * cos(t) * k * dir; - } - // Spatial scaling (coarse or fine turbulence) - grad *= s_scale; - - // The texture should have been blurred using a previous operation - // We use the alpha channel to cut off the blur that bleeds outside the tile, then we rescale - // the mask back up to 0.0 to 1.0 - highp float mask = clamp((zTexture(UV).a - 0.5)*2.0, 0.00, 1.0); - - // Calculate warped UV using the turbulence gradient - // The strength of the turbulence is encoded into the red channel of TEXTURE - // Give it a little polynomial boost: https://www.wolframalpha.com/input?i=-x%5E2+%2B2x+from+0+to+1 - highp float heatStrength = zTexture(UV).r*1.0; - heatStrength = clamp(-heatStrength*heatStrength + 2.0*heatStrength, 0.0, 1.0); - highp vec2 uvDist = UV + (strength_scale * strength_factor * heatStrength * mask) * grad; - - // Apply to the texture - COLOR = texture2D(SCREEN_TEXTURE, uvDist); - - // Uncomment the following two lines to view the strength buffer directly - // COLOR.rgb = vec3(heatStrength * mask); - // COLOR.a = mask; -}