using System.Numerics; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Serialization; namespace Content.Shared.Decals { public abstract partial class SharedDecalSystem : EntitySystem { [Dependency] protected ChunkEntitySystem ChunkEntities = default!; [Dependency] protected EntityQuery DecalChunkQuery = default!; protected bool PvsEnabled; // Legacy DecalGridComponent data was serialized in 32x32 chunks. Loading code must treat those keys as old // storage buckets and re-chunk decals by coordinates before migrating them to chunk entities. public const int LegacyChunkSize = 32; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnChunkStartup); SubscribeAllEvent(OnDecalPlacementRequest); SubscribeAllEvent(OnDecalRemovalRequest); } protected abstract void OnDecalPlacementRequest(RequestDecalPlacementEvent ev, EntitySessionEventArgs eventArgs); protected abstract void OnDecalRemovalRequest(RequestDecalRemovalEvent ev, EntitySessionEventArgs eventArgs); private void OnChunkStartup(Entity ent, ref ComponentStartup args) { RebuildFreeDecalIds(ent.Comp); } [SubscribeLocalEvent] [Obsolete("Uses obsolete DecalGridComponent.")] private void OnGetState(Entity ent, ref ComponentGetState args) { if (PvsEnabled && !args.ReplayState) return; // Should this be a full component state or a delta-state? if (args.FromTick <= ent.Comp.CreationTick || args.FromTick <= ent.Comp.ForceTick) { args.State = new DecalGridState(ent.Comp.ChunkCollection.ChunkCollection); return; } var data = new Dictionary(); foreach (var (index, chunk) in ent.Comp.ChunkCollection.ChunkCollection) { if (chunk.LastModified >= args.FromTick) data[index] = chunk; } args.State = new DecalGridDeltaState(data, new(ent.Comp.ChunkCollection.ChunkCollection.Keys)); } public HashSet<(DecalIndex Index, Decal Decal)> GetDecalsInRange(EntityUid gridId, Vector2 position, float distance = 0.75f, Func? validDelegate = null) { var bounds = new Box2(position - new Vector2(distance + 1f), position + new Vector2(distance + 1f)); var decalIds = GetDecalsIntersecting(gridId, bounds); decalIds.RemoveWhere(set => (position - set.Decal.Coordinates - new Vector2(0.5f, 0.5f)).Length() > distance || validDelegate != null && !validDelegate(set.Decal)); return decalIds; } public HashSet<(DecalIndex Index, Decal Decal)> GetDecalsIntersecting(EntityUid gridUid, Box2 bounds) { var decalIds = new HashSet<(DecalIndex, Decal)>(); foreach (var chunk in ChunkEntities.GetChunksIntersecting(gridUid, bounds, DecalChunkQuery)) { foreach (var (id, decal) in chunk.Comp2.Decals) { if (!bounds.Contains(decal.Coordinates)) continue; decalIds.Add((new DecalIndex(chunk.Comp1.Chunk, id), decal)); } } return decalIds; } public virtual bool RemoveDecal(EntityUid gridId, DecalIndex decal) { // NOOP on client atm. return true; } /// /// Adds a decal. /// public abstract bool TryAddDecal(Decal decal, EntityCoordinates coordinates, out DecalIndex decalId); private static void RebuildFreeDecalIds(DecalChunkComponent component) { component.FreeDecalIds.Clear(); // Saves only carry decal data and the highest allocated server id. The free list is a runtime cache. foreach (var id in component.Decals.Keys) { if (id <= DecalChunkComponent.MaxServerDecalId) component.MaxDecalId = Math.Max(component.MaxDecalId, id); } component.FreeDecalIds.EnsureCapacity(component.MaxDecalId + 1 - component.Decals.Count); for (var id = 0; id <= component.MaxDecalId; id++) { var decalId = (ushort) id; if (!component.Decals.ContainsKey(decalId)) component.FreeDecalIds.Add(decalId); } // Allocation pops from the end, so keep the lowest free id there for stable decal ids. component.FreeDecalIds.Sort((x, y) => y.CompareTo(x)); } } /// /// Sent by clients to request that a decal is placed on the server. /// [Serializable, NetSerializable] public sealed class RequestDecalPlacementEvent : EntityEventArgs { public Decal Decal; public NetCoordinates Coordinates; public RequestDecalPlacementEvent(Decal decal, NetCoordinates coordinates) { Decal = decal; Coordinates = coordinates; } } [Serializable, NetSerializable] public sealed class RequestDecalRemovalEvent : EntityEventArgs { public NetCoordinates Coordinates; public RequestDecalRemovalEvent(NetCoordinates coordinates) { Coordinates = coordinates; } } }