From 0d0d5a17de0a15135c46f96376faf655f807701c Mon Sep 17 00:00:00 2001 From: Milon Date: Sun, 4 Jan 2026 17:37:08 +0100 Subject: [PATCH] footprints prediction (#5111) * works * shuffle stuff around * no more fucky wuckies, fucky wuckies are GONE * maybe this will just fix my problems? * usings * thats not how flying works * review --- .../_EE/FootPrint/FootPrintsSystem.cs | 127 ------- .../_EE/FootPrint/PuddleFootPrintsSystem.cs | 57 --- Content.Shared/_DV/CCVars/DCCVars.cs | 21 ++ .../_EE/Footprint/FootPrintComponent.cs | 17 +- .../_EE/Footprint/FootPrintVisuals.cs | 34 ++ .../_EE/Footprint/FootPrintsComponent.cs | 89 +++-- .../_EE/Footprint/GridFootPrintsComponent.cs | 80 +++++ .../Footprint/PuddleFootPrintsComponent.cs | 19 +- .../Systems/FootPrintsSystem.HandleState.cs | 140 ++++++++ .../_EE/Footprint/Systems/FootPrintsSystem.cs | 330 ++++++++++++++++++ .../Systems/PuddleFootPrintsSystem.cs | 123 +++++++ .../Prototypes/Entities/Effects/puddle.yml | 45 +-- .../_DV/Entities/Effects/puddles.yml | 45 +++ 13 files changed, 866 insertions(+), 261 deletions(-) delete mode 100644 Content.Server/_EE/FootPrint/FootPrintsSystem.cs delete mode 100644 Content.Server/_EE/FootPrint/PuddleFootPrintsSystem.cs create mode 100644 Content.Shared/_EE/Footprint/GridFootPrintsComponent.cs create mode 100644 Content.Shared/_EE/Footprint/Systems/FootPrintsSystem.HandleState.cs create mode 100644 Content.Shared/_EE/Footprint/Systems/FootPrintsSystem.cs create mode 100644 Content.Shared/_EE/Footprint/Systems/PuddleFootPrintsSystem.cs diff --git a/Content.Server/_EE/FootPrint/FootPrintsSystem.cs b/Content.Server/_EE/FootPrint/FootPrintsSystem.cs deleted file mode 100644 index b8fff2a5aa..0000000000 --- a/Content.Server/_EE/FootPrint/FootPrintsSystem.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Content.Server.Atmos.Components; -using Content.Shared._EE.Flight; // DeltaV -using Content.Shared._EE.FootPrint; -using Content.Shared.Inventory; -using Content.Shared.Mobs; -using Content.Shared.Mobs.Components; -// using Content.Shared.Standing; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.Chemistry.EntitySystems; -using Robust.Shared.Map; -using Robust.Shared.Random; - -namespace Content.Server._EE.FootPrint; - -public sealed class FootPrintsSystem : EntitySystem -{ - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly InventorySystem _inventory = default!; - [Dependency] private readonly IMapManager _map = default!; - - [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV - - private EntityQuery _transformQuery; - private EntityQuery _mobThresholdQuery; - private EntityQuery _appearanceQuery; - -// private EntityQuery _layingQuery; - - public override void Initialize() - { - base.Initialize(); - - _transformQuery = GetEntityQuery(); - _mobThresholdQuery = GetEntityQuery(); - _appearanceQuery = GetEntityQuery(); -// _layingQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnStartupComponent); - SubscribeLocalEvent(OnMove); - } - - private void OnStartupComponent(EntityUid uid, FootPrintsComponent component, ComponentStartup args) - { - component.StepSize = Math.Max(0f, component.StepSize + _random.NextFloat(-0.05f, 0.05f)); - } - - private void OnMove(EntityUid uid, FootPrintsComponent component, ref MoveEvent args) - { - if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints - return; - - if (component.PrintsColor.A <= 0f - || !_transformQuery.TryComp(uid, out var transform) - || !_mobThresholdQuery.TryComp(uid, out var mobThreshHolds) - || !_map.TryFindGridAt(_transform.GetMapCoordinates((uid, transform)), out var gridUid, out _)) - return; - - var dragging = mobThreshHolds.CurrentThresholdState is MobState.Critical or MobState.Dead; - var distance = (transform.LocalPosition - component.StepPos).Length(); - var stepSize = dragging ? component.DragSize : component.StepSize; - - if (!(distance > stepSize)) - return; - - component.RightStep = !component.RightStep; - - var entity = Spawn(component.StepProtoId, CalcCoords(gridUid, component, transform, dragging)); - var footPrintComponent = EnsureComp(entity); - - footPrintComponent.PrintOwner = uid; - Dirty(entity, footPrintComponent); - - if (_appearanceQuery.TryComp(entity, out var appearance)) - { - _appearance.SetData(entity, FootPrintVisualState.State, PickState(uid, dragging), appearance); - _appearance.SetData(entity, FootPrintVisualState.Color, component.PrintsColor, appearance); - } - - if (!_transformQuery.TryComp(entity, out var stepTransform)) - return; - - stepTransform.LocalRotation = dragging - ? (transform.LocalPosition - component.StepPos).ToAngle() + Angle.FromDegrees(-90f) - : transform.LocalRotation + Angle.FromDegrees(180f); - - component.PrintsColor = component.PrintsColor.WithAlpha(Math.Max(0f, component.PrintsColor.A - component.ColorReduceAlpha)); - component.StepPos = transform.LocalPosition; - - if (!TryComp(entity, out var solutionContainer) - || !_solution.ResolveSolution((entity, solutionContainer), footPrintComponent.SolutionName, ref footPrintComponent.Solution, out var solution) - || string.IsNullOrWhiteSpace(component.ReagentToTransfer) || solution.Volume >= 1) - return; - - _solution.TryAddReagent(footPrintComponent.Solution.Value, component.ReagentToTransfer, 0.01, out _); //was 1 - } - - private EntityCoordinates CalcCoords(EntityUid uid, FootPrintsComponent component, TransformComponent transform, bool state) - { - if (state) - return new EntityCoordinates(uid, transform.LocalPosition); - - var offset = component.RightStep - ? new Angle(Angle.FromDegrees(180f) + transform.LocalRotation).RotateVec(component.OffsetPrint) - : new Angle(transform.LocalRotation).RotateVec(component.OffsetPrint); - - return new EntityCoordinates(uid, transform.LocalPosition + offset); - } - - private FootPrintVisuals PickState(EntityUid uid, bool dragging) - { - var state = FootPrintVisuals.BareFootPrint; - - if (_inventory.TryGetSlotEntity(uid, "shoes", out _)) - state = FootPrintVisuals.ShoesPrint; - - if (_inventory.TryGetSlotEntity(uid, "outerClothing", out var suit) && TryComp(suit, out _)) - state = FootPrintVisuals.SuitPrint; - - if (dragging) - state = FootPrintVisuals.Dragging; - - return state; - } -} diff --git a/Content.Server/_EE/FootPrint/PuddleFootPrintsSystem.cs b/Content.Server/_EE/FootPrint/PuddleFootPrintsSystem.cs deleted file mode 100644 index 765daa8e3d..0000000000 --- a/Content.Server/_EE/FootPrint/PuddleFootPrintsSystem.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Linq; -using Content.Shared._EE.Flight; // DeltaV -using Content.Shared._EE.FootPrint; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Fluids; -using Content.Shared.Fluids.Components; -using Robust.Shared.Physics.Events; - -namespace Content.Server._EE.FootPrint; - -public sealed class PuddleFootPrintsSystem : EntitySystem -{ - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; - [Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnStepTrigger); - } - - private void OnStepTrigger(EntityUid uid, PuddleFootPrintsComponent component, ref EndCollideEvent args) - { - if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints - return; - - if (!TryComp(uid, out var appearance) - || !TryComp(uid, out var puddle) - || !TryComp(args.OtherEntity, out var tripper) - || !TryComp(uid, out var solutionManager) - || !_solutionContainer.ResolveSolution((uid, solutionManager), puddle.SolutionName, ref puddle.Solution, out var solutions)) - return; - - var totalSolutionQuantity = solutions.Contents.Sum(sol => (float) sol.Quantity); - var waterQuantity = (from sol in solutions.Contents where sol.Reagent.Prototype == "Water" select (float) sol.Quantity).FirstOrDefault(); - - if (waterQuantity / (totalSolutionQuantity / 100f) > component.OffPercent || solutions.Contents.Count <= 0) - return; - - tripper.ReagentToTransfer = - solutions.Contents.Aggregate((l, r) => l.Quantity > r.Quantity ? l : r).Reagent.Prototype; - - if (_appearance.TryGetData(uid, PuddleVisuals.SolutionColor, out var color, appearance) - && _appearance.TryGetData(uid, PuddleVisuals.CurrentVolume, out var volume, appearance)) - AddColor((Color) color, (float) volume * component.SizeRatio, tripper); - - _solutionContainer.RemoveEachReagent(puddle.Solution.Value, 0.01); //was 1 - } - - private void AddColor(Color col, float quantity, FootPrintsComponent component) - { - component.PrintsColor = component.ColorQuantity == 0f ? col : Color.InterpolateBetween(component.PrintsColor, col, component.ColorInterpolationFactor); - component.ColorQuantity += quantity; - } -} diff --git a/Content.Shared/_DV/CCVars/DCCVars.cs b/Content.Shared/_DV/CCVars/DCCVars.cs index 165213fa67..e52735dceb 100644 --- a/Content.Shared/_DV/CCVars/DCCVars.cs +++ b/Content.Shared/_DV/CCVars/DCCVars.cs @@ -112,6 +112,27 @@ public sealed partial class DCCVars public static readonly CVarDef YearOffset = CVarDef.Create("game.current_year_offset", 550, CVar.SERVERONLY); + /* + * Footprints + */ + + /// + /// Maximum number of footprints allowed per tile. + /// Won't allow for new footprints to spawn on the tile once reached. + /// Set to 0 to disable per-tile limiting. + /// + public static readonly CVarDef MaxFootPrintsPerTile = + CVarDef.Create("footprints.max_per_tile", 2, CVar.REPLICATED); + + /// + /// Maximum total number of footprints allowed on a single grid. + /// When this limit is reached, the oldest footprint on the grid will be deleted. + /// Set to 0 to disable global limiting. + /// + public static readonly CVarDef MaxFootPrintsPerGrid = + CVarDef.Create("footprints.max_per_grid", 1000, CVar.REPLICATED); + + /* * Feedback webhook */ diff --git a/Content.Shared/_EE/Footprint/FootPrintComponent.cs b/Content.Shared/_EE/Footprint/FootPrintComponent.cs index e342275e45..88945cea1c 100644 --- a/Content.Shared/_EE/Footprint/FootPrintComponent.cs +++ b/Content.Shared/_EE/Footprint/FootPrintComponent.cs @@ -1,23 +1,30 @@ -using Content.Shared.Chemistry.Components; +using Content.Shared._EE.FootPrint.Systems; +using Content.Shared.Chemistry.Components; using Robust.Shared.GameStates; namespace Content.Shared._EE.FootPrint; /// -/// This is used for marking footsteps, handling footprint drawing. +/// Component attached to individual footprint entities spawned on the ground. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(FootPrintsSystem))] public sealed partial class FootPrintComponent : Component { /// - /// Owner (with ) of a print (this component). + /// The entity that created this footprint (must have ). /// - [AutoNetworkedField] + [DataField, AutoNetworkedField] public EntityUid PrintOwner; + /// + /// Name of the solution container for reagent residue left in the footprint. + /// [DataField] public string SolutionName = "step"; + /// + /// The solution container for this footprint's reagent residue. + /// [ViewVariables] public Entity? Solution; } diff --git a/Content.Shared/_EE/Footprint/FootPrintVisuals.cs b/Content.Shared/_EE/Footprint/FootPrintVisuals.cs index a2ca3d3191..6d31ac30d3 100644 --- a/Content.Shared/_EE/Footprint/FootPrintVisuals.cs +++ b/Content.Shared/_EE/Footprint/FootPrintVisuals.cs @@ -2,24 +2,58 @@ namespace Content.Shared._EE.FootPrint; +/// +/// Visual states for different types of footprints. +/// [Serializable, NetSerializable] public enum FootPrintVisuals : byte { + /// + /// Bare foot footprints (no shoes). + /// BareFootPrint, + + /// + /// Regular shoe footprints. + /// ShoesPrint, + + /// + /// Hardsuit footprints. + /// SuitPrint, + + /// + /// Drag marks from being pulled/dragged. + /// Dragging } +/// +/// Appearance data keys for footprint visuals. +/// [Serializable, NetSerializable] public enum FootPrintVisualState : byte { + /// + /// The current visual state (type of print). + /// State, + + /// + /// The color of the footprint. + /// Color } +/// +/// Sprite layers for footprint rendering. +/// [Serializable, NetSerializable] public enum FootPrintVisualLayers : byte { + /// + /// The footprint sprite layer. + /// Print } diff --git a/Content.Shared/_EE/Footprint/FootPrintsComponent.cs b/Content.Shared/_EE/Footprint/FootPrintsComponent.cs index 7c2da23b19..32a6830f57 100644 --- a/Content.Shared/_EE/Footprint/FootPrintsComponent.cs +++ b/Content.Shared/_EE/Footprint/FootPrintsComponent.cs @@ -1,29 +1,42 @@ using System.Numerics; +using Content.Shared._EE.FootPrint.Systems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Shared._EE.FootPrint; -[RegisterComponent] +/// +/// Component for entities that can leave footprints as they move. +/// Tracks color state, position, and configuration for footprint generation. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(FootPrintsSystem), typeof(PuddleFootPrintsSystem))] +[AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class FootPrintsComponent : Component { - [ViewVariables(VVAccess.ReadOnly), DataField] - public ResPath RsiPath = new("/Textures/_EE/Effects/footprints.rsi"); // DeltaV moved to its own space + /// + /// Path to the RSI containing footprint sprites. + /// + [DataField] + public ResPath RsiPath = new("/Textures/_EE/Effects/footprints.rsi"); - // all of those are set as a layer - [ViewVariables(VVAccess.ReadOnly), DataField] + #region Sprite State Names + + [DataField] public string LeftBarePrint = "footprint-left-bare-human"; - [ViewVariables(VVAccess.ReadOnly), DataField] + [DataField] public string RightBarePrint = "footprint-right-bare-human"; - [ViewVariables(VVAccess.ReadOnly), DataField] + [DataField] public string ShoesPrint = "footprint-shoes"; - [ViewVariables(VVAccess.ReadOnly), DataField] + [DataField] public string SuitPrint = "footprint-suit"; - [ViewVariables(VVAccess.ReadOnly), DataField] + [DataField] public string[] DraggingPrint = [ "dragging-1", @@ -32,57 +45,83 @@ public sealed partial class FootPrintsComponent : Component "dragging-4", "dragging-5", ]; - // yea, those - [ViewVariables(VVAccess.ReadOnly), DataField] + #endregion + + /// + /// Prototype ID for spawned footprint entities. + /// + [DataField] public EntProtoId StepProtoId = "Footstep"; - [ViewVariables(VVAccess.ReadOnly), DataField] + /// + /// The amount of solution to transfer with each footprint when stepping into a puddle. + /// + [DataField] + public FixedPoint2 AmountToTransfer = 0.01; + + /// + /// Current color of footprints being left. Alpha channel determines visibility. + /// + [DataField, AutoNetworkedField] public Color PrintsColor = Color.FromHex("#00000000"); /// - /// The size scaling factor for footprint steps. Must be positive. + /// The size scaling factor for footprint steps. Must be positive. /// - [DataField] + [DataField, AutoNetworkedField] public float StepSize = 0.7f; /// - /// The size scaling factor for drag marks. Must be positive. + /// The size scaling factor for drag marks. Must be positive. /// - [DataField] + [DataField, AutoNetworkedField] public float DragSize = 0.5f; /// - /// The amount of color to transfer from the source (e.g., puddle) to the footprint. + /// The total amount of color accumulated from stepping in puddles. + /// Used to determine when color should start fading. /// - [DataField] + [DataField, AutoNetworkedField] public float ColorQuantity; /// - /// The factor by which the alpha channel is reduced in subsequent footprints. + /// The factor by which the alpha channel is reduced with each footprint. + /// Higher values = faster fading. /// [DataField] public float ColorReduceAlpha = 0.1f; - [DataField] - public string? ReagentToTransfer; + /// + /// The reagent ID to transfer to footprint entities, set when stepping in puddles. + /// + [DataField, AutoNetworkedField] + public ProtoId? ReagentToTransfer; + /// + /// Offset applied to footprints perpendicular to movement direction. + /// Creates the left/right alternating pattern. + /// [DataField] public Vector2 OffsetPrint = new(0.1f, 0f); /// - /// Tracks which foot should make the next print. True for right foot, false for left. + /// Tracks which foot should make the next print. True for right foot, false for left. /// + [DataField, AutoNetworkedField] public bool RightStep = true; /// - /// The position of the last footprint in world coordinates. + /// The position of the last footprint in local coordinates. + /// Used to determine when enough distance has been traveled for the next print. /// + [DataField, AutoNetworkedField] public Vector2 StepPos = Vector2.Zero; /// - /// Controls how quickly the footprint color transitions between steps. - /// Value between 0 and 1, where higher values mean faster color changes. + /// Controls how quickly the footprint color transitions when stepping in new puddles. + /// Value between 0 and 1, where higher values mean faster color changes. /// + [DataField] public float ColorInterpolationFactor = 0.2f; } diff --git a/Content.Shared/_EE/Footprint/GridFootPrintsComponent.cs b/Content.Shared/_EE/Footprint/GridFootPrintsComponent.cs new file mode 100644 index 0000000000..75690a1769 --- /dev/null +++ b/Content.Shared/_EE/Footprint/GridFootPrintsComponent.cs @@ -0,0 +1,80 @@ +using Content.Shared._EE.FootPrint.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared._EE.FootPrint; + +/// +/// Component attached to grids to track footprints per tile. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(FootPrintsSystem))] +public sealed partial class GridFootPrintsComponent : Component +{ + /// + /// Tracks all footprints on this grid, organized by tile position. + /// NOTE: Not auto-networked - we use custom delta states instead + /// + [DataField] + public Dictionary> FootPrintsByTile = new(); + + /// + /// Total count of all footprints on this grid. + /// + [DataField] + public int TotalFootPrints; + + /// + /// Tracks which tiles have been modified since the last state send. + /// + [ViewVariables] + public HashSet DirtyTiles = new(); + + /// + /// Tracks which tiles have been completely removed since the last state send. + /// + [ViewVariables] + public HashSet RemovedTiles = new(); + + /// + /// If true, the next state will be a full state instead of delta. + /// + [ViewVariables] + public bool NeedFullState = true; +} + +/// +/// Custom component state that supports delta compression. +/// Only sends changed tiles instead of the entire dictionary. +/// +[Serializable, NetSerializable] +public sealed class GridFootPrintsComponentState( + bool fullState, + Dictionary>? modified, + HashSet? removed, + int totalFootPrints) + : IComponentState +{ + /// + /// If true, this is a full state sync. Client should replace entire dictionary. + /// If false, this is a delta - client should apply changes. + /// + public bool FullState = fullState; + + /// + /// Modified or added tiles and their footprint lists. + /// For full state: contains all tiles. + /// For delta: contains only changed tiles. + /// + public Dictionary>? Modified = modified; + + /// + /// Tiles that have been completely cleared (no footprints remaining). + /// Only relevant for delta states. + /// + public HashSet? Removed = removed; + + /// + /// Total count of footprints on the grid. + /// + public int TotalFootPrints = totalFootPrints; +} diff --git a/Content.Shared/_EE/Footprint/PuddleFootPrintsComponent.cs b/Content.Shared/_EE/Footprint/PuddleFootPrintsComponent.cs index b85fe92842..cf6f2dda92 100644 --- a/Content.Shared/_EE/Footprint/PuddleFootPrintsComponent.cs +++ b/Content.Shared/_EE/Footprint/PuddleFootPrintsComponent.cs @@ -1,11 +1,24 @@ +using Content.Shared._EE.FootPrint.Systems; +using Robust.Shared.GameStates; + namespace Content.Shared._EE.FootPrint; -[RegisterComponent] +/// +/// Component for puddles that can leave footprints when stepped on. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(PuddleFootPrintsSystem))] public sealed partial class PuddleFootPrintsComponent : Component { - [ViewVariables()] + /// + /// The ratio of the puddle's volume that determines color intensity transferred to footprints. + /// + [DataField, AutoNetworkedField] public float SizeRatio = 0.2f; - [ViewVariables()] + /// + /// Minimum percentage of water content required before the puddle will transfer footprints. + /// Prevents pure water puddles from leaving colored footprints. + /// + [DataField, AutoNetworkedField] public float OffPercent = 80f; } diff --git a/Content.Shared/_EE/Footprint/Systems/FootPrintsSystem.HandleState.cs b/Content.Shared/_EE/Footprint/Systems/FootPrintsSystem.HandleState.cs new file mode 100644 index 0000000000..db03679e9d --- /dev/null +++ b/Content.Shared/_EE/Footprint/Systems/FootPrintsSystem.HandleState.cs @@ -0,0 +1,140 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Timing; + +namespace Content.Shared._EE.FootPrint.Systems; + +public sealed partial class FootPrintsSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + + private void InitializeStateHandling() + { + SubscribeLocalEvent(OnGridFootPrintsGetState); + SubscribeLocalEvent(OnGridFootPrintsHandleState); + } + + private void OnGridFootPrintsGetState(Entity ent, ref ComponentGetState args) + { + // Determine if we should send full state or delta + // Send full state if: + // - Component explicitly needs it + // - This is the first send to this player (FromTick is very old) + // - Periodically for desync recovery + var sendFullState = ent.Comp.NeedFullState || + args.FromTick == GameTick.Zero || + args.FromTick < _timing.CurTick - + (uint)(TimeSpan.FromSeconds(30).Seconds * _timing.TickRate); + + if (sendFullState) + { + // Send complete state + var fullData = new Dictionary>(ent.Comp.FootPrintsByTile.Count); + foreach (var (tile, footprints) in ent.Comp.FootPrintsByTile) + { + // Deep copy the list to avoid reference sharing + fullData[tile] = new List(footprints); + } + + args.State = new GridFootPrintsComponentState( + fullState: true, + modified: fullData, + removed: null, + totalFootPrints: ent.Comp.TotalFootPrints + ); + + // Clear dirty tracking after full state send + ent.Comp.DirtyTiles.Clear(); + ent.Comp.RemovedTiles.Clear(); + ent.Comp.NeedFullState = false; + } + else + { + // Send delta state - only changed tiles + Dictionary>? modifiedTiles = null; + HashSet? removedTiles = null; + + if (ent.Comp.DirtyTiles.Count > 0) + { + modifiedTiles = new Dictionary>(ent.Comp.DirtyTiles.Count); + foreach (var tile in ent.Comp.DirtyTiles) + { + if (ent.Comp.FootPrintsByTile.TryGetValue(tile, out var footprints)) + { + // Deep copy the list + modifiedTiles[tile] = new List(footprints); + } + } + } + + if (ent.Comp.RemovedTiles.Count > 0) + { + removedTiles = new HashSet(ent.Comp.RemovedTiles); + } + + // Only create state if there are actual changes + if (modifiedTiles != null || removedTiles != null) + { + args.State = new GridFootPrintsComponentState( + fullState: false, + modified: modifiedTiles, + removed: removedTiles, + totalFootPrints: ent.Comp.TotalFootPrints + ); + + // Clear dirty tracking after delta send + ent.Comp.DirtyTiles.Clear(); + ent.Comp.RemovedTiles.Clear(); + } + // If no changes, args.State stays null and nothing is sent + } + } + + private void OnGridFootPrintsHandleState(Entity ent, ref ComponentHandleState args) + { + if (args.Current is not GridFootPrintsComponentState state) + return; + + if (state.FullState) + { + // Full state sync - replace entire dictionary + ent.Comp.FootPrintsByTile.Clear(); + + if (state.Modified != null) + { + foreach (var (tile, footprints) in state.Modified) + { + ent.Comp.FootPrintsByTile[tile] = new List(footprints); + } + } + } + else + { + // Delta state - apply changes + + // Apply removed tiles + if (state.Removed != null) + { + foreach (var tile in state.Removed) + { + ent.Comp.FootPrintsByTile.Remove(tile); + } + } + + // Apply modified/added tiles + if (state.Modified != null) + { + foreach (var (tile, footprints) in state.Modified) + { + ent.Comp.FootPrintsByTile[tile] = new List(footprints); + } + } + } + + // Always update total count + ent.Comp.TotalFootPrints = state.TotalFootPrints; + + // Clear any local dirty tracking on client + ent.Comp.DirtyTiles.Clear(); + ent.Comp.RemovedTiles.Clear(); + } +} diff --git a/Content.Shared/_EE/Footprint/Systems/FootPrintsSystem.cs b/Content.Shared/_EE/Footprint/Systems/FootPrintsSystem.cs new file mode 100644 index 0000000000..150d06d81d --- /dev/null +++ b/Content.Shared/_EE/Footprint/Systems/FootPrintsSystem.cs @@ -0,0 +1,330 @@ +using Content.Shared._EE.Flight; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Inventory; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Standing; +using Content.Shared.Tag; +using Robust.Shared.Map; +using Robust.Shared.Random; +using Robust.Shared.Configuration; +using Content.Shared._DV.CCVars; +using Content.Shared.Chemistry.Reagent; +using Robust.Shared.Prototypes; + +namespace Content.Shared._EE.FootPrint.Systems; + +/// +/// Handles creation of footprints as entities move. +/// +public sealed partial class FootPrintsSystem : EntitySystem +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IMapManager _map = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedFlightSystem _flight = default!; + [Dependency] private readonly SharedMapSystem _mapSystem = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly StandingStateSystem _standing = default!; + [Dependency] private readonly TagSystem _tag = default!; + + private EntityQuery _appearanceQuery; + private EntityQuery _flightQuery; + private EntityQuery _gridFootPrintsQuery; + private EntityQuery _mobThresholdQuery; + private EntityQuery _standingQuery; + private EntityQuery _transformQuery; + + private const string HardsuitTag = "Hardsuit"; + + private int _maxPerTile; + private int _maxPerGrid; + + public override void Initialize() + { + base.Initialize(); + + _appearanceQuery = GetEntityQuery(); + _flightQuery = GetEntityQuery(); + _gridFootPrintsQuery = GetEntityQuery(); + _mobThresholdQuery = GetEntityQuery(); + _standingQuery = GetEntityQuery(); + _transformQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnStartupComponent); + SubscribeLocalEvent(OnMove); + SubscribeLocalEvent(OnFootPrintRemoved); + + InitializeStateHandling(); + + // Subscribe to CVar changes + Subs.CVar(_cfg, DCCVars.MaxFootPrintsPerTile, value => _maxPerTile = value, true); + Subs.CVar(_cfg, DCCVars.MaxFootPrintsPerGrid, value => _maxPerGrid = value, true); + } + + private void OnStartupComponent(Entity ent, ref ComponentStartup args) + { + // Add slight random variation to step size for more natural-looking prints + ent.Comp.StepSize = Math.Max(0f, ent.Comp.StepSize + _random.NextFloat(-0.05f, 0.05f)); + DirtyField(ent.AsNullable(), nameof(FootPrintsComponent.StepSize)); + } + + private void OnMove(Entity ent, ref MoveEvent args) + { + // Don't create footprints if flying + if (_flightQuery.TryComp(ent, out var flight) && _flight.IsFlying((ent, flight))) + return; + + // Don't create footprints if color is fully transparent + if (ent.Comp.PrintsColor.A <= 0f) + return; + + if (!_transformQuery.TryComp(ent, out var transform)) + return; + + if (!_mobThresholdQuery.TryComp(ent, out var mobThresholds)) + return; + + // Check if entity is being dragged (critical or dead) + var isCrit = mobThresholds.CurrentThresholdState is MobState.Critical or MobState.Dead; + var dragging = isCrit || _standingQuery.TryComp(ent, out var standingComp) && _standing.IsDown((ent, standingComp)); + + // Calculate distance traveled since last footprint + var distance = (transform.LocalPosition - ent.Comp.StepPos).Length(); + var stepSize = dragging ? ent.Comp.DragSize : ent.Comp.StepSize; + + // Not enough distance traveled yet - MOST COMMON EARLY RETURN + if (distance <= stepSize) + return; + + // Need to be on a grid to leave footprints + if (!_map.TryFindGridAt(_transform.GetMapCoordinates((ent, transform)), out var gridUid, out var grid)) + return; + + // Calculate spawn coordinates + var coords = CalcCoords(gridUid, ent.Comp, transform, dragging); + + // Get the tile position for limit checking + if (!_mapSystem.TryGetTileRef(gridUid, grid, coords, out var tileRef)) + return; + + var tilePos = tileRef.GridIndices; + + // Check per-tile limit BEFORE spawning + // Use TryComp instead of EnsureComp to avoid adding component in hot path + if (_maxPerTile > 0 && _gridFootPrintsQuery.TryComp(gridUid, out var gridFootPrints)) + { + if (gridFootPrints.FootPrintsByTile.TryGetValue(tilePos, out var existingFootPrints) + && existingFootPrints.Count >= _maxPerTile) + { + // Tile is full, don't spawn + return; + } + } + + // Alternate feet + ent.Comp.RightStep = !ent.Comp.RightStep; + DirtyField(ent.AsNullable(), nameof(FootPrintsComponent.RightStep)); + + // Spawn the footprint entity + var entity = EntityManager.PredictedSpawnAtPosition(ent.Comp.StepProtoId.Id, coords); + + // Update appearance with current color and state + if (_appearanceQuery.TryComp(entity, out var appearance)) + { + _appearance.SetData(entity, FootPrintVisualState.State, PickState(ent, dragging), appearance); + _appearance.SetData(entity, FootPrintVisualState.Color, ent.Comp.PrintsColor, appearance); + } + + // Set rotation + if (_transformQuery.TryComp(entity, out var stepTransform)) + { + stepTransform.LocalRotation = dragging + ? (transform.LocalPosition - ent.Comp.StepPos).ToAngle() + Angle.FromDegrees(-90f) + : transform.LocalRotation + Angle.FromDegrees(180f); + } + + if (!TryComp(entity, out var footPrintComponent)) + return; + + // Set the owner reference + footPrintComponent.PrintOwner = ent; + Dirty(entity, footPrintComponent); + + // Track the footprint on the grid + TrackFootPrint(gridUid, GetNetEntity(ent), tilePos); + + // Reduce color alpha for next footprint + ent.Comp.PrintsColor = ent.Comp.PrintsColor.WithAlpha( + Math.Max(0f, ent.Comp.PrintsColor.A - ent.Comp.ColorReduceAlpha)); + DirtyField(ent.AsNullable(), nameof(FootPrintsComponent.PrintsColor)); + + // Update last step position + ent.Comp.StepPos = transform.LocalPosition; + DirtyField(ent.AsNullable(), nameof(FootPrintsComponent.StepPos)); + + // Handle reagent transfer + if (ent.Comp.ReagentToTransfer is { } reagent) + { + TryTransferReagent((entity, footPrintComponent), ent, reagent); + } + } + + private void OnFootPrintRemoved(Entity ent, ref ComponentRemove args) + { + // Clean up tracking when footprint is deleted + if (!_transformQuery.TryComp(ent, out var transform)) + return; + + if (transform.GridUid == null) + return; + + UntrackFootPrint(transform.GridUid.Value, GetNetEntity(ent)); + } + + private void TrackFootPrint(EntityUid gridUid, NetEntity footPrintUid, Vector2i tile) + { + var gridFootPrints = EnsureComp(gridUid); + + // Add to tile tracking + if (!gridFootPrints.FootPrintsByTile.TryGetValue(tile, out var tileFootPrints)) + { + tileFootPrints = new List(); + gridFootPrints.FootPrintsByTile[tile] = tileFootPrints; + } + + tileFootPrints.Add(footPrintUid); + gridFootPrints.TotalFootPrints++; + + // DELTA TRACKING: Mark tile as dirty + gridFootPrints.DirtyTiles.Add(tile); + // Remove from removed set if it was there + gridFootPrints.RemovedTiles.Remove(tile); + + // Enforce global limit + if (_maxPerGrid > 0 && gridFootPrints.TotalFootPrints > _maxPerGrid) + { + RemoveOldestFootPrint(gridFootPrints); + } + + Dirty(gridUid, gridFootPrints); + } + + private void UntrackFootPrint(EntityUid gridUid, NetEntity footPrintUid) + { + if (!_gridFootPrintsQuery.TryComp(gridUid, out var gridFootPrints)) + return; + + // Find and remove from tile tracking + foreach (var (tile, footPrints) in gridFootPrints.FootPrintsByTile) + { + if (footPrints.Remove(footPrintUid)) + { + gridFootPrints.TotalFootPrints--; + + // DELTA TRACKING + if (footPrints.Count == 0) + { + // Tile is now empty + gridFootPrints.FootPrintsByTile.Remove(tile); + gridFootPrints.RemovedTiles.Add(tile); + gridFootPrints.DirtyTiles.Remove(tile); + } + else + { + // Tile still has footprints, just modified + gridFootPrints.DirtyTiles.Add(tile); + } + + Dirty(gridUid, gridFootPrints); + break; + } + } + } + + private void RemoveOldestFootPrint(GridFootPrintsComponent gridFootPrints) + { + foreach (var (tile, footPrints) in gridFootPrints.FootPrintsByTile) + { + if (footPrints.Count > 0) + { + var toRemove = footPrints[0]; + footPrints.RemoveAt(0); + QueueDel(GetEntity(toRemove)); + gridFootPrints.TotalFootPrints--; + + // DELTA TRACKING + if (footPrints.Count == 0) + { + gridFootPrints.FootPrintsByTile.Remove(tile); + gridFootPrints.RemovedTiles.Add(tile); + gridFootPrints.DirtyTiles.Remove(tile); + } + else + { + gridFootPrints.DirtyTiles.Add(tile); + } + + return; + } + } + } + + private void TryTransferReagent(Entity ent, Entity tripper, ProtoId reagentId) + { + if (!TryComp(ent, out var solutionContainer)) + return; + + if (!_solution.ResolveSolution((ent, solutionContainer), + ent.Comp.SolutionName, + ref ent.Comp.Solution, + out var solution)) + return; + + // Don't overfill + if (solution.Volume >= 1) + return; + + // Transfer a small amount of reagent + _solution.TryAddReagent(ent.Comp.Solution.Value, reagentId, tripper.Comp.AmountToTransfer, out _); + } + + private EntityCoordinates CalcCoords(EntityUid gridUid, + FootPrintsComponent component, + TransformComponent transform, + bool dragging) + { + // For dragging, place footprint at center + if (dragging) + return new EntityCoordinates(gridUid, transform.LocalPosition); + + // For walking, offset left or right based on which foot + var offset = component.RightStep + ? new Angle(Angle.FromDegrees(180f) + transform.LocalRotation).RotateVec(component.OffsetPrint) + : new Angle(transform.LocalRotation).RotateVec(component.OffsetPrint); + + return new EntityCoordinates(gridUid, transform.LocalPosition + offset); + } + + private FootPrintVisuals PickState(Entity ent, bool dragging) + { + // If dragging, always use drag marks + if (dragging) + return FootPrintVisuals.Dragging; + + // Check for shoes + if (_inventory.TryGetSlotEntity(ent, "shoes", out _)) + return FootPrintVisuals.ShoesPrint; + + // Check for hardsuit + if (_inventory.TryGetSlotEntity(ent, "outerClothing", out var suit) && _tag.HasTag(suit.Value, HardsuitTag)) + return FootPrintVisuals.SuitPrint; + + // Default to bare feet + return FootPrintVisuals.BareFootPrint; + } +} diff --git a/Content.Shared/_EE/Footprint/Systems/PuddleFootPrintsSystem.cs b/Content.Shared/_EE/Footprint/Systems/PuddleFootPrintsSystem.cs new file mode 100644 index 0000000000..6189f48fda --- /dev/null +++ b/Content.Shared/_EE/Footprint/Systems/PuddleFootPrintsSystem.cs @@ -0,0 +1,123 @@ +using System.Linq; +using Content.Shared._EE.Flight; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Fluids; +using Content.Shared.Fluids.Components; +using Robust.Shared.Physics.Events; +using Robust.Shared.Prototypes; + +namespace Content.Shared._EE.FootPrint.Systems; + +/// +/// Handles transferring puddle colors and reagents to entities with footprints when they step in puddles. +/// +public sealed class PuddleFootPrintsSystem : EntitySystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedFlightSystem _flight = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + private EntityQuery _flightQuery; + + private static readonly ProtoId WaterPrototype = "Water"; + + public override void Initialize() + { + base.Initialize(); + + _flightQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnEndCollide); + } + + private void OnEndCollide(Entity ent, ref EndCollideEvent args) + { + var tripper = args.OtherEntity; + + // Don't process if the tripper is flying + if (_flightQuery.TryComp(ent, out var flight) && _flight.IsFlying((ent, flight))) + return; + + // Only process entities that can leave footprints + if (!TryComp(tripper, out var footPrints)) + return; + + // Get puddle appearance and solution data + if (!TryComp(ent, out var appearance)) + return; + + if (!TryComp(ent, out var puddleComp)) + return; + + if (!TryComp(ent, out var solutionManager)) + return; + + if (!_solutionContainer.ResolveSolution((ent, solutionManager), + puddleComp.SolutionName, + ref puddleComp.Solution, + out var solution)) + return; + + // Calculate total solution quantity and water percentage + var totalSolutionQuantity = solution.Contents.Sum(sol => (float)sol.Quantity); + + if (totalSolutionQuantity <= 0 || solution.Contents.Count <= 0) + return; + + var waterQuantity = solution.Contents + .Where(sol => sol.Reagent.Prototype == WaterPrototype) + .Sum(sol => (float)sol.Quantity); + + var waterPercent = (waterQuantity / totalSolutionQuantity) * 100f; + + // If puddle is mostly water, don't transfer color + if (waterPercent > ent.Comp.OffPercent) + return; + + // Find the reagent with the highest quantity to transfer + var primaryReagent = solution.Contents + .OrderByDescending(sol => sol.Quantity) + .FirstOrDefault(); + + if (primaryReagent.Reagent.Prototype == null) + return; + + // Set the reagent to transfer + footPrints.ReagentToTransfer = primaryReagent.Reagent.Prototype; + + // Transfer color from puddle to footprints + if (_appearance.TryGetData(ent, PuddleVisuals.SolutionColor, out var colorValue, appearance) + && _appearance.TryGetData(ent, PuddleVisuals.CurrentVolume, out var volumeValue, appearance)) + { + if (colorValue is Color color && volumeValue is float volume) + { + AddColor(color, volume * ent.Comp.SizeRatio, footPrints); + Dirty(tripper, footPrints); + } + } + + // Remove small amount of reagent from puddle + _solutionContainer.RemoveEachReagent(puddleComp.Solution.Value, footPrints.AmountToTransfer); + } + + private void AddColor(Color color, float quantity, FootPrintsComponent component) + { + // If no color yet, use the puddle's color directly + if (component.ColorQuantity == 0f) + { + component.PrintsColor = color; + } + else + { + // Interpolate between current color and new color + component.PrintsColor = Color.InterpolateBetween( + component.PrintsColor, + color, + component.ColorInterpolationFactor); + } + + component.ColorQuantity += quantity; + } +} diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index 6b404ca892..e3b223671a 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -215,47 +215,4 @@ - type: Tag tags: - DNASolutionScannable - - type: PuddleFootPrints # DeltaV- Begin updates from EE Blood Puddle - -- type: entity - name: footstep - id: Footstep - save: false - description: Trace of liquid - components: - - type: Clickable - - type: FootstepModifier - footstepSoundCollection: - collection: FootstepWater - params: - volume: 3 - - type: Transform - noRot: false - - type: Sprite - drawdepth: FloorObjects - color: "#FFFFFF80" - - type: Physics - bodyType: Static - - type: Fixtures - fixtures: - slipFixture: - shape: - !type:PhysShapeAabb - bounds: "-0.4,-0.4,0.4,0.4" - mask: - - ItemMask - layer: - - SlipLayer - hard: false - - type: SolutionContainerManager - solutions: - step: { maxVol: 2 } - - type: FootPrint - - type: Puddle - solution: step - - type: Appearance - - type: SpawnOnDespawn - prototype: PuddleSparkle - - type: TimedDespawn - lifetime: 60 # DeltaV End updates from EE Blood Puddle - + - type: PuddleFootPrints # DeltaV diff --git a/Resources/Prototypes/_DV/Entities/Effects/puddles.yml b/Resources/Prototypes/_DV/Entities/Effects/puddles.yml index 1a34855d46..7efcfbfa61 100644 --- a/Resources/Prototypes/_DV/Entities/Effects/puddles.yml +++ b/Resources/Prototypes/_DV/Entities/Effects/puddles.yml @@ -195,3 +195,48 @@ - id: PuddleRandomChems - id: PuddleRandomKitchen - id: PuddleRandomMisc + + +- type: entity + name: footstep + id: Footstep + save: false + description: Trace of liquid + components: + - type: Clickable + - type: FootstepModifier + footstepSoundCollection: + collection: FootstepWater + params: + volume: 3 + - type: Transform + noRot: false + - type: Sprite + drawdepth: FloorObjects + color: "#FFFFFF80" + - type: Physics + bodyType: Static + - type: Fixtures + fixtures: + slipFixture: + shape: + !type:PhysShapeAabb + bounds: "-0.4,-0.4,0.4,0.4" + mask: + - ItemMask + layer: + - SlipLayer + hard: false + - type: SolutionContainerManager + solutions: + step: { maxVol: 2 } + - type: FootPrint + - type: Puddle + solution: step + - type: Appearance + - type: SpawnOnDespawn + prototype: PuddleSparkle + - type: SpeedModifierContacts + ignoreWhitelist: + components: + - FootPrints