using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using Robust.Shared.Collections; using Robust.Shared.Configuration; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Map.Events; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Systems; using Robust.Shared.Serialization; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects { public abstract class SharedGridFixtureSystem : EntitySystem { [Dependency] private readonly FixtureSystem _fixtures = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; private bool _enabled; private float _fixtureEnlargement; internal const string ShowGridNodesCommand = "showgridnodes"; public override void Initialize() { base.Initialize(); UpdatesBefore.Add(typeof(SharedBroadphaseSystem)); _cfg.OnValueChanged(CVars.GenerateGridFixtures, SetEnabled, true); _cfg.OnValueChanged(CVars.GridFixtureEnlargement, SetEnlargement, true); SubscribeLocalEvent(OnGridInit); SubscribeLocalEvent(OnGridBoundsRegenerate); } private void OnGridBoundsRegenerate(ref RegenerateGridBoundsEvent ev) { RegenerateCollision(ev.Entity, ev.ChunkRectangles, ev.RemovedChunks); } protected virtual void OnGridInit(GridInitializeEvent ev) { if (HasComp(ev.EntityUid)) return; // This will also check for grid splits if applicable. var grid = Comp(ev.EntityUid); _map.RegenerateCollision(ev.EntityUid, grid, _map.GetMapChunks(ev.EntityUid, grid).Values.ToHashSet()); } public override void Shutdown() { base.Shutdown(); _cfg.UnsubValueChanged(CVars.GenerateGridFixtures, SetEnabled); _cfg.UnsubValueChanged(CVars.GridFixtureEnlargement, SetEnlargement); } private void SetEnabled(bool value) => _enabled = value; private void SetEnlargement(float value) => _fixtureEnlargement = value; internal void RegenerateCollision( EntityUid uid, Dictionary> mapChunks, List removedChunks) { if (!_enabled) return; if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? body)) { Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(body)}"); return; } if (!EntityManager.TryGetComponent(uid, out FixturesComponent? manager)) { Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(manager)}"); return; } if (!EntityManager.TryGetComponent(uid, out TransformComponent? xform)) { Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(TransformComponent)}"); return; } var fixtures = new Dictionary(mapChunks.Count); foreach (var (chunk, rectangles) in mapChunks) { UpdateFixture(uid, chunk, rectangles, body, manager, xform); foreach (var (id, fixture) in chunk.Fixtures) { fixtures[id] = fixture; } } EntityManager.EventBus.RaiseLocalEvent(uid,new GridFixtureChangeEvent {NewFixtures = fixtures}, true); _fixtures.FixtureUpdate(uid, manager: manager, body: body); CheckSplit(uid, mapChunks, removedChunks); } internal virtual void CheckSplit(EntityUid gridEuid, Dictionary> mapChunks, List removedChunks) {} internal virtual void CheckSplit(EntityUid gridEuid, MapChunk chunk, List rectangles) {} private bool UpdateFixture(EntityUid uid, MapChunk chunk, List rectangles, PhysicsComponent body, FixturesComponent manager, TransformComponent xform) { var origin = chunk.Indices * chunk.ChunkSize; // So we store a reference to the fixture on the chunk because it's easier to cross-reference it. // This is because when we get multiple fixtures per chunk there's no easy way to tell which the old one // corresponds with. // We also ideally want to avoid re-creating the fixture every time a tile changes and pushing that data // to the client hence we diff it. // Additionally, we need to handle map deserialization where content may have stored its own data // on the grid (e.g. mass) which we want to preserve. var newFixtures = new ValueList<(string Id, Fixture Fixture)>(); Span vertices = stackalloc Vector2[4]; foreach (var rectangle in rectangles) { var bounds = ((Box2) rectangle.Translated(origin)).Enlarged(_fixtureEnlargement); var poly = new PolygonShape(); vertices[0] = bounds.BottomLeft; vertices[1] = bounds.BottomRight; vertices[2] = bounds.TopRight; vertices[3] = bounds.TopLeft; poly.Set(vertices, 4); #pragma warning disable CS0618 var newFixture = new Fixture( poly, MapGridHelpers.CollisionGroup, MapGridHelpers.CollisionGroup, true) { Owner = uid }; #pragma warning restore CS0618 newFixtures.Add(($"grid_chunk-{bounds.Left}-{bounds.Bottom}", newFixture)); } var toRemove = new ValueList<(string Id, Fixture Fixture)>(); // Check if we even need to issue an eventbus event var updated = false; foreach (var (oldId, oldFixture) in chunk.Fixtures) { var existing = false; // Handle deleted / updated fixtures // (TODO: Check IDs and cross-reference for updates?) for (var i = newFixtures.Count - 1; i >= 0; i--) { var fixture = newFixtures[i].Fixture; if (!oldFixture.Equals(fixture)) continue; existing = true; newFixtures.RemoveSwap(i); break; } // Doesn't align with any new fixtures so delete if (existing) continue; toRemove.Add((oldId, oldFixture)); } foreach (var (id, fixture) in toRemove) { // TODO add a DestroyFixture() override that takes in a list. // reduced broadphase lookups chunk.Fixtures.Remove(id); _fixtures.DestroyFixture(uid, id, fixture, false, body: body, manager: manager, xform: xform); } if (newFixtures.Count > 0 || toRemove.Count > 0) { updated = true; } // Anything remaining is a new fixture (or at least, may have not serialized onto the chunk yet). foreach (var (id, fixture) in newFixtures) { var existingFixture = _fixtures.GetFixtureOrNull(uid, id, manager: manager); // Check if it's the same (otherwise remove anyway). if (existingFixture?.Shape is PolygonShape poly && poly.EqualsApprox((PolygonShape) fixture.Shape)) { chunk.Fixtures.Add(id, existingFixture); continue; } chunk.Fixtures.Add(id, fixture); _fixtures.CreateFixture(uid, id, fixture, false, manager, body, xform); } return updated; } } /// /// Event raised after a grids fixtures have changed, but before is called. /// Allows content to modify some fixture properties, like density. /// public sealed class GridFixtureChangeEvent : EntityEventArgs { public Dictionary NewFixtures { get; init; } = default!; } [Serializable, NetSerializable] public sealed class ChunkSplitDebugMessage : EntityEventArgs { public NetEntity Grid; public Dictionary>> Nodes = new (); public List<(Vector2 Start, Vector2 End)> Connections = new(); } /// /// Raised by a client who wants to receive gridsplitnode messages. /// [Serializable, NetSerializable] public sealed class RequestGridNodesMessage : EntityEventArgs {} [Serializable, NetSerializable] public sealed class StopGridNodesMessage : EntityEventArgs {} }