Compare commits

...

45 Commits

Author SHA1 Message Date
metalgearsloth
d12a238ecc Fix tests
Woops
2021-08-09 19:42:34 +10:00
metalgearsloth
ac46a7845d Fix container bounds 2021-08-09 18:46:51 +10:00
metalgearsloth
60b526f653 Dirty physics bodies on new fixtures 2021-08-08 15:59:59 +10:00
metalgearsloth
d58e380dd9 MapManager now uses accurate bounds for grids (#1918)
* MapManager now uses accurate bounds for grids

Instead of using the old WorldBounds (which was dirty for multiple reasons) it goes through physics instead using the actual fixtures attached to the grid.

* Fix nuke

* Test time

* AABB tests

* feex

* how is this failing aaa

* feex tests

* slightly better

Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
2021-08-06 22:25:14 +10:00
Visne
96a098a0c2 Fix outline when eye is rotated (#1929) 2021-08-06 09:53:14 +02:00
Vera Aguilera Puerto
c0d4e34089 Add Friend classes to C# with the help of Analyzers and Attributes. (#1928)
* Add Friend classes to C# with the help of Analyzers and Attributes.

* Revert to netstandard2.0

* Use LINQ instead of ^1 for array

* Address review.
Oops, forgot to push.
2021-08-06 09:50:22 +02:00
ShadowCommander
e16e0f4bd0 Fix containers with entities that do not exist yet (#1892)
* Fix containers that hold entities not on client

* Delete from ExpectedEntities when entity removed

* Fix ContainerSystem not registering on the server

* Move container state to entity system
Move client code to client

* Fix removal and clean up code

* Add test

* Add more checks to test

* Remove unneeded deletion event handler

When the child is deleted, if the entity does not exist on the client,
then HandleComponentState runs. If the entity does exist, then
HandleEntityInitialized would have run. Either way HandleEntityDeleted
is not needed.

* Renamed unexpected to removedExpected
2021-08-04 19:00:14 -07:00
metalgearsloth
d31ffd2794 Update broadphase every frame on client (#1925)
There was a reason I didn't do this prior to refactor but that reason is no longer relevant soooooo
2021-08-04 00:43:32 +10:00
metalgearsloth
04d94f87fc Fix movement lerping (#1924) 2021-08-04 00:11:04 +10:00
metalgearsloth
f809375389 Cache broadphase on PhysicsMap (#1923) 2021-08-03 22:24:01 +10:00
metalgearsloth
530ea5f4e6 Remove BroadphaseMapid from physicscomp
Was made redundant in the broadphase refactor
2021-08-03 19:08:45 +10:00
Visne
590a23d540 Make it possible to set lighting per map (#1921) 2021-08-03 09:55:54 +02:00
Vera Aguilera Puerto
52351e8b11 Clyde can now render multiple viewports with different maps. (#1917)
* Clyde can now render multiple viewports with different maps.

* remove todo comment
https://tenor.com/view/emoji-disintergrating-meme-emoji-disintegrating-meme-gif-21239978

* Fix tests
2021-08-03 13:24:11 +10:00
metalgearsloth
f75a764ff3 Multi-thread physics solver (#1905) 2021-08-02 13:50:26 +02:00
metalgearsloth
d14f4f0ce3 Use structs for position constraints (#1919)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
2021-08-02 21:47:57 +10:00
Vera Aguilera Puerto
5367570c7f Log simple human-readable error message when PVS mail generation throws an exception. 2021-08-02 12:29:37 +02:00
Vera Aguilera Puerto
e523a733c8 TestLogHandler logs exception, too. 2021-08-02 12:29:37 +02:00
metalgearsloth
06af61106c Fix grid rendering for rotation (#1881)
* Fix grid rendering

* Actually feex

* spellcheck

* Add a test for chunk bounds

* Revert that
2021-08-02 16:41:24 +10:00
metalgearsloth
2239c30924 Fix potential contact crash 2021-08-02 16:19:40 +10:00
Vera Aguilera Puerto
f9f55b6862 Transform sets GridId before raising EntParentChangedMessage event. 2021-08-01 13:35:58 +02:00
Vera Aguilera Puerto
83a5560850 Transform EntMapIdChangedMessage is raised directed. 2021-08-01 12:20:45 +02:00
Visne
9a8f139fb9 Make ScreenToMap not default to main viewport (#1916) 2021-07-31 23:45:08 +02:00
Pieter-Jan Briers
20dae60fd4 Add NumericsHelpers add method. 2021-07-31 16:52:24 +02:00
Pieter-Jan Briers
b4098668bb Use ring buffer for IGameTiming._realFrameTimes.
List.RemoveAt(0) is inefficient.
2021-07-31 16:06:08 +02:00
mirrorcult
9397cc4a6b Fix serv warnings (#1915) 2021-07-31 15:01:18 +10:00
Pieter-Jan Briers
1601e75879 Forgot the debug code oops. 2021-07-31 04:21:37 +02:00
Pieter-Jan Briers
a7b9c87926 Save 0.4% Windows server CPU. 2021-07-31 04:20:50 +02:00
metalgearsloth
8fea42ff9a Fix GetTilesIntersecting for circles (#1912) 2021-07-30 10:01:14 +02:00
metalgearsloth
86d20a0ef1 Bump up light radius
The ones around the singularity are 32 radius for whatever reason
2021-07-30 17:22:23 +10:00
metalgearsloth
09012ea4ff Change velocity constraints to structs (#1906) 2021-07-29 22:14:01 +02:00
metalgearsloth
e68297eb93 Multi-thread physics contacts (#1897) 2021-07-29 22:13:42 +02:00
metalgearsloth
c1a2e23ce2 Fix mission-critical typo 2021-07-30 00:08:17 +10:00
metalgearsloth
f208f6bfa9 Use ref var for manifold points (#1911) 2021-07-29 15:38:12 +02:00
Pieter-Jan Briers
ae526e2e10 FixedArray helpers.
May your stack allocations be fast and your GCs infrequent.
2021-07-29 15:37:43 +02:00
metalgearsloth
25549869b1 Use more values by reference in Collision (#1910)
Also a sneaky Span<ClipVertex> I forgot
2021-07-29 14:49:50 +02:00
metalgearsloth
f71e81d204 Use circle.Position for collisions. (#1909) 2021-07-29 14:49:18 +02:00
metalgearsloth
e67812fdb4 Add shutdowns to VirtualControllers (#1887) 2021-07-29 13:31:34 +02:00
Vera Aguilera Puerto
aa44b1cb8a RaiseLocalEvent non-generic overload that checks for GetType() (#1907)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2021-07-29 13:30:29 +02:00
Vera Aguilera Puerto
8ec75be244 Minor ViewSubscriber cleanup, remove all mentions of PVS eye 2021-07-29 13:29:54 +02:00
Vera Aguilera Puerto
48746b7bd3 Adds ViewSubscriberComponent and ViewSubscriberSystem (#1900) 2021-07-29 13:16:25 +02:00
metalgearsloth
a9791d2033 Fix potential SetTile crash when it unanchors entities (#1903) 2021-07-29 13:15:50 +02:00
Vera Aguilera Puerto
709f1f4284 EntityLookup now uses ILookupWorldBox2Component for getting entities' world AABB. 2021-07-29 13:01:24 +02:00
metalgearsloth
907094a5c8 Minor solver optimisation for invmass 2021-07-29 18:57:35 +10:00
Pieter-Jan Briers
a35a5e1645 Add EntitySystem.Subs property to aid helper methods. 2021-07-29 01:51:51 +02:00
Pieter-Jan Briers
ad8a59a72f Revert "Adds SetButtonDisabledRecursive() method to Control" (#1902)
This reverts commit e93c0f76a9.
2021-07-27 21:44:31 +02:00
90 changed files with 2564 additions and 782 deletions

View File

@@ -0,0 +1,107 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Robust.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class FriendAnalyzer : DiagnosticAnalyzer
{
const string FriendAttribute = "Robust.Shared.Analyzers.FriendAttribute";
public const string DiagnosticId = "RA0002";
private const string Title = "Tried to access friend-only member";
private const string MessageFormat = "Tried to access member \"{0}\" in class \"{1}\" which can only be accessed by friend classes";
private const string Description = "Make sure to specify the accessing class in the friends attribute.";
private const string Category = "Usage";
[SuppressMessage("ReSharper", "RS2008")]
private static readonly DiagnosticDescriptor Rule = new (DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, true, Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(CheckFriendship, SyntaxKind.SimpleMemberAccessExpression);
}
private void CheckFriendship(SyntaxNodeAnalysisContext context)
{
if (context.Node is not MemberAccessExpressionSyntax memberAccess)
return;
// We only do something if our parent is one of a few types.
switch (context.Node.Parent)
{
// If we're being assigned...
case AssignmentExpressionSyntax assignParent:
{
if (assignParent.Left != memberAccess)
return;
break;
}
// If we're being invoked...
case InvocationExpressionSyntax:
break;
// Otherwise, do nothing.
default:
return;
}
// Get the friend attribute
var friendAttr = context.Compilation.GetTypeByMetadataName(FriendAttribute);
// Get the type that is containing this expression, or, the class where this is happening.
if (context.ContainingSymbol?.ContainingType is not { } containingType)
return;
// We check all of our children and get only the identifiers.
foreach (var identifier in memberAccess.ChildNodes().Select(node => node as IdentifierNameSyntax))
{
if (identifier == null) continue;
// Get the type info of the identifier, so we can check the attributes...
if (context.SemanticModel.GetTypeInfo(identifier).ConvertedType is not { } type)
continue;
// Same-type access is always fine.
if (SymbolEqualityComparer.Default.Equals(type, containingType))
continue;
// Finally, get all attributes of the type, to check if we have any friend classes.
foreach (var attribute in type.GetAttributes())
{
// If the attribute isn't the friend attribute, continue.
if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, friendAttr))
continue;
// Check all types allowed in the friend attribute. (We assume there's only one constructor arg.)
foreach (var constant in attribute.ConstructorArguments[0].Values)
{
// Check if the value is a type...
if (constant.Value is not INamedTypeSymbol t)
continue;
// If we find that the containing class is specified in the attribute, return! All is good.
if (SymbolEqualityComparer.Default.Equals(containingType, t))
return;
}
// Not in a friend class! Report an error.
context.ReportDiagnostic(
Diagnostic.Create(Rule, context.Node.GetLocation(),
$"{context.Node.ToString().Split('.').LastOrDefault()}", $"{type.Name}"));
}
}
}
}
}

View File

@@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9</LangVersion>
</PropertyGroup>
<ItemGroup>

View File

@@ -0,0 +1,28 @@
using BenchmarkDotNet.Attributes;
namespace Robust.Benchmarks.NumericsHelpers
{
public class AddBenchmark
{
[Params(32, 128)]
public int N { get; set; }
private float[] _inputA = default!;
private float[] _inputB = default!;
private float[] _output = default!;
[GlobalSetup]
public void Setup()
{
_inputA = new float[N];
_inputB = new float[N];
_output = new float[N];
}
[Benchmark]
public void Bench()
{
Shared.Maths.NumericsHelpers.Add(_inputA, _inputB, _output);
}
}
}

View File

@@ -6,7 +6,7 @@ namespace Robust.Benchmarks
{
public static void Main(string[] args)
{
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
}
}

View File

@@ -871,7 +871,7 @@ namespace Robust.Client.Console.Commands
var chunkIndex = grid.LocalToChunkIndices(grid.MapToGrid(mousePos));
var chunk = internalGrid.GetChunk(chunkIndex);
shell.WriteLine($"worldBounds: {chunk.CalcWorldBounds()} localBounds: {chunk.CalcLocalBounds()}");
shell.WriteLine($"worldBounds: {chunk.CalcWorldAABB()} localBounds: {chunk.CalcLocalBounds()}");
}
}

View File

@@ -0,0 +1,17 @@
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
namespace Robust.Client.Console.Commands
{
public class GridChunkBBCommand : IConsoleCommand
{
public string Command => "showchunkbb";
public string Description => "Displays chunk bounds for the purposes of rendering";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<GridChunkBoundsDebugSystem>().Enabled ^= true;
}
}
}

View File

@@ -1,94 +0,0 @@
using System.Collections.Generic;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
public class ClientContainerSystem : ContainerSystem
{
private readonly HashSet<IEntity> _updateQueue = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UpdateContainerOcclusionMessage>(UpdateContainerOcclusion);
UpdatesBefore.Add(typeof(SpriteSystem));
}
private void UpdateContainerOcclusion(UpdateContainerOcclusionMessage ev)
{
_updateQueue.Add(ev.Entity);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
foreach (var toUpdate in _updateQueue)
{
if (toUpdate.Deleted)
{
continue;
}
UpdateEntityRecursively(toUpdate);
}
_updateQueue.Clear();
}
private static void UpdateEntityRecursively(IEntity entity)
{
// TODO: Since we are recursing down,
// we could cache ShowContents data here to speed it up for children.
// Am lazy though.
UpdateEntity(entity);
foreach (var child in entity.Transform.Children)
{
UpdateEntityRecursively(child.Owner);
}
}
private static void UpdateEntity(IEntity entity)
{
if (entity.TryGetComponent(out SpriteComponent? sprite))
{
sprite.ContainerOccluded = false;
// We have to recursively scan for containers upwards in case of nested containers.
var tempParent = entity;
while (tempParent.TryGetContainer(out var container))
{
if (!container.ShowContents)
{
sprite.ContainerOccluded = true;
break;
}
tempParent = container.Owner;
}
}
if (entity.TryGetComponent(out PointLightComponent? light))
{
light.ContainerOccluded = false;
// We have to recursively scan for containers upwards in case of nested containers.
var tempParent = entity;
while (tempParent.TryGetContainer(out var container))
{
if (container.OccludesLight)
{
light.ContainerOccluded = true;
break;
}
tempParent = container.Owner;
}
}
}
}
}

View File

@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using static Robust.Shared.Containers.ContainerManagerComponent;
namespace Robust.Client.GameObjects
{
public class ContainerSystem : SharedContainerSystem
{
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IDynamicTypeFactoryInternal _dynFactory = default!;
private readonly HashSet<IEntity> _updateQueue = new();
public readonly Dictionary<EntityUid, IContainer> ExpectedEntities = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UpdateContainerOcclusionMessage>(UpdateContainerOcclusion);
SubscribeLocalEvent<EntityInitializedMessage>(HandleEntityInitialized);
SubscribeLocalEvent<ContainerManagerComponent, ComponentHandleState>(HandleComponentState);
UpdatesBefore.Add(typeof(SpriteSystem));
}
private void UpdateContainerOcclusion(UpdateContainerOcclusionMessage ev)
{
_updateQueue.Add(ev.Entity);
}
private void HandleEntityInitialized(EntityInitializedMessage ev)
{
if (!ExpectedEntities.TryGetValue(ev.Entity.Uid, out var container))
return;
RemoveExpectedEntity(ev.Entity.Uid);
if (container.Deleted)
return;
container.Insert(ev.Entity);
}
private void HandleComponentState(EntityUid uid, ContainerManagerComponent component, ComponentHandleState args)
{
if (args.Current is not ContainerManagerComponentState cast)
return;
// Delete now-gone containers.
List<string>? toDelete = null;
foreach (var (id, container) in component.Containers)
{
if (!cast.ContainerSet.Any(data => data.Id == id))
{
container.Shutdown();
toDelete ??= new List<string>();
toDelete.Add(id);
}
}
if (toDelete != null)
{
foreach (var dead in toDelete)
{
component.Containers.Remove(dead);
}
}
// Add new containers and update existing contents.
foreach (var (containerType, id, showEnts, occludesLight, entityUids) in cast.ContainerSet)
{
if (!component.Containers.TryGetValue(id, out var container))
{
container = ContainerFactory(component, containerType, id);
component.Containers.Add(id, container);
}
// sync show flag
container.ShowContents = showEnts;
container.OccludesLight = occludesLight;
// Remove gone entities.
List<IEntity>? toRemove = null;
foreach (var entity in container.ContainedEntities)
{
if (!entityUids.Contains(entity.Uid))
{
toRemove ??= new List<IEntity>();
toRemove.Add(entity);
}
}
if (toRemove != null)
{
foreach (var goner in toRemove)
container.Remove(goner);
}
// Remove entities that were expected, but have been removed from the container.
List<EntityUid>? removedExpected = null;
foreach (var entityUid in container.ExpectedEntities)
{
if (!entityUids.Contains(entityUid))
{
removedExpected ??= new List<EntityUid>();
removedExpected.Add(entityUid);
}
}
if (removedExpected != null)
{
foreach (var entityUid in removedExpected)
RemoveExpectedEntity(entityUid);
}
// Add new entities.
foreach (var entityUid in entityUids)
{
if (!EntityManager.TryGetEntity(entityUid, out var entity))
{
AddExpectedEntity(entityUid, container);
continue;
}
if (!container.ContainedEntities.Contains(entity))
container.Insert(entity);
}
}
}
private IContainer ContainerFactory(ContainerManagerComponent component, string containerType, string id)
{
var type = _serializer.FindSerializedType(typeof(IContainer), containerType);
if (type is null) throw new ArgumentException($"Container of type {containerType} for id {id} cannot be found.");
var newContainer = _dynFactory.CreateInstanceUnchecked<BaseContainer>(type);
newContainer.ID = id;
newContainer.Manager = component;
return newContainer;
}
public void AddExpectedEntity(EntityUid uid, IContainer container)
{
if (ExpectedEntities.ContainsKey(uid))
return;
ExpectedEntities.Add(uid, container);
container.ExpectedEntities.Add(uid);
}
public void RemoveExpectedEntity(EntityUid uid)
{
if (!ExpectedEntities.TryGetValue(uid, out var container))
return;
ExpectedEntities.Remove(uid);
container.ExpectedEntities.Remove(uid);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
foreach (var toUpdate in _updateQueue)
{
if (toUpdate.Deleted)
{
continue;
}
UpdateEntityRecursively(toUpdate);
}
_updateQueue.Clear();
}
private static void UpdateEntityRecursively(IEntity entity)
{
// TODO: Since we are recursing down,
// we could cache ShowContents data here to speed it up for children.
// Am lazy though.
UpdateEntity(entity);
foreach (var child in entity.Transform.Children)
{
UpdateEntityRecursively(child.Owner);
}
}
private static void UpdateEntity(IEntity entity)
{
if (entity.TryGetComponent(out SpriteComponent? sprite))
{
sprite.ContainerOccluded = false;
// We have to recursively scan for containers upwards in case of nested containers.
var tempParent = entity;
while (tempParent.TryGetContainer(out var container))
{
if (!container.ShowContents)
{
sprite.ContainerOccluded = true;
break;
}
tempParent = container.Owner;
}
}
if (entity.TryGetComponent(out PointLightComponent? light))
{
light.ContainerOccluded = false;
// We have to recursively scan for containers upwards in case of nested containers.
var tempParent = entity;
while (tempParent.TryGetContainer(out var container))
{
if (container.OccludesLight)
{
light.ContainerOccluded = true;
break;
}
tempParent = container.Owner;
}
}
}
}
}

View File

@@ -0,0 +1,90 @@
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.GameObjects
{
public class GridChunkBoundsDebugSystem : EntitySystem
{
private GridChunkBoundsOverlay? _overlay;
public bool Enabled
{
get => _enabled;
set
{
if (_enabled == value) return;
_enabled = value;
if (_enabled)
{
DebugTools.Assert(_overlay == null);
_overlay = new GridChunkBoundsOverlay(
IoCManager.Resolve<IEntityManager>(),
IoCManager.Resolve<IEyeManager>(),
IoCManager.Resolve<IMapManager>());
IoCManager.Resolve<IOverlayManager>().AddOverlay(_overlay);
}
else
{
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(_overlay!);
_overlay = null;
}
}
}
private bool _enabled;
}
internal sealed class GridChunkBoundsOverlay : Overlay
{
private readonly IEntityManager _entityManager;
private readonly IEyeManager _eyeManager;
private readonly IMapManager _mapManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public GridChunkBoundsOverlay(IEntityManager entManager, IEyeManager eyeManager, IMapManager mapManager)
{
_entityManager = entManager;
_eyeManager = eyeManager;
_mapManager = mapManager;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
var currentMap = _eyeManager.CurrentMap;
var viewport = _eyeManager.GetWorldViewport();
foreach (var grid in _mapManager.FindGridsIntersecting(currentMap, viewport))
{
var mapGrid = (IMapGridInternal) grid;
var gridEnt = _entityManager.GetEntity(grid.GridEntityId);
var worldPos = gridEnt.Transform.WorldPosition;
var worldRot = gridEnt.Transform.WorldRotation;
foreach (var (_, chunk) in mapGrid.GetMapChunks())
{
var chunkBounds = chunk.CalcWorldBounds(worldPos, worldRot);
var aabb = chunkBounds.CalcBoundingBox();
// Calc world bounds for chunk.
if (!aabb.Intersects(in viewport))
{
continue;
}
args.WorldHandle.DrawRect(chunkBounds, Color.Green.WithAlpha(0.2f), true);
args.WorldHandle.DrawRect(aabb, Color.Red, false);
}
}
}
}
}

View File

@@ -111,10 +111,11 @@ namespace Robust.Client.Graphics
public MapCoordinates ScreenToMap(ScreenCoordinates point)
{
var (pos, window) = point;
if (window != MainViewport.Window?.Id)
if (_uiManager.MouseGetControl(point) is not IViewportControl viewport)
return default;
return MainViewport.ScreenToMap(pos);
return viewport.ScreenToMap(pos);
}
/// <inheritdoc />

View File

@@ -53,7 +53,7 @@ namespace Robust.Client.Graphics
ScreenCoordinates CoordinatesToScreen(EntityCoordinates point);
/// <summary>
/// Unprojects a point from UI screen space to world space using the current camera.
/// Unprojects a point from UI screen space to world space using the viewport under the screen coordinates.
/// </summary>
/// <remarks>
/// The game exists on the 2D X/Y plane, so this function returns a point o the plane
@@ -64,7 +64,7 @@ namespace Robust.Client.Graphics
MapCoordinates ScreenToMap(ScreenCoordinates point);
/// <summary>
/// Unprojects a point from UI screen space to world space using the current camera.
/// Unprojects a point from UI screen space to world space using the main viewport.
/// </summary>
/// <remarks>
/// The game exists on the 2D X/Y plane, so this function returns a point o the plane

View File

@@ -19,18 +19,17 @@ namespace Robust.Client.Graphics.Clyde
private int _verticesPerChunk(IMapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * 4;
private int _indicesPerChunk(IMapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
private void _drawGrids(Box2 worldBounds)
private void _drawGrids(Viewport viewport, Box2 worldBounds, IEye eye)
{
var mapId = _eyeManager.CurrentMap;
var mapId = eye.Position.MapId;
if (!_mapManager.MapExists(mapId))
{
// fall back to the default eye's map
_eyeManager.ClearCurrentEye();
mapId = _eyeManager.CurrentMap;
// fall back to nullspace map
mapId = MapId.Nullspace;
}
SetTexture(TextureUnit.Texture0, _tileDefinitionManager.TileTextureAtlas);
SetTexture(TextureUnit.Texture1, _lightingReady ? _currentViewport!.LightRenderTarget.Texture : _stockTextureWhite);
SetTexture(TextureUnit.Texture1, _lightingReady ? viewport.LightRenderTarget.Texture : _stockTextureWhite);
var (gridProgram, _) = ActivateShaderInstance(_defaultShader.Handle);
SetupGlobalUniformsImmediate(gridProgram, (ClydeTexture) _tileDefinitionManager.TileTextureAtlas);
@@ -52,11 +51,13 @@ namespace Robust.Client.Graphics.Clyde
var transform = compMan.GetComponent<ITransformComponent>(grid.GridEntityId);
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
var worldPos = transform.WorldPosition;
var worldRot = transform.WorldRotation;
foreach (var (_, chunk) in grid.GetMapChunks())
{
// Calc world bounds for chunk.
if (!chunk.CalcWorldBounds().Intersects(in worldBounds))
if (!chunk.CalcWorldAABB(worldPos, worldRot).Intersects(in worldBounds))
{
continue;
}

View File

@@ -201,9 +201,10 @@ namespace Robust.Client.Graphics.Clyde
}
private void DrawEntities(Viewport viewport, Box2 worldBounds)
private void DrawEntities(Viewport viewport, Box2 worldBounds, IEye eye)
{
if (_eyeManager.CurrentMap == MapId.Nullspace || !_mapManager.HasMapEntity(_eyeManager.CurrentMap))
var mapId = eye.Position.MapId;
if (mapId == MapId.Nullspace || !_mapManager.HasMapEntity(mapId))
{
return;
}
@@ -217,7 +218,7 @@ namespace Robust.Client.Graphics.Clyde
// TODO: Make this check more accurate.
var widerBounds = worldBounds.Enlarged(5);
ProcessSpriteEntities(_eyeManager.CurrentMap, widerBounds, _drawingSpriteList);
ProcessSpriteEntities(mapId, widerBounds, _drawingSpriteList);
var worldOverlays = new List<Overlay>();
@@ -291,6 +292,9 @@ namespace Robust.Client.Graphics.Clyde
// scale can be passed with PostShader as variable in future
var postShadeScale = 1.25f;
var screenSpriteSize = (Vector2i) ((screenRT - screenLB) * postShadeScale).Rounded();
// Rotate the vector by the eye angle, otherwise the bounding box will be incorrect
screenSpriteSize = (Vector2i) eye.Rotation.RotateVec(screenSpriteSize).Rounded();
screenSpriteSize.Y = -screenSpriteSize.Y;
// I'm not 100% sure why it works, but without it post-shader
@@ -467,13 +471,13 @@ namespace Robust.Client.Graphics.Clyde
using (DebugGroup("Grids"))
{
_drawGrids(worldBounds);
_drawGrids(viewport, worldBounds, eye);
}
// We will also render worldspace overlays here so we can do them under / above entities as necessary
using (DebugGroup("Entities"))
{
DrawEntities(viewport, worldBounds);
DrawEntities(viewport, worldBounds, eye);
}
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldBounds);

View File

@@ -324,11 +324,17 @@ namespace Robust.Client.Graphics.Clyde
return;
}
var map = eye.Position.MapId;
var mapId = eye.Position.MapId;
var (lights, count, expandedBounds) = GetLightsToRender(map, worldBounds);
// If this map has lighting disabled, return
if (!_mapManager.GetMapEntity(mapId).GetComponent<IMapComponent>().LightingEnabled)
{
return;
}
UpdateOcclusionGeometry(map, expandedBounds, eye.Position.Position);
var (lights, count, expandedBounds) = GetLightsToRender(mapId, worldBounds);
UpdateOcclusionGeometry(mapId, expandedBounds, eye.Position.Position);
DrawFov(viewport, eye);

View File

@@ -16,8 +16,7 @@ namespace Robust.Client.Physics
public override void Update(float frameTime)
{
_lastRem = _gameTiming.CurTime;
SimulateWorld(frameTime, !_gameTiming.InSimulation || !_gameTiming.IsFirstTimePredicted);
SimulateWorld(frameTime, _gameTiming.InPrediction);
}
public override void FrameUpdate(float frameTime)

View File

@@ -763,29 +763,6 @@ namespace Robust.Client.UserInterface
SetPositionInParent(Parent.ChildCount - 1);
}
/// <summary>
/// This searches recursively through all the children of "parent"
/// and sets the Disabled value of any buttons found to "val"
/// </summary>
/// <param name="parent">The control which childrens get searched</param>
/// <param name="val">The value to which disabled gets set</param>
public void SetButtonDisabledRecursive(Control parent, bool val)
{
foreach (var child in parent.Children)
{
if (child is Button but)
{
but.Disabled = val;
continue;
}
if (child.Children != null)
{
SetButtonDisabledRecursive(child, val);
}
}
}
/// <summary>
/// Called when this control receives keyboard focus.
/// </summary>

View File

@@ -94,10 +94,6 @@ namespace Robust.Server
private ILogHandler? _logHandler;
private IGameLoop _mainLoop = default!;
private TimeSpan _lastTitleUpdate;
private long _lastReceivedBytes;
private long _lastSentBytes;
private string? _shutdownReason;
private readonly ManualResetEventSlim _shutdownEvent = new(false);
@@ -515,28 +511,6 @@ namespace Robust.Server
_mainLoop = gameLoop;
}
/// <summary>
/// Updates the console window title with performance statistics.
/// </summary>
private void UpdateTitle()
{
if (!Environment.UserInteractive || System.Console.IsInputRedirected)
{
return;
}
// every 1 second update stats in the console window title
if ((_time.RealTime - _lastTitleUpdate).TotalSeconds < 1.0)
return;
var netStats = UpdateBps();
System.Console.Title = string.Format("FPS: {0:N2} SD: {1:N2}ms | Net: ({2}) | Memory: {3:N0} KiB",
Math.Round(_time.FramesPerSecondAvg, 2),
_time.RealFrameTimeStdDev.TotalMilliseconds,
netStats,
Process.GetCurrentProcess().PrivateMemorySize64 >> 10);
_lastTitleUpdate = _time.RealTime;
}
/// <summary>
/// Loads the server settings from the ConfigurationManager.
@@ -593,22 +567,9 @@ namespace Robust.Server
}
}
private string UpdateBps()
{
var stats = IoCManager.Resolve<IServerNetManager>().Statistics;
var bps =
$"Send: {(stats.SentBytes - _lastSentBytes) >> 10:N0} KiB/s, Recv: {(stats.ReceivedBytes - _lastReceivedBytes) >> 10:N0} KiB/s";
_lastSentBytes = stats.SentBytes;
_lastReceivedBytes = stats.ReceivedBytes;
return bps;
}
private void Input(FrameEventArgs args)
{
_systemConsole.Update();
_systemConsole.UpdateInput();
_network.ProcessPackets();
_taskManager.ProcessPendingTasks();
@@ -622,7 +583,7 @@ namespace Robust.Server
// These are always the same on the server, there is no prediction.
_time.LastRealTick = _time.CurTick;
UpdateTitle();
_systemConsole.UpdateTick();
using (TickUsage.WithLabels("PreEngine").NewTimer())
{

View File

@@ -8,12 +8,14 @@
/// <summary>
/// process input/output of the console. This needs to be called often.
/// </summary>
void Update();
void UpdateInput();
/// <summary>
/// Prints <paramref name="text" /> to the system console.
/// </summary>
/// <param name="text">Text to write to the system console.</param>
void Print(string text);
void UpdateTick();
}
}

View File

@@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Con = System.Console;
namespace Robust.Server.Console
@@ -12,6 +16,12 @@ namespace Robust.Server.Console
[Dependency] private readonly IServerConsoleHost _conShell = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IGameTiming _time = default!;
//
// Command entry stuff.
//
private readonly Dictionary<int, string> commandHistory = new();
private string currentBuffer = "";
@@ -21,6 +31,20 @@ namespace Robust.Server.Console
private int tabCompleteIndex;
private ConsoleKey lastKeyPressed = ConsoleKey.NoName;
//
// Title update stuff.
//
// This is ridiculously expensive to fetch for some reason.
// I'm gonna just assume that this can't change during the lifetime of the process. I hope.
// I want this ridiculous 0.1% CPU usage off my profiler.
private readonly bool _userInteractive = Environment.UserInteractive;
private TimeSpan _lastTitleUpdate;
private long _lastReceivedBytes;
private long _lastSentBytes;
public void Dispose()
{
if (Environment.UserInteractive)
@@ -37,7 +61,50 @@ namespace Robust.Server.Console
}
}
public void Update()
public void UpdateTick()
{
UpdateTitle();
}
/// <summary>
/// Updates the console window title with performance statistics.
/// </summary>
private void UpdateTitle()
{
if (!_userInteractive || System.Console.IsInputRedirected)
{
return;
}
// every 1 second update stats in the console window title
if ((_time.RealTime - _lastTitleUpdate).TotalSeconds < 1.0)
return;
var netStats = UpdateBps();
var privateSize = Process.GetCurrentProcess().GetPrivateMemorySize64NotSlowHolyFuckingShitMicrosoft();
System.Console.Title = string.Format("FPS: {0:N2} SD: {1:N2}ms | Net: ({2}) | Memory: {3:N0} KiB",
Math.Round(_time.FramesPerSecondAvg, 2),
_time.RealFrameTimeStdDev.TotalMilliseconds,
netStats,
privateSize >> 10);
_lastTitleUpdate = _time.RealTime;
}
private string UpdateBps()
{
var stats = _netManager.Statistics;
var bps =
$"Send: {(stats.SentBytes - _lastSentBytes) >> 10:N0} KiB/s, Recv: {(stats.ReceivedBytes - _lastReceivedBytes) >> 10:N0} KiB/s";
_lastSentBytes = stats.SentBytes;
_lastReceivedBytes = stats.ReceivedBytes;
return bps;
}
public void UpdateInput()
{
if (Con.IsInputRedirected)
{
@@ -161,7 +228,7 @@ namespace Robust.Server.Console
{
var currentLineCursor = Con.CursorTop;
Con.SetCursorPosition(0, Con.CursorTop);
Con.Write(new string(' ', Con.WindowWidth-1));
Con.Write(new string(' ', Con.WindowWidth - 1));
Con.SetCursorPosition(0, currentLineCursor);
}
@@ -174,7 +241,8 @@ namespace Robust.Server.Console
if (tabCompleteList.Count == 0)
{
tabCompleteList = _conShell.RegisteredCommands.Keys.Where(key => key.StartsWith(currentBuffer)).ToList();
tabCompleteList = _conShell.RegisteredCommands.Keys.Where(key => key.StartsWith(currentBuffer))
.ToList();
if (tabCompleteList.Count == 0)
{
return String.Empty;

View File

@@ -2,7 +2,7 @@ namespace Robust.Server.Console
{
internal sealed class SystemConsoleManagerDummy : ISystemConsoleManager
{
public void Update()
public void UpdateInput()
{
// Nada.
}
@@ -11,5 +11,10 @@ namespace Robust.Server.Console
{
// Nada.
}
public void UpdateTick()
{
// Nada.
}
}
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.Containers;
namespace Robust.Server.Containers
{
public class ContainerSystem : SharedContainerSystem
{
// Seems like shared EntitySystems aren't registered, so this is here to register it on the server.
// Registering the SharedContainerSystem causes conflicts on client where two entity systems are registered.
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects
{
[RegisterComponent]
internal class ViewSubscriberComponent : Component
{
public override string Name => "ViewSubscriber";
internal readonly HashSet<IPlayerSession> SubscribedSessions = new();
}
}

View File

@@ -141,7 +141,7 @@ namespace Robust.Server.GameObjects
}
}
public uint _renderOrder;
private uint _renderOrder;
[ViewVariables(VVAccess.ReadWrite)]
public uint RenderOrder
{

View File

@@ -0,0 +1,92 @@
using Robust.Server.Player;
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects
{
/// <summary>
/// Entity System that handles subscribing and unsubscribing to PVS views.
/// </summary>
public class ViewSubscriberSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ViewSubscriberComponent, ComponentShutdown>(OnViewSubscriberShutdown);
}
/// <summary>
/// Subscribes the session to get PVS updates from the point of view of the specified entity.
/// </summary>
public void AddViewSubscriber(EntityUid uid, IPlayerSession session)
{
// This will throw if you pass in an invalid uid.
var entity = EntityManager.GetEntity(uid);
// If the entity doesn't have the component, it will be added.
var viewSubscriber = entity.EnsureComponent<ViewSubscriberComponent>();
if (viewSubscriber.SubscribedSessions.Contains(session))
return; // Already subscribed, do nothing else.
viewSubscriber.SubscribedSessions.Add(session);
session.AddViewSubscription(uid);
RaiseLocalEvent(uid, new ViewSubscriberAddedEvent(entity, session));
}
/// <summary>
/// Unsubscribes the session from getting PVS updates from the point of view of the specified entity.
/// </summary>
public void RemoveViewSubscriber(EntityUid uid, IPlayerSession session)
{
if(!ComponentManager.TryGetComponent(uid, out ViewSubscriberComponent? viewSubscriber))
return; // Entity didn't have any subscriptions, do nothing.
if (!viewSubscriber.SubscribedSessions.Remove(session))
return; // Session wasn't subscribed, do nothing.
session.RemoveViewSubscription(uid);
RaiseLocalEvent(uid, new ViewSubscriberRemovedEvent(EntityManager.GetEntity(uid), session));
}
private void OnViewSubscriberShutdown(EntityUid uid, ViewSubscriberComponent component, ComponentShutdown _)
{
foreach (var session in component.SubscribedSessions)
{
session.RemoveViewSubscription(uid);
}
}
}
/// <summary>
/// Raised when a session subscribes to an entity's PVS view.
/// </summary>
public class ViewSubscriberAddedEvent : EntityEventArgs
{
public IEntity View { get; }
public IPlayerSession Subscriber { get; }
public ViewSubscriberAddedEvent(IEntity view, IPlayerSession subscriber)
{
View = view;
Subscriber = subscriber;
}
}
/// <summary>
/// Raised when a session is unsubscribed from an entity's PVS view.
/// Not raised when sessions are unsubscribed due to the component being removed.
/// </summary>
public class ViewSubscriberRemovedEvent : EntityEventArgs
{
public IEntity View { get; }
public IPlayerSession Subscriber { get; }
public ViewSubscriberRemovedEvent(IEntity view, IPlayerSession subscriber)
{
View = view;
Subscriber = subscriber;
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.ObjectPool;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -139,12 +140,15 @@ namespace Robust.Server.GameStates
if (session.Status != SessionStatus.InGame || session.AttachedEntityUid is null)
return viewers;
var query = _compMan.EntityQuery<ActorComponent>();
viewers.Add(session.AttachedEntityUid.Value);
foreach (var actorComp in query)
// This is awful, but we're not gonna add the list of view subscriptions to common session.
if (session is not IPlayerSession playerSession)
return viewers;
foreach (var uid in playerSession.ViewSubscriptions)
{
if (actorComp.PlayerSession == session)
viewers.Add(actorComp.Owner.Uid);
viewers.Add(uid);
}
return viewers;
@@ -276,8 +280,9 @@ namespace Robust.Server.GameStates
if (_compMan.TryGetComponent<EyeComponent>(eyeEuid, out var eyeComp))
visMask = eyeComp.VisibilityMask;
//Always include the map entity of the eye
//TODO: Add Map entity here
//Always include the map entity of the eye, if it exists.
if(_mapManager.MapExists(mapId))
visibleEnts.Add(_mapManager.GetMapEntityId(mapId));
//Always include viewable ent itself
visibleEnts.Add(eyeEuid);

View File

@@ -216,7 +216,7 @@ namespace Robust.Server.GameStates
}
catch (Exception e) // Catch EVERY exception
{
_logger.Log(LogLevel.Error, e, string.Empty);
_logger.Log(LogLevel.Error, e, "Caught exception while generating mail.");
}
var msg = _networkManager.CreateNetMessage<MsgState>();

View File

@@ -366,11 +366,6 @@ namespace Robust.Server.Maps
// Run Initialize on all components.
FinishEntitiesInitialization();
// Regardless of what the frequency is we'll process fixtures sometime on map load for anything
// that needs it before MapInit.
var gridFixtures = EntitySystem.Get<GridFixtureSystem>();
gridFixtures.Process();
// Run Startup on all components.
FinishEntitiesStartup();
}

View File

@@ -1,13 +1,9 @@
using System.Collections.Generic;
using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
@@ -21,47 +17,16 @@ namespace Robust.Server.Physics
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
// Is delaying fixture updates a good idea? IDEK. We definitely can't do them on every tile changed
// because if someone changes 50 tiles that will kill perf. We could probably just run it every Update
// (and at specific times due to race condition stuff).
// At any rate, cooldown given here if someone wants it. CD of 0 just runs it every tick.
private float _cooldown;
private float _accumulator;
private HashSet<MapChunk> _queuedChunks = new();
/*
* Currently we won't defer grid updates because content may alter a bunch of tiles then decide
* to start anchroing entities for example.
*/
public override void Initialize()
{
base.Initialize();
UpdatesBefore.Add(typeof(PhysicsSystem));
UpdatesBefore.Add(typeof(SharedBroadphaseSystem));
SubscribeLocalEvent<RegenerateChunkCollisionEvent>(HandleCollisionRegenerate);
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CVars.GridFixtureUpdateRate, value => _cooldown = value, true);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_accumulator += frameTime;
if (_accumulator < _cooldown) return;
_accumulator -= _cooldown;
Process();
}
/// <summary>
/// Go through every dirty chunk and re-generate their fixtures.
/// </summary>
public void Process()
{
foreach (var chunk in _queuedChunks)
{
RegenerateCollision(chunk);
}
_queuedChunks.Clear();
}
/// <summary>
@@ -70,13 +35,7 @@ namespace Robust.Server.Physics
/// <param name="ev"></param>
private void HandleCollisionRegenerate(RegenerateChunkCollisionEvent ev)
{
if (_cooldown <= 0f)
{
RegenerateCollision(ev.Chunk);
return;
}
_queuedChunks.Add(ev.Chunk);
RegenerateCollision(ev.Chunk);
}
private void RegenerateCollision(MapChunk chunk)

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Players;
@@ -32,6 +34,8 @@ namespace Robust.Server.Player
void OnConnect();
void OnDisconnect();
IReadOnlySet<EntityUid> ViewSubscriptions { get; }
/// <summary>
/// Persistent data for this player.
/// </summary>
@@ -43,5 +47,17 @@ namespace Robust.Server.Player
/// and <see cref="DetachFromEntity"/> instead.
/// </summary>
internal void SetAttachedEntity(IEntity? entity);
/// <summary>
/// Internal method to add an entity Uid to <see cref="ViewSubscriptions"/>.
/// Do NOT use this outside of <see cref="ViewSubscriberSystem"/>.
/// </summary>
internal void AddViewSubscription(EntityUid eye);
/// <summary>
/// Internal method to remove an entity Uid from <see cref="ViewSubscriptions"/>.
/// Do NOT use this outside of <see cref="ViewSubscriberSystem"/>.
/// </summary>
internal void RemoveViewSubscription(EntityUid eye);
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Robust.Server.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
@@ -35,6 +36,8 @@ namespace Robust.Server.Player
UpdatePlayerState();
}
private readonly HashSet<EntityUid> _viewSubscriptions = new();
[ViewVariables] public INetChannel ConnectedClient { get; }
[ViewVariables] public IEntity? AttachedEntity { get; set; }
@@ -104,6 +107,7 @@ namespace Robust.Server.Player
public NetUserId UserId { get; }
private readonly PlayerData _data;
[ViewVariables] public IPlayerData Data => _data;
/// <inheritdoc />
@@ -160,10 +164,13 @@ namespace Robust.Server.Player
{
Status = SessionStatus.Disconnected;
UnsubscribeAllViews();
DetachFromEntity();
UpdatePlayerState();
}
public IReadOnlySet<EntityUid> ViewSubscriptions => _viewSubscriptions;
private void SetAttachedEntityName()
{
if (Name != null && AttachedEntity != null)
@@ -193,6 +200,26 @@ namespace Robust.Server.Player
UpdatePlayerState();
}
void IPlayerSession.AddViewSubscription(EntityUid eye)
{
_viewSubscriptions.Add(eye);
}
void IPlayerSession.RemoveViewSubscription(EntityUid eye)
{
_viewSubscriptions.Remove(eye);
}
private void UnsubscribeAllViews()
{
var viewSubscriberSystem = EntitySystem.Get<ViewSubscriberSystem>();
foreach (var eye in _viewSubscriptions)
{
viewSubscriberSystem.RemoveViewSubscriber(eye, this);
}
}
private void UpdatePlayerState()
{
PlayerState.Status = Status;

View File

@@ -103,6 +103,9 @@ namespace Robust.Shared.Maths
[Pure]
public Vector2 RotateVec(in Vector2 vec)
{
// No calculation necessery when theta is zero
if (Theta == 0) return vec;
var (x, y) = vec;
var cos = Math.Cos(Theta);
var sin = Math.Sin(Theta);

View File

@@ -0,0 +1,15 @@
using System;
namespace Robust.Shared.Analyzers
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct)]
public class FriendAttribute : Attribute
{
public readonly Type[] Friends;
public FriendAttribute(params Type[] friends)
{
Friends = friends;
}
}
}

View File

@@ -224,7 +224,7 @@ namespace Robust.Shared
/// outside of our viewport.
/// </remarks>
public static readonly CVarDef<float> MaxLightRadius =
CVarDef.Create("light.max_radius", 20.0f, CVar.CLIENTONLY);
CVarDef.Create("light.max_radius", 32.1f, CVar.CLIENTONLY);
/*
* Lookup
@@ -351,6 +351,13 @@ namespace Robust.Shared
* PHYSICS
*/
// - Contacts
public static readonly CVarDef<int> ContactMultithreadThreshold =
CVarDef.Create("physics.contact_multithread_threshold", 32);
public static readonly CVarDef<int> ContactMinimumThreads =
CVarDef.Create("physics.contact_minimum_threads", 2);
// - Sleep
public static readonly CVarDef<float> AngularSleepTolerance =
CVarDef.Create("physics.angsleeptol", 2.0f / 180.0f * MathF.PI);
@@ -366,8 +373,21 @@ namespace Robust.Shared
CVarDef.Create("physics.timetosleep", 0.2f);
// - Solver
public static readonly CVarDef<int> PositionConstraintsPerThread =
CVarDef.Create("physics.position_constraints_per_thread", 32);
public static readonly CVarDef<int> PositionConstraintsMinimumThread =
CVarDef.Create("physics.position_constraints_minimum_threads", 2);
public static readonly CVarDef<int> VelocityConstraintsPerThread =
CVarDef.Create("physics.velocity_constraints_per_thread", 32);
public static readonly CVarDef<int> VelocityConstraintMinimumThreads =
CVarDef.Create("physics.velocity_constraints_minimum_threads", 2);
// These are the minimum recommended by Box2D with the standard being 8 velocity 3 position iterations.
// Trade-off is obviously performance vs how long it takes to stabilise.
// PhysX opts for fewer velocity iterations and more position but they also have a different solver.
public static readonly CVarDef<int> PositionIterations =
CVarDef.Create("physics.positer", 3);
@@ -462,13 +482,6 @@ namespace Robust.Shared
public static readonly CVarDef<float> MaxAngVelocity =
CVarDef.Create("physics.maxangvelocity", 15f);
/// <summary>
/// How frequently grid fixtures are updated. Given grid updates can be expensive they aren't run immediately.
/// Set to 0 to run them immediately.
/// </summary>
public static readonly CVarDef<float> GridFixtureUpdateRate =
CVarDef.Create("physics.grid_fixture_update_rate", 0.2f);
/*
* DISCORD
*/

View File

@@ -16,6 +16,9 @@ namespace Robust.Shared.Containers
[ViewVariables]
public abstract IReadOnlyList<IEntity> ContainedEntities { get; }
[ViewVariables]
public abstract List<EntityUid> ExpectedEntities { get; }
/// <inheritdoc />
public abstract string ContainerType { get; }

View File

@@ -24,9 +24,13 @@ namespace Robust.Shared.Containers
[DataField("ents")]
private readonly List<IEntity> _containerList = new();
private readonly List<EntityUid> _expectedEntities = new();
/// <inheritdoc />
public override IReadOnlyList<IEntity> ContainedEntities => _containerList;
public override List<EntityUid> ExpectedEntities => _expectedEntities;
/// <inheritdoc />
public override string ContainerType => ClassName;

View File

@@ -21,12 +21,11 @@ namespace Robust.Shared.Containers
[NetworkedComponent()]
public class ContainerManagerComponent : Component, IContainerManager
{
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IDynamicTypeFactoryInternal _dynFactory = default!;
[ViewVariables]
[DataField("containers")]
private Dictionary<string, IContainer> _containers = new();
public Dictionary<string, IContainer> Containers = new();
/// <inheritdoc />
public sealed override string Name => "ContainerContainer";
@@ -37,12 +36,12 @@ namespace Robust.Shared.Containers
base.OnRemove();
// IContianer.Shutdown modifies the _containers collection
foreach (var container in _containers.Values.ToArray())
foreach (var container in Containers.Values.ToArray())
{
container.Shutdown();
}
_containers.Clear();
Containers.Clear();
}
/// <inheritdoc />
@@ -50,7 +49,7 @@ namespace Robust.Shared.Containers
{
base.Initialize();
foreach (var container in _containers)
foreach (var container in Containers)
{
var baseContainer = (BaseContainer)container.Value;
baseContainer.Manager = this;
@@ -58,93 +57,13 @@ namespace Robust.Shared.Containers
}
}
/// <inheritdoc />
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (!(curState is ContainerManagerComponentState cast))
return;
// Delete now-gone containers.
List<string>? toDelete = null;
foreach (var (id, container) in _containers)
{
if (!cast.ContainerSet.Any(data => data.Id == id))
{
container.Shutdown();
toDelete ??= new List<string>();
toDelete.Add(id);
}
}
if (toDelete != null)
{
foreach (var dead in toDelete)
{
_containers.Remove(dead);
}
}
// Add new containers and update existing contents.
foreach (var (containerType, id, showEnts, occludesLight, entityUids) in cast.ContainerSet)
{
if (!_containers.TryGetValue(id, out var container))
{
container = ContainerFactory(containerType, id);
_containers.Add(id, container);
}
// sync show flag
container.ShowContents = showEnts;
container.OccludesLight = occludesLight;
// Remove gone entities.
List<IEntity>? toRemove = null;
foreach (var entity in container.ContainedEntities)
{
if (!entityUids.Contains(entity.Uid))
{
toRemove ??= new List<IEntity>();
toRemove.Add(entity);
}
}
if (toRemove != null)
{
foreach (var goner in toRemove)
{
container.Remove(goner);
}
}
// Add new entities.
foreach (var uid in entityUids)
{
var entity = Owner.EntityManager.GetEntity(uid);
if (!container.ContainedEntities.Contains(entity)) container.Insert(entity);
}
}
}
private IContainer ContainerFactory(string containerType, string id)
{
var type = _serializer.FindSerializedType(typeof(IContainer), containerType);
if (type is null) throw new ArgumentException($"Container of type {containerType} for id {id} cannot be found.");
var newContainer = _dynFactory.CreateInstanceUnchecked<BaseContainer>(type);
newContainer.ID = id;
newContainer.Manager = this;
return newContainer;
}
/// <inheritdoc />
public override ComponentState GetComponentState(ICommonSession player)
{
// naive implementation that just sends the full state of the component
List<ContainerManagerComponentState.ContainerData> containerSet = new();
foreach (var container in _containers.Values)
foreach (var container in Containers.Values)
{
var uidArr = new EntityUid[container.ContainedEntities.Count];
@@ -171,19 +90,19 @@ namespace Robust.Shared.Containers
/// <inheritdoc />
public IContainer GetContainer(string id)
{
return _containers[id];
return Containers[id];
}
/// <inheritdoc />
public bool HasContainer(string id)
{
return _containers.ContainsKey(id);
return Containers.ContainsKey(id);
}
/// <inheritdoc />
public bool TryGetContainer(string id, [NotNullWhen(true)] out IContainer? container)
{
var ret = _containers.TryGetValue(id, out var cont);
var ret = Containers.TryGetValue(id, out var cont);
container = cont!;
return ret;
}
@@ -191,7 +110,7 @@ namespace Robust.Shared.Containers
/// <inheritdoc />
public bool TryGetContainer(IEntity entity, [NotNullWhen(true)] out IContainer? container)
{
foreach (var contain in _containers.Values)
foreach (var contain in Containers.Values)
{
if (!contain.Deleted && contain.Contains(entity))
{
@@ -207,7 +126,7 @@ namespace Robust.Shared.Containers
/// <inheritdoc />
public bool ContainsEntity(IEntity entity)
{
foreach (var container in _containers.Values)
foreach (var container in Containers.Values)
{
if (!container.Deleted && container.Contains(entity)) return true;
}
@@ -218,7 +137,7 @@ namespace Robust.Shared.Containers
/// <inheritdoc />
public void ForceRemove(IEntity entity)
{
foreach (var container in _containers.Values)
foreach (var container in Containers.Values)
{
if (container.Contains(entity)) container.ForceRemove(entity);
}
@@ -227,13 +146,13 @@ namespace Robust.Shared.Containers
/// <inheritdoc />
public void InternalContainerShutdown(IContainer container)
{
_containers.Remove(container.ID);
Containers.Remove(container.ID);
}
/// <inheritdoc />
public bool Remove(IEntity entity)
{
foreach (var containers in _containers.Values)
foreach (var containers in Containers.Values)
{
if (containers.Contains(entity)) return containers.Remove(entity);
}
@@ -247,7 +166,7 @@ namespace Robust.Shared.Containers
base.Shutdown();
// On shutdown we won't get to process remove events in the containers so this has to be manually done.
foreach (var container in _containers.Values)
foreach (var container in Containers.Values)
{
foreach (var containerEntity in container.ContainedEntities)
{
@@ -265,7 +184,7 @@ namespace Robust.Shared.Containers
container.ID = id;
container.Manager = this;
_containers[id] = container;
Containers[id] = container;
Dirty();
return container;
}
@@ -366,7 +285,7 @@ namespace Robust.Shared.Containers
public AllContainersEnumerator(ContainerManagerComponent manager)
{
_enumerator = manager._containers.Values.GetEnumerator();
_enumerator = manager.Containers.Values.GetEnumerator();
Current = default;
}

View File

@@ -39,7 +39,10 @@ namespace Robust.Shared.Containers
}
}
public override List<EntityUid> ExpectedEntities => _expectedEntities;
private IEntity? _containedEntity;
private readonly List<EntityUid> _expectedEntities = new();
// Used by ContainedEntities to avoid allocating.
private readonly IEntity?[] _containedEntityArray = new IEntity[1];

View File

@@ -34,6 +34,8 @@ namespace Robust.Shared.Containers
/// </summary>
IReadOnlyList<IEntity> ContainedEntities { get; }
List<EntityUid> ExpectedEntities { get; }
/// <summary>
/// The type of this container.
/// </summary>

View File

@@ -1,12 +1,16 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Containers
{
public class ContainerSystem : EntitySystem
public abstract class SharedContainerSystem : EntitySystem
{
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EntParentChangedMessage>(HandleParentChanged);
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Maths;
namespace Robust.Shared.GameObjects
{
public interface ILookupWorldBox2Component
{
Box2 GetWorldAABB(Vector2? worldPos = null, Angle? worldRot = null);
}
}

View File

@@ -45,9 +45,10 @@ using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects
{
[ComponentReference(typeof(ILookupWorldBox2Component))]
[ComponentReference(typeof(IPhysBody))]
[NetworkedComponent()]
public sealed class PhysicsComponent : Component, IPhysBody, ISerializationHooks
public sealed class PhysicsComponent : Component, IPhysBody, ISerializationHooks, ILookupWorldBox2Component
{
[DataField("status", readOnly: true)]
private BodyStatus _bodyStatus = BodyStatus.OnGround;
@@ -552,16 +553,17 @@ namespace Robust.Shared.GameObjects
{
worldPos ??= Owner.Transform.WorldPosition;
worldRot ??= Owner.Transform.WorldRotation;
var transform = new Transform(worldPos.Value, (float) worldRot.Value.Theta);
var worldPosValue = worldPos.Value;
var worldRotValue = worldRot.Value;
var bounds = new Box2(worldPosValue, worldPosValue);
var bounds = new Box2(transform.Position, transform.Position);
foreach (var fixture in _fixtures)
{
var boundy = fixture.Shape.CalculateLocalBounds(worldRotValue);
bounds = bounds.Union(boundy.Translated(worldPosValue));
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
var boundy = fixture.Shape.ComputeAABB(transform, i);
bounds = bounds.Union(boundy);
}
}
return bounds;
@@ -1039,11 +1041,6 @@ namespace Robust.Shared.GameObjects
private bool _predict;
/// <summary>
/// As we defer updates need to store the MapId we used for broadphase.
/// </summary>
public MapId BroadphaseMapId { get; set; }
public IEnumerable<PhysicsComponent> GetBodiesIntersecting()
{
foreach (var entity in EntitySystem.Get<SharedBroadphaseSystem>().GetCollidingEntities(Owner.Transform.MapID, GetWorldAABB()))
@@ -1060,7 +1057,7 @@ namespace Robust.Shared.GameObjects
/// <returns>The corresponding local point relative to the body's origin.</returns>
public Vector2 GetLocalPoint(in Vector2 worldPoint)
{
return Physics.Transform.MulT(GetTransform(), worldPoint);
return Transform.MulT(GetTransform(), worldPoint);
}
/// <summary>

View File

@@ -13,6 +13,7 @@ namespace Robust.Shared.GameObjects
/// </summary>
public interface IMapComponent : IComponent
{
bool LightingEnabled { get; set; }
MapId WorldMap { get; }
void ClearMapId();
}
@@ -29,6 +30,10 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public override string Name => "Map";
[ViewVariables(VVAccess.ReadWrite)]
[DataField(("lightingEnabled"))]
public bool LightingEnabled { get; set; } = true;
/// <inheritdoc />
public MapId WorldMap
{
@@ -46,7 +51,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public override ComponentState GetComponentState(ICommonSession player)
{
return new MapComponentState(_mapIndex);
return new MapComponentState(_mapIndex, LightingEnabled);
}
/// <inheritdoc />
@@ -54,10 +59,11 @@ namespace Robust.Shared.GameObjects
{
base.HandleComponentState(curState, nextState);
if (!(curState is MapComponentState state))
if (curState is not MapComponentState state)
return;
_mapIndex = state.MapId;
LightingEnabled = state.LightingEnabled;
((TransformComponent) Owner.Transform).ChangeMapId(_mapIndex);
}
@@ -70,10 +76,12 @@ namespace Robust.Shared.GameObjects
internal class MapComponentState : ComponentState
{
public MapId MapId { get; }
public bool LightingEnabled { get; }
public MapComponentState(MapId mapId)
public MapComponentState(MapId mapId, bool lightingEnabled)
{
MapId = mapId;
LightingEnabled = lightingEnabled;
}
}
}

View File

@@ -302,9 +302,10 @@ namespace Robust.Shared.GameObjects
_parent = newParentEnt.Uid;
ChangeMapId(newConcrete.MapID);
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new EntParentChangedMessage(Owner, oldParent?.Owner));
// Cache new GridID before raising the event.
GridID = GetGridIndex();
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new EntParentChangedMessage(Owner, oldParent?.Owner));
}
// These conditions roughly emulate the effects of the code before I changed things,
@@ -383,6 +384,11 @@ namespace Robust.Shared.GameObjects
{
_anchored = Owner.EntityManager.GetEntity(grid.GridEntityId).GetComponent<IMapGridComponent>().AnchorEntity(this);
}
// If no grid found then unanchor it.
else
{
_anchored = false;
}
}
else if (value && !_anchored && _mapManager.TryFindGridAt(MapPosition, out var grid))
{
@@ -673,7 +679,7 @@ namespace Robust.Shared.GameObjects
private void MapIdChanged(MapId oldId)
{
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new EntMapIdChangedMessage(Owner, oldId));
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new EntMapIdChangedMessage(Owner, oldId));
}
public void AttachParent(IEntity parent)

View File

@@ -12,6 +12,8 @@ namespace Robust.Shared.GameObjects
void RaiseLocalEvent<TEvent>(EntityUid uid, TEvent args, bool broadcast = true)
where TEvent:EntityEventArgs;
void RaiseLocalEvent(EntityUid uid, EntityEventArgs args, bool broadcast = true);
void SubscribeLocalEvent<TComp, TEvent>(ComponentEventHandler<TComp, TEvent> handler)
where TComp : IComponent
where TEvent : EntityEventArgs;
@@ -82,6 +84,24 @@ namespace Robust.Shared.GameObjects
RaiseEvent(EventSource.Local, args);
}
/// <inheritdoc />
public void RaiseLocalEvent(EntityUid uid, EntityEventArgs args, bool broadcast = true)
{
var type = args.GetType();
if (_orderedEvents.Contains(type))
{
RaiseLocalOrdered(uid, args, broadcast);
return;
}
_eventTables.Dispatch(uid, type, args);
// we also broadcast it so the call site does not have to.
if(broadcast)
RaiseEvent(EventSource.Local, args);
}
/// <inheritdoc />
public void SubscribeLocalEvent<TComp, TEvent>(ComponentEventHandler<TComp, TEvent> handler)
where TComp : IComponent

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Robust.Shared.GameObjects
{
@@ -7,6 +8,11 @@ namespace Robust.Shared.GameObjects
{
private List<SubBase>? _subscriptions;
/// <summary>
/// A handle to allow subscription on this entity system's behalf.
/// </summary>
protected Subscriptions Subs { get; }
// NOTE: EntityEventHandler<T> and EntitySessionEventHandler<T> CANNOT BE ORDERED BETWEEN EACH OTHER.
protected void SubscribeNetworkEvent<T>(
@@ -136,6 +142,51 @@ namespace Robust.Shared.GameObjects
_subscriptions = null;
}
/// <summary>
/// API class to allow registering on an EntitySystem's behalf.
/// Intended to support creation of boilerplate-reduction-methods
/// that need to subscribe stuff on an entity system.
/// </summary>
[PublicAPI]
public sealed class Subscriptions
{
public EntitySystem System { get; }
internal Subscriptions(EntitySystem system)
{
System = system;
}
// Intended for helper methods, so minimal API.
public void SubEvent<T>(
EventSource src,
EntityEventHandler<T> handler,
Type[]? before = null, Type[]? after = null)
where T : notnull
{
System.SubEvent(src, handler, before, after);
}
public void SubSessionEvent<T>(
EventSource src,
EntitySessionEventHandler<T> handler,
Type[]? before = null, Type[]? after = null)
where T : notnull
{
System.SubSessionEvent(src, handler, before, after);
}
public void SubscribeLocalEvent<TComp, TEvent>(
ComponentEventHandler<TComp, TEvent> handler,
Type[]? before = null, Type[]? after = null)
where TComp : IComponent
where TEvent : EntityEventArgs
{
System.SubscribeLocalEvent(handler, before, after);
}
}
private abstract class SubBase
{
public abstract void Unsubscribe(EntitySystem sys, IEventBus bus);

View File

@@ -30,6 +30,11 @@ namespace Robust.Shared.GameObjects
IEnumerable<Type> IEntitySystem.UpdatesAfter => UpdatesAfter;
IEnumerable<Type> IEntitySystem.UpdatesBefore => UpdatesBefore;
protected EntitySystem()
{
Subs = new Subscriptions(this);
}
/// <inheritdoc />
public virtual void Initialize() { }
@@ -93,6 +98,11 @@ namespace Robust.Shared.GameObjects
EntityManager.EventBus.RaiseLocalEvent(uid, args, broadcast);
}
protected void RaiseLocalEvent(EntityUid uid, EntityEventArgs args, bool broadcast = true)
{
EntityManager.EventBus.RaiseLocalEvent(uid, args, broadcast);
}
#endregion
#region Static Helpers

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -586,12 +587,24 @@ namespace Robust.Shared.GameObjects
private static Box2 GetWorldAABB(in IEntity ent)
{
var pos = ent.Transform.WorldPosition;
Vector2 pos;
if (ent.Deleted || !ent.TryGetComponent(out PhysicsComponent? physics))
if (ent.Deleted)
{
pos = ent.Transform.WorldPosition;
return new Box2(pos, pos);
}
return physics.GetWorldAABB(pos);
if (ent.TryGetContainerMan(out var manager))
{
return GetWorldAABB(manager.Owner);
}
pos = ent.Transform.WorldPosition;
return ent.TryGetComponent(out ILookupWorldBox2Component? lookup) ?
lookup.GetWorldAABB(pos) :
new Box2(pos, pos);
}
#endregion

View File

@@ -15,6 +15,13 @@ namespace Robust.Shared.GameObjects
SubscribeLocalEvent<MapGridComponent, ComponentRemove>(RemoveHandler);
SubscribeLocalEvent<MapGridComponent, ComponentInit>(HandleGridInitialize);
SubscribeLocalEvent<MapGridComponent, ComponentStartup>(HandleGridStartup);
}
private void HandleGridStartup(EntityUid uid, MapGridComponent component, ComponentStartup args)
{
var msg = new GridStartupEvent(uid, component.GridIndex);
EntityManager.EventBus.RaiseLocalEvent(uid, msg);
}
private void RemoveHandler(EntityUid uid, MapGridComponent component, ComponentRemove args)
@@ -29,6 +36,18 @@ namespace Robust.Shared.GameObjects
}
}
public sealed class GridStartupEvent : EntityEventArgs
{
public EntityUid EntityUid { get; }
public GridId GridId { get; }
public GridStartupEvent(EntityUid uid, GridId gridId)
{
EntityUid = uid;
GridId = gridId;
}
}
/// <summary>
/// Raised whenever a grid is being initialized.
/// </summary>

View File

@@ -2,14 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using Prometheus;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Dynamics.Joints;
@@ -29,13 +26,11 @@ namespace Robust.Shared.GameObjects
* Raycasts for non-box shapes.
* SetTransformIgnoreContacts for teleports (and anything else left on the physics body in Farseer)
* Actual center of mass for shapes (currently just assumes center coordinate)
* Circle offsets to entity.
* TOI Solver (continuous collision detection)
* Poly cutting
* Chain shape
* (Content) grenade launcher grenades that explode after time rather than impact.
* pulling prediction
* PVS + Collide allocations / performance
* When someone yeets out of disposals need to have no collision on that object until they stop colliding
* A bunch of objects have collision on round start
* Need a way to specify conditional non-hard collisions (i.e. so items collide with players for IThrowCollide but can still be moved through freely but walls can't collide with them)
@@ -190,6 +185,11 @@ namespace Robust.Shared.GameObjects
{
base.Shutdown();
foreach (var controller in _controllers)
{
controller.Shutdown();
}
_mapManager.MapCreated -= HandleMapCreated;
_mapManager.MapDestroyed -= HandleMapDestroyed;
}
@@ -211,6 +211,7 @@ namespace Robust.Shared.GameObjects
var map = _maps[eventArgs.Map];
map.ContactManager.KinematicControllerCollision -= KinematicControllerCollision;
map.Shutdown();
_maps.Remove(eventArgs.Map);
Logger.DebugS("physics", $"Destroyed physics map for {eventArgs.Map}");
}
@@ -359,5 +360,13 @@ namespace Robust.Shared.GameObjects
_physicsManager.ClearTransforms();
}
internal static (int Batches, int BatchSize) GetBatch(int count, int minimumBatchSize)
{
var batches = Math.Min((int) MathF.Floor((float) count / minimumBatchSize), Math.Max(1, Environment.ProcessorCount));
var batchSize = (int) MathF.Ceiling((float) count / batches);
return (batches, batchSize);
}
}
}

View File

@@ -42,6 +42,8 @@ namespace Robust.Shared.GameObjects
foreach (var ent in anchoredEnts.ToList()) // changing anchored modifies this set
{
if (!EntityManager.EntityExists(ent)) continue;
ComponentManager.GetComponent<TransformComponent>(ent).Anchored = false;
}
}

View File

@@ -83,7 +83,17 @@ namespace Robust.Shared.Map
Box2i CalcLocalBounds();
Box2 CalcWorldBounds();
// TODO: We can rely on the fixture instead for the bounds in the future but we also need to
// update the rendering to account for it. Better working on accurate grid bounds after rotation IMO.
/// <summary>
/// Calculate the bounds of this chunk.
/// </summary>
Box2Rotated CalcWorldBounds(Vector2? worldPos = null, Angle? worldRot = null);
/// <summary>
/// Calculate the AABB for this chunk.
/// </summary>
Box2 CalcWorldAABB(Vector2? worldPos = null, Angle? worldRot = null);
/// <summary>
/// Tests if a point is on top of a non-empty tile.

View File

@@ -47,6 +47,11 @@ namespace Robust.Shared.Map
/// </summary>
Vector2 WorldPosition { get; set; }
/// <summary>
/// The rotation of the grid in world terms.
/// </summary>
Angle WorldRotation { get; set; }
Matrix3 WorldMatrix { get; }
Matrix3 InvWorldMatrix { get; }
@@ -87,6 +92,12 @@ namespace Robust.Shared.Map
/// <param name="tile">The tile to insert at the coordinates.</param>
void SetTile(Vector2i gridIndices, Tile tile);
/// <summary>
/// Modifies many tiles inside of a chunk. Avoids regenerating collision until the end.
/// </summary>
/// <param name="tiles"></param>
void SetTiles(List<(Vector2i GridIndices, Tile Tile)> tiles);
/// <summary>
/// Returns all tiles inside the area that match the predicate.
/// </summary>

View File

@@ -245,19 +245,28 @@ namespace Robust.Shared.Map
return _cachedBounds;
}
public Box2 CalcWorldBounds()
public Box2Rotated CalcWorldBounds(Vector2? gridPos = null, Angle? gridRot = null)
{
var worldPos = _grid.WorldPosition + Indices * _grid.TileSize * ChunkSize;
gridRot ??= _grid.WorldRotation;
gridPos ??= _grid.WorldPosition;
var worldPos = gridPos.Value + gridRot.Value.RotateVec(Indices * _grid.TileSize * ChunkSize);
var localBounds = CalcLocalBounds();
var ts = _grid.TileSize;
var scaledLocalBounds = new Box2(
var scaledLocalBounds = new Box2Rotated(new Box2(
localBounds.Left * ts,
localBounds.Bottom * ts,
localBounds.Right * ts,
localBounds.Top * ts);
localBounds.Top * ts).Translated(worldPos), gridRot.Value, worldPos);
return scaledLocalBounds.Translated(worldPos);
return scaledLocalBounds;
}
public Box2 CalcWorldAABB(Vector2? gridPos = null, Angle? gridRot = null)
{
var bounds = CalcWorldBounds(gridPos, gridRot);
return bounds.CalcBoundingBox();
}
/// <inheritdoc />

View File

@@ -112,6 +112,24 @@ namespace Robust.Shared.Map
}
}
/// <inheritdoc />
[ViewVariables]
public Angle WorldRotation
{
get
{
//TODO: Make grids real parents of entities.
if(GridEntityId.IsValid())
return _mapManager.EntityManager.GetEntity(GridEntityId).Transform.WorldRotation;
return Angle.Zero;
}
set
{
_mapManager.EntityManager.GetEntity(GridEntityId).Transform.WorldRotation = value;
LastModifiedTick = _mapManager.GameTiming.CurTick;
}
}
/// <inheritdoc />
[ViewVariables]
public Matrix3 WorldMatrix
@@ -182,6 +200,7 @@ namespace Robust.Shared.Map
// For now we'll just attach a fixture to each chunk.
// Not raising directed because the grid's EntityUid isn't set yet.
// Don't call GridFixtureSystem directly because it's server-only.
IoCManager
.Resolve<IEntityManager>()
.EventBus
@@ -240,6 +259,26 @@ namespace Robust.Shared.Map
chunk.SetTile((ushort)chunkTile.X, (ushort)chunkTile.Y, tile);
}
/// <inheritdoc />
public void SetTiles(List<(Vector2i GridIndices, Tile Tile)> tiles)
{
var chunks = new HashSet<IMapChunkInternal>();
foreach (var (gridIndices, tile) in tiles)
{
var (chunk, chunkTile) = ChunkAndOffsetForTile(gridIndices);
chunks.Add(chunk);
chunk.SuppressCollisionRegeneration = true;
chunk.SetTile((ushort)chunkTile.X, (ushort)chunkTile.Y, tile);
}
foreach (var chunk in chunks)
{
chunk.SuppressCollisionRegeneration = false;
chunk.RegenerateCollision();
}
}
/// <inheritdoc />
public IEnumerable<TileRef> GetTilesIntersecting(Box2 worldArea, bool ignoreEmpty = true, Predicate<TileRef>? predicate = null)
{
@@ -284,20 +323,13 @@ namespace Robust.Shared.Map
public IEnumerable<TileRef> GetTilesIntersecting(Circle worldArea, bool ignoreEmpty = true, Predicate<TileRef>? predicate = null)
{
var aabb = new Box2(worldArea.Position.X - worldArea.Radius, worldArea.Position.Y - worldArea.Radius, worldArea.Position.X + worldArea.Radius, worldArea.Position.Y + worldArea.Radius);
var circleGridPos = new EntityCoordinates(GridEntityId, WorldToLocal(worldArea.Position));
foreach (var tile in GetTilesIntersecting(aabb, ignoreEmpty))
{
var local = GridTileToLocal(tile.GridIndices);
var gridId = tile.GridIndex;
if (!_mapManager.TryGetGrid(gridId, out var grid))
{
continue;
}
var to = new EntityCoordinates(grid.GridEntityId, worldArea.Position);
if (!local.TryDistance(_entityManager, to, out var distance))
if (!local.TryDistance(_entityManager, circleGridPos, out var distance))
{
continue;
}
@@ -420,7 +452,7 @@ namespace Robust.Shared.Map
RemoveFromSnapGridCell(TileIndicesFor(coords), euid);
}
private (IMapChunk, Vector2i) ChunkAndOffsetForTile(Vector2i pos)
private (IMapChunkInternal, Vector2i) ChunkAndOffsetForTile(Vector2i pos)
{
var gridChunkIndices = GridTileToChunkIndices(pos);
var chunk = GetChunk(gridChunkIndices);

View File

@@ -8,6 +8,7 @@ using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -523,23 +524,33 @@ namespace Robust.Shared.Map
/// <inheritdoc />
public bool TryFindGridAt(MapId mapId, Vector2 worldPos, [NotNullWhen(true)] out IMapGrid? grid)
{
// TODO: this won't actually "work" but tests are fucking me hard
// We probably need to move these methods over to SharedBroadphaseSystem as they need to go through
// physics to find grids intersecting a point anyway but the level of refactoring required for that
// will kill me.
foreach (var mapGrid in _grids.Values)
foreach (var (_, mapGrid) in _grids)
{
if (mapGrid.ParentMapId != mapId)
continue;
if (!mapGrid.WorldBounds.Contains(worldPos))
continue;
var gridEnt = _entityManager.GetEntity(mapGrid.GridEntityId);
grid = mapGrid;
return true;
if (!gridEnt.TryGetComponent(out PhysicsComponent? body)) continue;
var transform = new Transform(gridEnt.Transform.WorldPosition, (float) gridEnt.Transform.WorldRotation);
// We can't rely on the broadphase proxies existing as they're deferred until physics requires the update.
foreach (var fixture in body.Fixtures)
{
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
// TODO: Need TestPoint from Box2D
if (fixture.Shape.ComputeAABB(transform, i).Contains(worldPos))
{
grid = mapGrid;
return true;
}
}
}
}
grid = default;
grid = null;
return false;
}
@@ -551,9 +562,44 @@ namespace Robust.Shared.Map
public IEnumerable<IMapGrid> FindGridsIntersecting(MapId mapId, Box2 worldArea)
{
// TODO: Unfortunately can't use BroadphaseSystem here as it will explode. Need to suss it out with DI
// TryFindGridAt works as is and helps with grid traversals.
return _grids.Values.Where(g => g.ParentMapId == mapId && g.WorldBounds.Intersects(worldArea));
foreach (var (_, grid) in _grids)
{
if (grid.ParentMapId != mapId) continue;
var found = false;
// TODO: Future optimisation; we can probably do a very fast check with no fixtures
// by just knowing the chunk bounds of the grid and then checking that first
// this will mainly be helpful once we get multiple grids.
var gridEnt = _entityManager.GetEntity(grid.GridEntityId);
var body = gridEnt.GetComponent<PhysicsComponent>();
var transform = new Transform(gridEnt.Transform.WorldPosition, (float) gridEnt.Transform.WorldRotation);
if (body.FixtureCount > 0)
{
// We can't rely on the broadphase proxies existing as they're deferred until physics requires the update.
foreach (var fixture in body.Fixtures)
{
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
// TODO: Need to use CollisionManager to test detailed overlap
if (fixture.Shape.ComputeAABB(transform, i).Intersects(worldArea))
{
yield return grid;
found = true;
break;
}
}
if (found)
break;
}
}
else if (worldArea.Contains(transform.Position))
{
yield return grid;
}
}
}
public IEnumerable<GridId> FindGridIdsIntersecting(MapId mapId, Box2 worldArea, bool includeInvalid = false)

View File

@@ -82,13 +82,8 @@ namespace Robust.Shared.Physics.Collision
/*
* Farseer had this as a static class with a ThreadStatic DistanceInput
*
* I also had to add Point initializers everywhere for the manifold as I just used an array but
* should also profile just using FixedArray2 / FixedArray3
*/
private DistanceInput _input = new();
/// <summary>
/// Test overlap between the two shapes.
/// </summary>
@@ -102,15 +97,16 @@ namespace Robust.Shared.Physics.Collision
bool ICollisionManager.TestOverlap(IPhysShape shapeA, int indexA, IPhysShape shapeB, int indexB,
in Transform xfA, in Transform xfB)
{
_input.ProxyA.Set(shapeA, indexA);
_input.ProxyB.Set(shapeB, indexB);
_input.TransformA = xfA;
_input.TransformB = xfB;
_input.UseRadii = true;
// TODO: Make this a struct.
var input = new DistanceInput();
SimplexCache cache;
DistanceOutput output;
DistanceManager.ComputeDistance(out output, out cache, _input);
input.ProxyA.Set(shapeA, indexA);
input.ProxyB.Set(shapeB, indexB);
input.TransformA = xfA;
input.TransformB = xfB;
input.UseRadii = true;
DistanceManager.ComputeDistance(out var output, out _, input);
return output.Distance < 10.0f * float.Epsilon;
}
@@ -177,8 +173,7 @@ namespace Robust.Shared.Physics.Collision
manifold.PointCount = 0;
// Compute circle in frame of edge
// TODO: Circle Position
Vector2 Q = Transform.MulT(transformA, Transform.Mul(transformB, Vector2.Zero));
Vector2 Q = Transform.MulT(transformA, Transform.Mul(transformB, circleB.Position));
Vector2 A = edgeA.Vertex1, B = edgeA.Vertex2;
Vector2 e = B - A;
@@ -227,13 +222,10 @@ namespace Robust.Shared.Physics.Collision
manifold.Type = ManifoldType.Circles;
manifold.LocalNormal = Vector2.Zero;
manifold.LocalPoint = P;
ManifoldPoint mp = new ManifoldPoint
{
Id = {Key = 0, Features = cf},
//LocalPoint = circleB.Position
LocalPoint = Vector2.Zero
};
manifold.Points[0] = mp;
ref var mp = ref manifold.Points[0];
mp.Id.Key = 0;
mp.Id.Features = cf;
mp.LocalPoint = circleB.Position;
return;
}
@@ -269,13 +261,10 @@ namespace Robust.Shared.Physics.Collision
manifold.Type = ManifoldType.Circles;
manifold.LocalNormal = Vector2.Zero;
manifold.LocalPoint = P;
ManifoldPoint mp = new ManifoldPoint
{
Id = {Key = 0, Features = cf},
//LocalPoint = circleB.Position
LocalPoint = Vector2.Zero
};
manifold.Points[0] = mp;
ref var mp = ref manifold.Points[0];
mp.Id.Key = 0;
mp.Id.Features = cf;
mp.LocalPoint = circleB.Position;
return;
}
@@ -304,14 +293,10 @@ namespace Robust.Shared.Physics.Collision
manifold.Type = ManifoldType.FaceA;
manifold.LocalNormal = n;
manifold.LocalPoint = A;
ManifoldPoint mp2 = new ManifoldPoint
{
Id = {Key = 0, Features = cf},
//LocalPoint = circleB.Position
LocalPoint = Vector2.Zero
};
manifold.Points[0] = mp2;
ref var mp2 = ref manifold.Points[0];
mp2.Id.Key = 0;
mp2.Id.Features = cf;
mp2.LocalPoint = circleB.Position;
}
public void CollideCircles(ref Manifold manifold, PhysShapeCircle circleA, in Transform xfA,
@@ -320,9 +305,8 @@ namespace Robust.Shared.Physics.Collision
{
manifold.PointCount = 0;
// TODO Circle / shape offsets
Vector2 pA = Transform.Mul(xfA, Vector2.Zero);
Vector2 pB = Transform.Mul(xfB, Vector2.Zero);
Vector2 pA = Transform.Mul(xfA, circleA.Position);
Vector2 pB = Transform.Mul(xfB, circleB.Position);
Vector2 d = pB - pA;
float distSqr = Vector2.Dot(d, d);
@@ -333,16 +317,14 @@ namespace Robust.Shared.Physics.Collision
}
manifold.Type = ManifoldType.Circles;
manifold.LocalPoint = Vector2.Zero; // Also here
manifold.LocalPoint = circleA.Position;
manifold.LocalNormal = Vector2.Zero;
manifold.PointCount = 1;
ManifoldPoint p0 = manifold.Points[0];
ref var p0 = ref manifold.Points[0];
p0.LocalPoint = Vector2.Zero; // Also here
p0.Id.Key = 0;
manifold.Points[0] = p0;
}
/// <summary>
@@ -364,6 +346,7 @@ namespace Robust.Shared.Physics.Collision
private class EPCollider
{
private float _polygonRadius;
private float _angularSlop;
private TempPolygon _polygonB;
@@ -379,6 +362,7 @@ namespace Robust.Shared.Physics.Collision
internal EPCollider(IConfigurationManager configManager)
{
_polygonRadius = configManager.GetCVar(CVars.PolygonRadius);
_angularSlop = configManager.GetCVar(CVars.AngularSlop);
_polygonB = new TempPolygon(configManager);
}
@@ -397,8 +381,7 @@ namespace Robust.Shared.Physics.Collision
_xf = Transform.MulT(xfA, xfB);
// TODO: Centroid
_centroidB = Transform.Mul(_xf, Vector2.Zero);
_centroidB = Transform.Mul(_xf, polygonB.Centroid);
_v0 = edgeA.Vertex0;
_v1 = edgeA.Vertex1;
@@ -639,7 +622,7 @@ namespace Robust.Shared.Physics.Collision
primaryAxis = edgeAxis;
}
ClipVertex[] ie = new ClipVertex[2];
Span<ClipVertex> ie = stackalloc ClipVertex[2];
ReferenceFace rf;
if (primaryAxis.Type == EPAxisType.EdgeA)
{
@@ -661,21 +644,19 @@ namespace Robust.Shared.Physics.Collision
int i1 = bestIndex;
int i2 = i1 + 1 < _polygonB.Count ? i1 + 1 : 0;
ClipVertex c0 = ie[0];
ref var c0 = ref ie[0];
c0.V = _polygonB.Vertices[i1];
c0.ID.Features.IndexA = 0;
c0.ID.Features.IndexB = (byte) i1;
c0.ID.Features.TypeA = (byte) ContactFeatureType.Face;
c0.ID.Features.TypeB = (byte) ContactFeatureType.Vertex;
ie[0] = c0;
ClipVertex c1 = ie[1];
ref var c1 = ref ie[1];
c1.V = _polygonB.Vertices[i2];
c1.ID.Features.IndexA = 0;
c1.ID.Features.IndexB = (byte) i2;
c1.ID.Features.TypeA = (byte) ContactFeatureType.Face;
c1.ID.Features.TypeB = (byte) ContactFeatureType.Vertex;
ie[1] = c1;
if (_front)
{
@@ -697,21 +678,19 @@ namespace Robust.Shared.Physics.Collision
else
{
manifold.Type = ManifoldType.FaceB;
ClipVertex c0 = ie[0];
ref var c0 = ref ie[0];
c0.V = _v1;
c0.ID.Features.IndexA = 0;
c0.ID.Features.IndexB = (byte) primaryAxis.Index;
c0.ID.Features.TypeA = (byte) ContactFeatureType.Vertex;
c0.ID.Features.TypeB = (byte) ContactFeatureType.Face;
ie[0] = c0;
ClipVertex c1 = ie[1];
ref var c1 = ref ie[1];
c1.V = _v2;
c1.ID.Features.IndexA = 0;
c1.ID.Features.IndexB = (byte) primaryAxis.Index;
c1.ID.Features.TypeA = (byte) ContactFeatureType.Vertex;
c1.ID.Features.TypeB = (byte) ContactFeatureType.Face;
ie[1] = c1;
rf.i1 = primaryAxis.Index;
rf.i2 = rf.i1 + 1 < _polygonB.Count ? rf.i1 + 1 : 0;
@@ -727,10 +706,9 @@ namespace Robust.Shared.Physics.Collision
// Clip incident edge against extruded edge1 side edges.
Span<ClipVertex> clipPoints1 = stackalloc ClipVertex[2];
int np;
// Clip to box side 1
np = ClipSegmentToLine(clipPoints1, ie, rf.sideNormal1, rf.sideOffset1, rf.i1);
var np = ClipSegmentToLine(clipPoints1, ie, rf.sideNormal1, rf.sideOffset1, rf.i1);
if (np < 2)
{
@@ -767,7 +745,7 @@ namespace Robust.Shared.Physics.Collision
if (separation <= _radius)
{
ManifoldPoint cp = manifold.Points[pointCount];
ref var cp = ref manifold.Points[pointCount];
if (primaryAxis.Type == EPAxisType.EdgeA)
{
@@ -783,7 +761,6 @@ namespace Robust.Shared.Physics.Collision
cp.Id.Features.IndexB = clipPoints2[i].ID.Features.IndexA;
}
manifold.Points[pointCount] = cp;
++pointCount;
}
}
@@ -837,19 +814,17 @@ namespace Robust.Shared.Physics.Collision
return axis;
}
var angularSlop = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.LinearSlop);
// Adjacency
if (Vector2.Dot(n, perp) >= 0.0f)
{
if (Vector2.Dot(n - _upperLimit, _normal) < -angularSlop)
if (Vector2.Dot(n - _upperLimit, _normal) < -_angularSlop)
{
continue;
}
}
else
{
if (Vector2.Dot(n - _lowerLimit, _normal) < -angularSlop)
if (Vector2.Dot(n - _lowerLimit, _normal) < -_angularSlop)
{
continue;
}
@@ -881,7 +856,7 @@ namespace Robust.Shared.Physics.Collision
manifold.PointCount = 0;
// Compute circle position in the frame of the polygon.
Vector2 c = Transform.Mul(xfB, Vector2.Zero); // TODO pos
Vector2 c = Transform.Mul(xfB, circleB.Position);
Vector2 cLocal = Transform.MulT(xfA, c);
// Find the min separating edge.
@@ -923,13 +898,11 @@ namespace Robust.Shared.Physics.Collision
manifold.LocalNormal = polygonA.Normals[normalIndex];
manifold.LocalPoint = (v1 + v2) * 0.5f;
ManifoldPoint p0 = manifold.Points[0];
ref var p0 = ref manifold.Points[0];
p0.LocalPoint = Vector2.Zero; // TODO pos
p0.LocalPoint = circleB.Position;
p0.Id.Key = 0;
manifold.Points[0] = p0;
return;
}
@@ -955,12 +928,10 @@ namespace Robust.Shared.Physics.Collision
manifold.LocalNormal.Y = manifold.LocalNormal.Y * factor;
manifold.LocalPoint = v1;
ManifoldPoint p0b = manifold.Points[0];
ref var p0b = ref manifold.Points[0];
p0b.LocalPoint = Vector2.Zero; // TODO pos
p0b.LocalPoint = circleB.Position;
p0b.Id.Key = 0;
manifold.Points[0] = p0b;
}
else if (u2 <= 0.0f)
{
@@ -981,12 +952,10 @@ namespace Robust.Shared.Physics.Collision
manifold.LocalNormal.Y = manifold.LocalNormal.Y * factor;
manifold.LocalPoint = v2;
ManifoldPoint p0c = manifold.Points[0];
ref var p0c = ref manifold.Points[0];
p0c.LocalPoint = Vector2.Zero; // TODO pos
p0c.LocalPoint = circleB.Position;
p0c.Id.Key = 0;
manifold.Points[0] = p0c;
}
else
{
@@ -1004,12 +973,10 @@ namespace Robust.Shared.Physics.Collision
manifold.LocalNormal = polygonA.Normals[vertIndex1];
manifold.LocalPoint = faceCenter;
ManifoldPoint p0d = manifold.Points[0];
ref var p0d = ref manifold.Points[0];
p0d.LocalPoint = Vector2.Zero; // TODO pos
p0d.LocalPoint = circleB.Position;
p0d.Id.Key = 0;
manifold.Points[0] = p0d;
}
}
@@ -1130,7 +1097,7 @@ namespace Robust.Shared.Physics.Collision
if (separation <= totalRadius)
{
ManifoldPoint cp = manifold.Points[pointCount];
ref var cp = ref manifold.Points[pointCount];
cp.LocalPoint = Transform.MulT(xf2, clipPoints2[i].V);
cp.Id = clipPoints2[i].ID;
@@ -1144,8 +1111,6 @@ namespace Robust.Shared.Physics.Collision
cp.Id.Features.TypeB = cf.TypeA;
}
manifold.Points[pointCount] = cp;
pointCount++;
}
}
@@ -1233,9 +1198,9 @@ namespace Robust.Shared.Physics.Collision
if (distance0 * distance1 < 0.0f)
{
// Find intersection point of edge and plane
float interp = distance0 / (distance0 - distance1);
var interp = distance0 / (distance0 - distance1);
ClipVertex cv = vOut[numOut];
ref var cv = ref vOut[numOut];
cv.V.X = v0.V.X + interp * (v1.V.X - v0.V.X);
cv.V.Y = v0.V.Y + interp * (v1.V.Y - v0.V.Y);
@@ -1246,8 +1211,6 @@ namespace Robust.Shared.Physics.Collision
cv.ID.Features.TypeA = (byte) ContactFeatureType.Vertex;
cv.ID.Features.TypeB = (byte) ContactFeatureType.Face;
vOut[numOut] = cv;
++numOut;
}
@@ -1305,10 +1268,10 @@ namespace Robust.Shared.Physics.Collision
var v1 = Transform.Mul(xf, v1s[i]);
// Find deepest point for normal i.
float si = float.MaxValue;
var si = float.MaxValue;
for (var j = 0; j < count2; ++j)
{
float sij = Vector2.Dot(n, v2s[j] - v1);
var sij = Vector2.Dot(n, v2s[j] - v1);
if (sij < si)
{
si = sij;
@@ -1326,26 +1289,27 @@ namespace Robust.Shared.Physics.Collision
return maxSeparation;
}
private static void FindIncidentEdge(Span<ClipVertex> c, PolygonShape poly1, in Transform xf1, int edge1,
PolygonShape poly2, in Transform xf2)
private static void FindIncidentEdge(Span<ClipVertex> c, PolygonShape poly1, in Transform xf1, int edge1, PolygonShape poly2, in Transform xf2)
{
List<Vector2> normals1 = poly1.Normals;
int count2 = poly2.Vertices.Count;
var count2 = poly2.Vertices.Count;
List<Vector2> vertices2 = poly2.Vertices;
List<Vector2> normals2 = poly2.Normals;
Debug.Assert(0 <= edge1 && edge1 < poly1.Vertices.Count);
// Get the normal of the reference edge in poly2's frame.
Vector2 normal1 = Transform.MulT(xf2.Quaternion2D, Transform.Mul(xf1.Quaternion2D, normals1[edge1]));
var normal1 = Transform.MulT(xf2.Quaternion2D, Transform.Mul(xf1.Quaternion2D, normals1[edge1]));
// Find the incident edge on poly2.
int index = 0;
float minDot = float.MaxValue;
var index = 0;
var minDot = float.MaxValue;
for (int i = 0; i < count2; ++i)
{
float dot = Vector2.Dot(normal1, normals2[i]);
var dot = Vector2.Dot(normal1, normals2[i]);
if (dot < minDot)
{
minDot = dot;
@@ -1354,10 +1318,10 @@ namespace Robust.Shared.Physics.Collision
}
// Build the clip vertices for the incident edge.
int i1 = index;
int i2 = i1 + 1 < count2 ? i1 + 1 : 0;
var i1 = index;
var i2 = i1 + 1 < count2 ? i1 + 1 : 0;
ClipVertex cv0 = c[0];
ref var cv0 = ref c[0];
cv0.V = Transform.Mul(xf2, vertices2[i1]);
cv0.ID.Features.IndexA = (byte) edge1;
@@ -1365,16 +1329,12 @@ namespace Robust.Shared.Physics.Collision
cv0.ID.Features.TypeA = (byte) ContactFeatureType.Face;
cv0.ID.Features.TypeB = (byte) ContactFeatureType.Vertex;
c[0] = cv0;
ClipVertex cv1 = c[1];
ref var cv1 = ref c[1];
cv1.V = Transform.Mul(xf2, vertices2[i2]);
cv1.ID.Features.IndexA = (byte) edge1;
cv1.ID.Features.IndexB = (byte) i2;
cv1.ID.Features.TypeA = (byte) ContactFeatureType.Face;
cv1.ID.Features.TypeB = (byte) ContactFeatureType.Vertex;
c[1] = cv1;
}
}

View File

@@ -22,6 +22,7 @@
using System;
using System.Runtime.InteropServices;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
namespace Robust.Shared.Physics.Collision
@@ -130,7 +131,7 @@ namespace Robust.Shared.Physics.Collision
/// All contact scenarios must be expressed in one of these types.
/// This structure is stored across time steps, so we keep it small.
/// </summary>
public struct Manifold
public struct Manifold : IEquatable<Manifold>, IApproxEquatable<Manifold>
{
public Vector2 LocalNormal;
@@ -144,48 +145,57 @@ namespace Robust.Shared.Physics.Collision
/// <summary>
/// Points of contact, can only be 0 -> 2.
/// </summary>
internal FixedArray2<ManifoldPoint> Points;
internal ManifoldPoint[] Points;
public ManifoldType Type;
}
internal struct FixedArray2<T>
{
private T _value0;
private T _value1;
public T this[int index]
public bool Equals(Manifold other)
{
get
if (!(PointCount == other.PointCount &&
Type == other.Type &&
LocalNormal.Equals(other.LocalNormal) &&
LocalPoint.Equals(other.LocalPoint))) return false;
for (var i = 0; i < PointCount; i++)
{
switch (index)
{
case 0:
return _value0;
case 1:
return _value1;
default:
throw new IndexOutOfRangeException();
}
if (!Points[i].Equals(other.Points[i])) return false;
}
set
return true;
}
public bool EqualsApprox(Manifold other)
{
if (!(PointCount == other.PointCount &&
Type == other.Type &&
LocalNormal.EqualsApprox(other.LocalNormal) &&
LocalPoint.EqualsApprox(other.LocalPoint))) return false;
for (var i = 0; i < PointCount; i++)
{
switch (index)
{
case 0:
_value0 = value;
break;
case 1:
_value1 = value;
break;
default:
throw new IndexOutOfRangeException();
}
if (!Points[i].EqualsApprox(other.Points[i])) return false;
}
return true;
}
public bool EqualsApprox(Manifold other, double tolerance)
{
if (!(PointCount == other.PointCount &&
Type == other.Type &&
LocalNormal.EqualsApprox(other.LocalNormal, tolerance) &&
LocalPoint.EqualsApprox(other.LocalPoint, tolerance))) return false;
for (var i = 0; i < PointCount; i++)
{
if (!Points[i].EqualsApprox(other.Points[i], tolerance)) return false;
}
return true;
}
}
public struct ManifoldPoint
public struct ManifoldPoint : IEquatable<ManifoldPoint>, IApproxEquatable<ManifoldPoint>
{
/// <summary>
/// Unique identifier for the contact point between 2 shapes.
@@ -239,5 +249,21 @@ namespace Robust.Shared.Physics.Collision
hashcode = (hashcode * 397) ^ TangentImpulse.GetHashCode();
return hashcode;
}
public bool EqualsApprox(ManifoldPoint other)
{
return Id == other.Id &&
LocalPoint.EqualsApprox(other.LocalPoint) &&
MathHelper.CloseTo(NormalImpulse, other.NormalImpulse) &&
MathHelper.CloseTo(TangentImpulse, other.TangentImpulse);
}
public bool EqualsApprox(ManifoldPoint other, double tolerance)
{
return Id == other.Id &&
LocalPoint.EqualsApprox(other.LocalPoint, tolerance) &&
MathHelper.CloseTo(NormalImpulse, other.NormalImpulse, tolerance) &&
MathHelper.CloseTo(TangentImpulse, other.TangentImpulse, tolerance);
}
}
}

View File

@@ -26,11 +26,11 @@ using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Collision.Shapes
{
[Serializable, NetSerializable]
[DataDefinition]
public sealed class EdgeShape : IPhysShape
{
internal Vector2 Centroid { get; set; } = Vector2.Zero;
@@ -141,6 +141,20 @@ namespace Robust.Shared.Physics.Collision.Shapes
return aabb;
}
public Box2 ComputeAABB(Transform transform, int childIndex)
{
DebugTools.Assert(childIndex == 0);
var v1 = Transform.Mul(transform, Vertex1);
var v2 = Transform.Mul(transform, Vertex2);
var lower = Vector2.ComponentMin(v1, v2);
var upper = Vector2.ComponentMax(v1, v2);
var radius = new Vector2(_radius, _radius);
return new Box2(lower - radius, upper + radius);
}
public float CalculateArea()
{
// It's a line

View File

@@ -36,15 +36,10 @@ namespace Robust.Shared.Physics.Collision.Shapes
ShapeType ShapeType { get; }
// TODO: Like raycasts these aren't using exact shapes so need to do that.
bool Intersects(Box2 worldAABB, Vector2 worldPos, Angle worldRot);
/// <summary>
/// Calculates the AABB of the shape.
/// Calculate the AABB of the shape.
/// </summary>
/// <param name="rotation"></param>
/// <returns></returns>
Box2 CalculateLocalBounds(Angle rotation);
Box2 ComputeAABB(Transform transform, int childIndex);
void ApplyState();

View File

@@ -72,6 +72,11 @@ namespace Robust.Shared.Physics.Collision.Shapes
_radius = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.PolygonRadius);
}
public Box2 ComputeAABB(Transform transform, int childIndex)
{
return new Box2Rotated(_localBounds.Translated(transform.Position), transform.Quaternion2D.Angle, transform.Position).CalcBoundingBox().Enlarged(_radius);
}
/// <inheritdoc />
public void ApplyState() { }

View File

@@ -2,6 +2,7 @@
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Physics.Collision.Shapes
@@ -58,23 +59,19 @@ namespace Robust.Shared.Physics.Collision.Shapes
}
}
public bool Intersects(Box2 worldAABB, Vector2 worldPos, Angle worldRot)
{
var bounds = CalculateLocalBounds(worldRot).Translated(worldPos);
return bounds.Intersects(worldAABB);
}
/// <inheritdoc />
public Box2 CalculateLocalBounds(Angle rotation)
{
return new(-_radius, -_radius, _radius, _radius);
}
public float CalculateArea()
{
return MathF.PI * _radius * _radius;
}
public Box2 ComputeAABB(Transform transform, int childIndex)
{
DebugTools.Assert(childIndex == 0);
var p = transform.Position + Transform.Mul(transform.Quaternion2D, Position);
return new Box2(p.X - _radius, p.Y - _radius, p.X + _radius, p.Y + _radius);
}
/// <inheritdoc />
public void ApplyState() { }

View File

@@ -53,6 +53,12 @@ namespace Robust.Shared.Physics.Collision.Shapes
[ViewVariables]
private Box2 _cachedBounds;
public Box2 ComputeAABB(Transform transform, int childIndex)
{
return new Box2Rotated(_cachedBounds.Translated(transform.Position), transform.Quaternion2D.Angle,
transform.Position).CalcBoundingBox().Enlarged(_radius);
}
/// <inheritdoc />
public void ApplyState() { }

View File

@@ -76,7 +76,6 @@ namespace Robust.Shared.Physics.Collision.Shapes
}
Centroid = ComputeCentroid(_vertices);
_cachedAABB = CalculateAABB();
// Compute the polygon mass data
// TODO: Update fixture. Maybe use events for it? Who tf knows.
@@ -86,8 +85,6 @@ namespace Robust.Shared.Physics.Collision.Shapes
private List<Vector2> _vertices = new();
private Box2 _cachedAABB;
internal Vector2 Centroid { get; set; } = Vector2.Zero;
[ViewVariables(VVAccess.ReadOnly)]
@@ -164,31 +161,18 @@ namespace Robust.Shared.Physics.Collision.Shapes
_radius = radius;
}
public void SetAsBox(float width, float height)
public void SetAsBox(float halfWidth, float halfHeight)
{
// TODO: Just have this set normals directly; look at Box2D to see how it does
Vertices = new List<Vector2>()
{
new(-width, -height),
new(width, -height),
new(width, height),
new(-width, height),
new(-halfWidth, -halfHeight),
new(halfWidth, -halfHeight),
new(halfWidth, halfHeight),
new(-halfWidth, halfHeight),
};
}
/// <summary>
/// A temporary optimisation that bypasses GiftWrapping until we get proper AABB and Rect collisions
/// </summary>
internal void SetVertices(List<Vector2> vertices)
{
DebugTools.Assert(vertices.Count == 4, "Vertices optimisation only usable on rectangles");
// TODO: Get what the normals should be
Vertices = vertices;
// _normals = new List<Vector2>()
// Verify on debug that the vertices skip was actually USEFUL
DebugTools.Assert(Vertices == vertices);
}
public bool Equals(IPhysShape? other)
{
if (other is not PolygonShape poly) return false;
@@ -202,44 +186,21 @@ namespace Robust.Shared.Physics.Collision.Shapes
return true;
}
public bool Intersects(Box2 worldAABB, Vector2 worldPos, Angle worldRot)
public Box2 ComputeAABB(Transform transform, int childIndex)
{
var aabb = CalculateLocalBounds(worldRot).Translated(worldPos);
if (!worldAABB.Intersects(aabb)) return false;
// TODO
return true;
}
/// <summary>
/// Calculating the AABB for a polyshape isn't cheap so we'll just cache it whenever its vertices change.
/// </summary>
private Box2 CalculateAABB()
{
if (_vertices.Count == 0) return new Box2();
var lower = Vertices[0];
DebugTools.Assert(childIndex == 0);
var lower = Transform.Mul(transform, _vertices[0]);
var upper = lower;
for (var i = 1; i < Vertices.Count; ++i)
for (var i = 1; i < _vertices.Count; ++i)
{
var v = Vertices[i];
var v = Transform.Mul(transform, _vertices[i]);
lower = Vector2.ComponentMin(lower, v);
upper = Vector2.ComponentMax(upper, v);
}
var r = new Vector2(Radius, Radius);
return new Box2
{
BottomLeft = lower - r,
TopRight = upper + r
};
}
public Box2 CalculateLocalBounds(Angle rotation)
{
return rotation == Angle.Zero ? _cachedAABB : new Box2Rotated(_cachedAABB, rotation).CalcBoundingBox();
var r = new Vector2(_radius, _radius);
return new Box2(lower - r, upper + r);
}
public void ApplyState()

