Grid splitting (#2743)

Co-authored-by: Vera Aguilera Puerto <gradientvera@outlook.com>
This commit is contained in:
metalgearsloth
2022-04-24 00:57:56 +10:00
committed by GitHub
parent 712c1f3559
commit 442de12b99
40 changed files with 1542 additions and 216 deletions

View File

@@ -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;
}

View File

@@ -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,
}

View File

@@ -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();
}

View File

@@ -1,9 +0,0 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
{
}
}

View 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);
}
}
}
}

View 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");
}
}

View File

@@ -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)
{

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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)
{

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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()
{

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 {}
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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)

View File

@@ -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]

View File

@@ -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.

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;

View File

@@ -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.

View File

@@ -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.

View File

@@ -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>

View 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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)
{

View File

@@ -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));

View 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);
}
}

View File

@@ -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]

View File

@@ -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.