mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Grid splitting (#2743)
Co-authored-by: Vera Aguilera Puerto <gradientvera@outlook.com>
This commit is contained in:
@@ -8,12 +8,12 @@ namespace Robust.Client.Console.Commands
|
||||
{
|
||||
public string Command => "physics";
|
||||
public string Description => $"Shows a debug physics overlay. The arg supplied specifies the overlay.";
|
||||
public string Help => $"{Command} <aabbs / contactnormals / contactpoints / joints / shapeinfo / shapes>";
|
||||
public string Help => $"{Command} <aabbs / com / contactnormals / contactpoints / joints / shapeinfo / shapes>";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteLine($"Invalid number of args supplied");
|
||||
shell.WriteError($"Invalid number of args supplied");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Robust.Client.Console.Commands
|
||||
system.Flags ^= PhysicsDebugFlags.Shapes;
|
||||
break;
|
||||
default:
|
||||
shell.WriteLine($"{args[0]} is not a recognised overlay");
|
||||
shell.WriteError($"{args[0]} is not a recognised overlay");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -151,10 +151,12 @@ namespace Robust.Client.Debugging
|
||||
/// Shows the world point for each contact in the viewport.
|
||||
/// </summary>
|
||||
ContactPoints = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Shows the world normal for each contact in the viewport.
|
||||
/// </summary>
|
||||
ContactNormals = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Shows all physics shapes in the viewport.
|
||||
/// </summary>
|
||||
@@ -162,6 +164,10 @@ namespace Robust.Client.Debugging
|
||||
ShapeInfo = 1 << 3,
|
||||
Joints = 1 << 4,
|
||||
AABBs = 1 << 5,
|
||||
|
||||
/// <summary>
|
||||
/// Shows Center of Mass for all bodies in the viewport.
|
||||
/// </summary>
|
||||
COM = 1 << 6,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
@@ -35,7 +33,8 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
SubscribeLocalEvent<OccluderDirtyEvent>(HandleDirtyEvent);
|
||||
|
||||
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(HandleAnchorChanged);
|
||||
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
SubscribeLocalEvent<ClientOccluderComponent, ReAnchorEvent>(OnReAnchor);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
@@ -62,7 +61,12 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleAnchorChanged(EntityUid uid, ClientOccluderComponent component, ref AnchorStateChangedEvent args)
|
||||
private static void OnAnchorChanged(EntityUid uid, ClientOccluderComponent component, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
component.AnchorStateChanged();
|
||||
}
|
||||
|
||||
private void OnReAnchor(EntityUid uid, ClientOccluderComponent component, ref ReAnchorEvent args)
|
||||
{
|
||||
component.AnchorStateChanged();
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
130
Robust.Client/Physics/GridFixtureSystem.cs
Normal file
130
Robust.Client/Physics/GridFixtureSystem.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Physics
|
||||
{
|
||||
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
|
||||
{
|
||||
public bool EnableDebug
|
||||
{
|
||||
get => _enableDebug;
|
||||
set
|
||||
{
|
||||
if (_enableDebug == value) return;
|
||||
|
||||
_enableDebug = value;
|
||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||
|
||||
if (_enableDebug)
|
||||
{
|
||||
var overlay = new GridSplitNodeOverlay(EntityManager, IoCManager.Resolve<IMapManager>(), this);
|
||||
overlayManager.AddOverlay(overlay);
|
||||
RaiseNetworkEvent(new RequestGridNodesMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
overlayManager.RemoveOverlay<GridSplitNodeOverlay>();
|
||||
RaiseNetworkEvent(new StopGridNodesMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _enableDebug = false;
|
||||
private readonly Dictionary<EntityUid, Dictionary<Vector2i, List<List<Vector2i>>>> _nodes = new();
|
||||
private readonly Dictionary<EntityUid, List<(Vector2, Vector2)>> _connections = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<ChunkSplitDebugMessage>(OnDebugMessage);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_nodes.Clear();
|
||||
_connections.Clear();
|
||||
}
|
||||
|
||||
private void OnDebugMessage(ChunkSplitDebugMessage ev)
|
||||
{
|
||||
if (!_enableDebug) return;
|
||||
|
||||
_nodes[ev.Grid] = ev.Nodes;
|
||||
_connections[ev.Grid] = ev.Connections;
|
||||
}
|
||||
|
||||
private sealed class GridSplitNodeOverlay : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
private IEntityManager _entManager;
|
||||
private IMapManager _mapManager;
|
||||
private GridFixtureSystem _system;
|
||||
|
||||
public GridSplitNodeOverlay(IEntityManager entManager, IMapManager mapManager, GridFixtureSystem system)
|
||||
{
|
||||
_entManager = entManager;
|
||||
_mapManager = mapManager;
|
||||
_system = system;
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var worldHandle = args.WorldHandle;
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var iGrid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
|
||||
{
|
||||
// May not have received nodes yet.
|
||||
if (!_system._nodes.TryGetValue(iGrid.GridEntityId, out var nodes)) continue;
|
||||
|
||||
var gridXform = xformQuery.GetComponent(iGrid.GridEntityId);
|
||||
worldHandle.SetTransform(gridXform.WorldMatrix);
|
||||
var grid = (MapGrid)iGrid;
|
||||
grid.GetMapChunks(args.WorldBounds, out var chunkEnumerator);
|
||||
|
||||
while (chunkEnumerator.MoveNext(out var chunk))
|
||||
{
|
||||
if (!nodes.TryGetValue(chunk.Indices, out var chunkNodes)) continue;
|
||||
|
||||
for (var i = 0; i < chunkNodes.Count; i++)
|
||||
{
|
||||
var group = chunkNodes[i];
|
||||
var offset = chunk.Indices * chunk.ChunkSize;
|
||||
var color = GetColor(chunk, i);
|
||||
|
||||
foreach (var index in group)
|
||||
{
|
||||
worldHandle.DrawRect(new Box2(offset + index, offset + index + 1).Enlarged(-0.1f), color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var connections = _system._connections[iGrid.GridEntityId];
|
||||
|
||||
foreach (var (start, end) in connections)
|
||||
{
|
||||
worldHandle.DrawLine(start, end, Color.Aquamarine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Color GetColor(MapChunk chunk, int index)
|
||||
{
|
||||
var red = Math.Abs(chunk.Indices.X * 30 % 255);
|
||||
var green = Math.Abs(chunk.Indices.Y * 30 % 255);
|
||||
var blue = index * 30 % 255;
|
||||
|
||||
return new Color(red, green, blue, 0.3f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Robust.Client/Physics/GridSplitVisualsCommand.cs
Normal file
18
Robust.Client/Physics/GridSplitVisualsCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Physics;
|
||||
|
||||
public sealed class GridSplitVisualCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => SharedGridFixtureSystem.ShowGridNodesCommand;
|
||||
public string Description => "Shows the nodes for grid split purposes";
|
||||
public string Help => $"{Command}";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GridFixtureSystem>();
|
||||
system.EnableDebug ^= true;
|
||||
shell.WriteLine($"Toggled gridsplit node visuals");
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ namespace Robust.Client.Physics
|
||||
// Also need to suss out having the client build the island anyway and just... not solving it?
|
||||
foreach (var body in AwakeBodies)
|
||||
{
|
||||
if (body.Island || body.LinearVelocity.Length > _linSleepTolerance / 2f || body.AngularVelocity > _angSleepTolerance / 2f) continue;
|
||||
if (body.LinearVelocity.Length > _linSleepTolerance / 2f || body.AngularVelocity > _angSleepTolerance / 2f) continue;
|
||||
body.SleepTime += frameTime;
|
||||
if (body.SleepTime > _timeToSleep)
|
||||
{
|
||||
|
||||
@@ -170,10 +170,9 @@ namespace Robust.Client.Placement
|
||||
/// </summary>
|
||||
public TileRef GetTileRef(EntityCoordinates coordinates)
|
||||
{
|
||||
var mapCoords = coordinates.ToMap(pManager.EntityManager);
|
||||
var gridId = coordinates.GetGridId(pManager.EntityManager);
|
||||
return gridId.IsValid() ? pManager.MapManager.GetGrid(gridId).GetTileRef(MouseCoords)
|
||||
: new TileRef(mapCoords.MapId, gridId,
|
||||
: new TileRef(gridId,
|
||||
MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager), Tile.Empty);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
|
||||
mouseWorldMap.Position);
|
||||
tile = new TileRef(mouseWorldMap.MapId, GridId.Invalid,
|
||||
tile = new TileRef(GridId.Invalid,
|
||||
mouseGridPos.ToVector2i(_entityManager, _mapManager), Tile.Empty);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
@@ -220,7 +221,7 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
|
||||
private void OnEntityMove(ref MoveEvent ev)
|
||||
{
|
||||
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var coordinates = _transform.GetMoverCoordinates(ev.Component);
|
||||
UpdateEntityRecursive(ev.Sender, ev.Component, coordinates, xformQuery, false);
|
||||
}
|
||||
@@ -228,7 +229,7 @@ internal sealed partial class PVSSystem : EntitySystem
|
||||
private void OnTransformStartup(EntityUid uid, TransformComponent component, ComponentStartup args)
|
||||
{
|
||||
// use Startup because GridId is not set during the eventbus init yet!
|
||||
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var coordinates = _transform.GetMoverCoordinates(component);
|
||||
UpdateEntityRecursive(uid, component, coordinates, xformQuery, false);
|
||||
}
|
||||
|
||||
@@ -371,6 +371,9 @@ namespace Robust.Server.Maps
|
||||
|
||||
// Run Startup on all components.
|
||||
FinishEntitiesStartup();
|
||||
|
||||
// Do this last so any entity transforms are fixed first and that they go to the new grids correctly.
|
||||
CheckGridSplits();
|
||||
}
|
||||
|
||||
private void VerifyEntitiesExist()
|
||||
@@ -459,6 +462,7 @@ namespace Robust.Server.Maps
|
||||
body.Broadphase = entManager.GetComponent<BroadphaseComponent>(mapUid);
|
||||
var fixtures = entManager.EnsureComponent<FixturesComponent>(grid.GridEntityId);
|
||||
// Regenerate grid collision.
|
||||
gridFixtures.EnsureGrid(grid.GridEntityId);
|
||||
gridFixtures.ProcessGrid(gridInternal);
|
||||
// Avoid duplicating the deserialization in FixtureSystem.
|
||||
fixtures.SerializedFixtures.Clear();
|
||||
@@ -757,6 +761,16 @@ namespace Robust.Server.Maps
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckGridSplits()
|
||||
{
|
||||
var gridFixtures = _serverEntityManager.EntitySysManager.GetEntitySystem<GridFixtureSystem>();
|
||||
foreach (var grid in Grids)
|
||||
{
|
||||
if (_serverEntityManager.Deleted(grid.GridEntityId)) continue;
|
||||
gridFixtures.CheckSplits(grid.GridEntityId);
|
||||
}
|
||||
}
|
||||
|
||||
// Serialization
|
||||
public void RegisterGrid(IMapGrid grid)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.Physics
|
||||
{
|
||||
@@ -7,5 +22,631 @@ namespace Robust.Server.Physics
|
||||
/// </summary>
|
||||
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
|
||||
|
||||
private ISawmill _logger = default!;
|
||||
private readonly Dictionary<EntityUid, Dictionary<Vector2i, ChunkNodeGroup>> _nodes = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sessions to receive nodes for debug purposes.
|
||||
/// </summary>
|
||||
private readonly HashSet<ICommonSession> _subscribedSessions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Recursion detection to avoid splitting while handling an existing split
|
||||
/// </summary>
|
||||
private bool _isSplitting;
|
||||
|
||||
private bool _splitAllowed = true;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_logger = Logger.GetSawmill("gsplit");
|
||||
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
|
||||
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoval);
|
||||
SubscribeNetworkEvent<RequestGridNodesMessage>(OnDebugRequest);
|
||||
SubscribeNetworkEvent<StopGridNodesMessage>(OnDebugStopRequest);
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
configManager.OnValueChanged(CVars.GridSplitting, SetSplitAllowed, true);
|
||||
}
|
||||
|
||||
private void SetSplitAllowed(bool value) => _splitAllowed = value;
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_subscribedSessions.Clear();
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
configManager.UnsubValueChanged(CVars.GridSplitting, SetSplitAllowed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Due to how MapLoader works need to ensure grid exists in dictionary before it's initialised.
|
||||
/// </summary>
|
||||
internal void EnsureGrid(EntityUid uid)
|
||||
{
|
||||
if (!_nodes.ContainsKey(uid))
|
||||
_nodes[uid] = new Dictionary<Vector2i, ChunkNodeGroup>();
|
||||
}
|
||||
|
||||
private void OnGridInit(GridInitializeEvent ev)
|
||||
{
|
||||
EnsureGrid(ev.EntityUid);
|
||||
}
|
||||
|
||||
private void OnGridRemoval(GridRemovalEvent ev)
|
||||
{
|
||||
_nodes.Remove(ev.EntityUid);
|
||||
}
|
||||
|
||||
#region Debug
|
||||
|
||||
private void OnDebugRequest(RequestGridNodesMessage msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var adminManager = IoCManager.Resolve<IConGroupController>();
|
||||
var pSession = (PlayerSession) args.SenderSession;
|
||||
|
||||
if (!adminManager.CanCommand(pSession, ShowGridNodesCommand)) return;
|
||||
|
||||
AddDebugSubscriber(args.SenderSession);
|
||||
}
|
||||
|
||||
private void OnDebugStopRequest(StopGridNodesMessage msg, EntitySessionEventArgs args)
|
||||
{
|
||||
RemoveDebugSubscriber(args.SenderSession);
|
||||
}
|
||||
|
||||
public bool IsSubscribed(ICommonSession session)
|
||||
{
|
||||
return _subscribedSessions.Contains(session);
|
||||
}
|
||||
|
||||
public void AddDebugSubscriber(ICommonSession session)
|
||||
{
|
||||
if (!_subscribedSessions.Add(session)) return;
|
||||
|
||||
foreach (var (uid, _) in _nodes)
|
||||
{
|
||||
SendNodeDebug(uid);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveDebugSubscriber(ICommonSession session)
|
||||
{
|
||||
_subscribedSessions.Remove(session);
|
||||
}
|
||||
|
||||
private void SendNodeDebug(EntityUid uid)
|
||||
{
|
||||
if (_subscribedSessions.Count == 0) return;
|
||||
|
||||
var msg = new ChunkSplitDebugMessage
|
||||
{
|
||||
Grid = uid,
|
||||
};
|
||||
|
||||
foreach (var (index, group) in _nodes[uid])
|
||||
{
|
||||
var list = new List<List<Vector2i>>();
|
||||
// To avoid double-sending connections.
|
||||
var conns = new HashSet<ChunkSplitNode>();
|
||||
|
||||
foreach (var node in group.Nodes)
|
||||
{
|
||||
conns.Add(node);
|
||||
list.Add(node.Indices.ToList());
|
||||
|
||||
foreach (var neighbor in node.Neighbors)
|
||||
{
|
||||
if (conns.Contains(neighbor)) continue;
|
||||
|
||||
msg.Connections.Add((
|
||||
node.GetCentre() + node.Group.Chunk.Indices * node.Group.Chunk.ChunkSize,
|
||||
neighbor.GetCentre() + neighbor.Group.Chunk.Indices * neighbor.Group.Chunk.ChunkSize));
|
||||
}
|
||||
}
|
||||
|
||||
msg.Nodes.Add(index, list);
|
||||
}
|
||||
|
||||
foreach (var session in _subscribedSessions)
|
||||
{
|
||||
RaiseNetworkEvent(msg, session.ConnectedClient);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Check for any potential splits after maploading is done.
|
||||
/// </summary>
|
||||
internal void CheckSplits(EntityUid uid)
|
||||
{
|
||||
var nodes = _nodes[uid];
|
||||
var dirtyNodes = new HashSet<ChunkSplitNode>(nodes.Count);
|
||||
|
||||
foreach (var (_, group) in nodes)
|
||||
{
|
||||
foreach (var node in group.Nodes)
|
||||
{
|
||||
dirtyNodes.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
CheckSplits(uid, dirtyNodes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for splits on the specified nodes.
|
||||
/// </summary>
|
||||
private void CheckSplits(EntityUid uid, HashSet<ChunkSplitNode> dirtyNodes)
|
||||
{
|
||||
if (_isSplitting || !_splitAllowed) return;
|
||||
|
||||
_isSplitting = true;
|
||||
var splitFrontier = new Queue<ChunkSplitNode>(4);
|
||||
var grids = new List<HashSet<ChunkSplitNode>>(1);
|
||||
|
||||
// TODO: At this point detect splits.
|
||||
while (dirtyNodes.Count > 0)
|
||||
{
|
||||
var originEnumerator = dirtyNodes.GetEnumerator();
|
||||
originEnumerator.MoveNext();
|
||||
var origin = originEnumerator.Current;
|
||||
originEnumerator.Dispose();
|
||||
splitFrontier.Enqueue(origin);
|
||||
var foundSplits = new HashSet<ChunkSplitNode>
|
||||
{
|
||||
origin
|
||||
};
|
||||
|
||||
while (splitFrontier.TryDequeue(out var split))
|
||||
{
|
||||
dirtyNodes.Remove(split);
|
||||
|
||||
foreach (var neighbor in split.Neighbors)
|
||||
{
|
||||
if (!foundSplits.Add(neighbor)) continue;
|
||||
|
||||
splitFrontier.Enqueue(neighbor);
|
||||
}
|
||||
}
|
||||
|
||||
grids.Add(foundSplits);
|
||||
}
|
||||
|
||||
var mapGrid = _mapManager.GetGrid(uid);
|
||||
|
||||
// Split time
|
||||
if (grids.Count > 1)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
// We'll leave the biggest group as the original grid
|
||||
// anything smaller gets split off.
|
||||
grids.Sort((x, y) =>
|
||||
x.Sum(o => o.Indices.Count)
|
||||
.CompareTo(y.Sum(o => o.Indices.Count)));
|
||||
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var bodyQuery = GetEntityQuery<PhysicsComponent>();
|
||||
var oldGridXform = xformQuery.GetComponent(mapGrid.GridEntityId);
|
||||
var (gridPos, gridRot) = oldGridXform.GetWorldPositionRotation(xformQuery);
|
||||
var mapBody = bodyQuery.GetComponent(mapGrid.GridEntityId);
|
||||
var oldGridComp = Comp<MapGridComponent>(mapGrid.GridEntityId);
|
||||
var newGrids = new GridId[grids.Count - 1];
|
||||
|
||||
for (var i = 0; i < grids.Count - 1; i++)
|
||||
{
|
||||
var group = grids[i];
|
||||
var splitGrid = _mapManager.CreateGrid(mapGrid.ParentMapId);
|
||||
newGrids[i] = splitGrid.Index;
|
||||
|
||||
// Keep same origin / velocity etc; this makes updating a lot faster and easier.
|
||||
splitGrid.WorldPosition = gridPos;
|
||||
splitGrid.WorldRotation = gridRot;
|
||||
var splitBody = bodyQuery.GetComponent(splitGrid.GridEntityId);
|
||||
var splitXform = xformQuery.GetComponent(splitGrid.GridEntityId);
|
||||
splitBody.LinearVelocity = mapBody.LinearVelocity;
|
||||
splitBody.AngularVelocity = mapBody.AngularVelocity;
|
||||
|
||||
var gridComp = Comp<MapGridComponent>(splitGrid.GridEntityId);
|
||||
var tileData = new List<(Vector2i GridIndices, Tile Tile)>(group.Sum(o => o.Indices.Count));
|
||||
|
||||
// Gather all tiles up front and set once to minimise fixture change events
|
||||
foreach (var node in group)
|
||||
{
|
||||
var offset = node.Group.Chunk.Indices * node.Group.Chunk.ChunkSize;
|
||||
|
||||
foreach (var index in node.Indices)
|
||||
{
|
||||
var tilePos = offset + index;
|
||||
tileData.Add((tilePos, mapGrid.GetTileRef(tilePos).Tile));
|
||||
}
|
||||
}
|
||||
|
||||
splitGrid.SetTiles(tileData);
|
||||
|
||||
// Set tiles on new grid + update anchored entities
|
||||
foreach (var node in group)
|
||||
{
|
||||
var offset = node.Group.Chunk.Indices * node.Group.Chunk.ChunkSize;
|
||||
|
||||
foreach (var tile in node.Indices)
|
||||
{
|
||||
var tilePos = offset + tile;
|
||||
|
||||
// Access it directly because we're gonna be hammering it and want to keep allocs down.
|
||||
var snapgrid = node.Group.Chunk.GetSnapGrid((ushort) tile.X, (ushort) tile.Y);
|
||||
if (snapgrid == null || snapgrid.Count == 0) continue;
|
||||
|
||||
for (var j = snapgrid.Count - 1; j >= 0; j--)
|
||||
{
|
||||
var ent = snapgrid[j];
|
||||
var xform = xformQuery.GetComponent(ent);
|
||||
_xformSystem.ReAnchor(xform, oldGridComp, gridComp, tilePos, oldGridXform, splitXform, xformQuery);
|
||||
DebugTools.Assert(xform.Anchored);
|
||||
}
|
||||
}
|
||||
|
||||
// Update lookup ents
|
||||
// Needs to be done before setting old tiles as they will be re-parented to the map.
|
||||
// TODO: Combine tiles into larger rectangles or something; this is gonna be the killer bit.
|
||||
foreach (var tile in node.Indices)
|
||||
{
|
||||
var tilePos = offset + tile;
|
||||
var bounds = _lookup.GetLocalBounds(tilePos, mapGrid.TileSize);
|
||||
|
||||
foreach (var ent in _lookup.GetEntitiesIntersecting(mapGrid.Index, tilePos, LookupFlags.None))
|
||||
{
|
||||
// Consider centre of entity position maybe?
|
||||
var entXform = xformQuery.GetComponent(ent);
|
||||
|
||||
if (entXform.ParentUid != mapGrid.GridEntityId ||
|
||||
!bounds.Contains(entXform.LocalPosition)) continue;
|
||||
|
||||
entXform.AttachParent(splitXform);
|
||||
}
|
||||
}
|
||||
|
||||
_nodes[mapGrid.GridEntityId][node.Group.Chunk.Indices].Nodes.Remove(node);
|
||||
}
|
||||
|
||||
for (var j = 0; j < tileData.Count; j++)
|
||||
{
|
||||
var (index, _) = tileData[j];
|
||||
tileData[j] = (index, Tile.Empty);
|
||||
}
|
||||
|
||||
// Set tiles on old grid
|
||||
mapGrid.SetTiles(tileData);
|
||||
GenerateSplitNodes((IMapGridInternal) splitGrid);
|
||||
SendNodeDebug(splitGrid.GridEntityId);
|
||||
}
|
||||
|
||||
// Cull all of the old chunk nodes.
|
||||
var toRemove = new RemQueue<ChunkNodeGroup>();
|
||||
|
||||
foreach (var (_, group) in _nodes[mapGrid.GridEntityId])
|
||||
{
|
||||
if (group.Nodes.Count > 0) continue;
|
||||
toRemove.Add(group);
|
||||
}
|
||||
|
||||
foreach (var group in toRemove)
|
||||
{
|
||||
_nodes[mapGrid.GridEntityId].Remove(group.Chunk.Indices);
|
||||
}
|
||||
|
||||
// Allow content to react to the grid being split...
|
||||
var ev = new GridSplitEvent(newGrids, mapGrid.Index);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
_logger.Debug($"Split {grids.Count} grids in {sw.Elapsed}");
|
||||
}
|
||||
|
||||
_isSplitting = false;
|
||||
SendNodeDebug(mapGrid.GridEntityId);
|
||||
}
|
||||
|
||||
private void GenerateSplitNodes(IMapGridInternal grid)
|
||||
{
|
||||
foreach (var (_, chunk) in grid.GetMapChunks())
|
||||
{
|
||||
var group = CreateNodes(grid.GridEntityId, grid, chunk);
|
||||
_nodes[grid.GridEntityId].Add(chunk.Indices, group);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates all of the splitting nodes within this chunk; also consider neighbor chunks.
|
||||
/// </summary>
|
||||
private ChunkNodeGroup CreateNodes(EntityUid gridEuid, IMapGridInternal grid, MapChunk chunk)
|
||||
{
|
||||
var group = new ChunkNodeGroup
|
||||
{
|
||||
Chunk = chunk,
|
||||
};
|
||||
|
||||
var tiles = new HashSet<Vector2i>(chunk.ChunkSize * chunk.ChunkSize);
|
||||
|
||||
for (var x = 0; x < chunk.ChunkSize; x++)
|
||||
{
|
||||
for (var y = 0; y < chunk.ChunkSize; y++)
|
||||
{
|
||||
tiles.Add(new Vector2i(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
var frontier = new Queue<Vector2i>();
|
||||
var node = new ChunkSplitNode
|
||||
{
|
||||
Group = group,
|
||||
};
|
||||
|
||||
// Simple BFS search to get all of the nodes in the chunk.
|
||||
while (tiles.Count > 0)
|
||||
{
|
||||
var originEnumerator = tiles.GetEnumerator();
|
||||
originEnumerator.MoveNext();
|
||||
var origin = originEnumerator.Current;
|
||||
frontier.Enqueue(origin);
|
||||
originEnumerator.Dispose();
|
||||
|
||||
// Just reuse the node if we couldn't use it last time.
|
||||
// This is in case weh ave 1 chunk with 255 empty tiles and 1 valid tile.
|
||||
if (node.Indices.Count > 0)
|
||||
{
|
||||
node = new ChunkSplitNode
|
||||
{
|
||||
Group = group,
|
||||
};
|
||||
}
|
||||
|
||||
tiles.Remove(origin);
|
||||
|
||||
// Check for valid neighbours and add them to the frontier.
|
||||
while (frontier.TryDequeue(out var index))
|
||||
{
|
||||
var tile = chunk.GetTile((ushort) index.X, (ushort) index.Y);
|
||||
if (tile.IsEmpty) continue;
|
||||
|
||||
node.Indices.Add(index);
|
||||
var enumerator = new NeighborEnumerator(chunk, index);
|
||||
|
||||
while (enumerator.MoveNext(out var neighbor))
|
||||
{
|
||||
// Already iterated this tile before so just ignore it.
|
||||
if (!tiles.Remove(neighbor.Value)) continue;
|
||||
frontier.Enqueue(neighbor.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.Indices.Count == 0) continue;
|
||||
|
||||
group.Nodes.Add(node);
|
||||
}
|
||||
|
||||
// Build neighbors
|
||||
ChunkSplitNode? neighborNode;
|
||||
MapChunk? neighborChunk;
|
||||
|
||||
// Check each tile for node neighbours on other chunks (not possible for us to have neighbours on the same chunk
|
||||
// as they would already be in our node).
|
||||
// TODO: This could be better (maybe only check edges of the chunk or something).
|
||||
foreach (var chunkNode in group.Nodes)
|
||||
{
|
||||
foreach (var index in chunkNode.Indices)
|
||||
{
|
||||
// Check for edge tiles.
|
||||
if (index.X == 0)
|
||||
{
|
||||
// Check West
|
||||
if (grid.TryGetChunk(new Vector2i(chunk.Indices.X - 1, chunk.Indices.Y), out neighborChunk) &&
|
||||
TryGetNode(gridEuid, neighborChunk, new Vector2i(chunk.ChunkSize - 1, index.Y), out neighborNode))
|
||||
{
|
||||
chunkNode.Neighbors.Add(neighborNode);
|
||||
neighborNode.Neighbors.Add(chunkNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (index.Y == 0)
|
||||
{
|
||||
// Check South
|
||||
if (grid.TryGetChunk(new Vector2i(chunk.Indices.X, chunk.Indices.Y - 1), out neighborChunk) &&
|
||||
TryGetNode(gridEuid, neighborChunk, new Vector2i(index.X, chunk.ChunkSize - 1), out neighborNode))
|
||||
{
|
||||
chunkNode.Neighbors.Add(neighborNode);
|
||||
neighborNode.Neighbors.Add(chunkNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (index.X == chunk.ChunkSize - 1)
|
||||
{
|
||||
// Check East
|
||||
if (grid.TryGetChunk(new Vector2i(chunk.Indices.X + 1, chunk.Indices.Y), out neighborChunk) &&
|
||||
TryGetNode(gridEuid, neighborChunk, new Vector2i(0, index.Y), out neighborNode))
|
||||
{
|
||||
chunkNode.Neighbors.Add(neighborNode);
|
||||
neighborNode.Neighbors.Add(chunkNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (index.Y == chunk.ChunkSize - 1)
|
||||
{
|
||||
// Check North
|
||||
if (grid.TryGetChunk(new Vector2i(chunk.Indices.X, chunk.Indices.Y + 1), out neighborChunk) &&
|
||||
TryGetNode(gridEuid, neighborChunk, new Vector2i(index.X, 0), out neighborNode))
|
||||
{
|
||||
chunkNode.Neighbors.Add(neighborNode);
|
||||
neighborNode.Neighbors.Add(chunkNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
internal override void GenerateSplitNode(EntityUid gridEuid, MapChunk chunk, bool checkSplit = true)
|
||||
{
|
||||
if (_isSplitting) return;
|
||||
|
||||
var grid = (IMapGridInternal) _mapManager.GetGrid(gridEuid);
|
||||
var dirtyNodes = new HashSet<ChunkSplitNode>();
|
||||
|
||||
Cleanup(gridEuid, chunk, dirtyNodes);
|
||||
var group = CreateNodes(gridEuid, grid, chunk);
|
||||
_nodes[grid.GridEntityId][chunk.Indices] = group;
|
||||
|
||||
foreach (var chunkNode in group.Nodes)
|
||||
{
|
||||
dirtyNodes.Add(chunkNode);
|
||||
}
|
||||
|
||||
if (checkSplit)
|
||||
CheckSplits(gridEuid, dirtyNodes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the relevant split node from a neighbor chunk.
|
||||
/// </summary>
|
||||
private bool TryGetNode(EntityUid gridEuid, MapChunk chunk, Vector2i index, [NotNullWhen(true)] out ChunkSplitNode? node)
|
||||
{
|
||||
if (!_nodes[gridEuid].TryGetValue(chunk.Indices, out var neighborGroup))
|
||||
{
|
||||
node = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var neighborNode in neighborGroup.Nodes)
|
||||
{
|
||||
if (!neighborNode.Indices.Contains(index)) continue;
|
||||
node = neighborNode;
|
||||
return true;
|
||||
}
|
||||
|
||||
node = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Cleanup(EntityUid gridEuid, MapChunk chunk, HashSet<ChunkSplitNode> dirtyNodes)
|
||||
{
|
||||
if (!_nodes[gridEuid].TryGetValue(chunk.Indices, out var group)) return;
|
||||
|
||||
foreach (var node in group.Nodes)
|
||||
{
|
||||
// Most important thing is updating our neighbor nodes.
|
||||
foreach (var neighbor in node.Neighbors)
|
||||
{
|
||||
neighbor.Neighbors.Remove(node);
|
||||
// If neighbor is on a different chunk mark it for checking connections later.
|
||||
if (neighbor.Group.Equals(group)) continue;
|
||||
dirtyNodes.Add(neighbor);
|
||||
}
|
||||
|
||||
node.Indices.Clear();
|
||||
node.Neighbors.Clear();
|
||||
}
|
||||
|
||||
_nodes[gridEuid].Remove(chunk.Indices);
|
||||
}
|
||||
|
||||
private sealed class ChunkNodeGroup
|
||||
{
|
||||
internal MapChunk Chunk = default!;
|
||||
public HashSet<ChunkSplitNode> Nodes = new();
|
||||
}
|
||||
|
||||
private sealed class ChunkSplitNode
|
||||
{
|
||||
public ChunkNodeGroup Group = default!;
|
||||
public HashSet<Vector2i> Indices { get; set; } = new();
|
||||
public HashSet<ChunkSplitNode> Neighbors { get; set; } = new();
|
||||
|
||||
public Vector2 GetCentre()
|
||||
{
|
||||
var centre = Vector2.Zero;
|
||||
|
||||
foreach (var index in Indices)
|
||||
{
|
||||
centre += index;
|
||||
}
|
||||
|
||||
centre /= Indices.Count;
|
||||
return centre;
|
||||
}
|
||||
}
|
||||
|
||||
private struct NeighborEnumerator
|
||||
{
|
||||
private MapChunk _chunk;
|
||||
private Vector2i _index;
|
||||
private int _count = -1;
|
||||
|
||||
public NeighborEnumerator(MapChunk chunk, Vector2i index)
|
||||
{
|
||||
_chunk = chunk;
|
||||
_index = index;
|
||||
}
|
||||
|
||||
public bool MoveNext([NotNullWhen(true)] out Vector2i? neighbor)
|
||||
{
|
||||
_count++;
|
||||
|
||||
// Just go through S E N W
|
||||
switch (_count)
|
||||
{
|
||||
case 0:
|
||||
if (_index.Y == 0) break;
|
||||
neighbor = new Vector2i(_index.X, _index.Y - 1);
|
||||
return true;
|
||||
case 1:
|
||||
if (_index.X == _chunk.ChunkSize - 1) break;
|
||||
neighbor = new Vector2i(_index.X + 1, _index.Y);
|
||||
return true;
|
||||
case 2:
|
||||
if (_index.Y == _chunk.ChunkSize + 1) break;
|
||||
neighbor = new Vector2i(_index.X, _index.Y + 1);
|
||||
return true;
|
||||
case 3:
|
||||
if (_index.X == 0) break;
|
||||
neighbor = new Vector2i(_index.X - 1, _index.Y);
|
||||
return true;
|
||||
default:
|
||||
neighbor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return MoveNext(out neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on a grid that has been split into multiple grids.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly struct GridSplitEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the IDs of the newly created grids.
|
||||
/// </summary>
|
||||
public readonly GridId[] NewGrids;
|
||||
|
||||
/// <summary>
|
||||
/// The grid that has been split.
|
||||
/// </summary>
|
||||
public readonly GridId Grid;
|
||||
|
||||
public GridSplitEvent(GridId[] newGrids, GridId grid)
|
||||
{
|
||||
NewGrids = newGrids;
|
||||
Grid = grid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,6 +888,12 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<bool> GenerateGridFixtures =
|
||||
CVarDef.Create("physics.grid_fixtures", true, CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Can grids split if not connected by cardinals
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> GridSplitting =
|
||||
CVarDef.Create("physics.grid_splitting", true, CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// How much to enlarge grids when determining their fixture bounds.
|
||||
/// </summary>
|
||||
|
||||
@@ -141,7 +141,8 @@ namespace Robust.Shared.GameObjects
|
||||
if (_awake || _bodyType == BodyType.Static) return;
|
||||
|
||||
_awake = true;
|
||||
_entMan.EventBus.RaiseEvent(EventSource.Local, new PhysicsWakeMessage(this));
|
||||
var ev = new PhysicsWakeEvent(this);
|
||||
_entMan.EventBus.RaiseEvent(EventSource.Local, ref ev);
|
||||
}
|
||||
|
||||
// We'll also block Static bodies from ever being awake given they don't need to move.
|
||||
@@ -168,11 +169,13 @@ namespace Robust.Shared.GameObjects
|
||||
if (value)
|
||||
{
|
||||
_sleepTime = 0.0f;
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, new PhysicsWakeMessage(this));
|
||||
var ev = new PhysicsWakeEvent(this);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ref ev);
|
||||
}
|
||||
else
|
||||
{
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, new PhysicsSleepMessage(this));
|
||||
var ev = new PhysicsSleepEvent(this);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ref ev);
|
||||
ResetDynamics();
|
||||
_sleepTime = 0.0f;
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
GridId GridIndex { get; }
|
||||
IMapGrid Grid { get; }
|
||||
|
||||
bool AnchorEntity(TransformComponent transform);
|
||||
void UnanchorEntity(TransformComponent transform);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMapGridComponent"/>
|
||||
@@ -61,11 +58,12 @@ namespace Robust.Shared.GameObjects
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
var mapId = _entMan.GetComponent<TransformComponent>(Owner).MapID;
|
||||
var xform = _entMan.GetComponent<TransformComponent>(Owner);
|
||||
var mapId = xform.MapID;
|
||||
|
||||
if (_mapManager.HasMapEntity(mapId))
|
||||
{
|
||||
_entMan.GetComponent<TransformComponent>(Owner).AttachParent(_mapManager.GetMapEntityIdOrThrow(mapId));
|
||||
xform.AttachParent(_mapManager.GetMapEntityIdOrThrow(mapId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,49 +74,6 @@ namespace Robust.Shared.GameObjects
|
||||
base.OnRemove();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool AnchorEntity(TransformComponent transform)
|
||||
{
|
||||
var xform = transform;
|
||||
var tileIndices = Grid.TileIndicesFor(transform.Coordinates);
|
||||
var result = Grid.AddToSnapGridCell(tileIndices, transform.Owner);
|
||||
|
||||
if (result)
|
||||
{
|
||||
xform.ParentUid = Owner;
|
||||
|
||||
// anchor snapping
|
||||
xform.LocalPosition = Grid.GridTileToLocal(tileIndices).Position;
|
||||
|
||||
xform.SetAnchored(result);
|
||||
|
||||
if (_entMan.TryGetComponent<PhysicsComponent?>(xform.Owner, out var physicsComponent))
|
||||
{
|
||||
physicsComponent.BodyType = BodyType.Static;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UnanchorEntity(TransformComponent transform)
|
||||
{
|
||||
//HACK: Client grid pivot causes this.
|
||||
//TODO: make grid components the actual grid
|
||||
if(GridIndex == GridId.Invalid)
|
||||
return;
|
||||
|
||||
var xform = transform;
|
||||
var tileIndices = Grid.TileIndicesFor(transform.Coordinates);
|
||||
Grid.RemoveFromSnapGridCell(tileIndices, transform.Owner);
|
||||
xform.SetAnchored(false);
|
||||
if (_entMan.TryGetComponent<PhysicsComponent?>(xform.Owner, out var physicsComponent))
|
||||
{
|
||||
physicsComponent.BodyType = BodyType.Dynamic;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
|
||||
@@ -24,13 +24,12 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
[DataField("parent")]
|
||||
private EntityUid _parent;
|
||||
[DataField("parent")] internal EntityUid _parent;
|
||||
[DataField("pos")] internal Vector2 _localPosition = Vector2.Zero; // holds offset from grid, or offset from parent
|
||||
[DataField("rot")] internal Angle _localRotation; // local rotation
|
||||
[DataField("noRot")] internal bool _noLocalRotation;
|
||||
[DataField("anchored")]
|
||||
private bool _anchored;
|
||||
internal bool _anchored;
|
||||
|
||||
private Matrix3 _localMatrix = Matrix3.Identity;
|
||||
private Matrix3 _invLocalMatrix = Matrix3.Identity;
|
||||
@@ -92,7 +91,7 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private GridId _gridId = GridId.Invalid;
|
||||
internal GridId _gridId = GridId.Invalid;
|
||||
|
||||
/// <summary>
|
||||
/// Disables or enables to ability to locally rotate the entity. When set it removes any local rotation.
|
||||
@@ -379,7 +378,7 @@ namespace Robust.Shared.GameObjects
|
||||
//TODO: This is a hack, look into WHY we can't call GridPosition before the comp is Running
|
||||
if (Running)
|
||||
{
|
||||
if (!oldPosition.Position.Equals(Coordinates.Position))
|
||||
if (!oldPosition.Equals(Coordinates))
|
||||
{
|
||||
var moveEvent = new MoveEvent(Owner, oldPosition, Coordinates, this);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent);
|
||||
@@ -455,7 +454,7 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
if (value && _mapManager.TryFindGridAt(MapPosition, out var grid))
|
||||
{
|
||||
_anchored = _entMan.GetComponent<IMapGridComponent>(grid.GridEntityId).AnchorEntity(this);
|
||||
_anchored = _entMan.EntitySysManager.GetEntitySystem<SharedTransformSystem>().AnchorEntity(this, grid);
|
||||
}
|
||||
// If no grid found then unanchor it.
|
||||
else
|
||||
@@ -465,17 +464,14 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
else if (value && !_anchored && _mapManager.TryFindGridAt(MapPosition, out var grid))
|
||||
{
|
||||
_anchored = _entMan.GetComponent<IMapGridComponent>(grid.GridEntityId).AnchorEntity(this);
|
||||
_anchored = _entMan.EntitySysManager.GetEntitySystem<SharedTransformSystem>().AnchorEntity(this, grid);
|
||||
}
|
||||
else if (!value && _anchored)
|
||||
{
|
||||
// An anchored entity is always parented to the grid.
|
||||
// If Transform.Anchored is true in the prototype but the entity was not spawned with a grid as the parent,
|
||||
// then this will be false.
|
||||
if (_entMan.TryGetComponent<IMapGridComponent>(ParentUid, out var gridComp))
|
||||
gridComp.UnanchorEntity(this);
|
||||
else
|
||||
SetAnchored(false);
|
||||
_entMan.EntitySysManager.GetEntitySystem<SharedTransformSystem>().Unanchor(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -961,13 +957,16 @@ namespace Robust.Shared.GameObjects
|
||||
return $"pos/rot/wpos/wrot: {Coordinates}/{LocalRotation}/{WorldPosition}/{WorldRotation}";
|
||||
}
|
||||
|
||||
internal void SetAnchored(bool value)
|
||||
internal void SetAnchored(bool value, bool issueEvent = true)
|
||||
{
|
||||
_anchored = value;
|
||||
Dirty(_entMan);
|
||||
|
||||
var anchorStateChangedEvent = new AnchorStateChangedEvent(Owner, value);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ref anchorStateChangedEvent);
|
||||
if (issueEvent)
|
||||
{
|
||||
var anchorStateChangedEvent = new AnchorStateChangedEvent(Owner, value);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ref anchorStateChangedEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1046,7 +1045,6 @@ namespace Robust.Shared.GameObjects
|
||||
public readonly struct AnchorStateChangedEvent
|
||||
{
|
||||
public readonly EntityUid Entity;
|
||||
|
||||
public readonly bool Anchored;
|
||||
|
||||
public AnchorStateChangedEvent(EntityUid entity, bool anchored)
|
||||
@@ -1055,4 +1053,28 @@ namespace Robust.Shared.GameObjects
|
||||
Anchored = anchored;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an entity is re-anchored to another grid.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly struct ReAnchorEvent
|
||||
{
|
||||
public readonly EntityUid Entity;
|
||||
public readonly GridId OldGrid;
|
||||
public readonly GridId GridId;
|
||||
|
||||
/// <summary>
|
||||
/// Tile on both the old and new grid being re-anchored.
|
||||
/// </summary>
|
||||
public readonly Vector2i TilePos;
|
||||
|
||||
public ReAnchorEvent(EntityUid uid, GridId oldGrid, GridId gridId, Vector2i tilePos)
|
||||
{
|
||||
Entity = uid;
|
||||
OldGrid = oldGrid;
|
||||
GridId = gridId;
|
||||
TilePos = tilePos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<CollideOnAnchorComponent, ComponentStartup>(OnStartup);
|
||||
// Shouldn't need to handle re-anchor.
|
||||
SubscribeLocalEvent<CollideOnAnchorComponent, AnchorStateChangedEvent>(OnAnchor);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ namespace Robust.Shared.GameObjects
|
||||
SubscribeLocalEvent<CollisionWakeComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<CollisionWakeComponent, ComponentHandleState>(OnHandleState);
|
||||
|
||||
SubscribeLocalEvent<CollisionWakeComponent, PhysicsWakeMessage>(HandleWake);
|
||||
SubscribeLocalEvent<CollisionWakeComponent, PhysicsSleepMessage>(HandleSleep);
|
||||
SubscribeLocalEvent<CollisionWakeComponent, PhysicsWakeEvent>(OnWake);
|
||||
SubscribeLocalEvent<CollisionWakeComponent, PhysicsSleepEvent>(OnSleep);
|
||||
|
||||
SubscribeLocalEvent<CollisionWakeComponent, JointAddedEvent>(HandleJointAdd);
|
||||
SubscribeLocalEvent<CollisionWakeComponent, JointRemovedEvent>(HandleJointRemove);
|
||||
@@ -87,12 +87,12 @@ namespace Robust.Shared.GameObjects
|
||||
args.OurBody.CanCollide = true;
|
||||
}
|
||||
|
||||
private void HandleWake(EntityUid uid, CollisionWakeComponent component, PhysicsWakeMessage args)
|
||||
private void OnWake(EntityUid uid, CollisionWakeComponent component, ref PhysicsWakeEvent args)
|
||||
{
|
||||
UpdateCanCollide(uid, component, args.Body, checkTerminating: false);
|
||||
}
|
||||
|
||||
private void HandleSleep(EntityUid uid, CollisionWakeComponent component, PhysicsSleepMessage args)
|
||||
private void OnSleep(EntityUid uid, CollisionWakeComponent component, ref PhysicsSleepEvent args)
|
||||
{
|
||||
UpdateCanCollide(uid, component, args.Body);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
@@ -17,19 +18,24 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
[Dependency] private readonly FixtureSystem _fixtures = default!;
|
||||
|
||||
private ISawmill _logger = default!;
|
||||
private bool _enabled;
|
||||
|
||||
private float _fixtureEnlargement;
|
||||
private bool _convexHulls = true;
|
||||
|
||||
internal const string ShowGridNodesCommand = "showgridnodes";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_logger = Logger.GetSawmill("physics");
|
||||
UpdatesBefore.Add(typeof(SharedBroadphaseSystem));
|
||||
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
|
||||
configManager.OnValueChanged(CVars.GenerateGridFixtures, SetEnabled, true);
|
||||
configManager.OnValueChanged(CVars.GridFixtureEnlargement, SetEnlargement, true);
|
||||
configManager.OnValueChanged(CVars.ConvexHullPolygons, SetConvexHulls, true);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -40,22 +46,58 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
configManager.UnsubValueChanged(CVars.GenerateGridFixtures, SetEnabled);
|
||||
configManager.UnsubValueChanged(CVars.GridFixtureEnlargement, SetEnlargement);
|
||||
configManager.UnsubValueChanged(CVars.ConvexHullPolygons, SetConvexHulls);
|
||||
}
|
||||
|
||||
private void SetEnabled(bool value) => _enabled = value;
|
||||
|
||||
private void SetEnlargement(float value) => _fixtureEnlargement = value;
|
||||
|
||||
private void SetConvexHulls(bool value) => _convexHulls = value;
|
||||
|
||||
internal void ProcessGrid(IMapGridInternal gridInternal)
|
||||
{
|
||||
// Just in case there's any deleted we'll ToArray
|
||||
foreach (var (_, chunk) in gridInternal.GetMapChunks().ToArray())
|
||||
{
|
||||
gridInternal.RegenerateCollision(chunk);
|
||||
gridInternal.RegenerateCollision(chunk, false);
|
||||
}
|
||||
}
|
||||
|
||||
internal void RegenerateCollision(EntityUid gridEuid, MapChunk chunk, List<Box2i> rectangles)
|
||||
internal void RegenerateCollision(EntityUid gridEuid, Dictionary<MapChunk, List<Box2i>> mapChunks, bool checkSplit = true)
|
||||
{
|
||||
if (!_enabled) return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(gridEuid, out PhysicsComponent? physicsComponent))
|
||||
{
|
||||
_logger.Error($"Trying to regenerate collision for {gridEuid} that doesn't have {nameof(physicsComponent)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(gridEuid, out FixturesComponent? fixturesComponent))
|
||||
{
|
||||
_logger.Error($"Trying to regenerate collision for {gridEuid} that doesn't have {nameof(fixturesComponent)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var fixtures = new List<Fixture>(mapChunks.Count);
|
||||
|
||||
foreach (var (chunk, rectangles) in mapChunks)
|
||||
{
|
||||
UpdateFixture(chunk, rectangles, physicsComponent, fixturesComponent);
|
||||
fixtures.AddRange(chunk.Fixtures);
|
||||
}
|
||||
|
||||
_fixtures.FixtureUpdate(fixturesComponent, physicsComponent);
|
||||
EntityManager.EventBus.RaiseLocalEvent(gridEuid,new GridFixtureChangeEvent {NewFixtures = fixtures});
|
||||
|
||||
foreach (var (chunk, _) in mapChunks)
|
||||
{
|
||||
GenerateSplitNode(gridEuid, chunk, checkSplit);
|
||||
}
|
||||
}
|
||||
|
||||
internal void RegenerateCollision(EntityUid gridEuid, MapChunk chunk, List<Box2i> rectangles, bool checkSplit = true)
|
||||
{
|
||||
if (!_enabled) return;
|
||||
|
||||
@@ -63,16 +105,26 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
if (!EntityManager.TryGetComponent(gridEuid, out PhysicsComponent? physicsComponent))
|
||||
{
|
||||
Logger.ErrorS("physics", $"Trying to regenerate collision for {gridEuid} that doesn't have {nameof(physicsComponent)}");
|
||||
_logger.Error($"Trying to regenerate collision for {gridEuid} that doesn't have {nameof(physicsComponent)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(gridEuid, out FixturesComponent? fixturesComponent))
|
||||
{
|
||||
Logger.ErrorS("physics", $"Trying to regenerate collision for {gridEuid} that doesn't have {nameof(fixturesComponent)}");
|
||||
_logger.Error($"Trying to regenerate collision for {gridEuid} that doesn't have {nameof(fixturesComponent)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (UpdateFixture(chunk, rectangles, physicsComponent, fixturesComponent))
|
||||
{
|
||||
_fixtures.FixtureUpdate(fixturesComponent, physicsComponent);
|
||||
EntityManager.EventBus.RaiseLocalEvent(gridEuid,new GridFixtureChangeEvent {NewFixtures = chunk.Fixtures});
|
||||
GenerateSplitNode(gridEuid, chunk, checkSplit);
|
||||
}
|
||||
}
|
||||
|
||||
private bool UpdateFixture(MapChunk chunk, List<Box2i> rectangles, PhysicsComponent physicsComponent, FixturesComponent fixturesComponent)
|
||||
{
|
||||
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.
|
||||
@@ -97,7 +149,7 @@ namespace Robust.Shared.GameObjects
|
||||
vertices[2] = bounds.TopRight;
|
||||
vertices[3] = bounds.TopLeft;
|
||||
|
||||
poly.SetVertices(vertices);
|
||||
poly.SetVertices(vertices, _convexHulls);
|
||||
|
||||
var newFixture = new Fixture(
|
||||
poly,
|
||||
@@ -161,16 +213,31 @@ namespace Robust.Shared.GameObjects
|
||||
_fixtures.CreateFixture(physicsComponent, fixture, false, fixturesComponent);
|
||||
}
|
||||
|
||||
if (updated)
|
||||
{
|
||||
_fixtures.FixtureUpdate(fixturesComponent, physicsComponent);
|
||||
EntityManager.EventBus.RaiseLocalEvent(gridEuid,new GridFixtureChangeEvent {NewFixtures = chunk.Fixtures});
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
internal virtual void GenerateSplitNode(EntityUid gridEuid, MapChunk chunk, bool checkSplit = true) {}
|
||||
}
|
||||
|
||||
public sealed class GridFixtureChangeEvent : EntityEventArgs
|
||||
{
|
||||
public List<Fixture> NewFixtures { get; init; } = default!;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ChunkSplitDebugMessage : EntityEventArgs
|
||||
{
|
||||
public EntityUid Grid;
|
||||
public Dictionary<Vector2i, List<List<Vector2i>>> Nodes = new ();
|
||||
public List<(Vector2 Start, Vector2 End)> Connections = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised by a client who wants to receive gridsplitnode messages.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RequestGridNodesMessage : EntityEventArgs {}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StopGridNodesMessage : EntityEventArgs {}
|
||||
}
|
||||
|
||||
@@ -152,12 +152,18 @@ namespace Robust.Shared.GameObjects
|
||||
/// <summary>
|
||||
/// Creates a new instance of this class.
|
||||
/// </summary>
|
||||
public TileChangedEvent(TileRef newTile, Tile oldTile)
|
||||
public TileChangedEvent(EntityUid uid, TileRef newTile, Tile oldTile)
|
||||
{
|
||||
Entity = uid;
|
||||
NewTile = newTile;
|
||||
OldTile = oldTile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EntityUid of the grid with the tile-change. TileRef stores the GridId.
|
||||
/// </summary>
|
||||
public EntityUid Entity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// New tile that replaced the old one.
|
||||
/// </summary>
|
||||
|
||||
@@ -18,7 +18,7 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body._linearVelocity = velocity;
|
||||
body.Dirty(EntityManager);
|
||||
Dirty(body);
|
||||
}
|
||||
|
||||
public Box2 GetWorldAABB(PhysicsComponent body, TransformComponent xform, EntityQuery<TransformComponent> xforms, EntityQuery<FixturesComponent> fixtures)
|
||||
|
||||
@@ -70,8 +70,8 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
SubscribeLocalEvent<GridInitializeEvent>(HandleGridInit);
|
||||
SubscribeLocalEvent<CollisionChangeMessage>(HandlePhysicsUpdateMessage);
|
||||
SubscribeLocalEvent<PhysicsWakeMessage>(HandleWakeMessage);
|
||||
SubscribeLocalEvent<PhysicsSleepMessage>(HandleSleepMessage);
|
||||
SubscribeLocalEvent<PhysicsWakeEvent>(OnWake);
|
||||
SubscribeLocalEvent<PhysicsSleepEvent>(OnSleep);
|
||||
SubscribeLocalEvent<EntInsertedIntoContainerMessage>(HandleContainerInserted);
|
||||
SubscribeLocalEvent<EntRemovedFromContainerMessage>(HandleContainerRemoved);
|
||||
SubscribeLocalEvent<PhysicsComponent, EntParentChangedMessage>(OnParentChange);
|
||||
@@ -121,7 +121,7 @@ namespace Robust.Shared.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.CanCollide)
|
||||
if (body._canCollide)
|
||||
_broadphase.UpdateBroadphase(body, xform: xform);
|
||||
|
||||
// Handle map change
|
||||
@@ -130,7 +130,7 @@ namespace Robust.Shared.GameObjects
|
||||
if (args.OldMapId != mapId)
|
||||
HandleMapChange(body, xform, args.OldMapId, mapId);
|
||||
|
||||
if (mapId != MapId.Nullspace && !_container.IsEntityInContainer(uid, meta))
|
||||
if (body.BodyType != BodyType.Static && mapId != MapId.Nullspace && body._canCollide)
|
||||
HandleParentChangeVelocity(uid, body, ref args, xform);
|
||||
}
|
||||
|
||||
@@ -232,26 +232,26 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleWakeMessage(PhysicsWakeMessage message)
|
||||
private void OnWake(ref PhysicsWakeEvent @event)
|
||||
{
|
||||
var mapId = EntityManager.GetComponent<TransformComponent>(message.Body.Owner).MapID;
|
||||
var mapId = EntityManager.GetComponent<TransformComponent>(@event.Body.Owner).MapID;
|
||||
|
||||
if (mapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
EntityUid tempQualifier = MapManager.GetMapEntityId(mapId);
|
||||
EntityManager.GetComponent<SharedPhysicsMapComponent>(tempQualifier).AddAwakeBody(message.Body);
|
||||
EntityManager.GetComponent<SharedPhysicsMapComponent>(tempQualifier).AddAwakeBody(@event.Body);
|
||||
}
|
||||
|
||||
private void HandleSleepMessage(PhysicsSleepMessage message)
|
||||
private void OnSleep(ref PhysicsSleepEvent @event)
|
||||
{
|
||||
var mapId = EntityManager.GetComponent<TransformComponent>(message.Body.Owner).MapID;
|
||||
var mapId = EntityManager.GetComponent<TransformComponent>(@event.Body.Owner).MapID;
|
||||
|
||||
if (mapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
EntityUid tempQualifier = MapManager.GetMapEntityId(mapId);
|
||||
EntityManager.GetComponent<SharedPhysicsMapComponent>(tempQualifier).RemoveSleepBody(message.Body);
|
||||
EntityManager.GetComponent<SharedPhysicsMapComponent>(tempQualifier).RemoveSleepBody(@event.Body);
|
||||
}
|
||||
|
||||
private void HandleContainerInserted(EntInsertedIntoContainerMessage message)
|
||||
|
||||
@@ -4,12 +4,145 @@ using JetBrains.Annotations;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
public abstract partial class SharedTransformSystem
|
||||
{
|
||||
#region Anchoring
|
||||
|
||||
internal void ReAnchor(TransformComponent xform,
|
||||
MapGridComponent oldGrid,
|
||||
MapGridComponent newGrid,
|
||||
Vector2i tilePos,
|
||||
TransformComponent oldGridXform,
|
||||
TransformComponent newGridXform,
|
||||
EntityQuery<TransformComponent> xformQuery)
|
||||
{
|
||||
// Bypass some of the expensive stuff in unanchoring / anchoring.
|
||||
oldGrid.Grid.RemoveFromSnapGridCell(tilePos, xform.Owner);
|
||||
newGrid.Grid.AddToSnapGridCell(tilePos, xform.Owner);
|
||||
// TODO: Could do this re-parent way better.
|
||||
// Unfortunately we don't want any anchoring events to go out hence... this.
|
||||
xform._anchored = false;
|
||||
oldGridXform._children.Remove(xform.Owner);
|
||||
newGridXform._children.Add(xform.Owner);
|
||||
xform._parent = newGrid.Owner;
|
||||
xform._anchored = true;
|
||||
|
||||
SetGridId(xform, newGrid.GridIndex, xformQuery);
|
||||
var movEevee = new MoveEvent(xform.Owner,
|
||||
new EntityCoordinates(oldGrid.Owner, xform._localPosition),
|
||||
new EntityCoordinates(newGrid.Owner, xform._localPosition), xform);
|
||||
RaiseLocalEvent(xform.Owner, ref movEevee);
|
||||
|
||||
DebugTools.Assert(xformQuery.GetComponent(oldGrid.Owner).MapID == xformQuery.GetComponent(newGrid.Owner).MapID);
|
||||
DebugTools.Assert(xform._anchored);
|
||||
|
||||
Dirty(xform);
|
||||
var ev = new ReAnchorEvent(xform.Owner, oldGrid.GridIndex, newGrid.GridIndex, tilePos);
|
||||
RaiseLocalEvent(xform.Owner, ref ev);
|
||||
}
|
||||
|
||||
|
||||
public bool AnchorEntity(TransformComponent xform, IMapGrid grid, Vector2i tileIndices)
|
||||
{
|
||||
var result = grid.AddToSnapGridCell(tileIndices, xform.Owner);
|
||||
|
||||
if (result)
|
||||
{
|
||||
// Mark as static first to avoid the velocity change on parent change.
|
||||
if (TryComp<PhysicsComponent>(xform.Owner, out var physicsComponent))
|
||||
physicsComponent.BodyType = BodyType.Static;
|
||||
|
||||
// anchor snapping
|
||||
// Internally it will do the parent update; doing it separately just triggers a redundant move.
|
||||
xform.Coordinates = new EntityCoordinates(grid.GridEntityId, grid.GridTileToLocal(tileIndices).Position);
|
||||
xform.SetAnchored(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool AnchorEntity(TransformComponent xform, IMapGridComponent component)
|
||||
{
|
||||
return AnchorEntity(xform, component.Grid);
|
||||
}
|
||||
|
||||
public bool AnchorEntity(TransformComponent xform, IMapGrid grid)
|
||||
{
|
||||
var tileIndices = grid.TileIndicesFor(xform.Coordinates);
|
||||
return AnchorEntity(xform, grid, tileIndices);
|
||||
}
|
||||
|
||||
public bool AnchorEntity(TransformComponent xform)
|
||||
{
|
||||
if (!_mapManager.TryGetGrid(xform.GridID, out var grid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var tileIndices = grid.TileIndicesFor(xform.Coordinates);
|
||||
return AnchorEntity(xform, grid, tileIndices);
|
||||
}
|
||||
|
||||
public void Unanchor(TransformComponent xform)
|
||||
{
|
||||
//HACK: Client grid pivot causes this.
|
||||
//TODO: make grid components the actual grid
|
||||
if(xform.GridID == GridId.Invalid)
|
||||
return;
|
||||
|
||||
UnanchorEntity(xform, Comp<IMapGridComponent>(_mapManager.GetGridEuid(xform.GridID)));
|
||||
}
|
||||
|
||||
public void UnanchorEntity(TransformComponent xform, IMapGridComponent grid)
|
||||
{
|
||||
var tileIndices = grid.Grid.TileIndicesFor(xform.Coordinates);
|
||||
grid.Grid.RemoveFromSnapGridCell(tileIndices, xform.Owner);
|
||||
if (TryComp<PhysicsComponent>(xform.Owner, out var physicsComponent))
|
||||
{
|
||||
physicsComponent.BodyType = BodyType.Dynamic;
|
||||
}
|
||||
|
||||
xform.SetAnchored(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GridId
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="GridId"/> for the transformcomponent. Does not Dirty it.
|
||||
/// </summary>
|
||||
public void SetGridId(TransformComponent xform, GridId gridId)
|
||||
{
|
||||
SetGridId(xform, gridId, GetEntityQuery<TransformComponent>());
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SetGridId"/> />
|
||||
private void SetGridId(TransformComponent xform, GridId gridId, EntityQuery<TransformComponent> xformQuery)
|
||||
{
|
||||
if (xform.GridID == gridId) return;
|
||||
|
||||
SetGridIdRecursive(xform, gridId, xformQuery);
|
||||
}
|
||||
|
||||
private static void SetGridIdRecursive(TransformComponent xform, GridId gridId, EntityQuery<TransformComponent> xformQuery)
|
||||
{
|
||||
xform._gridId = gridId;
|
||||
var childEnumerator = xform.ChildEnumerator;
|
||||
|
||||
while (childEnumerator.MoveNext(out var child))
|
||||
{
|
||||
SetGridIdRecursive(xformQuery.GetComponent(child.Value), gridId, xformQuery);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region States
|
||||
|
||||
private void ActivateLerp(TransformComponent xform)
|
||||
@@ -84,7 +217,18 @@ public abstract partial class SharedTransformSystem
|
||||
component._prevPosition = newState.LocalPosition;
|
||||
component._prevRotation = newState.Rotation;
|
||||
|
||||
component.Anchored = newState.Anchored;
|
||||
// Anchored currently does a TryFindGridAt internally which may fail in particularly... violent situations.
|
||||
if (newState.Anchored && !component.Anchored)
|
||||
{
|
||||
var iGrid = Comp<MapGridComponent>(_mapManager.GetGridEuid(component.GridID));
|
||||
AnchorEntity(component, iGrid);
|
||||
component.SetAnchored(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
component.Anchored = newState.Anchored;
|
||||
}
|
||||
|
||||
component._noLocalRotation = newState.NoLocalRotation;
|
||||
|
||||
// This is not possible, because client entities don't exist on the server, so the parent HAS to be a shared entity.
|
||||
@@ -121,6 +265,35 @@ public abstract partial class SharedTransformSystem
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parent
|
||||
|
||||
/* TODO: Need to peel out relevant bits of AttachParent e.g. children updates.
|
||||
public void SetParent(TransformComponent xform, EntityUid parent, bool move = true)
|
||||
{
|
||||
if (xform.ParentUid == parent) return;
|
||||
|
||||
if (!parent.IsValid())
|
||||
{
|
||||
xform.AttachToGridOrMap();
|
||||
return;
|
||||
}
|
||||
|
||||
if (xform.Anchored)
|
||||
{
|
||||
xform.Anchored = false;
|
||||
}
|
||||
|
||||
if (move)
|
||||
xform.AttachParent(parent);
|
||||
else
|
||||
xform._parent = parent;
|
||||
|
||||
Dirty(xform);
|
||||
}
|
||||
*/
|
||||
|
||||
#endregion
|
||||
|
||||
#region World Matrix
|
||||
|
||||
[Pure]
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Robust.Shared.GameObjects
|
||||
var mapTransform = Transform(_mapManager.GetMapEntityId(grid.ParentMapId));
|
||||
var aabb = _entityLookup.GetLocalBounds(tileIndices, grid.TileSize);
|
||||
|
||||
foreach (var entity in _entityLookup.GetEntitiesIntersecting(gridId, tileIndices).ToList())
|
||||
foreach (var entity in _entityLookup.GetEntitiesIntersecting(gridId, tileIndices, LookupFlags.Anchored).ToList())
|
||||
{
|
||||
// If a tile is being removed due to an explosion or somesuch, some entities are likely being deleted.
|
||||
// Avoid unnecessary entity updates.
|
||||
|
||||
@@ -88,7 +88,12 @@ namespace Robust.Shared.Map
|
||||
/// Returns all tiles in the grid, in row-major order [xTileIndex, yTileIndex].
|
||||
/// </summary>
|
||||
/// <returns>All tiles in the chunk.</returns>
|
||||
IEnumerable<TileRef> GetAllTiles(bool ignoreSpace = true);
|
||||
IEnumerable<TileRef> GetAllTiles(bool ignoreEmpty = true);
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumerator that gets all tiles in the grid without empty ones, in row-major order [xTileIndex, yTileIndex].
|
||||
/// </summary>
|
||||
GridTileEnumerator GetAllTilesEnumerator(bool ignoreEmpty = true);
|
||||
|
||||
/// <summary>
|
||||
/// Replaces a single tile inside of the grid.
|
||||
@@ -127,6 +132,7 @@ namespace Robust.Shared.Map
|
||||
|
||||
#region SnapGridAccess
|
||||
|
||||
int AnchoredEntityCount(Vector2i pos);
|
||||
IEnumerable<EntityUid> GetLocalAnchoredEntities(Box2 localAABB);
|
||||
IEnumerable<EntityUid> GetAnchoredEntities(MapCoordinates coords);
|
||||
IEnumerable<EntityUid> GetAnchoredEntities(EntityCoordinates coords);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Shared.Map
|
||||
@@ -24,6 +25,13 @@ namespace Robust.Shared.Map
|
||||
/// </summary>
|
||||
void RemoveChunk(Vector2i origin);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to return a chunk at the given indices.
|
||||
/// </summary>
|
||||
/// <param name="chunk"></param>
|
||||
/// <returns></returns>
|
||||
bool TryGetChunk(Vector2i chunkIndices, [NotNullWhen(true)] out MapChunk? chunk);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the chunk at the given indices. If the chunk does not exist,
|
||||
/// then a new one is generated that is filled with empty space.
|
||||
@@ -33,7 +41,7 @@ namespace Robust.Shared.Map
|
||||
MapChunk GetChunk(Vector2i chunkIndices);
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether a chunk exists with the specified indices.
|
||||
/// Returns whether a chunk exists with the specified indices.
|
||||
/// </summary>
|
||||
bool HasChunk(Vector2i chunkIndices);
|
||||
|
||||
@@ -56,7 +64,7 @@ namespace Robust.Shared.Map
|
||||
/// <summary>
|
||||
/// Regenerates the chunk local bounds of this chunk.
|
||||
/// </summary>
|
||||
void RegenerateCollision(MapChunk mapChunk);
|
||||
void RegenerateCollision(MapChunk mapChunk, bool checkSplit = true);
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the world space AABB for this chunk.
|
||||
@@ -66,11 +74,10 @@ namespace Robust.Shared.Map
|
||||
/// <summary>
|
||||
/// Returns the tile at the given chunk indices.
|
||||
/// </summary>
|
||||
/// <param name="mapId"></param>
|
||||
/// <param name="mapChunk"></param>
|
||||
/// <param name="xIndex">The X tile index relative to the chunk origin.</param>
|
||||
/// <param name="yIndex">The Y tile index relative to the chunk origin.</param>
|
||||
/// <returns>A reference to a tile.</returns>
|
||||
TileRef GetTileRef(MapId mapId, MapChunk mapChunk, ushort xIndex, ushort yIndex);
|
||||
TileRef GetTileRef(MapChunk mapChunk, ushort xIndex, ushort yIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,22 +198,21 @@ namespace Robust.Shared.Map
|
||||
if (!_chunks.TryGetValue(chunkIndices, out var output))
|
||||
{
|
||||
// Chunk doesn't exist, return a tileRef to an empty (space) tile.
|
||||
return new TileRef(ParentMapId, Index, tileCoordinates.X, tileCoordinates.Y, default);
|
||||
return new TileRef(Index, tileCoordinates.X, tileCoordinates.Y, default);
|
||||
}
|
||||
|
||||
var chunkTileIndices = output.GridTileToChunkTile(tileCoordinates);
|
||||
return GetTileRef(ParentMapId, output, (ushort)chunkTileIndices.X, (ushort)chunkTileIndices.Y);
|
||||
return GetTileRef(output, (ushort)chunkTileIndices.X, (ushort)chunkTileIndices.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the tile at the given chunk indices.
|
||||
/// </summary>
|
||||
/// <param name="mapId">The MapId of the tile. Passed in as this isn't cached on grids.</param>
|
||||
/// <param name="mapChunk"></param>
|
||||
/// <param name="xIndex">The X tile index relative to the chunk origin.</param>
|
||||
/// <param name="yIndex">The Y tile index relative to the chunk origin.</param>
|
||||
/// <returns>A reference to a tile.</returns>
|
||||
public TileRef GetTileRef(MapId mapId, MapChunk mapChunk, ushort xIndex, ushort yIndex)
|
||||
public TileRef GetTileRef(MapChunk mapChunk, ushort xIndex, ushort yIndex)
|
||||
{
|
||||
if (xIndex >= mapChunk.ChunkSize)
|
||||
throw new ArgumentOutOfRangeException(nameof(xIndex), "Tile indices out of bounds.");
|
||||
@@ -222,11 +221,11 @@ namespace Robust.Shared.Map
|
||||
throw new ArgumentOutOfRangeException(nameof(yIndex), "Tile indices out of bounds.");
|
||||
|
||||
var indices = mapChunk.ChunkTileToGridTile(new Vector2i(xIndex, yIndex));
|
||||
return new TileRef(ParentMapId, Index, indices, mapChunk.GetTile(xIndex, yIndex));
|
||||
return new TileRef(Index, indices, mapChunk.GetTile(xIndex, yIndex));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TileRef> GetAllTiles(bool ignoreSpace = true)
|
||||
public IEnumerable<TileRef> GetAllTiles(bool ignoreEmpty = true)
|
||||
{
|
||||
foreach (var kvChunk in _chunks)
|
||||
{
|
||||
@@ -237,16 +236,22 @@ namespace Robust.Shared.Map
|
||||
{
|
||||
var tile = chunk.GetTile(x, y);
|
||||
|
||||
if (ignoreSpace && tile.IsEmpty)
|
||||
if (ignoreEmpty && tile.IsEmpty)
|
||||
continue;
|
||||
|
||||
var (gridX, gridY) = new Vector2i(x, y) + chunk.Indices * ChunkSize;
|
||||
yield return new TileRef(ParentMapId, Index, gridX, gridY, tile);
|
||||
yield return new TileRef(Index, gridX, gridY, tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public GridTileEnumerator GetAllTilesEnumerator(bool ignoreEmpty = true)
|
||||
{
|
||||
return new GridTileEnumerator(Index, _chunks.GetEnumerator(), ChunkSize, ignoreEmpty);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetTile(EntityCoordinates coords, Tile tile)
|
||||
{
|
||||
@@ -277,8 +282,9 @@ namespace Robust.Shared.Map
|
||||
foreach (var chunk in chunks)
|
||||
{
|
||||
chunk.SuppressCollisionRegeneration = false;
|
||||
RegenerateCollision(chunk);
|
||||
}
|
||||
|
||||
RegenerateCollision(chunks);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -310,7 +316,6 @@ namespace Robust.Shared.Map
|
||||
{
|
||||
// TODO: Should move the intersecting calls onto mapmanager system and then allow people to pass in xform / xformquery
|
||||
// that way we can avoid the GetComp here.
|
||||
var mapId = ParentMapId;
|
||||
var gridTileLb = new Vector2i((int)Math.Floor(localArea.Left), (int)Math.Floor(localArea.Bottom));
|
||||
var gridTileRt = new Vector2i((int)Math.Floor(localArea.Right), (int)Math.Floor(localArea.Top));
|
||||
|
||||
@@ -323,7 +328,7 @@ namespace Robust.Shared.Map
|
||||
if (_chunks.TryGetValue(gridChunk, out var chunk))
|
||||
{
|
||||
var chunkTile = chunk.GridTileToChunkTile(new Vector2i(x, y));
|
||||
var tile = GetTileRef(mapId, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y);
|
||||
var tile = GetTileRef(chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y);
|
||||
|
||||
if (ignoreEmpty && tile.Tile.IsEmpty)
|
||||
continue;
|
||||
@@ -333,7 +338,7 @@ namespace Robust.Shared.Map
|
||||
}
|
||||
else if (!ignoreEmpty)
|
||||
{
|
||||
var tile = new TileRef(mapId, Index, x, y, new Tile());
|
||||
var tile = new TileRef(Index, x, y, new Tile());
|
||||
|
||||
if (predicate == null || predicate(tile))
|
||||
yield return tile;
|
||||
@@ -396,6 +401,11 @@ namespace Robust.Shared.Map
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetChunk(Vector2i chunkIndices, [NotNullWhen(true)] out MapChunk? chunk)
|
||||
{
|
||||
return _chunks.TryGetValue(chunkIndices, out chunk);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MapChunk GetChunk(Vector2i chunkIndices)
|
||||
{
|
||||
@@ -492,6 +502,17 @@ namespace Robust.Shared.Map
|
||||
|
||||
#region SnapGridAccess
|
||||
|
||||
/// <inheritdoc />
|
||||
public int AnchoredEntityCount(Vector2i pos)
|
||||
{
|
||||
var gridChunkPos = GridTileToChunkIndices(pos);
|
||||
|
||||
if (!_chunks.TryGetValue(gridChunkPos, out var chunk)) return 0;
|
||||
|
||||
var (x, y) = chunk.GridTileToChunkTile(pos);
|
||||
return chunk.GetSnapGrid((ushort) x, (ushort) y)?.Count ?? 0; // ?
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<EntityUid> GetAnchoredEntities(MapCoordinates coords)
|
||||
{
|
||||
@@ -704,9 +725,11 @@ namespace Robust.Shared.Map
|
||||
for (var y = -n; y <= n; ++y)
|
||||
for (var x = -n; x <= n; ++x)
|
||||
{
|
||||
foreach (var cell in GetAnchoredEntities(position + new Vector2i(x, y)))
|
||||
var enumerator = GetAnchoredEntitiesEnumerator(position + new Vector2i(x, y));
|
||||
|
||||
while (enumerator.MoveNext(out var cell))
|
||||
{
|
||||
yield return cell;
|
||||
yield return cell.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -836,7 +859,7 @@ namespace Robust.Shared.Map
|
||||
}
|
||||
|
||||
var cTileIndices = chunk.GridTileToChunkTile(indices);
|
||||
tile = GetTileRef(ParentMapId, chunk, (ushort)cTileIndices.X, (ushort)cTileIndices.Y);
|
||||
tile = GetTileRef(chunk, (ushort)cTileIndices.X, (ushort)cTileIndices.Y);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -854,14 +877,74 @@ namespace Robust.Shared.Map
|
||||
|
||||
#endregion Transforms
|
||||
|
||||
/// <summary>
|
||||
/// Regenerate collision for multiple chunks at once; faster than doing it individually.
|
||||
/// </summary>
|
||||
public void RegenerateCollision(IReadOnlySet<MapChunk> chunks, bool checkSplit = true)
|
||||
{
|
||||
var chunkRectangles = new Dictionary<MapChunk, List<Box2i>>(chunks.Count);
|
||||
var fixtureSystem = EntitySystem.Get<FixtureSystem>();
|
||||
|
||||
foreach (var mapChunk in chunks)
|
||||
{
|
||||
// Even if the chunk is still removed still need to make sure bounds are updated (for now...)
|
||||
// generate collision rectangles for this chunk based on filled tiles.
|
||||
GridChunkPartition.PartitionChunk(mapChunk, out var localBounds, out var rectangles);
|
||||
mapChunk.CachedBounds = localBounds;
|
||||
|
||||
if (mapChunk.FilledTiles > 0)
|
||||
chunkRectangles.Add(mapChunk, rectangles);
|
||||
else
|
||||
{
|
||||
RemoveChunk(mapChunk.Indices);
|
||||
// Gone. Reduced to atoms
|
||||
foreach (var fixture in mapChunk.Fixtures)
|
||||
{
|
||||
fixtureSystem.DestroyFixture(fixture, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LocalBounds = new Box2();
|
||||
foreach (var chunk in _chunks.Values)
|
||||
{
|
||||
var chunkBounds = chunk.CachedBounds;
|
||||
|
||||
if(chunkBounds.Size.Equals(Vector2i.Zero))
|
||||
continue;
|
||||
|
||||
if (LocalBounds.Size == Vector2.Zero)
|
||||
{
|
||||
var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize);
|
||||
LocalBounds = gridBounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize);
|
||||
LocalBounds = LocalBounds.Union(gridBounds);
|
||||
}
|
||||
}
|
||||
|
||||
if (chunkRectangles.Count == 0)
|
||||
fixtureSystem.FixtureUpdate(_entityManager.GetComponent<FixturesComponent>(GridEntityId));
|
||||
else if (_entityManager.EntitySysManager.TryGetEntitySystem(out SharedGridFixtureSystem? system))
|
||||
system.RegenerateCollision(GridEntityId, chunkRectangles, checkSplit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regenerates the chunk local bounds of this chunk.
|
||||
/// </summary>
|
||||
public void RegenerateCollision(MapChunk mapChunk)
|
||||
public void RegenerateCollision(MapChunk mapChunk, bool checkSplit = true)
|
||||
{
|
||||
// Even if the chunk is still removed still need to make sure bounds are updated (for now...)
|
||||
if (mapChunk.FilledTiles == 0)
|
||||
{
|
||||
var fixtureSystem = EntitySystem.Get<FixtureSystem>();
|
||||
foreach (var fixture in mapChunk.Fixtures)
|
||||
{
|
||||
fixtureSystem.DestroyFixture(fixture);
|
||||
}
|
||||
|
||||
RemoveChunk(mapChunk.Indices);
|
||||
}
|
||||
|
||||
@@ -891,7 +974,7 @@ namespace Robust.Shared.Map
|
||||
|
||||
// TryGet because unit tests YAY
|
||||
if (mapChunk.FilledTiles > 0 && _entityManager.EntitySysManager.TryGetEntitySystem(out SharedGridFixtureSystem? system))
|
||||
system.RegenerateCollision(GridEntityId, mapChunk, rectangles);
|
||||
system.RegenerateCollision(GridEntityId, mapChunk, rectangles, checkSplit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -926,7 +1009,7 @@ namespace Robust.Shared.Map
|
||||
// ParentMapId is not able to be accessed on unbound grids, so we can't even call this function for unbound grids.
|
||||
if(!_mapManager.SuppressOnTileChanged)
|
||||
{
|
||||
var newTileRef = new TileRef(ParentMapId, Index, gridTile, newTile);
|
||||
var newTileRef = new TileRef(Index, gridTile, newTile);
|
||||
_mapManager.RaiseOnTileChanged(newTileRef, oldTile);
|
||||
}
|
||||
|
||||
@@ -944,4 +1027,56 @@ namespace Robust.Shared.Map
|
||||
{
|
||||
public GridId GridId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all tiles on a grid.
|
||||
/// </summary>
|
||||
public struct GridTileEnumerator
|
||||
{
|
||||
private GridId _gridId;
|
||||
private Dictionary<Vector2i, MapChunk>.Enumerator _chunkEnumerator;
|
||||
private readonly ushort _chunkSize;
|
||||
private int _index;
|
||||
private bool _ignoreEmpty;
|
||||
|
||||
internal GridTileEnumerator(GridId gridId, Dictionary<Vector2i, MapChunk>.Enumerator chunkEnumerator, ushort chunkSize, bool ignoreEmpty)
|
||||
{
|
||||
_gridId = gridId;
|
||||
_chunkEnumerator = chunkEnumerator;
|
||||
_chunkSize = chunkSize;
|
||||
_index = _chunkSize * _chunkSize;
|
||||
_ignoreEmpty = ignoreEmpty;
|
||||
}
|
||||
|
||||
public bool MoveNext([NotNullWhen(true)] out TileRef? tileRef)
|
||||
{
|
||||
if (_index == _chunkSize * _chunkSize)
|
||||
{
|
||||
if (!_chunkEnumerator.MoveNext())
|
||||
{
|
||||
tileRef = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
_index = 0;
|
||||
}
|
||||
|
||||
var (chunkOrigin, chunk) = _chunkEnumerator.Current;
|
||||
|
||||
var x = (ushort) (_index / _chunkSize);
|
||||
var y = (ushort) (_index % _chunkSize);
|
||||
var tile = chunk.GetTile(x, y);
|
||||
_index++;
|
||||
|
||||
if (!_ignoreEmpty && tile.IsEmpty)
|
||||
{
|
||||
return MoveNext(out tileRef);
|
||||
}
|
||||
|
||||
var gridX = x + chunkOrigin.X * _chunkSize;
|
||||
var gridY = y + chunkOrigin.Y * _chunkSize;
|
||||
tileRef = new TileRef(_gridId, gridX, gridY, tile);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ internal partial class MapManager
|
||||
|
||||
TileChanged?.Invoke(this, new TileChangedEventArgs(tileRef, oldTile));
|
||||
var euid = GetGridEuid(tileRef.GridIndex);
|
||||
EntityManager.EventBus.RaiseLocalEvent(euid, new TileChangedEvent(tileRef, oldTile));
|
||||
EntityManager.EventBus.RaiseLocalEvent(euid, new TileChangedEvent(euid, tileRef, oldTile));
|
||||
}
|
||||
|
||||
protected MapGrid CreateGrid(MapId currentMapId, GridId? forcedGridId, ushort chunkSize, EntityUid forcedGridEuid)
|
||||
|
||||
@@ -42,21 +42,6 @@ internal sealed class NetworkedMapManager : MapManager, INetworkedMapManager
|
||||
}
|
||||
|
||||
chunks.Add((GameTiming.CurTick, chunk.Indices));
|
||||
|
||||
// Seemed easier than having this method on GridFixtureSystem
|
||||
if (!TryGetGrid(gridId, out var grid) ||
|
||||
!EntityManager.TryGetComponent(grid.GridEntityId, out PhysicsComponent? body) ||
|
||||
chunk.Fixtures.Count == 0)
|
||||
return;
|
||||
|
||||
// TODO: Like MapManager injecting this is a PITA so need to work out an easy way to do it.
|
||||
// Maybe just add like a PostInject method that gets called way later?
|
||||
var fixtureSystem = EntitySystem.Get<FixtureSystem>();
|
||||
|
||||
foreach (var fixture in chunk.Fixtures)
|
||||
{
|
||||
fixtureSystem.DestroyFixture(body, fixture);
|
||||
}
|
||||
}
|
||||
|
||||
public GameStateMapData? GetStateData(GameTick fromTick)
|
||||
|
||||
@@ -10,12 +10,7 @@ namespace Robust.Shared.Map
|
||||
[PublicAPI]
|
||||
public readonly struct TileRef : IEquatable<TileRef>
|
||||
{
|
||||
public static TileRef Zero => new(MapId.Nullspace, GridId.Invalid, Vector2i.Zero, Tile.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the <see cref="MapManager.Map"/> this Tile belongs to.
|
||||
/// </summary>
|
||||
public readonly MapId MapIndex;
|
||||
public static TileRef Zero => new(GridId.Invalid, Vector2i.Zero, Tile.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the <see cref="MapGrid"/> this Tile belongs to.
|
||||
@@ -35,24 +30,21 @@ namespace Robust.Shared.Map
|
||||
/// <summary>
|
||||
/// Constructs a new instance of TileRef.
|
||||
/// </summary>
|
||||
/// <param name="mapId">Identifier of the map this tile belongs to.</param>
|
||||
/// <param name="gridId">Identifier of the grid this tile belongs to.</param>
|
||||
/// <param name="xIndex">Positional X index of this tile on the grid.</param>
|
||||
/// <param name="yIndex">Positional Y index of this tile on the grid.</param>
|
||||
/// <param name="tile">Actual data of this tile.</param>
|
||||
internal TileRef(MapId mapId, GridId gridId, int xIndex, int yIndex, Tile tile)
|
||||
: this(mapId, gridId, new Vector2i(xIndex, yIndex), tile) { }
|
||||
internal TileRef(GridId gridId, int xIndex, int yIndex, Tile tile)
|
||||
: this(gridId, new Vector2i(xIndex, yIndex), tile) { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of TileRef.
|
||||
/// </summary>
|
||||
/// <param name="mapId">Identifier of the map this tile belongs to.</param>
|
||||
/// <param name="gridId">Identifier of the grid this tile belongs to.</param>
|
||||
/// <param name="gridIndices">Positional indices of this tile on the grid.</param>
|
||||
/// <param name="tile">Actual data of this tile.</param>
|
||||
internal TileRef(MapId mapId, GridId gridId, Vector2i gridIndices, Tile tile)
|
||||
internal TileRef(GridId gridId, Vector2i gridIndices, Tile tile)
|
||||
{
|
||||
MapIndex = mapId;
|
||||
GridIndex = gridId;
|
||||
GridIndices = gridIndices;
|
||||
Tile = tile;
|
||||
@@ -77,10 +69,9 @@ namespace Robust.Shared.Map
|
||||
/// <inheritdoc />
|
||||
public bool Equals(TileRef other)
|
||||
{
|
||||
return MapIndex.Equals(other.MapIndex)
|
||||
&& GridIndex.Equals(other.GridIndex)
|
||||
&& GridIndices.Equals(other.GridIndices)
|
||||
&& Tile.Equals(other.Tile);
|
||||
return GridIndex.Equals(other.GridIndex) &&
|
||||
GridIndices.Equals(other.GridIndices) &&
|
||||
Tile.Equals(other.Tile);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -112,8 +103,7 @@ namespace Robust.Shared.Map
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hashCode = MapIndex.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ GridIndex.GetHashCode();
|
||||
var hashCode = GridIndex.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ GridIndices.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ Tile.GetHashCode();
|
||||
return hashCode;
|
||||
|
||||
@@ -89,10 +89,14 @@ namespace Robust.Shared.Physics.Collision.Shapes
|
||||
{
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
DebugTools.Assert(vertices.Length >= 3 && vertices.Length <= configManager.GetCVar(CVars.MaxPolygonVertices));
|
||||
SetVertices(vertices, configManager.GetCVar(CVars.ConvexHullPolygons));
|
||||
}
|
||||
|
||||
public void SetVertices(Span<Vector2> vertices, bool convexHulls)
|
||||
{
|
||||
var vertexCount = vertices.Length;
|
||||
|
||||
if (configManager.GetCVar(CVars.ConvexHullPolygons))
|
||||
if (convexHulls)
|
||||
{
|
||||
//FPE note: This check is required as the GiftWrap algorithm early exits on triangles
|
||||
//So instead of giftwrapping a triangle, we just force it to be clock wise.
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Robust.Shared.Physics.Dynamics
|
||||
|
||||
[DataField("id")]
|
||||
private string? _id;
|
||||
|
||||
|
||||
[ViewVariables]
|
||||
[field: NonSerialized]
|
||||
public FixtureProxy[] Proxies { get; set; } = Array.Empty<FixtureProxy>();
|
||||
@@ -127,7 +127,7 @@ namespace Robust.Shared.Physics.Dynamics
|
||||
}
|
||||
|
||||
[DataField("restitution")]
|
||||
private float _restitution = 0f;
|
||||
internal float _restitution = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Non-hard <see cref="IPhysBody"/>s will not cause action collision (e.g. blocking of movement)
|
||||
@@ -146,6 +146,7 @@ namespace Robust.Shared.Physics.Dynamics
|
||||
return;
|
||||
|
||||
_hard = value;
|
||||
Body.CanCollide = true;
|
||||
Body.Awake = true;
|
||||
EntitySystem.Get<FixtureSystem>().FixtureUpdate(IoCManager.Resolve<IEntityManager>().GetComponent<FixturesComponent>(Body.Owner), Body);
|
||||
}
|
||||
@@ -179,7 +180,7 @@ namespace Robust.Shared.Physics.Dynamics
|
||||
}
|
||||
|
||||
[DataField("mass")]
|
||||
private float _mass;
|
||||
internal float _mass;
|
||||
|
||||
/// <summary>
|
||||
/// Bitmask of the collision layers the component is a part of.
|
||||
|
||||
@@ -388,6 +388,28 @@ namespace Robust.Shared.Physics
|
||||
}
|
||||
}
|
||||
|
||||
#region Mass
|
||||
|
||||
public void SetMass(Fixture fixture, float value, FixturesComponent? manager = null, bool update = true)
|
||||
{
|
||||
fixture._mass = value;
|
||||
if (update && Resolve(fixture.Body.Owner, ref manager))
|
||||
FixtureUpdate(manager);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Restitution
|
||||
|
||||
public void SetRestitution(Fixture fixture, float value, FixturesComponent? manager = null, bool update = true)
|
||||
{
|
||||
fixture._restitution = value;
|
||||
if (update && Resolve(fixture.Body.Owner, ref manager))
|
||||
FixtureUpdate(manager);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Updates all of the cached physics information on the body derived from fixtures.
|
||||
/// </summary>
|
||||
|
||||
26
Robust.Shared/Physics/PhysicsWakeEvent.cs
Normal file
26
Robust.Shared/Physics/PhysicsWakeEvent.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Shared.Physics
|
||||
{
|
||||
[ByRefEvent]
|
||||
public readonly struct PhysicsWakeEvent
|
||||
{
|
||||
public readonly PhysicsComponent Body;
|
||||
|
||||
public PhysicsWakeEvent(PhysicsComponent component)
|
||||
{
|
||||
Body = component;
|
||||
}
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public readonly struct PhysicsSleepEvent
|
||||
{
|
||||
public readonly PhysicsComponent Body;
|
||||
|
||||
public PhysicsSleepEvent(PhysicsComponent component)
|
||||
{
|
||||
Body = component;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Shared.Physics
|
||||
{
|
||||
// Real pros use the system messages
|
||||
public sealed class PhysicsWakeMessage : EntityEventArgs
|
||||
{
|
||||
public PhysicsComponent Body { get; }
|
||||
|
||||
public PhysicsWakeMessage(PhysicsComponent component)
|
||||
{
|
||||
Body = component;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PhysicsSleepMessage : EntityEventArgs
|
||||
{
|
||||
public PhysicsComponent Body { get; }
|
||||
|
||||
public PhysicsSleepMessage(PhysicsComponent component)
|
||||
{
|
||||
Body = component;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,9 +81,9 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems
|
||||
entMan.EventBus.SubscribeEvent<MoveEvent>(EventSource.Local, subscriber, MoveEventHandler);
|
||||
|
||||
// Act
|
||||
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(ent1).Anchored = true;
|
||||
entMan.GetComponent<TransformComponent>(ent1).Anchored = true;
|
||||
|
||||
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(ent1).WorldPosition, Is.EqualTo(new Vector2(7.5f, 7.5f))); // centered on tile
|
||||
Assert.That(entMan.GetComponent<TransformComponent>(ent1).WorldPosition, Is.EqualTo(new Vector2(7.5f, 7.5f))); // centered on tile
|
||||
Assert.That(calledCount, Is.EqualTo(1)); // because the ent was moved from snapping, a MoveEvent was raised.
|
||||
void MoveEventHandler(ref MoveEvent ev)
|
||||
{
|
||||
|
||||
@@ -44,17 +44,17 @@ namespace Robust.UnitTesting.Shared.Map
|
||||
Assert.That(MathHelper.CloseToPercent(Box2.Area(bounds), 1.0f, 0.1f));
|
||||
|
||||
// Now do 2 tiles (same chunk)
|
||||
grid.SetTile(Vector2i.One, new Tile(1));
|
||||
grid.SetTile(new Vector2i(0, 1), new Tile(1));
|
||||
|
||||
Assert.That(manager.FixtureCount, Is.EqualTo(2));
|
||||
Assert.That(manager.FixtureCount, Is.EqualTo(1));
|
||||
bounds = manager.Fixtures.First().Value.Shape.ComputeAABB(new Transform(Vector2.Zero, (float) Angle.Zero.Theta), 0);
|
||||
|
||||
// Even if we add a new tile old fixture should stay the same if they don't connect.
|
||||
Assert.That(MathHelper.CloseToPercent(Box2.Area(bounds), 1.0f, 0.1f));
|
||||
Assert.That(MathHelper.CloseToPercent(Box2.Area(bounds), 2.0f, 0.1f));
|
||||
|
||||
// If we add a new chunk should be 3 now
|
||||
grid.SetTile(new Vector2i(-1, -1), new Tile(1));
|
||||
Assert.That(manager.FixtureCount, Is.EqualTo(3));
|
||||
// If we add a new chunk should be 2 now
|
||||
grid.SetTile(new Vector2i(0, -1), new Tile(1));
|
||||
Assert.That(manager.FixtureCount, Is.EqualTo(2));
|
||||
|
||||
gridBody.LinearVelocity = Vector2.One;
|
||||
Assert.That(gridBody.LinearVelocity.Length, Is.EqualTo(0f));
|
||||
|
||||
133
Robust.UnitTesting/Shared/Map/GridSplit_Tests.cs
Normal file
133
Robust.UnitTesting/Shared/Map/GridSplit_Tests.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.UnitTesting.Server;
|
||||
|
||||
namespace Robust.UnitTesting.Shared.Map;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class GridSplit_Tests
|
||||
{
|
||||
[Test]
|
||||
public void SimpleSplit()
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
|
||||
var mapManager = sim.Resolve<IMapManager>();
|
||||
var mapId = mapManager.CreateMap();
|
||||
var grid = mapManager.CreateGrid(mapId);
|
||||
|
||||
for (var x = 0; x < 3; x++)
|
||||
{
|
||||
grid.SetTile(new Vector2i(x, 0), new Tile(1));
|
||||
}
|
||||
|
||||
Assert.That(mapManager.GetAllMapGrids(mapId).Count(), Is.EqualTo(1));
|
||||
|
||||
grid.SetTile(new Vector2i(1, 0), Tile.Empty);
|
||||
Assert.That(mapManager.GetAllMapGrids(mapId).Count(), Is.EqualTo(2));
|
||||
|
||||
mapManager.DeleteMap(mapId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DonutSplit()
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
|
||||
var mapManager = sim.Resolve<IMapManager>();
|
||||
var mapId = mapManager.CreateMap();
|
||||
var grid = mapManager.CreateGrid(mapId);
|
||||
|
||||
for (var x = 0; x < 3; x++)
|
||||
{
|
||||
for (var y = 0; y < 3; y++)
|
||||
{
|
||||
grid.SetTile(new Vector2i(x, y), new Tile(1));
|
||||
}
|
||||
}
|
||||
|
||||
Assert.That(mapManager.GetAllMapGrids(mapId).Count(), Is.EqualTo(1));
|
||||
|
||||
grid.SetTile(Vector2i.One, Tile.Empty);
|
||||
Assert.That(mapManager.GetAllMapGrids(mapId).Count(), Is.EqualTo(1));
|
||||
|
||||
grid.SetTile(new Vector2i(1, 2), Tile.Empty);
|
||||
Assert.That(mapManager.GetAllMapGrids(mapId).Count(), Is.EqualTo(1));
|
||||
|
||||
grid.SetTile(new Vector2i(1, 0), Tile.Empty);
|
||||
Assert.That(mapManager.GetAllMapGrids(mapId).Count(), Is.EqualTo(2));
|
||||
|
||||
mapManager.DeleteMap(mapId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TriSplit()
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
|
||||
var mapManager = sim.Resolve<IMapManager>();
|
||||
var mapId = mapManager.CreateMap();
|
||||
var grid = mapManager.CreateGrid(mapId);
|
||||
|
||||
for (var x = 0; x < 3; x++)
|
||||
{
|
||||
grid.SetTile(new Vector2i(x, 0), new Tile(1));
|
||||
}
|
||||
|
||||
grid.SetTile(Vector2i.One, new Tile(1));
|
||||
|
||||
Assert.That(mapManager.GetAllMapGrids(mapId).Count(), Is.EqualTo(1));
|
||||
|
||||
grid.SetTile(new Vector2i(1, 0), Tile.Empty);
|
||||
Assert.That(mapManager.GetAllMapGrids(mapId).Count(), Is.EqualTo(3));
|
||||
|
||||
mapManager.DeleteMap(mapId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks GridId and Parents update correctly for re-parented entities.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ReparentSplit()
|
||||
{
|
||||
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
|
||||
var entManager = sim.Resolve<IEntityManager>();
|
||||
var mapManager = sim.Resolve<IMapManager>();
|
||||
var mapId = mapManager.CreateMap();
|
||||
var grid = mapManager.CreateGrid(mapId);
|
||||
|
||||
for (var x = 0; x < 4; x++)
|
||||
{
|
||||
grid.SetTile(new Vector2i(x, 0), new Tile(1));
|
||||
}
|
||||
|
||||
Assert.That(mapManager.GetAllMapGrids(mapId).Count(), Is.EqualTo(1));
|
||||
|
||||
var dummy = entManager.SpawnEntity(null, new EntityCoordinates(grid.GridEntityId, new Vector2(3.5f, 0.5f)));
|
||||
var dummyXform = entManager.GetComponent<TransformComponent>(dummy);
|
||||
var anchored = entManager.SpawnEntity(null, new EntityCoordinates(grid.GridEntityId, new Vector2(3.5f, 0.5f)));
|
||||
var anchoredXform = entManager.GetComponent<TransformComponent>(anchored);
|
||||
anchoredXform.Anchored = true;
|
||||
Assert.That(anchoredXform.Anchored);
|
||||
|
||||
grid.SetTile(new Vector2i(2, 0), Tile.Empty);
|
||||
Assert.That(mapManager.GetAllMapGrids(mapId).Count(), Is.EqualTo(2));
|
||||
|
||||
var newGrid = mapManager.GetAllMapGrids(mapId).Last();
|
||||
var newGridXform = entManager.GetComponent<TransformComponent>(newGrid.GridEntityId);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// Assertions baby
|
||||
Assert.That(anchoredXform.Anchored);
|
||||
Assert.That(anchoredXform.ParentUid, Is.EqualTo(newGrid.GridEntityId));
|
||||
Assert.That(anchoredXform.GridID, Is.EqualTo(newGrid.Index));
|
||||
Assert.That(newGridXform._children, Does.Contain(anchored));
|
||||
|
||||
Assert.That(dummyXform.ParentUid, Is.EqualTo(newGrid.GridEntityId));
|
||||
Assert.That(dummyXform.GridID, Is.EqualTo(newGrid.Index));
|
||||
Assert.That(newGridXform._children, Does.Contain(dummy));
|
||||
});
|
||||
mapManager.DeleteMap(mapId);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace Robust.UnitTesting.Shared.Map
|
||||
|
||||
Assert.That(grid.ChunkCount, Is.EqualTo(1));
|
||||
Assert.That(grid.GetMapChunks().Keys.ToList()[0], Is.EqualTo(new Vector2i(-2, -1)));
|
||||
Assert.That(result, Is.EqualTo(new TileRef(mapId, grid.Index, new Vector2i(-9,-1), new Tile(1, (TileRenderFlag)1, 1))));
|
||||
Assert.That(result, Is.EqualTo(new TileRef(grid.Index, new Vector2i(-9,-1), new Tile(1, (TileRenderFlag)1, 1))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -151,7 +151,7 @@ namespace Robust.UnitTesting.Shared.Map
|
||||
Assert.That(foundTile, Is.True);
|
||||
Assert.That(grid.ChunkCount, Is.EqualTo(1));
|
||||
Assert.That(grid.GetMapChunks().Keys.ToList()[0], Is.EqualTo(new Vector2i(-2, -1)));
|
||||
Assert.That(tileRef, Is.EqualTo(new TileRef(mapId, grid.Index, new Vector2i(-9, -1), new Tile(1, (TileRenderFlag)1, 1))));
|
||||
Assert.That(tileRef, Is.EqualTo(new TileRef(grid.Index, new Vector2i(-9, -1), new Tile(1, (TileRenderFlag)1, 1))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -39,14 +39,17 @@ namespace Robust.UnitTesting.Shared.Physics
|
||||
IMapGrid grid = default!;
|
||||
MapId mapId = default!;
|
||||
PhysicsComponent physics = default!;
|
||||
TransformComponent xform = default!;
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapId = mapManager.CreateMap();
|
||||
grid = mapManager.CreateGrid(mapId);
|
||||
grid.SetTile(Vector2i.Zero, new Tile(1));
|
||||
|
||||
var entity = entManager.SpawnEntity("CollisionWakeTestItem", new MapCoordinates(Vector2.One, mapId));
|
||||
var entity = entManager.SpawnEntity("CollisionWakeTestItem", new MapCoordinates(Vector2.One * 2f, mapId));
|
||||
physics = entManager.GetComponent<PhysicsComponent>(entity);
|
||||
xform = entManager.GetComponent<TransformComponent>(entity);
|
||||
});
|
||||
|
||||
// Should still be collidable
|
||||
@@ -57,7 +60,8 @@ namespace Robust.UnitTesting.Shared.Physics
|
||||
Assert.That(physics.Awake, Is.EqualTo(false));
|
||||
Assert.That(physics.CanCollide, Is.EqualTo(true));
|
||||
|
||||
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(physics.Owner).AttachParent(grid.GridEntityId);
|
||||
xform.LocalPosition = new Vector2(0.5f, 0.5f);
|
||||
xform.AttachParent(grid.GridEntityId);
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(1);
|
||||
@@ -67,7 +71,8 @@ namespace Robust.UnitTesting.Shared.Physics
|
||||
Assert.That(physics.Awake, Is.EqualTo(false));
|
||||
Assert.That(physics.CanCollide, Is.EqualTo(false));
|
||||
|
||||
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(physics.Owner).AttachParent(mapManager.GetMapEntityId(mapId));
|
||||
xform.LocalPosition = Vector2.One * 2f;
|
||||
xform.AttachParent(mapManager.GetMapEntityId(mapId));
|
||||
});
|
||||
|
||||
// Juussttt in case we'll re-parent it to the map and check its collision is back on.
|
||||
|
||||
Reference in New Issue
Block a user