View File

@@ -26,6 +26,8 @@ namespace Robust.Shared.Physics.Controllers
IoCManager.InjectDependencies(this);
}
public virtual void Shutdown() {}
/// <summary>
/// Run before any map processing starts.
/// </summary>

View File

@@ -28,14 +28,14 @@
*/
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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.Physics.Collision;
using Robust.Shared.Physics.Dynamics.Contacts;
using Robust.Shared.Utility;
@@ -60,10 +60,10 @@ namespace Robust.Shared.Physics.Dynamics
/// </summary>
internal event Action<Fixture, Fixture, float, Vector2>? KinematicControllerCollision;
private int _contactMultithreadThreshold;
private int _contactMinimumThreads;
// TODO: Also need to clean the station up to not have 160 contacts on roundstart
// TODO: CollideMultiCore
private List<Contact> _startCollisions = new();
private List<Contact> _endCollisions = new();
public ContactManager()
{
@@ -74,9 +74,30 @@ namespace Robust.Shared.Physics.Dynamics
public void Initialize()
{
IoCManager.InjectDependencies(this);
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CVars.ContactMultithreadThreshold, OnContactMultithreadThreshold, true);
configManager.OnValueChanged(CVars.ContactMinimumThreads, OnContactMinimumThreads, true);
InitializePool();
}
public void Shutdown()
{
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.UnsubValueChanged(CVars.ContactMultithreadThreshold, OnContactMultithreadThreshold);
configManager.UnsubValueChanged(CVars.ContactMinimumThreads, OnContactMinimumThreads);
}
private void OnContactMultithreadThreshold(int value)
{
_contactMultithreadThreshold = value;
}
private void OnContactMinimumThreads(int value)
{
_contactMinimumThreads = value;
}
private void InitializePool()
{
for (var i = 0; i < ContactPoolInitialSize; i++)
@@ -247,8 +268,14 @@ namespace Robust.Shared.Physics.Dynamics
internal void Collide()
{
// Due to the fact some contacts may be removed (and we need to update this array as we iterate).
// the length may not match the actual contact count, hence we track the index.
var contacts = ArrayPool<Contact>.Shared.Rent(ContactCount);
var index = 0;
// Can be changed while enumerating
// TODO: check for null instead?
// Work out which contacts are still valid before we decide to update manifolds.
for (var contact = ContactList.Next; contact != ContactList;)
{
if (contact == null) break;
@@ -348,43 +375,103 @@ namespace Robust.Shared.Physics.Dynamics
continue;
}
// The contact persists.
contact.Update(_physicsManager, _startCollisions, _endCollisions);
contacts[index++] = contact;
contact = contact.Next;
}
foreach (var contact in _startCollisions)
var status = ArrayPool<ContactStatus>.Shared.Rent(index);
// To avoid race conditions with the dictionary we'll cache all of the transforms up front.
// Caching should provide better perf than multi-threading the GetTransform() as we can also re-use
// these in PhysicsIsland as well.
for (var i = 0; i < index; i++)
{
if (!contact.IsTouching) continue;
var contact = contacts[i];
var bodyA = contact.FixtureA!.Body;
var bodyB = contact.FixtureB!.Body;
var fixtureA = contact.FixtureA!;
var fixtureB = contact.FixtureB!;
var bodyA = fixtureA.Body;
var bodyB = fixtureB.Body;
_entityManager.EventBus.RaiseLocalEvent(bodyA.Owner.Uid, new StartCollideEvent(fixtureA, fixtureB));
_entityManager.EventBus.RaiseLocalEvent(bodyB.Owner.Uid, new StartCollideEvent(fixtureB, fixtureA));
_physicsManager.CreateTransform(bodyA);
_physicsManager.CreateTransform(bodyB);
}
foreach (var contact in _endCollisions)
// Update contacts all at once.
BuildManifolds(contacts, index, status);
// Single-threaded so content doesn't need to worry about race conditions.
for (var i = 0; i < index; i++)
{
var fixtureA = contact.FixtureA;
var fixtureB = contact.FixtureB;
var contact = contacts[i];
// If something under StartCollideEvent potentially nukes other contacts (e.g. if the entity is deleted)
// then we'll just skip the EndCollide.
if (fixtureA == null || fixtureB == null) continue;
switch (status[i])
{
case ContactStatus.StartTouching:
{
if (!contact.IsTouching) continue;
var bodyA = fixtureA.Body;
var bodyB = fixtureB.Body;
var fixtureA = contact.FixtureA!;
var fixtureB = contact.FixtureB!;
var bodyA = fixtureA.Body;
var bodyB = fixtureB.Body;
_entityManager.EventBus.RaiseLocalEvent(bodyA.Owner.Uid, new EndCollideEvent(fixtureA, fixtureB));
_entityManager.EventBus.RaiseLocalEvent(bodyB.Owner.Uid, new EndCollideEvent(fixtureB, fixtureA));
_entityManager.EventBus.RaiseLocalEvent(bodyA.Owner.Uid, new StartCollideEvent(fixtureA, fixtureB));
_entityManager.EventBus.RaiseLocalEvent(bodyB.Owner.Uid, new StartCollideEvent(fixtureB, fixtureA));
break;
}
case ContactStatus.Touching:
break;
case ContactStatus.EndTouching:
{
var fixtureA = contact.FixtureA;
var fixtureB = contact.FixtureB;
// If something under StartCollideEvent potentially nukes other contacts (e.g. if the entity is deleted)
// then we'll just skip the EndCollide.
if (fixtureA == null || fixtureB == null) continue;
var bodyA = fixtureA.Body;
var bodyB = fixtureB.Body;
_entityManager.EventBus.RaiseLocalEvent(bodyA.Owner.Uid, new EndCollideEvent(fixtureA, fixtureB));
_entityManager.EventBus.RaiseLocalEvent(bodyB.Owner.Uid, new EndCollideEvent(fixtureB, fixtureA));
break;
}
case ContactStatus.NoContact:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
_startCollisions.Clear();
_endCollisions.Clear();
ArrayPool<Contact>.Shared.Return(contacts);
ArrayPool<ContactStatus>.Shared.Return(status);
}
private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status)
{
if (count > _contactMultithreadThreshold * _contactMinimumThreads)
{
var (batches, batchSize) = SharedPhysicsSystem.GetBatch(count, _contactMultithreadThreshold);
Parallel.For(0, batches, i =>
{
var start = i * batchSize;
var end = Math.Min(start + batchSize, count);
UpdateContacts(contacts, start, end, status);
});
}
else
{
UpdateContacts(contacts, 0, count, status);
}
}
private void UpdateContacts(Contact[] contacts, int start, int end, ContactStatus[] status)
{
for (var i = start; i < end; i++)
{
status[i] = contacts[i].Update(_physicsManager);
}
}
public void PreSolve(float frameTime)
@@ -465,4 +552,12 @@ namespace Robust.Shared.Physics.Dynamics
}
#endregion
internal enum ContactStatus : byte
{
NoContact = 0,
StartTouching = 1,
Touching = 2,
EndTouching = 3,
}
}

View File

@@ -177,6 +177,10 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
#if DEBUG
_debugPhysics = EntitySystem.Get<SharedDebugPhysicsSystem>();
#endif
Manifold = new Manifold
{
Points = new ManifoldPoint[2]
};
Reset(fixtureA, indexA, fixtureB, indexB);
}
@@ -275,8 +279,8 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
PhysicsComponent bodyB = FixtureB?.Body!;
IPhysShape shapeA = FixtureA?.Shape!;
IPhysShape shapeB = FixtureB?.Shape!;
var bodyATransform = physicsManager.GetTransform(bodyA);
var bodyBTransform = physicsManager.GetTransform(bodyB);
var bodyATransform = physicsManager.EnsureTransform(bodyA);
var bodyBTransform = physicsManager.EnsureTransform(bodyB);
ContactSolver.InitializeManifold(ref Manifold, bodyATransform, bodyBTransform, shapeA.Radius, shapeB.Radius, out normal, points);
}
@@ -285,7 +289,8 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
/// Update the contact manifold and touching status.
/// Note: do not assume the fixture AABBs are overlapping or are valid.
/// </summary>
internal void Update(IPhysicsManager physicsManager, List<Contact> startCollisions, List<Contact> endCollisions)
/// <returns>What current status of the contact is (e.g. start touching, end touching, etc.)</returns>
internal ContactStatus Update(IPhysicsManager physicsManager)
{
PhysicsComponent bodyA = FixtureA!.Body;
PhysicsComponent bodyB = FixtureB!.Body;
@@ -320,7 +325,7 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
// Match old contact ids to new contact ids and copy the
// stored impulses to warm start the solver.
for (int i = 0; i < Manifold.PointCount; ++i)
for (var i = 0; i < Manifold.PointCount; ++i)
{
var mp2 = Manifold.Points[i];
mp2.NormalImpulse = 0.0f;
@@ -350,28 +355,31 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
}
IsTouching = touching;
var status = ContactStatus.NoContact;
if (!wasTouching)
{
if (touching)
{
startCollisions.Add(this);
status = ContactStatus.StartTouching;
}
}
else
{
if (!touching)
{
endCollisions.Add(this);
status = ContactStatus.EndTouching;
}
}
if (sensor)
return;
#if DEBUG
_debugPhysics.HandlePreSolve(this, oldManifold);
if (!sensor)
{
_debugPhysics.HandlePreSolve(this, oldManifold);
}
#endif
return status;
}
/// <summary>

View File

@@ -25,7 +25,7 @@ using Robust.Shared.Physics.Collision;
namespace Robust.Shared.Physics.Dynamics.Contacts
{
internal sealed class ContactPositionConstraint
internal struct ContactPositionConstraint
{
/// <summary>
/// Index of BodyA in the island.
@@ -37,7 +37,7 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
/// </summary>
public int IndexB { get; set; }
public Vector2[] LocalPoints = new Vector2[2];
public Vector2[] LocalPoints;
public Vector2 LocalNormal;

View File

@@ -21,6 +21,9 @@
*/
using System;
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Utility;
@@ -47,6 +50,11 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
private ContactVelocityConstraint[] _velocityConstraints = Array.Empty<ContactVelocityConstraint>();
private ContactPositionConstraint[] _positionConstraints = Array.Empty<ContactPositionConstraint>();
private int _velocityConstraintsPerThread;
private int _velocityConstraintsMinimumThreads;
private int _positionConstraintsPerThread;
private int _positionConstraintsMinimumThreads;
public void LoadConfig(in IslandCfg cfg)
{
_warmStarting = cfg.WarmStarting;
@@ -54,6 +62,10 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
_baumgarte = cfg.Baumgarte;
_linearSlop = cfg.LinearSlop;
_maxLinearCorrection = cfg.MaxLinearCorrection;
_positionConstraintsPerThread = cfg.PositionConstraintsPerThread;
_positionConstraintsMinimumThreads = cfg.PositionConstraintsMinimumThreads;
_velocityConstraintsPerThread = cfg.VelocityConstraintsPerThread;
_velocityConstraintsMinimumThreads = cfg.VelocityConstraintsMinimumThreads;
}
public void Reset(SolverData data, int contactCount, Contact[] contacts)
@@ -78,8 +90,31 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
for (var i = oldLength; i < _velocityConstraints.Length; i++)
{
_velocityConstraints[i] = new ContactVelocityConstraint();
_positionConstraints[i] = new ContactPositionConstraint();
var velocity = new ContactVelocityConstraint
{
K = new Vector2[2],
Points = new VelocityConstraintPoint[2],
NormalMass = new Vector2[2],
};
for (var j = 0; j < 2; j++)
{
velocity.Points[j] = new VelocityConstraintPoint();
}
_velocityConstraints[i] = velocity;
var position = new ContactPositionConstraint()
{
LocalPoints = new Vector2[2],
};
for (var j = 0; j < 2; j++)
{
position.LocalPoints[j] = Vector2.Zero;
}
_positionConstraints[i] = position;
}
}
@@ -101,14 +136,16 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
int pointCount = manifold.PointCount;
DebugTools.Assert(pointCount > 0);
var velocityConstraint = _velocityConstraints[i];
ref var velocityConstraint = ref _velocityConstraints[i];
velocityConstraint.Friction = contact.Friction;
velocityConstraint.Restitution = contact.Restitution;
velocityConstraint.TangentSpeed = contact.TangentSpeed;
velocityConstraint.IndexA = bodyA.IslandIndex[data.IslandIndex];
velocityConstraint.IndexB = bodyB.IslandIndex[data.IslandIndex];
(velocityConstraint.InvMassA, velocityConstraint.InvMassB) = GetInvMass(bodyA, bodyB);
var (invMassA, invMassB) = GetInvMass(bodyA, bodyB);
(velocityConstraint.InvMassA, velocityConstraint.InvMassB) = (invMassA, invMassB);
velocityConstraint.InvIA = bodyA.InvI;
velocityConstraint.InvIB = bodyB.InvI;
velocityConstraint.ContactIndex = i;
@@ -120,10 +157,10 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
velocityConstraint.NormalMass[x] = Vector2.Zero;
}
var positionConstraint = _positionConstraints[i];
ref var positionConstraint = ref _positionConstraints[i];
positionConstraint.IndexA = bodyA.IslandIndex[data.IslandIndex];
positionConstraint.IndexB = bodyB.IslandIndex[data.IslandIndex];
(positionConstraint.InvMassA, positionConstraint.InvMassB) = GetInvMass(bodyA, bodyB);
(positionConstraint.InvMassA, positionConstraint.InvMassB) = (invMassA, invMassB);
// TODO: Dis
// positionConstraint.LocalCenterA = bodyA._sweep.LocalCenter;
// positionConstraint.LocalCenterB = bodyB._sweep.LocalCenter;
@@ -139,10 +176,10 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
positionConstraint.RadiusB = radiusB;
positionConstraint.Type = manifold.Type;
for (int j = 0; j < pointCount; ++j)
for (var j = 0; j < pointCount; ++j)
{
var contactPoint = manifold.Points[j];
var constraintPoint = velocityConstraint.Points[j];
ref var constraintPoint = ref velocityConstraint.Points[j];
if (_warmStarting)
{
@@ -211,7 +248,7 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
for (var i = 0; i < _contactCount; ++i)
{
var velocityConstraint = _velocityConstraints[i];
ref var velocityConstraint = ref _velocityConstraints[i];
var positionConstraint = _positionConstraints[i];
var radiusA = positionConstraint.RadiusA;
@@ -240,13 +277,12 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
DebugTools.Assert(manifold.PointCount > 0);
Transform xfA = new Transform(angleA);
Transform xfB = new Transform(angleB);
var xfA = new Transform(angleA);
var xfB = new Transform(angleB);
xfA.Position = centerA - Transform.Mul(xfA.Quaternion2D, localCenterA);
xfB.Position = centerB - Transform.Mul(xfB.Quaternion2D, localCenterB);
Vector2 normal;
InitializeManifold(ref manifold, xfA, xfB, radiusA, radiusB, out normal, points);
InitializeManifold(ref manifold, xfA, xfB, radiusA, radiusB, out var normal, points);
velocityConstraint.Normal = normal;
@@ -254,7 +290,7 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
for (int j = 0; j < pointCount; ++j)
{
VelocityConstraintPoint vcp = velocityConstraint.Points[j];
ref var vcp = ref velocityConstraint.Points[j];
vcp.RelativeVelocityA = points[j] - centerA;
vcp.RelativeVelocityB = points[j] - centerB;
@@ -359,10 +395,28 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
public void SolveVelocityConstraints()
{
// Here be dragons
for (var i = 0; i < _contactCount; ++i)
if (_contactCount > _velocityConstraintsPerThread * _velocityConstraintsMinimumThreads)
{
var velocityConstraint = _velocityConstraints[i];
var (batches, batchSize) = SharedPhysicsSystem.GetBatch(_contactCount, _velocityConstraintsPerThread);
Parallel.For(0, batches, i =>
{
var start = i * batchSize;
var end = Math.Min(start + batchSize, _contactCount);
SolveVelocityConstraints(start, end);
});
}
else
{
SolveVelocityConstraints(0, _contactCount);
}
}
public void SolveVelocityConstraints(int start, int end)
{
// Here be dragons
for (var i = start; i < end; ++i)
{
ref var velocityConstraint = ref _velocityConstraints[i];
var indexA = velocityConstraint.IndexA;
var indexB = velocityConstraint.IndexB;
@@ -387,7 +441,7 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
// than friction.
for (var j = 0; j < pointCount; ++j)
{
VelocityConstraintPoint velConstraintPoint = velocityConstraint.Points[j];
ref var velConstraintPoint = ref velocityConstraint.Points[j];
// Relative velocity at contact
var dv = vB + Vector2.Cross(wB, velConstraintPoint.RelativeVelocityB) - vA - Vector2.Cross(wA, velConstraintPoint.RelativeVelocityA);
@@ -415,7 +469,7 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
// Solve normal constraints
if (velocityConstraint.PointCount == 1)
{
VelocityConstraintPoint vcp = velocityConstraint.Points[0];
ref var vcp = ref velocityConstraint.Points[0];
// Relative velocity at contact
Vector2 dv = vB + Vector2.Cross(wB, vcp.RelativeVelocityB) - vA - Vector2.Cross(wA, vcp.RelativeVelocityA);
@@ -472,8 +526,8 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
// = A * x + b'
// b' = b - A * a;
VelocityConstraintPoint cp1 = velocityConstraint.Points[0];
VelocityConstraintPoint cp2 = velocityConstraint.Points[1];
ref var cp1 = ref velocityConstraint.Points[0];
ref var cp2 = ref velocityConstraint.Points[1];
Vector2 a = new Vector2(cp1.NormalImpulse, cp2.NormalImpulse);
DebugTools.Assert(a.X >= 0.0f && a.Y >= 0.0f);
@@ -642,34 +696,52 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
public void StoreImpulses()
{
for (int i = 0; i < _contactCount; ++i)
for (var i = 0; i < _contactCount; ++i)
{
ContactVelocityConstraint velocityConstraint = _velocityConstraints[i];
var manifold = _contacts[velocityConstraint.ContactIndex].Manifold;
ref var manifold = ref _contacts[velocityConstraint.ContactIndex].Manifold;
for (int j = 0; j < velocityConstraint.PointCount; ++j)
for (var j = 0; j < velocityConstraint.PointCount; ++j)
{
ManifoldPoint point = manifold.Points[j];
ref var point = ref manifold.Points[j];
point.NormalImpulse = velocityConstraint.Points[j].NormalImpulse;
point.TangentImpulse = velocityConstraint.Points[j].TangentImpulse;
manifold.Points[j] = point;
}
_contacts[velocityConstraint.ContactIndex].Manifold = manifold;
}
}
public bool SolvePositionConstraints()
{
if (_contactCount > _positionConstraintsPerThread * _positionConstraintsMinimumThreads)
{
var unsolved = 0;
var (batches, batchSize) = SharedPhysicsSystem.GetBatch(_contactCount, _positionConstraintsPerThread);
Parallel.For(0, batches, i =>
{
var start = i * batchSize;
var end = Math.Min(start + batchSize, _contactCount);
if (!SolvePositionConstraints(start, end))
Interlocked.Increment(ref unsolved);
});
return unsolved == 0;
}
return SolvePositionConstraints(0, _contactCount);
}
/// <summary>
/// Tries to solve positions for all contacts specified.
/// </summary>
/// <returns>true if all positions solved</returns>
public bool SolvePositionConstraints()
public bool SolvePositionConstraints(int start, int end)
{
float minSeparation = 0.0f;
for (int i = 0; i < _contactCount; ++i)
for (int i = start; i < end; ++i)
{
ContactPositionConstraint pc = _positionConstraints[i];
var pc = _positionConstraints[i];
int indexA = pc.IndexA;
int indexB = pc.IndexB;

View File

@@ -24,7 +24,7 @@ using Robust.Shared.Maths;
namespace Robust.Shared.Physics.Dynamics.Contacts
{
internal sealed class ContactVelocityConstraint
internal struct ContactVelocityConstraint
{
public int ContactIndex { get; set; }
@@ -39,13 +39,13 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
public int IndexB { get; set; }
// Use 2 as its the max number of manifold points.
public VelocityConstraintPoint[] Points = new VelocityConstraintPoint[2];
public VelocityConstraintPoint[] Points;
public Vector2 Normal;
public Vector2[] NormalMass = new Vector2[2];
public Vector2[] NormalMass;
public Vector2[] K = new Vector2[2];
public Vector2[] K;
public float InvMassA;
@@ -62,17 +62,9 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
public float TangentSpeed;
public int PointCount;
public ContactVelocityConstraint()
{
for (var i = 0; i < 2; i++)
{
Points[i] = new VelocityConstraintPoint();
}
}
}
internal sealed class VelocityConstraintPoint
internal struct VelocityConstraintPoint
{
public Vector2 RelativeVelocityA;

View File

@@ -57,7 +57,6 @@ namespace Robust.Shared.Physics.Dynamics.Joints
/// this as a massless, rigid rod.
/// </summary>
[Serializable, NetSerializable]
[DataDefinition]
public sealed class DistanceJoint : Joint, IEquatable<DistanceJoint>
{
// Sloth note:

View File

@@ -48,7 +48,6 @@ namespace Robust.Shared.Physics.Dynamics.Joints
/// It provides 2D translational friction and angular friction.
/// </summary>
[Serializable, NetSerializable]
[DataDefinition]
public sealed class FrictionJoint : Joint, IEquatable<FrictionJoint>
{
// Solver shared

View File

@@ -57,7 +57,6 @@ namespace Robust.Shared.Physics.Dynamics.Joints
}
[Serializable, NetSerializable]
[DataDefinition]
public abstract class Joint : IEquatable<Joint>
{
/// <summary>

View File

@@ -326,7 +326,7 @@ stored in a single array since multiple arrays lead to multiple misses.
var body = Bodies[i];
// Didn't use the old variable names because they're hard to read
var transform = _physicsManager.GetTransform(body);
var transform = _physicsManager.EnsureTransform(body);
var position = transform.Position;
// DebugTools.Assert(!float.IsNaN(position.X) && !float.IsNaN(position.Y));
var angle = transform.Quaternion2D.Angle;
@@ -626,5 +626,9 @@ stored in a single array since multiple arrays lead to multiple misses.
public float Baumgarte;
public float LinearSlop;
public float MaxLinearCorrection;
public int VelocityConstraintsPerThread;
public int VelocityConstraintsMinimumThreads;
public int PositionConstraintsPerThread;
public int PositionConstraintsMinimumThreads;
}
}

View File

@@ -37,10 +37,10 @@ namespace Robust.Shared.Physics.Dynamics
{
public sealed class PhysicsMap
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IIslandManager _islandManager = default!;
private SharedBroadphaseSystem _broadphaseSystem = default!;
private SharedPhysicsSystem _physicsSystem = default!;
internal ContactManager ContactManager = new();
@@ -124,6 +124,7 @@ namespace Robust.Shared.Physics.Dynamics
public PhysicsMap(MapId mapId)
{
MapId = mapId;
_broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
_physicsSystem = EntitySystem.Get<SharedPhysicsSystem>();
}
@@ -133,8 +134,21 @@ namespace Robust.Shared.Physics.Dynamics
ContactManager.Initialize();
ContactManager.MapId = MapId;
_autoClearForces = _configManager.GetCVar(CVars.AutoClearForces);
_configManager.OnValueChanged(CVars.AutoClearForces, value => _autoClearForces = value);
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CVars.AutoClearForces, OnAutoClearChange, true);
}
public void Shutdown()
{
ContactManager.Shutdown();
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.UnsubValueChanged(CVars.AutoClearForces, OnAutoClearChange);
}
private void OnAutoClearChange(bool value)
{
_autoClearForces = value;
}
#region AddRemove
@@ -385,7 +399,7 @@ namespace Robust.Shared.Physics.Dynamics
// Box2D does this at the end of a step and also here when there's a fixture update.
// Given external stuff can move bodies we'll just do this here.
// Unfortunately this NEEDS to be predicted to make pushing remotely fucking good.
EntitySystem.Get<SharedBroadphaseSystem>().FindNewContacts(MapId);
_broadphaseSystem.FindNewContacts(MapId);
var invDt = frameTime > 0.0f ? 1.0f / frameTime : 0.0f;
var dtRatio = _invDt0 * frameTime;

View File

@@ -101,6 +101,10 @@ namespace Robust.Shared.Physics
CfgVar(CVars.Baumgarte, value => _islandCfg.Baumgarte = value);
CfgVar(CVars.LinearSlop, value => _islandCfg.LinearSlop = value);
CfgVar(CVars.MaxLinearCorrection, value => _islandCfg.MaxLinearCorrection = value);
CfgVar(CVars.VelocityConstraintsPerThread, value => _islandCfg.VelocityConstraintsPerThread = value);
CfgVar(CVars.VelocityConstraintMinimumThreads, value => _islandCfg.VelocityConstraintsMinimumThreads = value);
CfgVar(CVars.PositionConstraintsPerThread, value => _islandCfg.PositionConstraintsPerThread = value);
CfgVar(CVars.PositionConstraintsMinimumThread, value => _islandCfg.PositionConstraintsMinimumThreads = value);
CfgVar(CVars.MaxLinVelocity, value =>
{
_maxLinearVelocityRaw = value;

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Physics
@@ -10,8 +10,12 @@ namespace Robust.Shared.Physics
/// </summary>
void ClearTransforms();
public bool CreateTransform(PhysicsComponent body);
public Transform EnsureTransform(PhysicsComponent body);
/// <summary>
/// Get / create a cached transform for physics use.
/// Get a cached transform for physics use.
/// </summary>
public Transform GetTransform(PhysicsComponent body);
}
@@ -26,18 +30,24 @@ namespace Robust.Shared.Physics
_transforms.Clear();
}
public bool CreateTransform(PhysicsComponent body)
{
if (_transforms.ContainsKey(body)) return false;
_transforms[body] = body.GetTransform();
return true;
}
public Transform EnsureTransform(PhysicsComponent body)
{
CreateTransform(body);
return _transforms[body];
}
/// <inheritdoc />
public Transform GetTransform(PhysicsComponent body)
{
if (_transforms.TryGetValue(body, out var transform))
{
return transform;
}
transform = body.GetTransform();
_transforms[body] = transform;
return transform;
return _transforms[body];
}
}
}

View File

@@ -86,7 +86,7 @@ namespace Robust.Shared.Physics
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
//ProcessUpdates();
ProcessUpdates();
}
/// <summary>
@@ -480,6 +480,7 @@ namespace Robust.Shared.Physics
// Supposed to be wrapped in density but eh
body.ResetMassData();
body.Dirty();
// TODO: Set newcontacts to true.
}
@@ -521,6 +522,7 @@ namespace Robust.Shared.Physics
body.FixtureCount -= 1;
body.ResetMassData();
body.Dirty();
}
private void SynchronizeFixtures(PhysicsComponent body, Vector2 worldPos)
@@ -585,8 +587,8 @@ namespace Robust.Shared.Physics
{
var proxy = fixture.Proxies[i];
var aabb1 = fixture.Shape.CalculateLocalBounds(angle1).Translated(relativePos1.Position);
var aabb2 = fixture.Shape.CalculateLocalBounds(angle2).Translated(relativePos2.Position);
var aabb1 = fixture.Shape.ComputeAABB(relativePos1, i);
var aabb2 = fixture.Shape.ComputeAABB(relativePos2, i);
var aabb = aabb1.Union(aabb2);
proxy.AABB = aabb;
@@ -614,15 +616,14 @@ namespace Robust.Shared.Physics
_broadphasePositions[broadphase] = broadphaseOffset;
}
// TODO: This needs a matrix transform ya dingus
var relativePos1 = new Transform(transform1.Position - broadphaseOffset.Position,
transform1.Quaternion2D.Angle - broadphaseOffset.Rotation);
var angle1 = new Angle(relativePos1.Quaternion2D.Angle);
for (var i = 0; i < proxyCount; i++)
{
var proxy = fixture.Proxies[i];
var aabb = fixture.Shape.CalculateLocalBounds(angle1).Translated(relativePos1.Position);
var aabb = fixture.Shape.ComputeAABB(relativePos1, i);
proxy.AABB = aabb;
var displacement = Vector2.Zero;
broadphase.Tree.MoveProxy(proxy.ProxyId, aabb, displacement);
@@ -704,11 +705,12 @@ namespace Robust.Shared.Physics
var posDiff = worldPos - broadphasePos;
var rotDiff = worldRot - broadphaseRot;
var transform = new Transform(posDiff, (float) rotDiff.Theta);
var mapId = broadphase.Owner.Transform.MapID;
for (var i = 0; i < proxyCount; i++)
{
var aabb = fixture.Shape.CalculateLocalBounds(rotDiff).Translated(posDiff);
var aabb = fixture.Shape.ComputeAABB(transform, i);
var proxy = new FixtureProxy(aabb, fixture, i);
proxy.ProxyId = broadphase.Tree.AddProxy(ref proxy);
fixture.Proxies[i] = proxy;
@@ -848,16 +850,32 @@ namespace Robust.Shared.Physics
foreach (var broadphase in ComponentManager.EntityQuery<BroadphaseComponent>(true))
{
if (broadphase.Owner.Transform.MapID != mapId) continue;
// Always return map... for now
if (broadphase.Owner.HasComponent<MapComponent>())
{
yield return broadphase;
continue;
}
if (broadphase.Owner.TryGetComponent(out PhysicsComponent? physicsComponent) &&
Intersects(physicsComponent, aabb))
if (!broadphase.Owner.TryGetComponent(out PhysicsComponent? physicsComponent)) continue;
var transform = physicsComponent.GetTransform();
var found = false;
// TODO: Need CollisionManager for accurate checks
foreach (var fixture in physicsComponent.Fixtures)
{
yield return broadphase;
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
if (!fixture.Shape.ComputeAABB(transform, i).Intersects(aabb)) continue;
yield return broadphase;
found = true;
break;
}
if (found)
break;
}
}
}
@@ -867,22 +885,6 @@ namespace Robust.Shared.Physics
return GetBroadphases(mapId, new Box2(worldPos, worldPos));
}
private bool Intersects(PhysicsComponent physicsComponent, Box2 aabb)
{
var worldPos = physicsComponent.Owner.Transform.WorldPosition;
var worldRot = physicsComponent.Owner.Transform.WorldRotation;
var bodyAABB = physicsComponent.GetWorldAABB(worldPos, worldRot);
if (!aabb.Intersects(bodyAABB)) return false;
foreach (var fixture in physicsComponent.Fixtures)
{
if (fixture.Shape.Intersects(aabb, worldPos, worldRot)) return true;
}
return false;
}
/// <summary>
/// Checks to see if the specified collision rectangle collides with any of the physBodies under management.
/// Also fires the OnCollide event of the first managed physBody to intersect with the collider.

View File

@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Utility;
namespace Robust.Shared.Timing
@@ -16,7 +13,8 @@ namespace Robust.Shared.Timing
private const int NumFrames = 60;
private readonly IStopwatch _realTimer = new Stopwatch();
private readonly List<long> _realFrameTimes = new(NumFrames);
private readonly long[] _realFrameTimes = new long[NumFrames];
private int _frameIdx;
private TimeSpan _lastRealTime;
/// <summary>
@@ -182,9 +180,8 @@ namespace Robust.Shared.Timing
_lastRealTime = curRealTime;
// update profiling
if (_realFrameTimes.Count >= NumFrames)
_realFrameTimes.RemoveAt(0);
_realFrameTimes.Add(RealFrameTime.Ticks);
_frameIdx = (1 + _frameIdx) % _realFrameTimes.Length;
_realFrameTimes[_frameIdx] = RealFrameTime.Ticks;
}
private TimeSpan CalcFrameTime()
@@ -257,9 +254,6 @@ namespace Robust.Shared.Timing
/// <returns></returns>
private double CalcFpsAvg()
{
if (_realFrameTimes.Count == 0)
return 0;
return 1 / (_realFrameTimes.Average() / TimeSpan.TicksPerSecond);
}
@@ -270,7 +264,7 @@ namespace Robust.Shared.Timing
private TimeSpan CalcRftStdDev()
{
var sum = _realFrameTimes.Sum();
var count = _realFrameTimes.Count;
var count = _realFrameTimes.Length;
var avg = sum / (double)count;
double devSquared = 0.0f;
for (var i = 0; i < count; ++i)

View File

@@ -0,0 +1,376 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
#pragma warning disable 169
namespace Robust.Shared.Utility
{
/// <summary>
/// Fixed size array stack allocation helpers.
/// </summary>
/// <remarks>
/// <para>
/// This helper class can be used to work around the limitation that <c>stackalloc</c>
/// cannot be used with ref types.
/// </para>
/// <para>
/// To use, call it like so:
/// <code>
/// var span = FixedArray.Alloc32&lt;object&gt;(out _);
/// </code>
/// There is an <c>out</c> parameter that you should probably always discard (as shown in the example).
/// This is so that stack space is properly allocated in your stack frame.
/// </para>
/// <para>
/// Do NOT under ANY CIRCUMSTANCE return the span given up the stack in any way.
/// You will break the GC and stack and (worst of all) I will shed you.
/// </para>
/// <para>
/// This class cannot be used with variable size allocations.
/// Just allocate a we'll-never-need-more bound like 128 and slim it down.
/// </para>
/// </remarks>
[PublicAPI]
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
internal static class FixedArray
{
public static Span<T> Alloc2<T>(out FixedArray2<T> discard)
{
discard = new();
return discard.AsSpan;
}
public static Span<T> Alloc4<T>(out FixedArray4<T> discard)
{
discard = new();
return discard.AsSpan;
}
public static Span<T> Alloc8<T>(out FixedArray8<T> discard)
{
discard = new();
return discard.AsSpan;
}
public static Span<T> Alloc16<T>(out FixedArray16<T> discard)
{
discard = new();
return discard.AsSpan;
}
public static Span<T> Alloc32<T>(out FixedArray32<T> discard)
{
discard = new();
return discard.AsSpan;
}
public static Span<T> Alloc64<T>(out FixedArray64<T> discard)
{
discard = new();
return discard.AsSpan;
}
public static Span<T> Alloc128<T>(out FixedArray128<T> discard)
{
discard = new();
return discard.AsSpan;
}
}
internal struct FixedArray2<T>
{
private T _00;
private T _01;
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 2);
}
internal struct FixedArray4<T>
{
private T _00;
private T _01;
private T _02;
private T _03;
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 4);
}
internal struct FixedArray8<T>
{
private T _00;
private T _01;
private T _02;
private T _03;
private T _04;
private T _05;
private T _06;
private T _07;
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 8);
}
internal struct FixedArray16<T>
{
private T _00;
private T _01;
private T _02;
private T _03;
private T _04;
private T _05;
private T _06;
private T _07;
private T _08;
private T _09;
private T _10;
private T _11;
private T _12;
private T _13;
private T _14;
private T _15;
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 16);
}
internal struct FixedArray32<T>
{
private T _00;
private T _01;
private T _02;
private T _03;
private T _04;
private T _05;
private T _06;
private T _07;
private T _08;
private T _09;
private T _10;
private T _11;
private T _12;
private T _13;
private T _14;
private T _15;
private T _16;
private T _17;
private T _18;
private T _19;
private T _20;
private T _21;
private T _22;
private T _23;
private T _24;
private T _25;
private T _26;
private T _27;
private T _28;
private T _29;
private T _30;
private T _31;
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 32);
}
internal struct FixedArray64<T>
{
private T _00;
private T _01;
private T _02;
private T _03;
private T _04;
private T _05;
private T _06;
private T _07;
private T _08;
private T _09;
private T _10;
private T _11;
private T _12;
private T _13;
private T _14;
private T _15;
private T _16;
private T _17;
private T _18;
private T _19;
private T _20;
private T _21;
private T _22;
private T _23;
private T _24;
private T _25;
private T _26;
private T _27;
private T _28;
private T _29;
private T _30;
private T _31;
private T _32;
private T _33;
private T _34;
private T _35;
private T _36;
private T _37;
private T _38;
private T _39;
private T _40;
private T _41;
private T _42;
private T _43;
private T _44;
private T _45;
private T _46;
private T _47;
private T _48;
private T _49;
private T _50;
private T _51;
private T _52;
private T _53;
private T _54;
private T _55;
private T _56;
private T _57;
private T _58;
private T _59;
private T _60;
private T _61;
private T _62;
private T _63;
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 64);
}
internal struct FixedArray128<T>
{
private T _00;
private T _01;
private T _02;
private T _03;
private T _04;
private T _05;
private T _06;
private T _07;
private T _08;
private T _09;
private T _10;
private T _11;
private T _12;
private T _13;
private T _14;
private T _15;
private T _16;
private T _17;
private T _18;
private T _19;
private T _20;
private T _21;
private T _22;
private T _23;
private T _24;
private T _25;
private T _26;
private T _27;
private T _28;
private T _29;
private T _30;
private T _31;
private T _32;
private T _33;
private T _34;
private T _35;
private T _36;
private T _37;
private T _38;
private T _39;
private T _40;
private T _41;
private T _42;
private T _43;
private T _44;
private T _45;
private T _46;
private T _47;
private T _48;
private T _49;
private T _50;
private T _51;
private T _52;
private T _53;
private T _54;
private T _55;
private T _56;
private T _57;
private T _58;
private T _59;
private T _60;
private T _61;
private T _62;
private T _63;
private T _64;
private T _65;
private T _66;
private T _67;
private T _68;
private T _69;
private T _70;
private T _71;
private T _72;
private T _73;
private T _74;
private T _75;
private T _76;
private T _77;
private T _78;
private T _79;
private T _80;
private T _81;
private T _82;
private T _83;
private T _84;
private T _85;
private T _86;
private T _87;
private T _88;
private T _89;
private T _90;
private T _91;
private T _92;
private T _93;
private T _94;
private T _95;
private T _96;
private T _97;
private T _98;
private T _99;
private T _100;
private T _101;
private T _102;
private T _103;
private T _104;
private T _105;
private T _106;
private T _107;
private T _108;
private T _109;
private T _110;
private T _111;
private T _112;
private T _113;
private T _114;
private T _115;
private T _116;
private T _117;
private T _118;
private T _119;
private T _120;
private T _121;
private T _122;
private T _123;
private T _124;
private T _125;
private T _126;
private T _127;
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 128);
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// ReSharper disable InconsistentNaming
// ReSharper disable IdentifierTypo
// ReSharper disable CommentTypo
#pragma warning disable 649
namespace Robust.Shared.Utility
{
internal static unsafe class ProcessExt
{
// THIS ENTIRE METHOD EXISTS PURELY OUT OF SPITE FOR MICROSOFT.
// NOT TO SAVE 0.3% CPU USAGE ON WINDOWS. NAH.
// TO SPITE MICROSOFT.
// WHAT ABOUT THE 2.2% CPU USAGE IN CONSOLE.KEYAVAILABLE? WHAT ABOUT THAT WHATABOUTISM HUH???
// I WANTED TO GO TO BED NOW IT'S 4:20 AM (BASED) GOD DAMNIT.
// AS A RESULT, EVERYTHING HERE WILL BE WRITTEN IN *FUCKING* CAPS.
[MethodImpl(MethodImplOptions.NoInlining)]
public static long GetPrivateMemorySize64NotSlowHolyFuckingShitMicrosoft(this Process process)
{
if (!OperatingSystem.IsWindows())
// IT'S NOT SLOW ON LINUX, *LUCKILY*.
// WELL I DIDN'T PROFILE IT BUT THEY DON'T DO A STUPID SEARCH OVER EVERY PROCESS.
// IT USES PROCFS, IF YOU'RE CURIOUS.
return process.PrivateMemorySize64;
//
// GLASS TOLD ME TO PUT A TL;DR IN SO TL;DR
// PROCESS.PRIVATEMEMORYSIZE64 (AND A BUNCH OF OTHERS) ARE SLOW AS BALLS (2+ms for me).
// THIS ISN'T.
//
// AS IT *FUCKING* TURNS OUT.
// *MANY* OF THE FIELDS ON PROCESS (THE CLASS, YOU KNOW THE ONE THIS METHOD IS AN EXTENSION FOR) HAVE TO FETCH A "PROCESS INFO".
// HOW DO YOU FETCH A PROCESS INFO ON WINDOWS? OH YEAH YOU CALL NTQUERYSYSTEMINFORMATION APPARENTLY.
// OH WAIT. THAT RETURNS INFO FOR *EVERY* PROCESS ON THE FUCKING SYSTEM.
// SO YES. IF YOU LOOK UP PROCESS.PRIVATEMEMORYSIZE64, IT HAS TO SEARCH THROUGH *EVERY PROCESS ON THE FUCKING SYSTEM* TO FIND THE PROCESS IT'S LOOKING FOR.
// THIS TAKES MORE THAN 2 FUCKING MILLISECONDS PER LOOKUP ON MY SYSTEM.
// IF THE TITLE BAR ON THE CONSOLE WAS UPDATED EVERY TICK INSTEAD OF ONCE PER FRAME, THIS WOULD LITERALLY BE MOST OF THE IDLE SERVER CPU USAGE.
// ***ARE YOU FUCKING KIDDING ME ????***
// OH YEAH, NTQUERYSYSTEMINFORMATION IS DOCUMENTED AS AN UNSTABLE API THAT CAN CHANGE AT ANY TIME, APPARENTLY.
// .NET FEELS IT'S FINE TO USE THAT (YEAH GUESS THEY'RE NEVER REMOVING IT FROM WINDOWS LMAO)
// ARE YOU FUCKING TELLING ME THE .NET TEAM COULDN'T HAVE WALKED TO THE OTHER SIDE OF THE OFFICE AND TOLD THE KERNEL TEAM TO ADD A VERSION OF NTQUERYSYSTEMINFORMATION THAT FETCHES INFO FOR A SINGLE PROCESS?
// THEY THOUGHT THIS SHIT WAS FUCKING ACCEPTABLE?
// JESUS FUCKING CHRIST.
// ANYWAYS, THIS EXTENSION METHOD USES GETPROCESSMEMORYINFO() INSTEAD BECAUSE THAT'S PROBABLY NOT FUCKING MORONIC.
//
PROCESS_MEMORY_COUNTERS_EX counters;
if (GetProcessMemoryInfo(process.Handle, &counters, sizeof(PROCESS_MEMORY_COUNTERS_EX)) == 0)
return 0;
var count = counters.PrivateUsage;
return count;
}
private struct PROCESS_MEMORY_COUNTERS_EX
{
public int cb;
public int PageFaultCount;
public nint PeakWorkingSetSize;
public nint WorkingSetSize;
public nint QuotaPeakPagedPoolUsage;
public nint QuotaPagedPoolUsage;
public nint QuotaPeakNonPagedPoolUsage;
public nint QuotaNonPagedPoolUsage;
public nint PagefileUsage;
public nint PeakPagefileUsage;
public nint PrivateUsage;
}
// ReSharper disable once StringLiteralTypo
[DllImport("psapi.dll", SetLastError = true)]
private static extern int GetProcessMemoryInfo(
IntPtr Process,
PROCESS_MEMORY_COUNTERS_EX* ppsmemCounters,
int cb);
}
}

View File

@@ -20,10 +20,6 @@ namespace Robust.UnitTesting.Client.GameObjects.Components
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterComponents(factory =>
{
factory.RegisterClass<ContainerManagerComponent>();
})
.InitializeInstance();
var mapManager = sim.Resolve<IMapManager>();

View File

@@ -16,7 +16,6 @@ namespace Robust.UnitTesting.Server.GameObjects.Components
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterComponents(factory => { factory.RegisterClass<ContainerManagerComponent>(); })
.RegisterPrototypes(protoMan => protoMan.LoadString(PROTOTYPES))
.InitializeInstance();
@@ -281,12 +280,15 @@ namespace Robust.UnitTesting.Server.GameObjects.Components
/// The generic container class uses a list of entities
/// </summary>
private readonly List<IEntity> _containerList = new();
private readonly List<EntityUid> _expectedEntities = new();
public override string ContainerType => nameof(ContainerOnlyContainer);
/// <inheritdoc />
public override IReadOnlyList<IEntity> ContainedEntities => _containerList;
public override List<EntityUid> ExpectedEntities => _expectedEntities;
/// <inheritdoc />
protected override void InternalInsert(IEntity toinsert)
{

View File

@@ -11,6 +11,7 @@ using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Containers;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
@@ -229,6 +230,7 @@ namespace Robust.UnitTesting.Server
compFactory.RegisterClass<PhysicsComponent>();
compFactory.RegisterClass<EntityLookupComponent>();
compFactory.RegisterClass<BroadphaseComponent>();
compFactory.RegisterClass<ContainerManagerComponent>();
_regDelegate?.Invoke(compFactory);

View File

@@ -0,0 +1,277 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Client.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
namespace Robust.UnitTesting.Shared.GameObjects
{
public class ContainerTests : RobustIntegrationTest
{
/// <summary>
/// Tests container states with children that do not exist on the client
/// and tests that said children are added to the container when they do arrive on the client.
/// </summary>
/// <returns></returns>
[Test]
public async Task TestContainerNonexistantItems()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => IoCManager.Resolve<IClientNetManager>().ClientConnect(null!, 0, null!));
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
// Setup
var mapId = MapId.Nullspace;
var mapPos = MapCoordinates.Nullspace;
EntityUid entityUid = default!;
EntityUid itemUid = default!;
await server.WaitAssertion(() =>
{
var mapMan = IoCManager.Resolve<IMapManager>();
var entMan = IoCManager.Resolve<IEntityManager>();
var playerMan = IoCManager.Resolve<IPlayerManager>();
mapId = mapMan.CreateMap();
mapPos = new MapCoordinates((0, 0), mapId);
var entity = entMan.SpawnEntity(null, mapPos);
entity.Name = "Container";
entityUid = entity.Uid;
var container = entity.EnsureContainer<Container>("dummy");
// Setup PVS
entity.AddComponent<Robust.Server.GameObjects.EyeComponent>();
var player = playerMan.GetAllPlayers().First();
player.AttachToEntity(entity);
player.JoinGame();
});
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
await server.WaitAssertion(() =>
{
var entMan = IoCManager.Resolve<IEntityManager>();
var item = entMan.SpawnEntity(null, mapPos);
item.Name = "Item";
itemUid = item.Uid;
Assert.That(entMan.TryGetEntity(entityUid, out var entity));
var container = entity!.EnsureContainer<Container>("dummy");
container.Insert(item);
// Move item out of PVS so that it doesn't get sent to the client
item.Transform.LocalPosition = (100000, 0);
});
// Needs minimum 4 to sync to client because buffer size is 3
await server.WaitRunTicks(1);
await client.WaitRunTicks(4);
await client.WaitAssertion(() =>
{
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetEntity(entityUid, out var entity)
|| !entity.TryGetComponent<ContainerManagerComponent>(out var containerManagerComp))
{
Assert.Fail();
return;
}
var container = containerManagerComp.GetContainer("dummy");
Assert.That(container.ContainedEntities.Count, Is.EqualTo(1));
Assert.That(container.ExpectedEntities.Count, Is.EqualTo(0));
/*
var containerSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ContainerSystem>();
Assert.That(containerSystem.ExpectedEntities.ContainsKey(itemUid));
Assert.That(containerSystem.ExpectedEntities.Count, Is.EqualTo(1));
*/
});
await server.WaitAssertion(() =>
{
var compMan = IoCManager.Resolve<IComponentManager>();
// Move item into PVS so it gets sent to the client
compMan.GetComponent<ITransformComponent>(itemUid).LocalPosition = (0, 0);
});
await server.WaitRunTicks(1);
await client.WaitRunTicks(4);
await client.WaitAssertion(() =>
{
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetEntity(entityUid, out var entity)
|| !entity.TryGetComponent<ContainerManagerComponent>(out var containerManagerComp))
{
Assert.Fail();
return;
}
var container = containerManagerComp.GetContainer("dummy");
Assert.That(container.ContainedEntities.Count, Is.EqualTo(1));
Assert.That(container.ExpectedEntities.Count, Is.EqualTo(0));
var containerSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ContainerSystem>();
Assert.That(!containerSystem.ExpectedEntities.ContainsKey(itemUid));
Assert.That(containerSystem.ExpectedEntities.Count, Is.EqualTo(0));
});
}
/// <summary>
/// Tests container states with children that do not exist on the client
/// and that if those children are deleted that they get properly removed from the expected entities list.
/// </summary>
/// <returns></returns>
[Test]
public async Task TestContainerExpectedEntityDeleted()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => IoCManager.Resolve<IClientNetManager>().ClientConnect(null!, 0, null!));
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
// Setup
var mapId = MapId.Nullspace;
var mapPos = MapCoordinates.Nullspace;
EntityUid entityUid = default!;
EntityUid itemUid = default!;
await server.WaitAssertion(() =>
{
var mapMan = IoCManager.Resolve<IMapManager>();
var entMan = IoCManager.Resolve<IEntityManager>();
var playerMan = IoCManager.Resolve<IPlayerManager>();
mapId = mapMan.CreateMap();
mapPos = new MapCoordinates((0, 0), mapId);
var entity = entMan.SpawnEntity(null, mapPos);
entity.Name = "Container";
entityUid = entity.Uid;
var container = entity.EnsureContainer<Container>("dummy");
// Setup PVS
entity.AddComponent<Robust.Server.GameObjects.EyeComponent>();
var player = playerMan.GetAllPlayers().First();
player.AttachToEntity(entity);
player.JoinGame();
});
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
await server.WaitAssertion(() =>
{
var entMan = IoCManager.Resolve<IEntityManager>();
var item = entMan.SpawnEntity(null, mapPos);
item.Name = "Item";
itemUid = item.Uid;
Assert.That(entMan.TryGetEntity(entityUid, out var entity));
var container = entity!.EnsureContainer<Container>("dummy");
container.Insert(item);
// Move item out of PVS so that it doesn't get sent to the client
item.Transform.LocalPosition = (100000, 0);
});
// Needs minimum 4 to sync to client because buffer size is 3
await server.WaitRunTicks(1);
await client.WaitRunTicks(4);
await client.WaitAssertion(() =>
{
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetEntity(entityUid, out var entity)
|| !entity.TryGetComponent<ContainerManagerComponent>(out var containerManagerComp))
{
Assert.Fail();
return;
}
var container = containerManagerComp.GetContainer("dummy");
Assert.That(container.ContainedEntities.Count, Is.EqualTo(1));
Assert.That(container.ExpectedEntities.Count, Is.EqualTo(0));
/*
var containerSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ContainerSystem>();
Assert.That(containerSystem.ExpectedEntities.ContainsKey(itemUid));
Assert.That(containerSystem.ExpectedEntities.Count, Is.EqualTo(1));
*/
});
await server.WaitAssertion(() =>
{
var entMan = IoCManager.Resolve<IEntityManager>();
// If possible it'd be best to only have the DeleteEntity, but right now
// the entity deleted event is not played on the client if the entity does not exist on the client.
if (entMan.TryGetEntity(itemUid, out var item)
&& ContainerHelpers.TryGetContainer(item, out var container))
container.ForceRemove(item);
entMan.DeleteEntity(itemUid);
});
await server.WaitRunTicks(1);
await client.WaitRunTicks(4);
await client.WaitAssertion(() =>
{
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetEntity(entityUid, out var entity)
|| !entity.TryGetComponent<ContainerManagerComponent>(out var containerManagerComp))
{
Assert.Fail();
return;
}
var container = containerManagerComp.GetContainer("dummy");
Assert.That(container.ContainedEntities.Count, Is.EqualTo(0));
Assert.That(container.ExpectedEntities.Count, Is.EqualTo(0));
/*
var containerSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ContainerSystem>();
Assert.That(!containerSystem.ExpectedEntities.ContainsKey(itemUid));
Assert.That(containerSystem.ExpectedEntities.Count, Is.EqualTo(0));
*/
});
}
}
}

View File

@@ -31,10 +31,6 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterComponents(factory =>
{
factory.RegisterClass<ContainerManagerComponent>();
})
.RegisterPrototypes(f=>
{
f.LoadString(Prototypes);
@@ -166,16 +162,13 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems
var tileIndices = grid.TileIndicesFor(ent1.Transform.Coordinates);
grid.SetTile(tileIndices, new Tile(1));
// Don't need the default grid for this.
entMan.ComponentManager.RemoveComponent<PhysicsComponent>(grid.GridEntityId);
// Act
ent1.Transform.Anchored = true;
Assert.That(grid.GetAnchoredEntities(tileIndices).First(), Is.EqualTo(ent1.Uid));
Assert.That(grid.GetTileRef(tileIndices).Tile, Is.Not.EqualTo(Tile.Empty));
Assert.That(ent1.HasComponent<PhysicsComponent>(), Is.False);
Assert.That(entMan.GetEntity(grid.GridEntityId).HasComponent<PhysicsComponent>(), Is.False);
Assert.That(entMan.GetEntity(grid.GridEntityId).HasComponent<PhysicsComponent>(), Is.True);
}
/// <summary>

View File

@@ -37,26 +37,23 @@ namespace Robust.UnitTesting.Shared.Map
// 1 fixture if we only ever update the 1 chunk
grid.SetTile(Vector2i.Zero, new Tile(1));
gridFixtures.Process();
Assert.That(gridBody.Fixtures.Count, Is.EqualTo(1));
// Also should only be a single tile.
var bounds = gridBody.Fixtures[0].Shape.CalculateLocalBounds(Angle.Zero);
var bounds = gridBody.Fixtures[0].Shape.ComputeAABB(new Transform(Vector2.Zero, (float) Angle.Zero.Theta), 0);
// Poly probably has Box2D's radius added to it so won't be a unit square
Assert.That(MathHelper.CloseTo(Box2.Area(bounds), 1.0f, 0.1f));
// Now do 2 tiles (same chunk)
grid.SetTile(Vector2i.One, new Tile(1));
gridFixtures.Process();
Assert.That(gridBody.Fixtures.Count, Is.EqualTo(1));
bounds = gridBody.Fixtures[0].Shape.CalculateLocalBounds(Angle.Zero);
bounds = gridBody.Fixtures[0].Shape.ComputeAABB(new Transform(Vector2.Zero, (float) Angle.Zero.Theta), 0);
// Because it's a diagonal tile it will actually be 2x2 area (until we get accurate hitboxes anyway).
Assert.That(MathHelper.CloseTo(Box2.Area(bounds), 4.0f, 0.1f));
// If we add a new chunk should be 2 now
grid.SetTile(new Vector2i(-1, -1), new Tile(1));
gridFixtures.Process();
Assert.That(gridBody.Fixtures.Count, Is.EqualTo(2));
gridBody.LinearVelocity = Vector2.One;

View File

@@ -1,6 +1,8 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Server.Physics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -15,7 +17,7 @@ namespace Robust.UnitTesting.Shared.Map
// need to rotate points about the grid's origin which is a /very/ common source of bugs.
[Test]
public async Task Test()
public async Task TestLocalWorldConversions()
{
var server = StartServer();
@@ -50,5 +52,64 @@ namespace Robust.UnitTesting.Shared.Map
Assert.That(grid.LocalToWorld(coordinates.Position).EqualsApprox(new Vector2(0, -10)));
});
}
[Test]
public async Task TestChunkRotations()
{
// This is mainly checking for the purposes of rendering at this stage.
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.ResolveDependency<IEntityManager>();
var mapMan = server.ResolveDependency<IMapManager>();
await server.WaitAssertion(() =>
{
var mapId = mapMan.CreateMap();
var grid = mapMan.CreateGrid(mapId);
var gridEnt = entMan.GetEntity(grid.GridEntityId);
var gridInternal = (IMapGridInternal) grid;
/* Test for map chunk rotations */
var tile = new Tile(1);
for (var x = 0; x < 2; x++)
{
for (var y = 0; y < 10; y++)
{
grid.SetTile(new Vector2i(x, y), tile);
}
}
var chunks = gridInternal.GetMapChunks().Select(c => c.Value).ToList();
Assert.That(chunks.Count, Is.EqualTo(1));
var chunk = chunks[0];
var aabb = chunk.CalcWorldAABB();
var bounds = new Box2(new Vector2(0, 0), new Vector2(2, 10));
// With all cardinal directions these should align.
Assert.That(aabb, Is.EqualTo(bounds));
gridEnt.Transform.LocalRotation = new Angle(Math.PI);
aabb = chunk.CalcWorldAABB();
bounds = new Box2(new Vector2(-2, -10), new Vector2(0, 0));
Assert.That(aabb.EqualsApprox(bounds), $"Expected bounds of {aabb} and got {bounds}");
gridEnt.Transform.LocalRotation = new Angle(-Math.PI / 2);
aabb = chunk.CalcWorldAABB();
bounds = new Box2(new Vector2(0, -2), new Vector2(10, 0));
Assert.That(aabb.EqualsApprox(bounds), $"Expected bounds of {aabb} and got {bounds}");
gridEnt.Transform.LocalRotation = new Angle(-Math.PI / 4);
aabb = chunk.CalcWorldAABB();
bounds = new Box2(new Vector2(0, -1.4142135f), new Vector2(8.485281f, 7.071068f));
Assert.That(aabb.EqualsApprox(bounds), $"Expected bounds of {aabb} and got {bounds}");
});
}
}
}

View File

@@ -75,7 +75,10 @@ namespace Robust.UnitTesting.Shared.Physics
{
var transformB = new Transform(Vector2.One, 0f);
var transformA = new Transform(transformB.Position + new Vector2(0.5f, 0.0f), 0f);
var manifold = new Manifold();
var manifold = new Manifold()
{
Points = new ManifoldPoint[2]
};
var expectedManifold = new Manifold
{
@@ -83,13 +86,19 @@ namespace Robust.UnitTesting.Shared.Physics
LocalNormal = new Vector2(-1, 0),
LocalPoint = new Vector2(-0.5f, 0),
PointCount = 2,
Points = new FixedArray2<ManifoldPoint>
Points = new ManifoldPoint[]
{
[0] = new() {LocalPoint = new Vector2(0.5f, -0.5f), Id = new ContactID {Key = 65538}},
[1] = new() {LocalPoint = new Vector2(0.5f, 0.5f), Id = new ContactID {Key = 65794}}
new() {LocalPoint = new Vector2(0.5f, -0.5f), Id = new ContactID {Key = 65538}},
new() {LocalPoint = new Vector2(0.5f, 0.5f), Id = new ContactID {Key = 65794}}
}
};
_collisionManager.CollidePolygons(ref manifold, _polyA, transformA, _polyB, transformB);
for (var i = 0; i < manifold.Points.Length; i++)
{
Assert.That(manifold.Points[i], Is.EqualTo(expectedManifold.Points[i]));
}
Assert.That(manifold, Is.EqualTo(expectedManifold));
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
namespace Robust.UnitTesting.Shared.Physics
{
[TestFixture]
public class ShapeAABB_Test : RobustUnitTest
{
private Transform _transform;
private Transform _rotatedTransform;
[OneTimeSetUp]
public void Setup()
{
_transform = new Transform(Vector2.One, 0f);
// We'll use 45 degrees as it's easier to spot bugs
_rotatedTransform = new Transform(Vector2.One, MathF.PI / 4f);
}
[Test]
public async Task TestCircleAABB()
{
var circle = new PhysShapeCircle {Radius = 0.5f};
var aabb = circle.ComputeAABB(_transform, 0);
Assert.That(aabb.Width, Is.EqualTo(1f));
Assert.That(aabb, Is.EqualTo(new Box2(0.5f, 0.5f, 1.5f, 1.5f)));
}
[Test]
public async Task TestRotatedCircleAABB()
{
var circle = new PhysShapeCircle {Radius = 0.5f};
var aabb = circle.ComputeAABB(_rotatedTransform, 0);
Assert.That(aabb.Width, Is.EqualTo(1f));
Assert.That(aabb, Is.EqualTo(new Box2(0.5f, 0.5f, 1.5f, 1.5f)));
}
[Test]
public async Task TestEdgeAABB()
{
var edge = new EdgeShape(Vector2.Zero, Vector2.One);
var aabb = edge.ComputeAABB(_transform, 0);
Assert.That(aabb.Width, Is.EqualTo(1.02f));
Assert.That(aabb, Is.EqualTo(new Box2(0.99f, 0.99f, 2.01f, 2.01f)));
}
[Test]
public async Task TestRotatedEdgeAABB()
{
var edge = new EdgeShape(Vector2.Zero, Vector2.One);
var aabb = edge.ComputeAABB(_rotatedTransform, 0);
Assert.That(MathHelper.CloseTo(aabb.Width, 0.02f));
Assert.That(aabb.EqualsApprox(new Box2(0.99f, 0.99f, 1.01f, 2.42f), 0.01f));
}
[Test]
public async Task TestPolyAABB()
{
var polygon = new PolygonShape();
// Radius is added to the AABB hence we'll just deduct it here for simplicity
polygon.SetAsBox(0.49f, 0.49f);
var aabb = polygon.ComputeAABB(_transform, 0);
Assert.That(aabb.Width, Is.EqualTo(1f));
Assert.That(aabb, Is.EqualTo(new Box2(0.5f, 0.5f, 1.5f, 1.5f)));
}
[Test]
public async Task TestRotatedPolyAABB()
{
var polygon = new PolygonShape();
// Radius is added to the AABB hence we'll just deduct it here for simplicity
polygon.SetAsBox(0.49f, 0.49f);
var aabb = polygon.ComputeAABB(_rotatedTransform, 0);
// I already had a rough idea of what the AABB should be, I just put these in so the test passes.
Assert.That(aabb.Width, Is.EqualTo(1.40592933f));
Assert.That(aabb, Is.EqualTo(new Box2(0.29703534f, 0.29703534f, 1.7029647f, 1.7029647f)));
}
}
}

View File

@@ -43,7 +43,7 @@ namespace Robust.UnitTesting
return;
_writer.Flush();
Assert.Fail(line);
Assert.Fail($"{line} Exception: {message.Exception}");
}
private string GetPrefix()