Compare commits

...

63 Commits

Author SHA1 Message Date
PJB3005
f0a4446fb6 Version: 262.0.2 2025-09-19 09:17:26 +02:00
Skye
64307228cc Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:26 +02:00
PJB3005
702c616dfe Version: 262.0.1 2025-09-14 14:55:50 +02:00
PJB3005
7d493f3f7e Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

    This (and the other couple past commits) reported by Elelzedel.

commit 7654d38612
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 22:50:51 2025 +0200

    Move CEF cache out of data directory

    Don't want content messing with this...

commit cdcc255123
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:11:16 2025 +0200

    Make Robust.Client.WebView.Cef.Program internal.

commit 2f56a6a110
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:10:46 2025 +0200

    Update SpaceWizards.NFluidSynth to 0.2.2

commit 16fc48cef2
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:09:43 2025 +0200

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
2025-09-14 14:55:50 +02:00
metalgearsloth
18849be0b4 Version: 262.0.0 2025-06-09 23:56:58 +10:00
Leon Friedrich
c6a1d82bb1 Validate that Toolshed command arguments have parsers (#6014)
* Add Nullable<T> support to ToolshedManager.TryParse

* Check that command arguments are parseable

* release notes

* a

* A is for Array

* Fix test

* Fix indentation
2025-06-09 23:47:29 +10:00
Leon Friedrich
d89e1a43c6 Add PvsResetTest (#6015) 2025-06-09 23:39:06 +10:00
Leon Friedrich
d894ef70ef Misc SpriteSystem fixes (#6001)
* Move GetPrototypeTextures to SpriteSystem

* Fix tests

* Fix #6002

* release notes

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-06-09 18:59:37 +10:00
DrSmugleaf
c7ea2793ca Fix TransformComponent state handling changing the coordinates of detached entities (#6006)
* Fix TransformComponent state handling changing the coordinates of detached entities

* Make ResetPredictedEntities not handle state for detached entities
2025-06-09 17:44:36 +10:00
Perry Fraser
0c61ff2bee fix: default audio params for PlayStatic (#6011) 2025-06-09 00:28:12 +02:00
Tayrtahn
343a34eac7 Fix warning CS0168 in EntityDeserializer (#6013)
* Fix warning CS0168 in EntityDeserializer

* Actually, why use try-catch just to rethrow anyway?
2025-06-09 00:04:03 +02:00
metalgearsloth
7be41f4890 Add ignoredcomps to IsDefault (#5998) 2025-06-05 23:54:47 +10:00
metalgearsloth
293470a5fe Fix incorrect saved window positions (#5927)
* Fix incorrect saved window positions

As of however many UI PRs ago windows store their last position on the client and it re-opens windows at that position.

The issue is that the code to avoid windows being able to go off-screen was immediately bulldozing this value, at least if the x <= 0. Now we just don't run it until we have a valid measure (probably the frame after) and avoid unnecessarily having an incorrect position applied.

* Explainer
2025-06-05 23:35:28 +10:00
metalgearsloth
2b8057acf0 Version: 261.2.0 2025-06-05 22:55:09 +10:00
Tayrtahn
bec3caa5da Fix error when using tpto on a grid (#5991)
* Fix error when using tpto on a grid

* Calculate map coords outside of loop
2025-06-05 22:45:30 +10:00
metalgearsloth
ea6126563b Add NearestChunkEnumerator (#5972)
Not super fast but want it for biome loading to prio chunks nearby first.
2025-06-05 22:36:22 +10:00
slarticodefast
00494ad9eb fix TryQueueDelete (#5996) 2025-06-05 22:34:50 +10:00
Tayrtahn
6672b7b1bd Correct misleading error message in ShareMapSystem.OnParentChange (#5992) 2025-06-05 22:29:47 +10:00
ruddygreat
8dc55e8748 fix the lifestage checks on predicted entity deletion (#5993)
Co-authored-by: Ruddygreat <ruddygreat1@gmail.com>
2025-06-05 22:29:22 +10:00
Tayrtahn
44ea2cd396 Implement IEquatable for ResolvedPathSpecifier and ResolvedCollectionSpecifier (#5980) 2025-06-01 18:10:55 +10:00
metalgearsloth
2c5604432b Update some GetComponentName usages (#5942)
Rider tells me to use generic and generic one seems better.
2025-06-01 17:54:43 +10:00
Tayrtahn
c696466522 Remove ITileDefinition.ID (#5982) 2025-06-01 17:51:59 +10:00
slarticodefast
01bb98e400 fix static grid center of mass (#5985) 2025-05-29 23:05:54 +10:00
metalgearsloth
af08e747de Defer grid state handling TileChangedEvent (#5981)
Rather than doing the old raise-event-per-tile we just raise it at the end.
2025-05-29 09:21:45 +10:00
metalgearsloth
8c35c2c380 Version: 261.1.0 2025-05-29 00:15:26 +10:00
Tayrtahn
6d46d3f4a5 Cleanup most warnings in unit tests (#5946)
* 2 warnings in JointDeletion_Test

* 1 warning in Collision_Test

* 2 warnings in Color_Test (deleted test of deprecated HCY color space)

* 1 warning in MapVelocity_Test

* 2 warnings in MapManager_Tests

* 2 warnings in MapPauseTests

* 1 warning in NetDisconnectMessageTest

* 1 warning in ContainerTests

* Suppress 1 warning in EntityEventBusTests.ComponentEvent

* 4 warnings in MapGridMap_Tests

* 1 warning in GridDeletion_Test

* Remove TryGetContainingContainer foolishness

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-05-29 00:12:58 +10:00
Tayrtahn
50e06e43fa Automatic Sawmill generation for UIControllers (#5967) 2025-05-29 00:06:55 +10:00
metalgearsloth
986b0f979d Fix physics forces not autoclearing (#5978)
* Fix physics forces not autoclearing

* Changes
2025-05-29 00:04:55 +10:00
metalgearsloth
a51d786dee Version: 261.0.0 2025-05-28 19:34:47 +10:00
metalgearsloth
5f5fed5d6c Update contact xform usage (#5977)
Forgot this one as well.
2025-05-28 19:30:50 +10:00
Tayrtahn
e475cc7898 Cleanup warning in SpriteBoundsOverlay (#5944)
* Cleanup warning in SpriteBoundsOverlay

* Make better use of the primary constructor
2025-05-28 19:28:53 +10:00
metalgearsloth
ee8ea4ec3b Purge PhysicsMapComponent (#5766)
* Replace PhysicsMapComponent

- Dumb idea
- Lots of book-keeping and perf overhead.
- Much saner this way.

* stuff

* More work

* Purge

* Fixes

* Eh?

* Fixes

* Also this

* weh

* Fixes

* ice-cream

* Fix

* Fix stacking / gravity

* Gravity query

* MoveBuffer optimisations

* Fixes for test

* World gravity

* Fix build

* Avoid some transform resolves for contactless ents

* Less getcomps

* Fix contact caching

* Possibly less copies

* reh

* bulldoze

* Test "fix"

* seikrets

* a

* I saw this but now I decideded against it

* true
2025-05-28 19:18:36 +10:00
slarticodefast
7482451ec4 optimize ToMapCoordinates (#5953) 2025-05-28 12:07:23 +10:00
metalgearsloth
dddf5cd2fb Add entities to SpawnEntitiesAttachedTo (#5971)
* Add entities to SpawnEntitiesAttachedTo

Need it for biome stuff.

* factorio
2025-05-27 19:45:48 +10:00
metalgearsloth
01979c451d Make RaiseMoveEvent internal (#5918)
I don't think content should really be calling this tbh.
2025-05-27 19:45:04 +10:00
slarticodefast
181a5ef0b4 fix GetMapLinearVelocity (#5950)
* fix GetMapLinearVelocity

* resolve and adjust other methods
2025-05-27 19:41:43 +10:00
Princess Cheeseballs
e7c7011cc0 Init Commit (#5909) 2025-05-27 19:34:32 +10:00
metalgearsloth
dc97615fd4 Add some Box2i methods (#5969)
Equivalent to Box2.
2025-05-27 19:26:50 +10:00
metalgearsloth
3b4944376b Fix FastNoiseLite fractal bounding (#5970)
This shouldn't be datafielded because it gets set by other datafields.
2025-05-27 19:14:49 +10:00
Tayrtahn
fa6bd8f7ba Cleanup TypeSerializer Logger warnings (#5966) 2025-05-24 20:23:28 +02:00
Whatstone
2398cbcf26 GameController: init LocMgr before init broadcast (#5965) 2025-05-24 19:02:26 +02:00
Tayrtahn
38ce48a83f Cleanup 3 warnings in SharedContainerSystem (#5949)
* Cleanup 3 warnings in SharedContainerSystem

* Don't call Transform twice
2025-05-24 19:00:57 +02:00
PJB3005
4e7de2f272 File dialog fixes and improvements
File dialog requests can now specify the share and access mode they want out of the opened file. This means read-only access is now possible.

While doing this I noticed that the SDL3 backend had a memory leak *and* didn't match the behavior of the other backends. Cleaned up the code to avoid that.

In-engine commands that *can* specify read-only access on file open now do.
2025-05-24 16:38:01 +02:00
Cami
b61075c660 Stopped recursive updates for controls that are not visible, as it vi… (#5960)
* Stopped recursive updates for controls that are not visible, as it violates framerate in large menus.

* Update Robust.Client/UserInterface/Control.cs

---------

Co-authored-by: Cam <Nop>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-05-23 17:42:47 +02:00
Tayrtahn
7b571dc80e Fix 2 instances of warning CS0162 (#5951) 2025-05-23 17:37:08 +02:00
TemporalOroboros
f1c76ca899 Remove unused obsolete TryGetContainingContainer override (#5660)
* Remove unused obsolete TryGetContainingContainer override

* poke tests

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-05-23 17:36:38 +02:00
metalgearsloth
84dcd658aa Version: 260.2.0 2025-05-21 23:30:58 +10:00
metalgearsloth
a634d6bd04 Add WorldNormal to StartCollideEvent (#5954)
We already have the value just a matter of adding it to the event.
2025-05-21 20:41:57 +10:00
DrSmugleaf
36f9df3079 Add System.Text.StringBuilder Insert(int, string) to sandbox.yml (#5955) 2025-05-21 11:20:10 +02:00
keronshb
824c018a69 Version: 260.1.0 2025-05-19 13:11:32 -04:00
Tayrtahn
4b6b688c72 Cleanup warnings in PlacementManager (#5939) 2025-05-18 19:14:16 +10:00
Tayrtahn
71df25b251 Cleanup warning in Clyde.Sprite (#5940) 2025-05-18 18:51:27 +10:00
metalgearsloth
be14a3c249 Expose CompFactory to systems (#5941) 2025-05-18 00:56:09 -04:00
metalgearsloth
3c2a4d5c79 Version: 260.0.0 2025-05-18 03:07:24 +10:00
metalgearsloth
44180b3ee0 Fix / remove startcollidevent worldpoint (#5936)
Now it's worldpoints because it may not necessarily be 1 pointr and internally we fix the actual points themselves.
2025-05-18 03:03:12 +10:00
metalgearsloth
bb0e77e937 Add some EntProtoId overloads (#5938)
Need it for some content stuff didn't feel like doing the rest yet.
2025-05-17 18:28:12 +10:00
ArtisticRoomba
684b9bc852 Add new Vertical property to progress bars (#5932) 2025-05-17 18:27:44 +10:00
Tayrtahn
9f3db6693e Add SpriteSystem dependency to VisualizerSystem (#5935)
* Add protected SpriteSystem reference to VisualizerSystem

* Capital S
2025-05-17 13:26:40 +10:00
metalgearsloth
40d869948d Version: 259.0.0 2025-05-15 20:26:10 +10:00
Tayrtahn
5c97b15849 Mark Entity methods as readonly (#5919)
* Mark Entity methods as readonly

* Add to GenericEntityPrint

* No but really
2025-05-15 20:23:29 +10:00
Tayrtahn
3d8a9a41fa Combine TileChangedEvents in SetTiles (#5912)
* Combine TileChangedEvents in SetTiles

* Raise event after regenerating collision

* continue, not return

* No need for GetComponent

* Swap TileRef for Tile + Vector2i

* Estimate size of tileChanges
2025-05-15 20:22:05 +10:00
metalgearsloth
92fc8722da Version: 258.0.1 2025-05-15 19:28:25 +10:00
metalgearsloth
73f6555624 Fix static ent collision spawn (#5933)
* Fix static ent collision spawn

* Fix test

* cool
2025-05-15 19:11:20 +10:00
129 changed files with 1860 additions and 1424 deletions

View File

@@ -57,7 +57,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,6 +54,156 @@ END TEMPLATE-->
*None yet*
## 262.0.2
## 262.0.1
## 262.0.0
### Breaking changes
* Toolshed commands will now validate that each non-generic command argument is parseable (i.e., has a corresponding type parser). This check can be disabled by explicitly marking the argument as unparseable via `CommandArgumentAttribute.Unparseable`.
### New features
* `ToolshedManager.TryParse` now also supports nullable value types.
* Add an ignoredComponents arg to IsDefault.
### Bugfixes
* Fix `SpriteComponent.Layer.Visible` setter not marking a sprite's bounding box as dirty.
* The audio params in the passed SoundSpecifier for PlayStatic(SoundSpecifier, Filter, ...) will now be used as a default like other PlayStatic overrides.
* Fix windows not saving their positions correctly when their x position is <= 0.
* Fix transform state handling overriding PVS detachment.
## 261.2.0
### New features
* Implement IEquatable for ResolvedPathSpecifier & ResolvedCollectionSpecifier.
* Add NearestChunkEnumerator.
### Bugfixes
* Fix static entities not having the center of mass updated.
* Fix TryQueueDelete.
* Fix tpto potentially parenting grids to non-map entities.
### Other
* TileChangedEvent is now raised once in clientside grid state handling rather than per tile.
* Removed ITileDefinition.ID as it was redundant.
* Change the lifestage checks on predicted entity deletion to check for terminating.
### Internal
* Update some `GetComponentName<T>` uses to generic.
## 261.1.0
### New features
* Automatically create logger sawmills for `UIController`s similar to `EntitySystem`s.
### Bugfixes
* Fix physics forces not auto-clearing / respecting the cvar.
### Internal
* Cleanup more compiler warnings in unit tests.
## 261.0.0
### Breaking changes
* Remove unused TryGetContainingContainer override.
* Stop recursive FrameUpdates for controls that are not visible.
* Initialize LocMgr earlier in the callstack for GameController.
* Fix FastNoiseLise fractal bounding and remove its DataField property as it should be derived on other properties updating.
* Make RaiseMoveEvent internal.
* MovedGridsComponent and PhysicsMapComponent are now purged and properties on `SharedPhysicsSystem`. Additionally the TransformComponent for Awake entities is stored alongside the PhysicsComponent for them.
* TransformComponent is now stored on physics contacts.
* Gravity2DComponent and Gravity2DController were moved to SharedPhysicsSystem.
### New features
* `IFileDialogManager` now allows specifying `FileAccess` and `FileShare` modes.
* Add Intersects and Enlarged to Box2i in line with Box2.
* Make `KeyFrame`s on `AnimationTrackProperty` public settable.
* Add the spawned entities to a returned array from `SpawnEntitiesAttachedTo`.
### Bugfixes
* Fixed SDL3 file dialog implementation having a memory leak and not opening files read-write.
* Fix GetMapLinearVelocity.
### Other
* `uploadfile` and `loadprototype` commands now only open files with read access.
* Optimize `ToMapCoordinates`.
### Internal
* Cleanup on internals of `IFileDialogManager`, removing duplicate code.
* Fix Contacts not correctly being marked as `Touching` while contact is ongoing.
## 260.2.0
### New features
* Add `StringBuilder.Insert(int, string)` to sandbox.
* Add the WorldNormal to the StartCollideEvent.
## 260.1.0
### New features
* `ComponentFactory` is now exposed to `EntitySystem` as `Factory`
### Other
* Cleanup warnings in PLacementManager
* Cleanup warnings in Clide.Sprite
## 260.0.0
### Breaking changes
* Fix / change `StartCollideEvent.WorldPoint` to return all points for the collision which may be up to 2 instead of 1.
### New features
* Add SpriteSystem dependency to VisualizerSystem.
* Add Vertical property to progress bars
* Add some `EntProtoId` overloads for group entity spawn methods.
## 259.0.0
### Breaking changes
* TileChangedEvent now has an array of tile changed entries rather than raising an individual event for every single tile changed.
### Other
* `Entity<T>` methods were marked as `readonly` as appropriate.
## 258.0.1
### Bugfixes
* Fix static physics bodies not generating contacts if they spawn onto sleeping bodies.
## 258.0.0
### Breaking changes

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
public static class Program
internal static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameControllerInternal _gameController = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var settings = new CefSettings()
{

View File

@@ -13,7 +13,7 @@ namespace Robust.Client.Animations
/// </summary>
public abstract class AnimationTrackProperty : AnimationTrack
{
public List<KeyFrame> KeyFrames { get; protected set; } = new();
public List<KeyFrame> KeyFrames { get; set; } = new();
/// <summary>
/// How to interpolate values when between two keyframes.

View File

@@ -21,7 +21,7 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
{
// TODO SPRITE optimize this
// Because the just take the BB of the rotated BB, I'mt pretty sure we do a lot of unnecessary maths.
// Because the just take the BB of the rotated BB, I'm pretty sure we do a lot of unnecessary maths.
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
}

View File

@@ -160,6 +160,7 @@ namespace Robust.Client
}
_serializationManager.Initialize();
_loc.Initialize();
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
@@ -182,7 +183,6 @@ namespace Robust.Client
_serializer.Initialize();
_inputManager.Initialize();
_console.Initialize();
_loc.Initialize();
// Make sure this is done before we try to load prototypes,
// avoid any possibility of race conditions causing the check to not finish
@@ -387,7 +387,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)

View File

@@ -296,7 +296,7 @@ namespace Robust.Client.GameObjects
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityDeleted
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
@@ -322,7 +322,7 @@ namespace Robust.Client.GameObjects
{
if (IsQueuedForDeletion(ent.Owner)
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityDeleted
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;

View File

@@ -24,9 +24,7 @@ namespace Robust.Client.GameObjects
public sealed class SpriteBoundsSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
private SpriteBoundsOverlay? _overlay;
@@ -42,7 +40,7 @@ namespace Robust.Client.GameObjects
if (_enabled)
{
DebugTools.AssertNull(_overlay);
_overlay = new SpriteBoundsOverlay(_spriteTree, _xformSystem);
_overlay = new SpriteBoundsOverlay(EntityManager);
_overlayManager.AddOverlay(_overlay);
}
else
@@ -57,18 +55,13 @@ namespace Robust.Client.GameObjects
private bool _enabled;
}
public sealed class SpriteBoundsOverlay : Overlay
public sealed class SpriteBoundsOverlay(IEntityManager entMan) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly SharedTransformSystem _xformSystem;
private SpriteTreeSystem _renderTree;
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, SharedTransformSystem xformSystem)
{
_renderTree = renderTree;
_xformSystem = xformSystem;
}
private readonly SharedTransformSystem _xformSystem = entMan.System<SharedTransformSystem>();
private readonly SpriteSystem _spriteSystem = entMan.System<SpriteSystem>();
private readonly SpriteTreeSystem _renderTree = entMan.System<SpriteTreeSystem>();
protected internal override void Draw(in OverlayDrawArgs args)
{
@@ -76,10 +69,11 @@ namespace Robust.Client.GameObjects
var currentMap = args.MapId;
var viewport = args.WorldBounds;
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
foreach (var entry in _renderTree.QueryAabb(currentMap, viewport))
{
var (sprite, xform) = entry;
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
var bounds = _spriteSystem.CalculateBounds((entry.Uid, sprite), worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
// Get scaled down bounds used to indicate the "south" of a sprite.
var localBound = bounds.Box;

View File

@@ -1224,6 +1224,8 @@ namespace Robust.Client.GameObjects
return;
_visible = value;
Owner.Comp.BoundsDirty = true;
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
if (_parent.Owner != EntityUid.Invalid)
Owner.Comp.Sys?.QueueUpdateIsInert(Owner);
@@ -1791,76 +1793,15 @@ namespace Robust.Client.GameObjects
[Obsolete("Use SpriteSystem.GetPrototypeTextures() instead")]
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot)
{
var results = new List<IDirectionalTextureProvider>();
noRot = false;
// TODO when moving to a non-static method in a system, pass in IComponentFactory
if (prototype.TryGetComponent(out IconComponent? icon))
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
results.Add(sys.GetIcon(icon));
return results;
}
if (!prototype.Components.TryGetValue("Sprite", out _))
{
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
EntitySystem.Get<AppearanceSystem>().OnChangeData(dummy, spriteComponent);
foreach (var layer in spriteComponent.AllLayers)
{
if (!layer.Visible) continue;
if (layer.Texture != null)
{
results.Add(layer.Texture);
continue;
}
if (!layer.RsiState.IsValid) continue;
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
if (rsi == null ||
!rsi.TryGetState(layer.RsiState, out var state))
continue;
results.Add(state);
}
noRot = spriteComponent.NoRotation;
entityManager.DeleteEntity(dummy);
if (results.Count == 0)
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
return results;
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
return sys.GetPrototypeTextures(prototype, out noRot);
}
[Obsolete("Use SpriteSystem.GetPrototypeIcon() instead")]
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
// TODO when moving to a non-static method in a system, pass in IComponentFactory
if (prototype.TryGetComponent(out IconComponent? icon))
return sys.GetIcon(icon);
if (!prototype.Components.ContainsKey("Sprite"))
return sys.GetFallbackState();
var entityManager = IoCManager.Resolve<IEntityManager>();
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? sys.GetFallbackState();
entityManager.DeleteEntity(dummy);
return result;
return sys.GetPrototypeIcon(prototype);
}
}
}

View File

@@ -56,10 +56,6 @@ public sealed partial class SpriteSystem
/// </summary>
public IRsiStateLike GetPrototypeIcon(string prototype)
{
// Check if this prototype has been cached before, and if so return the result.
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
return cachedResult;
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
{
// The specified prototype doesn't exist, return the fallback "error" sprite.
@@ -67,11 +63,7 @@ public sealed partial class SpriteSystem
return GetFallbackState();
}
// Generate the icon and cache it in case it's ever needed again.
var result = GetPrototypeIcon(entityPrototype);
_cachedPrototypeIcons[prototype] = result;
return result;
return GetPrototypeIcon(entityPrototype);
}
/// <summary>
@@ -79,13 +71,19 @@ public sealed partial class SpriteSystem
/// This method does NOT cache the result.
/// </summary>
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
{
// This method may spawn & delete an entity to get an accruate RSI state, hence we cache the results
if (_cachedPrototypeIcons.TryGetValue(prototype.ID, out var cachedResult))
return cachedResult;
return _cachedPrototypeIcons[prototype.ID] = GetPrototypeIconInternal(prototype);
}
private IRsiStateLike GetPrototypeIconInternal(EntityPrototype prototype)
{
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
if (prototype.Components.TryGetValue("Icon", out var compData)
&& compData.Component is IconComponent icon)
{
if (prototype.TryGetComponent(out IconComponent? icon, _factory))
return GetIcon(icon);
}
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
if (!prototype.Components.ContainsKey("Sprite"))
@@ -102,6 +100,63 @@ public sealed partial class SpriteSystem
return result;
}
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto) =>
GetPrototypeTextures(proto, out _);
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto, out bool noRot)
{
var results = new List<IDirectionalTextureProvider>();
noRot = false;
if (proto.TryGetComponent(out IconComponent? icon, _factory))
{
results.Add(GetIcon(icon));
return results;
}
if (!proto.Components.ContainsKey("Sprite"))
{
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
var dummy = Spawn(proto.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
// TODO SPRITE is this needed?
// And if it is, shouldn't GetPrototypeIconInternal also use this?
_appearance.OnChangeData(dummy, spriteComponent);
foreach (var layer in spriteComponent.AllLayers)
{
if (!layer.Visible)
continue;
if (layer.Texture != null)
{
results.Add(layer.Texture);
continue;
}
if (!layer.RsiState.IsValid)
continue;
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.RsiState, out var state))
continue;
results.Add(state);
}
noRot = spriteComponent.NoRotation;
Del(dummy);
if (results.Count == 0)
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
[Pure]
public RSI.State GetFallbackState()
{

View File

@@ -34,8 +34,12 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
// Note that any new system dependencies have to be added to RobustUnitTest.BaseSetup()
[Dependency] private readonly SharedTransformSystem _xforms = default!;
[Dependency] private readonly SpriteTreeSystem _tree = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();

View File

@@ -11,6 +11,7 @@ public abstract class VisualizerSystem<T> : EntitySystem
{
[Dependency] protected readonly AppearanceSystem AppearanceSystem = default!;
[Dependency] protected readonly AnimationPlayerSystem AnimationSystem = default!;
[Dependency] protected readonly SpriteSystem SpriteSystem = default!;
public override void Initialize()
{

View File

@@ -631,7 +631,7 @@ namespace Robust.Client.GameStates
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
if (compState != null)
if ((meta.Flags & MetaDataFlags.Detached) == 0 && compState != null)
{
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(entity, comp, ref handleState);

View File

@@ -412,8 +412,11 @@ namespace Robust.Client.Graphics.Clyde
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
{
var gridData = _mapChunkData.GetOrNew(args.Entity);
if (gridData.TryGetValue(args.ChunkIndex, out var data))
data.Dirty = true;
foreach (var change in args.Changes)
{
if (gridData.TryGetValue(change.ChunkIndex, out var data))
data.Dirty = true;
}
}
private void _updateOnGridCreated(GridStartupEvent ev)

View File

@@ -153,7 +153,7 @@ internal partial class Clyde
// special casing angle = n*pi/2 to avoid box rotation & bounding calculations doesn't seem to give significant speedups.
data.SpriteScreenBB = TransformCenteredBox(
data.Sprite.Bounds,
_spriteSystem.GetLocalBounds((data.Uid, data.Sprite)),
finalRotation,
pos + batch.PreScaleViewOffset,
batch.ViewScale);

View File

@@ -10,6 +10,7 @@ internal sealed partial class Clyde
private MapSystem _mapSystem = default!;
private LightTreeSystem _lightTreeSystem = default!;
private TransformSystem _transformSystem = default!;
private SpriteSystem _spriteSystem = default!;
private SpriteTreeSystem _spriteTreeSystem = default!;
private ClientOccluderSystem _occluderSystem = default!;
@@ -24,6 +25,7 @@ internal sealed partial class Clyde
_mapSystem = _entitySystemManager.GetEntitySystem<MapSystem>();
_lightTreeSystem = _entitySystemManager.GetEntitySystem<LightTreeSystem>();
_transformSystem = _entitySystemManager.GetEntitySystem<TransformSystem>();
_spriteSystem = _entitySystemManager.GetEntitySystem<SpriteSystem>();
_spriteTreeSystem = _entitySystemManager.GetEntitySystem<SpriteTreeSystem>();
_occluderSystem = _entitySystemManager.GetEntitySystem<ClientOccluderSystem>();
}
@@ -33,6 +35,7 @@ internal sealed partial class Clyde
_mapSystem = null!;
_lightTreeSystem = null!;
_transformSystem = null!;
_spriteSystem = null!;
_spriteTreeSystem = null!;
_occluderSystem = null!;
}

View File

@@ -467,7 +467,7 @@ namespace Robust.Client.Graphics.Clyde
_windowing!.RunOnWindowThread(a);
}
public IFileDialogManager? FileDialogImpl => _windowing as IFileDialogManager;
public IFileDialogManagerImplementation? FileDialogImpl => _windowing as IFileDialogManagerImplementation;
private abstract class WindowReg
{

View File

@@ -307,7 +307,7 @@ namespace Robust.Client.Graphics.Clyde
action();
}
public IFileDialogManager? FileDialogImpl => null;
public IFileDialogManagerImplementation? FileDialogImpl => null;
private sealed class DummyCursor : ICursor
{

View File

@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -12,31 +11,16 @@ namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl : IFileDialogManager
private sealed partial class Sdl3WindowingImpl : IFileDialogManagerImplementation
{
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
public async Task<string?> OpenFile(FileDialogFilters? filters)
{
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
if (fileName == null)
return null;
return File.OpenRead(fileName);
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
}
public async Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
public async Task<string?> SaveFile(FileDialogFilters? filters)
{
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
if (fileName == null)
return null;
try
{
return (File.Open(fileName, truncate ? FileMode.Truncate : FileMode.Open), true);
}
catch (FileNotFoundException)
{
return (File.Open(fileName, FileMode.Create), false);
}
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
}
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
@@ -74,6 +58,8 @@ internal partial class Clyde
NativeMemory.Free(filter.name);
NativeMemory.Free(filter.pattern);
}
NativeMemory.Free(filtersAlloc);
}
return task;

View File

@@ -71,6 +71,6 @@ namespace Robust.Client.Graphics
void RunOnWindowThread(Action action);
IFileDialogManager? FileDialogImpl { get; }
IFileDialogManagerImplementation? FileDialogImpl { get; }
}
}

View File

@@ -47,7 +47,7 @@ namespace Robust.Client.Physics
* This will draw above every body involved in a particular island solve.
*/
public readonly Queue<(TimeSpan Time, List<PhysicsComponent> Bodies)> IslandSolve = new();
public readonly Queue<(TimeSpan Time, List<Entity<PhysicsComponent, TransformComponent>> Bodies)> IslandSolve = new();
public const float SolveDuration = 0.1f;
public override void Initialize()

View File

@@ -90,9 +90,10 @@ public sealed partial class PhysicsSystem
// existing contacts for predicted entities before performing any actual prediction.
var contacts = new List<Contact>();
var maps = new HashSet<EntityUid>();
var enumerator = AllEntityQuery<PredictedPhysicsComponent, PhysicsComponent, TransformComponent>();
_broadphase.FindNewContacts();
while (enumerator.MoveNext(out _, out var physics, out var xform))
{
DebugTools.Assert(physics.Predict);
@@ -100,10 +101,6 @@ public sealed partial class PhysicsSystem
if (xform.MapUid is not { } map)
continue;
if (maps.Add(map) && PhysMapQuery.TryGetComponent(map, out var physMap) &&
MapQuery.TryGetComponent(map, out var mapComp))
_broadphase.FindNewContacts(physMap, mapComp.MapId);
contacts.AddRange(physics.Contacts);
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics;
@@ -23,21 +24,23 @@ namespace Robust.Client.Physics
SimulateWorld(frameTime, _gameTiming.InPrediction);
}
protected override void Cleanup(PhysicsMapComponent component, float frameTime)
protected override void Cleanup(float frameTime)
{
var toRemove = new List<Entity<PhysicsComponent>>();
var toRemove = new ValueList<Entity<PhysicsComponent, TransformComponent>>();
// Because we're not predicting 99% of bodies its sleep timer never gets incremented so we'll just do it ourselves.
// (and serializing it over the network isn't necessary?)
// This is a client-only problem.
// Also need to suss out having the client build the island anyway and just... not solving it?
foreach (var body in component.AwakeBodies)
foreach (var ent in AwakeBodies)
{
var body = ent.Comp1;
if (!body.SleepingAllowed || body.LinearVelocity.Length() > LinearToleranceSqr / 2f || body.AngularVelocity * body.AngularVelocity > AngularToleranceSqr / 2f) continue;
body.SleepTime += frameTime;
if (body.SleepTime > TimeToSleep)
{
toRemove.Add(new Entity<PhysicsComponent>(body.Owner, body));
toRemove.Add(ent);
}
}
@@ -46,37 +49,38 @@ namespace Robust.Client.Physics
SetAwake(body, false);
}
base.Cleanup(component, frameTime);
base.Cleanup(frameTime);
}
protected override void UpdateLerpData(PhysicsMapComponent component, List<PhysicsComponent> bodies, EntityQuery<TransformComponent> xformQuery)
protected override void UpdateLerpData(List<Entity<PhysicsComponent, TransformComponent>> bodies)
{
foreach (var body in bodies)
foreach (var bodyEnt in bodies)
{
var body = bodyEnt.Comp1;
var xform = bodyEnt.Comp2;
if (body.BodyType == BodyType.Static ||
component.LerpData.TryGetValue(body.Owner, out var lerpData) ||
!xformQuery.TryGetComponent(body.Owner, out var xform) ||
lerpData.ParentUid == xform.ParentUid)
LerpData.TryGetValue(bodyEnt, out var lerpData) ||
lerpData == xform.ParentUid)
{
continue;
}
component.LerpData[xform.Owner] = (xform.ParentUid, xform.LocalPosition, xform.LocalRotation);
LerpData[bodyEnt.Owner] = xform.ParentUid;
}
}
/// <summary>
/// Flush all of our lerping data.
/// </summary>
protected override void FinalStep(PhysicsMapComponent component)
protected override void FinalStep()
{
base.FinalStep(component);
var xformQuery = GetEntityQuery<TransformComponent>();
base.FinalStep();
foreach (var (uid, (parentUid, position, rotation)) in component.LerpData)
foreach (var (uid, parentUid) in LerpData)
{
if (!xformQuery.TryGetComponent(uid, out var xform) ||
!parentUid.IsValid())
// Can't just re-use xform from before as movement events may cause event subs to fire.
if (!XformQuery.TryGetComponent(uid, out var xform) || !parentUid.IsValid())
{
continue;
}
@@ -85,7 +89,7 @@ namespace Robust.Client.Physics
_transform.SetLocalPositionRotation(uid, xform.LocalPosition, xform.LocalRotation, xform);
}
component.LerpData.Clear();
LerpData.Clear();
}
}
}

View File

@@ -47,6 +47,7 @@ namespace Robust.Client.Placement
private SharedMapSystem Maps => EntityManager.System<SharedMapSystem>();
private SharedTransformSystem XformSystem => EntityManager.System<SharedTransformSystem>();
private SpriteSystem Sprite => EntityManager.System<SpriteSystem>();
/// <summary>
/// How long before a pending tile change is dropped.
@@ -359,12 +360,15 @@ namespace Robust.Client.Placement
private void HandleTileChanged(ref TileChangedEvent args)
{
var coords = Maps.GridTileToLocal(
args.NewTile.GridUid,
EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid),
args.NewTile.GridIndices);
foreach (var change in args.Changes)
{
var coords = Maps.GridTileToLocal(
args.Entity,
args.Entity.Comp,
change.GridIndices);
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
}
}
/// <inheritdoc />
@@ -708,11 +712,11 @@ namespace Robust.Client.Placement
CurrentPlacementOverlayEntity = null;
}
private SpriteComponent SetupPlacementOverlayEntity()
private Entity<SpriteComponent> SetupPlacementOverlayEntity()
{
EnsureNoPlacementOverlayEntity();
CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace);
return EntityManager.EnsureComponent<SpriteComponent>(CurrentPlacementOverlayEntity.Value);
return (CurrentPlacementOverlayEntity.Value, EntityManager.EnsureComponent<SpriteComponent>(CurrentPlacementOverlayEntity.Value));
}
private void PreparePlacement(string templateName)
@@ -729,10 +733,16 @@ namespace Robust.Client.Placement
EntityManager.GetComponent<MetaDataComponent>(CurrentPlacementOverlayEntity.Value));
}
public void PreparePlacementSprite(SpriteComponent sprite)
public void PreparePlacementSprite(Entity<SpriteComponent> sprite)
{
var sc = SetupPlacementOverlayEntity();
sc.CopyFrom(sprite);
Sprite.CopySprite(sprite.AsNullable(), sc.AsNullable());
}
[Obsolete("Use the Entity<SpriteComponent> overload.")]
public void PreparePlacementSprite(SpriteComponent sprite)
{
PreparePlacementSprite((sprite.Owner, sprite));
}
public void PreparePlacementTexList(List<IDirectionalTextureProvider>? texs, bool noRot, EntityPrototype? prototype)
@@ -743,27 +753,27 @@ namespace Robust.Client.Placement
// This one covers most cases (including Construction)
foreach (var v in texs)
{
if (v is RSI.State)
if (v is RSI.State st)
{
var st = (RSI.State) v;
sc.AddLayer(st.StateId, st.RSI);
Sprite.AddRsiLayer(sc.AsNullable(), st.StateId, st.RSI);
}
else
{
// Fallback
sc.AddLayer(v.Default);
Sprite.AddTextureLayer(sc.AsNullable(), v.Default);
}
}
}
else
{
sc.AddLayer(new ResPath("/Textures/Interface/tilebuildoverlay.png"));
Sprite.AddTextureLayer(sc.AsNullable(), new ResPath("/Textures/Interface/tilebuildoverlay.png"));
}
sc.NoRotation = noRot;
sc.Comp.NoRotation = noRot;
if (prototype != null && prototype.TryGetComponent<SpriteComponent>("Sprite", out var spriteComp))
{
sc.Scale = spriteComp.Scale;
Sprite.SetScale(sc.AsNullable(), spriteComp.Scale);
}
}
@@ -771,7 +781,7 @@ namespace Robust.Client.Placement
private void PreparePlacementTile()
{
var sc = SetupPlacementOverlayEntity();
sc.AddLayer(new ResPath("/Textures/Interface/tilebuildoverlay.png"));
Sprite.AddTextureLayer(sc.AsNullable(), new ResPath("/Textures/Interface/tilebuildoverlay.png"));
IsActive = true;
}

View File

@@ -166,7 +166,7 @@ namespace Robust.Client.Player
{
if (_client.RunLevel != ClientRunLevel.SinglePlayerGame)
Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues.");
var eye = (EyeComponent) Factory.GetComponent(typeof(EyeComponent));
var eye = Factory.GetComponent<EyeComponent>();
eye.NetSyncEnabled = false;
EntManager.AddComponent(uid.Value, eye);
}

View File

@@ -22,7 +22,7 @@ public sealed class LoadPrototypeCommand : IConsoleCommand
var dialogManager = IoCManager.Resolve<IFileDialogManager>();
var loadManager = IoCManager.Resolve<IGamePrototypeLoadManager>();
var stream = await dialogManager.OpenFile();
var stream = await dialogManager.OpenFile(access: FileAccess.Read);
if (stream is null)
return;

View File

@@ -1,3 +1,4 @@
using System.IO;
using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Configuration;
@@ -36,7 +37,7 @@ public sealed class UploadFileCommand : IConsoleCommand
var path = new ResPath(args[0]).ToRelativePath();
var filters = new FileDialogFilters(new FileDialogFilters.Group(path.Extension));
await using var file = await _dialog.OpenFile(filters);
await using var file = await _dialog.OpenFile(filters, FileAccess.Read);
if (file == null)
{

View File

@@ -994,6 +994,9 @@ namespace Robust.Client.UserInterface
internal int DoFrameUpdateRecursive(FrameEventArgs args)
{
if (!Visible)
return 0;
var total = 1;
FrameUpdate(args);

View File

@@ -1,6 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.UserInterface.Controllers;
@@ -11,11 +14,14 @@ namespace Robust.Client.UserInterface.Controllers;
/// and <see cref="UISystemDependencyAttribute"/> to depend on <see cref="EntitySystem"/>s, which will be automatically
/// injected once they are created.
/// </summary>
public abstract partial class UIController
public abstract partial class UIController : IPostInjectInit
{
[Dependency] protected readonly IUserInterfaceManager UIManager = default!;
[Dependency] protected readonly IEntitySystemManager EntitySystemManager = default!;
[Dependency] protected readonly IEntityManager EntityManager = default!;
[Dependency] protected readonly ILogManager LogManager = default!;
public ISawmill Log { get; protected set; } = default!;
public virtual void Initialize()
{
@@ -24,4 +30,39 @@ public abstract partial class UIController
public virtual void FrameUpdate(FrameEventArgs args)
{
}
protected virtual string SawmillName
{
get
{
var name = GetType().Name;
// Strip trailing "UIController"
if (name.EndsWith("UIController"))
name = name.Substring(0, name.Length - "UIController".Length);
// Convert CamelCase to snake_case
// Ignore if all uppercase, assume acronym (e.g. NPC or HTN)
if (name.All(char.IsUpper))
{
name = name.ToLower(CultureInfo.InvariantCulture);
}
else
{
name = string.Concat(name.Select(x => char.IsUpper(x) ? $"_{char.ToLower(x)}" : x.ToString()));
name = name.Trim('_');
}
return $"ui.{name}";
}
}
public void PostInject()
{
Log = LogManager.GetSawmill(SawmillName);
#if !DEBUG
Log.Level = LogLevel.Info;
#endif
}
}

View File

@@ -14,6 +14,27 @@ namespace Robust.Client.UserInterface.Controls
private StyleBox? _backgroundStyleBoxOverride;
private StyleBox? _foregroundStyleBoxOverride;
private bool _vertical;
/// <summary>
/// Whether the progress bar is oriented vertically.
/// </summary>
/// <remarks>
/// A vertical progress bar fills from bottom to top.
/// </remarks>
public bool Vertical
{
get => _vertical;
set
{
if (_vertical != value)
{
_vertical = value;
InvalidateMeasure();
}
}
}
public StyleBox? BackgroundStyleBoxOverride
{
get => _backgroundStyleBoxOverride;
@@ -70,11 +91,23 @@ namespace Robust.Client.UserInterface.Controls
{
return;
}
var minSize = fg.MinimumSize;
var size = PixelWidth * GetAsRatio() - minSize.X;
if (size > 0)
if (_vertical)
{
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, PixelHeight), UIScale);
var size = PixelHeight * GetAsRatio();
if (size > 0)
{
fg.Draw(handle, UIBox2.FromDimensions(0, PixelHeight - size, PixelWidth, size), UIScale);
}
}
else
{
var minSize = fg.MinimumSize;
var size = PixelWidth * GetAsRatio() - minSize.X;
if (size > 0)
{
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, PixelHeight), UIScale);
}
}
}

View File

@@ -145,6 +145,11 @@ namespace Robust.Client.UserInterface.CustomControls
protected override void FrameUpdate(FrameEventArgs args)
{
// This is to avoid unnecessarily setting a position where our size isn't yet fully updated.
// This most commonly happens with saved window positions if your window position is <= 0.
if (!IsMeasureValid)
return;
var (spaceX, spaceY) = Parent!.Size;
var maxX = spaceX - ((AllowOffScreen & DirectionFlag.West) == 0 ? Size.X : WindowEdgeSeparation);

View File

@@ -8,12 +8,19 @@ namespace Robust.Client.UserInterface
/// </summary>
internal sealed class DummyFileDialogManager : IFileDialogManager
{
public Task<Stream?> OpenFile(FileDialogFilters? filters = null)
public Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null)
{
return Task.FromResult<Stream?>(null);
}
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
FileDialogFilters? filters = null,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None)
{
return Task.FromResult<(Stream fileStream, bool alreadyExisted)?>(null);
}

View File

@@ -7,9 +7,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Robust.Client.Graphics;
using Robust.Shared;
using Robust.Shared.Console;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
@@ -30,18 +28,28 @@ namespace Robust.Client.UserInterface
private bool _kDialogAvailable;
private bool _checkedKDialogAvailable;
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
public async Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null)
{
if ((access & FileAccess.ReadWrite) != access)
throw new ArgumentException("Invalid file access specified");
var realShare = share ?? (access == FileAccess.Read ? FileShare.Read : FileShare.None);
if ((realShare & (FileShare.ReadWrite | FileShare.Delete)) != realShare)
throw new ArgumentException("Invalid file share specified");
string? name;
if (_clyde.FileDialogImpl is { } clydeImpl)
return await clydeImpl.OpenFile(filters);
name = await clydeImpl.OpenFile(filters);
else
name = await GetOpenFileName(filters);
var name = await GetOpenFileName(filters);
if (name == null)
{
return null;
}
return File.Open(name, FileMode.Open);
return File.Open(name, FileMode.Open, access, realShare);
}
private async Task<string?> GetOpenFileName(FileDialogFilters? filters)
@@ -54,24 +62,34 @@ namespace Robust.Client.UserInterface
return await OpenFileNfd(filters);
}
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
public async Task<(Stream, bool)?> SaveFile(
FileDialogFilters? filters,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None)
{
if (_clyde.FileDialogImpl is { } clydeImpl)
return await clydeImpl.SaveFile(filters);
if ((access & FileAccess.ReadWrite) != access)
throw new ArgumentException("Invalid file access specified");
if ((share & (FileShare.ReadWrite | FileShare.Delete)) != share)
throw new ArgumentException("Invalid file share specified");
string? name;
if (_clyde.FileDialogImpl is { } clydeImpl)
name = await clydeImpl.SaveFile(filters);
else
name = await GetSaveFileName(filters);
var name = await GetSaveFileName(filters);
if (name == null)
{
return null;
}
try
{
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open), true);
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open, access, share), true);
}
catch (FileNotFoundException)
{
return (File.Open(name, FileMode.Create), false);
return (File.Open(name, FileMode.Create, access, share), false);
}
}

View File

@@ -1,5 +1,6 @@
using System.IO;
using System.Threading.Tasks;
using Robust.Client.Graphics;
namespace Robust.Client.UserInterface
{
@@ -19,7 +20,17 @@ namespace Robust.Client.UserInterface
/// The file stream for the file the user opened.
/// <see langword="null" /> if the user cancelled the action.
/// </returns>
Task<Stream?> OpenFile(FileDialogFilters? filters = null);
/// <param name="filters">Filters for file types that the user can select.</param>
/// <param name="access">What access is desired from the file operation.</param>
/// <param name="share">
/// What sharing mode is desired from the file operation.
/// If null is provided and <paramref name="access"/> is <see cref="FileAccess.Read"/>,
/// <see cref="FileShare.Read"/> is selected, otherwise <see cref="FileShare.None"/>.
/// </param>
Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null);
/// <summary>
/// Open a file dialog used for saving a single file.
@@ -29,6 +40,21 @@ namespace Robust.Client.UserInterface
/// Null if the user cancelled the action.
/// </returns>
/// <param name="truncate">Should we truncate an existing file to 0-size then write or append.</param>
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true);
/// <param name="access">What access is desired from the file operation.</param>
/// <param name="share">Sharing mode for the opened file.</param>
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
FileDialogFilters? filters = null,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None);
}
/// <summary>
/// Internal implementation interface used to connect <see cref="FileDialogManager"/> and <see cref="IClydeInternal"/>.
/// </summary>
internal interface IFileDialogManagerImplementation
{
Task<string?> OpenFile(FileDialogFilters? filters);
Task<string?> SaveFile(FileDialogFilters? filters);
}
}

View File

@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;

View File

@@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
namespace Robust.Server.Physics.Commands;
public sealed class BoxStackCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _entManager = default!;
public string Command => "boxstack";
public string Description => string.Empty;
public string Help => "boxstack [mapid] [columns] [rows]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 3 ||
!int.TryParse(args[0], out var mapInt) ||
!int.TryParse(args[1], out var columns) ||
!int.TryParse(args[2], out var rows))
{
return;
}
var mapId = new MapId(mapInt);
var fixtureSystem = _entManager.System<FixtureSystem>();
var physSystem = _entManager.System<SharedPhysicsSystem>();
physSystem.SetGravity(new Vector2(0f, -9.8f));
var groundUid = _entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var ground = _entManager.AddComponent<PhysicsComponent>(groundUid);
var groundManager = _entManager.EnsureComponent<FixturesComponent>(groundUid);
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
fixtureSystem.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 1, 1, true), manager: groundManager, body: ground);
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
fixtureSystem.CreateFixture(groundUid, "fix2", new Fixture(vertical, 1, 1, true), manager: groundManager, body: ground);
physSystem.WakeBody(groundUid, manager: groundManager, body: ground);
var xs = new[]
{
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
};
for (var j = 0; j < columns; j++)
{
for (var i = 0; i < rows; i++)
{
var x = 0.0f;
var boxUid = _entManager.SpawnEntity(null,
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 2.1f * i), mapId));
var box = _entManager.AddComponent<PhysicsComponent>(boxUid);
var manager = _entManager.EnsureComponent<FixturesComponent>(boxUid);
physSystem.SetBodyType(boxUid, BodyType.Dynamic, manager: manager, body: box);
var poly = new PolygonShape(0.001f);
poly.Set(new List<Vector2>()
{
new(0.5f, -0.5f),
new(0.5f, 0.5f),
new(-0.5f, 0.5f),
new(-0.5f, -0.5f),
});
fixtureSystem.CreateFixture(boxUid, "fix1", new Fixture(poly, 1, 1, true), manager: manager, body: box);
physSystem.WakeBody(boxUid, manager: manager, body: box);
}
}
}
}

View File

@@ -140,7 +140,7 @@ namespace Robust.Server.Placement
{
// Replace existing entities if relevant.
if (msg.Replacement && _prototype.Index<EntityPrototype>(entityTemplateName).Components.TryGetValue(
_factory.GetComponentName(typeof(PlacementReplacementComponent)), out var compRegistry))
_factory.GetComponentName<PlacementReplacementComponent>(), out var compRegistry))
{
var key = ((PlacementReplacementComponent)compRegistry.Component).Key;
var gridUid = _xformSystem.GetGrid(coordinates);

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.Contracts;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -101,6 +102,7 @@ namespace Robust.Shared.Maths
/// <summary>
/// Returns the smallest rectangle that contains both of the rectangles.
/// </summary>
[Pure]
public readonly Box2i Union(in Box2i other)
{
var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft);
@@ -207,6 +209,7 @@ namespace Robust.Shared.Maths
/// <summary>
/// Multiplies each side of the box by the scalar.
/// </summary>
[Pure]
public Box2i Scale(int scalar)
{
return new Box2i(
@@ -215,6 +218,21 @@ namespace Robust.Shared.Maths
Right * scalar,
Top * scalar);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool Intersects(in Box2i other)
{
return other.Bottom <= this.Top && other.Top >= this.Bottom && other.Right >= this.Left &&
other.Left <= this.Right;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public readonly Box2i Enlarged(int size)
{
return new(Left - size, Bottom - size, Right + size, Top + size);
}
}
/// <summary>

View File

@@ -13,7 +13,8 @@ namespace Robust.Shared.Audio;
/// <seealso cref="ResolvedPathSpecifier"/>
/// <seealso cref="ResolvedCollectionSpecifier"/>
[Serializable, NetSerializable]
public abstract partial class ResolvedSoundSpecifier {
public abstract partial class ResolvedSoundSpecifier
{
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
public static implicit operator ResolvedSoundSpecifier(string s) => new ResolvedPathSpecifier(s);
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
@@ -22,8 +23,10 @@ public abstract partial class ResolvedSoundSpecifier {
/// <summary>
/// Returns whether <c>s</c> is null, or if it contains an empty path/collection ID.
/// </summary>
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s) {
return s switch {
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s)
{
return s switch
{
null => true,
ResolvedPathSpecifier path => path.Path.ToString() == "",
ResolvedCollectionSpecifier collection => string.IsNullOrEmpty(collection.Collection),
@@ -37,7 +40,8 @@ public abstract partial class ResolvedSoundSpecifier {
/// </summary>
/// <seealso cref="ResolvedCollectionSpecifier"/>
[Serializable, NetSerializable]
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedPathSpecifier>
{
/// <summary>
/// The resource path of the sound.
/// </summary>
@@ -57,6 +61,21 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
public ResolvedPathSpecifier(string path) : this(new ResPath(path))
{
}
public bool Equals(ResolvedPathSpecifier? other)
{
return Path.Equals(other?.Path);
}
public override bool Equals(object? obj)
{
return Equals(obj as ResolvedPathSpecifier);
}
public override int GetHashCode()
{
return Path.GetHashCode();
}
}
/// <summary>
@@ -64,7 +83,9 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
/// </summary>
/// <seealso cref="ResolvedPathSpecifier"/>
[Serializable, NetSerializable]
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier {
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedCollectionSpecifier>
{
/// <summary>
/// The ID of the <see cref="SoundCollectionPrototype">sound collection</see> to look up.
/// </summary>
@@ -87,4 +108,19 @@ public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier
Collection = collection;
Index = index;
}
public bool Equals(ResolvedCollectionSpecifier? other)
{
return Collection.Equals(other?.Collection) && Index.Equals(other?.Index);
}
public override bool Equals(object? obj)
{
return Equals(obj as ResolvedCollectionSpecifier);
}
public override int GetHashCode()
{
return HashCode.Combine(Collection, Index);
}
}

View File

@@ -665,7 +665,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams);
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams ?? sound.Params);
}
/// <summary>

View File

@@ -137,9 +137,10 @@ public sealed class TeleportToCommand : LocalizedEntityCommands
}
}
var targetMapCoords = _transform.ToMapCoordinates(targetCoords);
foreach (var victim in victims)
{
_transform.SetCoordinates(victim.Entity, targetCoords);
_transform.SetMapCoordinates(victim.Entity, targetMapCoords);
_transform.AttachToGridOrMap(victim.Entity, victim.Transform);
}
}

View File

@@ -279,16 +279,6 @@ namespace Robust.Shared.Containers
#region Container Helpers
[Obsolete("Use Entity<T> variant")]
public bool TryGetContainingContainer(
EntityUid uid,
[NotNullWhen(true)] out BaseContainer? container,
MetaDataComponent? meta = null,
TransformComponent? transform = null)
{
return TryGetContainingContainer((uid, transform, meta), out container);
}
public bool TryGetContainingContainer(
Entity<TransformComponent?, MetaDataComponent?> ent,
[NotNullWhen(true)] out BaseContainer? container)
@@ -606,7 +596,7 @@ namespace Robust.Shared.Containers
/// <param name="force">Whether to forcibly remove the entity from the container.</param>
/// <param name="wasInContainer">Whether the entity was actually inside a container or not.</param>
/// <returns>If the entity could be removed. Also returns false if it wasn't inside a container.</returns>
public bool TryRemoveFromContainer(EntityUid entity, bool force, out bool wasInContainer)
public bool TryRemoveFromContainer(Entity<TransformComponent?, MetaDataComponent?> entity, bool force, out bool wasInContainer)
{
DebugTools.Assert(Exists(entity));
@@ -631,7 +621,7 @@ namespace Robust.Shared.Containers
/// <param name="entity">Entity that might be inside a container.</param>
/// <param name="force">Whether to forcibly remove the entity from the container.</param>
/// <returns>If the entity could be removed. Also returns false if it wasn't inside a container.</returns>
public bool TryRemoveFromContainer(EntityUid entity, bool force = false)
public bool TryRemoveFromContainer(Entity<TransformComponent?, MetaDataComponent?> entity, bool force = false)
{
return TryRemoveFromContainer(entity, force, out _);
}
@@ -678,9 +668,8 @@ namespace Robust.Shared.Containers
{
// TODO make this check upwards for any container, and parent to that.
// Currently this just checks the direct parent, so entities will still teleport through containers.
if (!transform.Comp.ParentUid.IsValid()
|| !TryGetContainingContainer(transform.Comp.ParentUid, out var container)
|| !TryGetContainingContainer((transform.Comp.ParentUid, Transform(transform.Comp.ParentUid)), out var container)
|| !TryInsertIntoContainer(transform, container))
{
_transform.AttachToGridOrMap(transform, transform.Comp);
@@ -692,8 +681,9 @@ namespace Robust.Shared.Containers
if (Insert((transform.Owner, transform.Comp, null, null), container))
return true;
if (Transform(container.Owner).ParentUid.IsValid()
&& TryGetContainingContainer(container.Owner, out var newContainer))
var ownerXform = Transform(container.Owner);
if (ownerXform.ParentUid.IsValid()
&& TryGetContainingContainer((container.Owner, ownerXform), out var newContainer))
return TryInsertIntoContainer(transform, newContainer);
return false;

View File

@@ -60,9 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
// Sanitise platform-specific path and standardize it for engine use.
.Replace(Path.DirectorySeparatorChar, '/');
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
}
/// <inheritdoc />

View File

@@ -14,7 +14,11 @@ namespace Robust.Shared.ContentPack
/// The directory to use for user data.
/// If null, a virtual temporary file system is used instead.
/// </param>
void Initialize(string? userData);
/// <param name="hideUserDataDir">
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
/// <see cref="IResourceManager.UserData"/>.
/// </param>
void Initialize(string? userData, bool hideUserDataDir);
/// <summary>
/// Mounts a single stream as a content file. Useful for unit testing.

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
{
/// <summary>
/// The root path of this provider.
/// Can be null if it's a virtual provider.
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
/// </summary>
string? RootDir { get; }

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
{
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
!OperatingSystem.IsWindows()
&& !OperatingSystem.IsMacOS();
internal static string SafeGetResourcePath(string baseDir, ResPath path)
{
var relSysPath = path.ToRelativeSystemPath();
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
// better safe than sorry check
if (!retPath.StartsWith(baseDir))
{
// Allow path to match if it's just missing the directory separator at the end.
if (retPath != baseDir.TrimEnd(Path.DirectorySeparatorChar))
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return retPath;
}
}
}

View File

@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
public IWritableDirProvider UserData { get; private set; } = default!;
/// <inheritdoc />
public virtual void Initialize(string? userData)
public virtual void Initialize(string? userData, bool hideRootDir)
{
Sawmill = _logManager.GetSawmill("res");
if (userData != null)
{
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
}
else
{
@@ -379,6 +379,10 @@ namespace Robust.Shared.ContentPack
{
var rootDir = loader.GetPath(new ResPath(@"/"));
// TODO: GET RID OF THIS.
// This code shouldn't be passing OS disk paths through ResPath.
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
yield return new ResPath(rootDir);
}
}

View File

@@ -875,6 +875,7 @@ Types:
- "System.Text.StringBuilder Insert(int, object)"
- "System.Text.StringBuilder Insert(int, sbyte)"
- "System.Text.StringBuilder Insert(int, short)"
- "System.Text.StringBuilder Insert(int, string)"
- "System.Text.StringBuilder Insert(int, string, int)"
- "System.Text.StringBuilder Insert(int, System.Decimal)"
- "System.Text.StringBuilder Insert(int, System.ReadOnlySpan`1<char>)"

View File

@@ -10,17 +10,22 @@ namespace Robust.Shared.ContentPack
/// <inheritdoc />
internal sealed class WritableDirProvider : IWritableDirProvider
{
/// <inheritdoc />
private readonly bool _hideRootDir;
public string RootDir { get; }
string? IWritableDirProvider.RootDir => _hideRootDir ? null : RootDir;
/// <summary>
/// Constructs an instance of <see cref="WritableDirProvider"/>.
/// </summary>
/// <param name="rootDir">Root file system directory to allow writing.</param>
public WritableDirProvider(DirectoryInfo rootDir)
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
{
// FullName does not have a trailing separator, and we MUST have a separator.
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
_hideRootDir = hideRootDir;
}
#region File Access
@@ -119,7 +124,7 @@ namespace Robust.Shared.ContentPack
throw new FileNotFoundException();
var dirInfo = new DirectoryInfo(GetFullPath(path));
return new WritableDirProvider(dirInfo);
return new WritableDirProvider(dirInfo, _hideRootDir);
}
/// <inheritdoc />
@@ -180,20 +185,7 @@ namespace Robust.Shared.ContentPack
path = path.Clean();
return GetFullPath(RootDir, path);
}
private static string GetFullPath(string root, ResPath path)
{
var relPath = path.ToRelativeSystemPath();
if (relPath.Contains("\\..") || relPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return Path.GetFullPath(Path.Combine(root, relPath));
return PathHelpers.SafeGetResourcePath(RootDir, path);
}
}
}

View File

@@ -547,19 +547,20 @@ public sealed class EntityDeserializer :
_stopwatch.Restart();
foreach (var (entity, data) in Entities)
{
#if EXCEPTION_TOLERANCE
try
{
#endif
CurrentReadingEntity = data;
LoadEntity(entity, _metaQuery.Comp(entity), data.Components, data.MissingComponents);
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
{
#if !EXCEPTION_TOLERANCE
throw;
#endif
ToDelete.Add(entity);
_log.Error($"Encountered error while loading entity. Yaml uid: {data.YamlId}. Loaded loaded entity: {EntMan.ToPrettyString(entity)}. Error:\n{e}.");
}
#endif
}
CurrentReadingEntity = null;

View File

@@ -162,8 +162,8 @@ public sealed class EntitySerializer : ISerializationContext,
_log = _logMan.GetSawmill("entity_serializer");
SerializerProvider.RegisterSerializer(this);
_metaName = _factory.GetComponentName(typeof(MetaDataComponent));
_xformName = _factory.GetComponentName(typeof(TransformComponent));
_metaName = _factory.GetComponentName<MetaDataComponent>();
_xformName = _factory.GetComponentName<TransformComponent>();
_emptyMetaNode = _serialization.WriteValueAs<MappingDataNode>(typeof(MetaDataComponent), new MetaDataComponent(), alwaysWrite: true, context: this);
CurrentComponent = _xformName;

View File

@@ -273,11 +273,11 @@ namespace Robust.Shared.GameObjects
public IComponent GetComponent(Type componentType)
{
if (!_types.ContainsKey(componentType))
if (!_types.TryGetValue(componentType, out var value))
{
throw new InvalidOperationException($"{componentType} is not a registered component.");
}
return _typeFactory.CreateInstanceUnchecked<IComponent>(_types[componentType].Type);
return _typeFactory.CreateInstanceUnchecked<IComponent>(value.Type);
}
public IComponent GetComponent(CompIdx componentType)
@@ -287,11 +287,11 @@ namespace Robust.Shared.GameObjects
public T GetComponent<T>() where T : IComponent, new()
{
if (!_types.ContainsKey(typeof(T)))
if (!_types.TryGetValue(typeof(T), out var reg))
{
throw new InvalidOperationException($"{typeof(T)} is not a registered component.");
}
return _typeFactory.CreateInstanceUnchecked<T>(_types[typeof(T)].Type);
return _typeFactory.CreateInstanceUnchecked<T>(reg.Type);
}
public IComponent GetComponent(ComponentRegistration reg)

View File

@@ -633,7 +633,7 @@ namespace Robust.Shared.GameObjects
/// An invalid entity UID indicates that this entity has intentionally been removed from broadphases and should
/// not automatically be re-added by movement events.
/// </remarks>
internal record struct BroadphaseData(EntityUid Uid, EntityUid PhysicsMap, bool CanCollide, bool Static)
internal record struct BroadphaseData(EntityUid Uid, bool CanCollide, bool Static)
{
public bool IsValid() => Uid.IsValid();
public bool Valid => IsValid();

View File

@@ -11,7 +11,7 @@ public record struct Entity<T> : IFluentEntityUid, IAsType<EntityUid>
{
public EntityUid Owner;
public T Comp;
EntityUid IFluentEntityUid.FluentOwner => Owner;
readonly EntityUid IFluentEntityUid.FluentOwner => Owner;
public Entity(EntityUid owner, T comp)
{
@@ -48,9 +48,9 @@ public record struct Entity<T> : IFluentEntityUid, IAsType<EntityUid>
}
public override int GetHashCode() => Owner.GetHashCode();
public Entity<T?> AsNullable() => new(Owner, Comp);
public EntityUid AsType() => Owner;
public override readonly int GetHashCode() => Owner.GetHashCode();
public readonly Entity<T?> AsNullable() => new(Owner, Comp);
public readonly EntityUid AsType() => Owner;
}
[NotYamlSerializable]
@@ -60,7 +60,7 @@ public record struct Entity<T1, T2> : IFluentEntityUid, IAsType<EntityUid>
public EntityUid Owner;
public T1 Comp1;
public T2 Comp2;
EntityUid IFluentEntityUid.FluentOwner => Owner;
readonly EntityUid IFluentEntityUid.FluentOwner => Owner;
public Entity(EntityUid owner, T1 comp1, T2 comp2)
{
@@ -119,9 +119,9 @@ public record struct Entity<T1, T2> : IFluentEntityUid, IAsType<EntityUid>
return new Entity<T1>(ent.Owner, ent.Comp1);
}
public override int GetHashCode() => Owner.GetHashCode();
public Entity<T1?, T2?> AsNullable() => new(Owner, Comp1, Comp2);
public EntityUid AsType() => Owner;
public override readonly int GetHashCode() => Owner.GetHashCode();
public readonly Entity<T1?, T2?> AsNullable() => new(Owner, Comp1, Comp2);
public readonly EntityUid AsType() => Owner;
}
[NotYamlSerializable]
@@ -132,7 +132,7 @@ public record struct Entity<T1, T2, T3> : IFluentEntityUid, IAsType<EntityUid>
public T1 Comp1;
public T2 Comp2;
public T3 Comp3;
EntityUid IFluentEntityUid.FluentOwner => Owner;
readonly EntityUid IFluentEntityUid.FluentOwner => Owner;
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3)
{
@@ -226,9 +226,9 @@ public record struct Entity<T1, T2, T3> : IFluentEntityUid, IAsType<EntityUid>
#endregion
public override int GetHashCode() => Owner.GetHashCode();
public Entity<T1?, T2?, T3?> AsNullable() => new(Owner, Comp1, Comp2, Comp3);
public EntityUid AsType() => Owner;
public override readonly int GetHashCode() => Owner.GetHashCode();
public readonly Entity<T1?, T2?, T3?> AsNullable() => new(Owner, Comp1, Comp2, Comp3);
public readonly EntityUid AsType() => Owner;
}
[NotYamlSerializable]
@@ -240,7 +240,7 @@ public record struct Entity<T1, T2, T3, T4> : IFluentEntityUid, IAsType<EntityUi
public T2 Comp2;
public T3 Comp3;
public T4 Comp4;
EntityUid IFluentEntityUid.FluentOwner => Owner;
readonly EntityUid IFluentEntityUid.FluentOwner => Owner;
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4)
{
@@ -357,9 +357,9 @@ public record struct Entity<T1, T2, T3, T4> : IFluentEntityUid, IAsType<EntityUi
#endregion
public override int GetHashCode() => Owner.GetHashCode();
public Entity<T1?, T2?, T3?, T4?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4);
public EntityUid AsType() => Owner;
public override readonly int GetHashCode() => Owner.GetHashCode();
public readonly Entity<T1?, T2?, T3?, T4?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4);
public readonly EntityUid AsType() => Owner;
}
[NotYamlSerializable]
@@ -372,7 +372,7 @@ public record struct Entity<T1, T2, T3, T4, T5> : IFluentEntityUid, IAsType<Enti
public T3 Comp3;
public T4 Comp4;
public T5 Comp5;
EntityUid IFluentEntityUid.FluentOwner => Owner;
readonly EntityUid IFluentEntityUid.FluentOwner => Owner;
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5)
{
@@ -512,9 +512,9 @@ public record struct Entity<T1, T2, T3, T4, T5> : IFluentEntityUid, IAsType<Enti
#endregion
public override int GetHashCode() => Owner.GetHashCode();
public Entity<T1?, T2?, T3?, T4?, T5?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5);
public EntityUid AsType() => Owner;
public override readonly int GetHashCode() => Owner.GetHashCode();
public readonly Entity<T1?, T2?, T3?, T4?, T5?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5);
public readonly EntityUid AsType() => Owner;
}
[NotYamlSerializable]
@@ -528,7 +528,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6> : IFluentEntityUid, IAsType<
public T4 Comp4;
public T5 Comp5;
public T6 Comp6;
EntityUid IFluentEntityUid.FluentOwner => Owner;
readonly EntityUid IFluentEntityUid.FluentOwner => Owner;
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5, T6 comp6)
{
@@ -691,9 +691,9 @@ public record struct Entity<T1, T2, T3, T4, T5, T6> : IFluentEntityUid, IAsType<
#endregion
public override int GetHashCode() => Owner.GetHashCode();
public Entity<T1?, T2?, T3?, T4?, T5?, T6?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6);
public EntityUid AsType() => Owner;
public override readonly int GetHashCode() => Owner.GetHashCode();
public readonly Entity<T1?, T2?, T3?, T4?, T5?, T6?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6);
public readonly EntityUid AsType() => Owner;
}
[NotYamlSerializable]
@@ -708,7 +708,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6, T7> : IFluentEntityUid, IAsT
public T5 Comp5;
public T6 Comp6;
public T7 Comp7;
EntityUid IFluentEntityUid.FluentOwner => Owner;
readonly EntityUid IFluentEntityUid.FluentOwner => Owner;
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5, T6 comp6, T7 comp7)
{
@@ -894,9 +894,9 @@ public record struct Entity<T1, T2, T3, T4, T5, T6, T7> : IFluentEntityUid, IAsT
#endregion
public override int GetHashCode() => Owner.GetHashCode();
public Entity<T1?, T2?, T3?, T4?, T5?, T6?, T7?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6, Comp7);
public EntityUid AsType() => Owner;
public override readonly int GetHashCode() => Owner.GetHashCode();
public readonly Entity<T1?, T2?, T3?, T4?, T5?, T6?, T7?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6, Comp7);
public readonly EntityUid AsType() => Owner;
}
[NotYamlSerializable]
@@ -912,7 +912,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6, T7, T8> : IFluentEntityUid,
public T6 Comp6;
public T7 Comp7;
public T8 Comp8;
EntityUid IFluentEntityUid.FluentOwner => Owner;
readonly EntityUid IFluentEntityUid.FluentOwner => Owner;
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5, T6 comp6, T7 comp7, T8 comp8)
{
@@ -1121,7 +1121,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6, T7, T8> : IFluentEntityUid,
#endregion
public override int GetHashCode() => Owner.GetHashCode();
public Entity<T1?, T2?, T3?, T4?, T5?, T6?, T7?, T8?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6, Comp7, Comp8);
public EntityUid AsType() => Owner;
public override readonly int GetHashCode() => Owner.GetHashCode();
public readonly Entity<T1?, T2?, T3?, T4?, T5?, T6?, T7?, T8?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6, Comp7, Comp8);
public readonly EntityUid AsType() => Owner;
}

View File

@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
@@ -52,6 +53,16 @@ public partial class EntityManager
return ents;
}
public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params EntProtoId[] protoNames)
{
var ents = new EntityUid[protoNames.Length];
for (var i = 0; i < protoNames.Length; i++)
{
ents[i] = SpawnAttachedTo(protoNames[i], coordinates);
}
return ents;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, List<string?> protoNames)
{
@@ -74,6 +85,19 @@ public partial class EntityManager
return ents;
}
public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, IEnumerable<EntProtoId> protoNames)
{
var ents = new ValueList<EntityUid>();
foreach (var protoName in protoNames)
{
var uid = SpawnAttachedTo(protoName, coordinates);
ents.Add(uid);
}
return ents.ToArray();
}
public virtual EntityUid SpawnAttachedTo(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
{
if (!coordinates.IsValid(this))

View File

@@ -161,7 +161,7 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Returns true if the entity's data (apart from transform) is default.
/// </summary>
public bool IsDefault(EntityUid uid)
public bool IsDefault(EntityUid uid, ICollection<string>? ignoredComps = null)
{
if (!MetaQuery.TryGetComponent(uid, out var metadata) || metadata.EntityPrototype == null)
return false;
@@ -195,6 +195,9 @@ namespace Robust.Shared.GameObjects
var compName = _componentFactory.GetComponentName(compType);
if (ignoredComps?.Contains(compName) == true)
continue;
// If the component isn't on the prototype then it's custom.
if (!protoData.TryGetValue(compName, out var protoMapping))
return false;
@@ -498,7 +501,7 @@ namespace Robust.Shared.GameObjects
if (Deleted(uid.Value))
return false;
if (!QueuedDeletionsSet.Add(uid.Value))
if (QueuedDeletionsSet.Contains(uid.Value))
return false;
QueueDeleteEntity(uid);

View File

@@ -29,6 +29,8 @@ namespace Robust.Shared.GameObjects
[Dependency] private readonly IReplayRecordingManager _replayMan = default!;
[Dependency] protected readonly ILocalizationManager Loc = default!;
protected IComponentFactory Factory => EntityManager.ComponentFactory;
public ISawmill Log { get; private set; } = default!;
protected virtual string SawmillName

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -22,6 +23,8 @@ public partial interface IEntityManager
EntityUid[] SpawnEntities(MapCoordinates coordinates, params string?[] protoNames);
EntityUid[] SpawnEntities(MapCoordinates coordinates, string? prototype, int count);
EntityUid[] SpawnEntities(MapCoordinates coordinates, List<string?> protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, IEnumerable<EntProtoId> protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params EntProtoId[] protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, List<string?> protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params string?[] protoNames);

View File

@@ -89,7 +89,6 @@ public sealed partial class EntityLookupSystem : EntitySystem
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<PhysicsMapComponent> _physMapQuery;
private EntityQuery<TransformComponent> _xformQuery;
/// <summary>
@@ -119,7 +118,6 @@ public sealed partial class EntityLookupSystem : EntitySystem
_gridQuery = GetEntityQuery<MapGridComponent>();
_metaQuery = GetEntityQuery<MetaDataComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_physMapQuery = GetEntityQuery<PhysicsMapComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<BroadphaseComponent, EntityTerminatingEvent>(OnBroadphaseTerminating);
@@ -153,15 +151,12 @@ public sealed partial class EntityLookupSystem : EntitySystem
private void OnBroadphaseTerminating(EntityUid uid, BroadphaseComponent component, ref EntityTerminatingEvent args)
{
var xform = _xformQuery.GetComponent(uid);
var map = xform.MapUid;
_physMapQuery.TryGetComponent(map, out var physMap);
RemoveChildrenFromTerminatingBroadphase(xform, component, physMap);
RemoveChildrenFromTerminatingBroadphase(xform, component);
RemComp(uid, component);
}
private void RemoveChildrenFromTerminatingBroadphase(TransformComponent xform,
BroadphaseComponent component,
PhysicsMapComponent? map)
BroadphaseComponent component)
{
foreach (var child in xform._children)
{
@@ -179,19 +174,15 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (childXform.Broadphase.Value.CanCollide && _fixturesQuery.TryGetComponent(child, out var fixtures))
{
if (map == null)
_physMapQuery.TryGetComponent(childXform.Broadphase.Value.PhysicsMap, out map);
DebugTools.Assert(map == null || childXform.Broadphase.Value.PhysicsMap == map.Owner);
var tree = childXform.Broadphase.Value.Static ? component.StaticTree : component.DynamicTree;
foreach (var fixture in fixtures.Fixtures.Values)
{
DestroyProxies(fixture, tree, map);
DestroyProxies(fixture, tree);
}
}
childXform.Broadphase = null;
RemoveChildrenFromTerminatingBroadphase(childXform, component, map);
RemoveChildrenFromTerminatingBroadphase(childXform, component);
}
}
@@ -227,26 +218,18 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (xform.MapUid == null)
return;
if (!_physMapQuery.TryGetComponent(xform.MapUid, out var physMap))
{
throw new InvalidOperationException(
$"Broadphase's map is missing a physics map comp. Broadphase: {ToPrettyString(broadphase.Owner)}");
}
var ent = new Entity<TransformComponent, BroadphaseComponent>(broadphase, xform, broadphase);
var map = new Entity<PhysicsMapComponent>(xform.MapUid.Value, physMap);
var enumerator = xform.ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
if (!_broadQuery.HasComp(child))
InitializeChild(child, ent, map);
InitializeChild(child, ent);
}
}
private void InitializeChild(
EntityUid child,
Entity<TransformComponent, BroadphaseComponent> broadphase,
Entity<PhysicsMapComponent> map)
Entity<TransformComponent, BroadphaseComponent> broadphase)
{
if (LifeStage(child) <= EntityLifeStage.PreInit)
return;
@@ -258,7 +241,6 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (!xform.Broadphase.Value.IsValid())
return; // Entity is intentionally not on a broadphase (deferred updating?).
_physMapQuery.TryGetComponent(xform.Broadphase.Value.PhysicsMap, out var oldPhysMap);
if (!_broadQuery.TryGetComponent(xform.Broadphase.Value.Uid, out var oldBroadphase))
{
DebugTools.Assert("Encountered deleted broadphase.");
@@ -275,16 +257,15 @@ public sealed partial class EntityLookupSystem : EntitySystem
}
else if (oldBroadphase != broadphase.Comp2)
{
RemoveFromEntityTree(xform.Broadphase.Value.Uid, oldBroadphase, ref oldPhysMap, child, xform);
RemoveFromEntityTree(xform.Broadphase.Value.Uid, oldBroadphase,child, xform);
}
}
DebugTools.Assert(xform.Broadphase is not {} x || x.Uid == broadphase.Owner && (!x.CanCollide || x.PhysicsMap == map.Owner));
DebugTools.Assert(xform.Broadphase is not {} x || x.Uid == broadphase.Owner && !x.CanCollide);
AddOrUpdateEntityTree(
broadphase.Owner,
broadphase.Comp2,
broadphase.Comp1,
map.Comp,
child,
xform);
}
@@ -315,9 +296,6 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (!TryGetCurrentBroadphase(xform, out var broadphase))
return;
if (!_physMapQuery.TryGetComponent(xform.MapUid, out var physMap))
throw new InvalidOperationException();
var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform);
var mapTransform = new Transform(worldPos, worldRot);
@@ -326,10 +304,10 @@ public sealed partial class EntityLookupSystem : EntitySystem
var tree = body.BodyType == BodyType.Static ? broadphase.StaticTree : broadphase.DynamicTree;
DebugTools.Assert(fixture.ProxyCount == 0);
AddOrMoveProxies(uid, fixtureId, fixture, body, tree, broadphaseTransform, mapTransform, physMap.MoveBuffer);
AddOrMoveProxies((uid, body, xform), fixtureId, fixture, tree, broadphaseTransform);
}
internal void DestroyProxies(EntityUid uid, string fixtureId, Fixture fixture, TransformComponent xform, BroadphaseComponent broadphase, PhysicsMapComponent? physicsMap)
internal void DestroyProxies(EntityUid uid, string fixtureId, Fixture fixture, TransformComponent xform, BroadphaseComponent broadphase)
{
DebugTools.AssertNotNull(xform.Broadphase);
DebugTools.Assert(xform.Broadphase!.Value.Uid == broadphase.Owner);
@@ -344,7 +322,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
}
var tree = xform.Broadphase.Value.Static ? broadphase.StaticTree : broadphase.DynamicTree;
DestroyProxies(fixture, tree, physicsMap);
DestroyProxies(fixture, tree);
}
#endregion
@@ -395,8 +373,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
var fixtures = Comp<FixturesComponent>(uid);
if (old.CanCollide)
{
_physMapQuery.TryGetComponent(old.PhysicsMap, out var physicsMap);
RemoveBroadTree(broadphase, fixtures, old.Static, physicsMap);
RemoveBroadTree(broadphase, fixtures, old.Static);
}
else
(old.Static ? broadphase.StaticSundriesTree : broadphase.SundriesTree).Remove(uid);
@@ -408,23 +385,23 @@ public sealed partial class EntityLookupSystem : EntitySystem
AddOrUpdateSundriesTree(old.Uid, broadphase, uid, xform, body.BodyType == BodyType.Static);
}
private void RemoveBroadTree(BroadphaseComponent lookup, FixturesComponent manager, bool staticBody, PhysicsMapComponent? map)
private void RemoveBroadTree(BroadphaseComponent lookup, FixturesComponent manager, bool staticBody)
{
var tree = staticBody ? lookup.StaticTree : lookup.DynamicTree;
foreach (var fixture in manager.Fixtures.Values)
{
DestroyProxies(fixture, tree, map);
DestroyProxies(fixture, tree);
}
}
internal void DestroyProxies(Fixture fixture, IBroadPhase tree, PhysicsMapComponent? map)
internal void DestroyProxies(Fixture fixture, IBroadPhase tree)
{
var buffer = map?.MoveBuffer;
var buffer = _physics.MoveBuffer;
for (var i = 0; i < fixture.ProxyCount; i++)
{
var proxy = fixture.Proxies[i];
tree.RemoveProxy(proxy.ProxyId);
buffer?.Remove(proxy);
buffer.Remove(proxy);
}
fixture.ProxyCount = 0;
@@ -438,10 +415,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (broadphaseXform.MapID == MapId.Nullspace)
return;
if (!_physMapQuery.TryGetComponent(broadphaseXform.MapUid, out var physMap))
throw new InvalidOperationException($"Physics Broadphase is missing physics map. {ToPrettyString(broadUid)}");
AddOrUpdatePhysicsTree(uid, broadUid, broadphase, broadphaseXform, physMap, xform, body, fixtures);
AddOrUpdatePhysicsTree(uid, broadUid, broadphase, broadphaseXform, xform, body, fixtures);
}
private void AddOrUpdatePhysicsTree(
@@ -449,16 +423,15 @@ public sealed partial class EntityLookupSystem : EntitySystem
EntityUid broadUid,
BroadphaseComponent broadphase,
TransformComponent broadphaseXform,
PhysicsMapComponent physicsMap,
TransformComponent xform,
PhysicsComponent body,
FixturesComponent manager)
{
DebugTools.Assert(!_container.IsEntityOrParentInContainer(body.Owner, null, xform));
DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadphase.Owner, physicsMap.Owner, body.CanCollide, body.BodyType == BodyType.Static));
DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadphase.Owner, body.CanCollide, body.BodyType == BodyType.Static));
DebugTools.Assert(broadphase.Owner == broadUid);
xform.Broadphase ??= new(broadUid, physicsMap.Owner, body.CanCollide, body.BodyType == BodyType.Static);
xform.Broadphase ??= new(broadUid, body.CanCollide, body.BodyType == BodyType.Static);
var tree = body.BodyType == BodyType.Static ? broadphase.StaticTree : broadphase.DynamicTree;
// TOOD optimize this. This function iterates UP through parents, while we are currently iterating down.
@@ -470,20 +443,19 @@ public sealed partial class EntityLookupSystem : EntitySystem
foreach (var (id, fixture) in manager.Fixtures)
{
AddOrMoveProxies(uid, id, fixture, body, tree, broadphaseTransform, mapTransform, physicsMap.MoveBuffer);
AddOrMoveProxies((uid, body, xform), id, fixture, tree, broadphaseTransform);
}
}
private void AddOrMoveProxies(
EntityUid uid,
Entity<PhysicsComponent, TransformComponent> ent,
string fixtureId,
Fixture fixture,
PhysicsComponent body,
IBroadPhase tree,
Transform broadphaseTransform,
Transform mapTransform,
Dictionary<FixtureProxy, Box2> moveBuffer)
Transform broadphaseTransform)
{
var moveBuffer = _physics.MoveBuffer;
// Moving
if (fixture.ProxyCount > 0)
{
@@ -493,7 +465,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
var proxy = fixture.Proxies[i];
tree.MoveProxy(proxy.ProxyId, bounds);
proxy.AABB = bounds;
moveBuffer[proxy] = fixture.Shape.ComputeAABB(mapTransform, i);
moveBuffer.Add(proxy);
}
return;
@@ -505,11 +477,11 @@ public sealed partial class EntityLookupSystem : EntitySystem
for (var i = 0; i < count; i++)
{
var bounds = fixture.Shape.ComputeAABB(broadphaseTransform, i);
var proxy = new FixtureProxy(uid, body, bounds, fixtureId, fixture, i);
var proxy = new FixtureProxy(ent.Owner, ent.Comp1, ent.Comp2, bounds, fixtureId, fixture, i);
proxy.ProxyId = tree.AddProxy(ref proxy);
proxy.AABB = bounds;
proxies[i] = proxy;
moveBuffer[proxy] = fixture.Shape.ComputeAABB(mapTransform, i);
moveBuffer.Add(proxy);
}
fixture.Proxies = proxies;
@@ -519,8 +491,8 @@ public sealed partial class EntityLookupSystem : EntitySystem
private void AddOrUpdateSundriesTree(EntityUid broadUid, BroadphaseComponent broadphase, EntityUid uid, TransformComponent xform, bool staticBody, Box2? aabb = null)
{
DebugTools.Assert(!_container.IsEntityOrParentInContainer(uid));
DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadUid, default, false, staticBody));
xform.Broadphase ??= new(broadUid, default, false, staticBody);
DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadUid, false, staticBody));
xform.Broadphase ??= new(broadUid, false, staticBody);
(staticBody ? broadphase.StaticSundriesTree : broadphase.SundriesTree).AddOrUpdate(uid, aabb);
}
@@ -538,8 +510,12 @@ public sealed partial class EntityLookupSystem : EntitySystem
{
if (args.Component.GridUid == args.Sender)
{
if (args.ParentChanged) // grid changed maps, need to update children and clear the move buffer.
// If grid changes map MoveBuffer will have incorrect worldpositions for all children.
if (args.ParentChanged)
{
OnGridChangedMap(args);
}
return;
}
DebugTools.Assert(!_gridQuery.HasComp(args.Sender));
@@ -563,84 +539,21 @@ public sealed partial class EntityLookupSystem : EntitySystem
return;
// We need to recursively update the cached data and remove children from the move buffer
DebugTools.Assert(_gridQuery.HasComp(args.Sender));
DebugTools.Assert(!newMap.IsValid() || _mapQuery.HasComp(newMap));
DebugTools.Assert(!oldMap.IsValid() || _mapQuery.HasComp(oldMap));
var oldBuffer = _physMapQuery.CompOrNull(oldMap)?.MoveBuffer;
var newBuffer = _physMapQuery.CompOrNull(newMap)?.MoveBuffer;
foreach (var child in args.Component._children)
{
RecursiveOnGridChangedMap(child, oldMap, newMap, oldBuffer, newBuffer);
}
}
private void RecursiveOnGridChangedMap(
EntityUid uid,
EntityUid oldMap,
EntityUid newMap,
Dictionary<FixtureProxy, Box2>? oldBuffer,
Dictionary<FixtureProxy, Box2>? newBuffer)
{
if (!_xformQuery.TryGetComponent(uid, out var xform))
return;
foreach (var child in xform._children)
{
RecursiveOnGridChangedMap(child, oldMap, newMap, oldBuffer, newBuffer);
}
if (xform.Broadphase == null || !xform.Broadphase.Value.CanCollide)
return;
DebugTools.Assert(_netMan.IsClient || !xform.Broadphase.Value.PhysicsMap.IsValid() || xform.Broadphase.Value.PhysicsMap == oldMap);
xform.Broadphase = xform.Broadphase.Value with { PhysicsMap = newMap };
if (!_fixturesQuery.TryGetComponent(uid, out var fixtures))
return;
if (oldBuffer != null)
{
foreach (var fix in fixtures.Fixtures.Values)
foreach (var prox in fix.Proxies)
{
oldBuffer.Remove(prox);
}
}
if (newBuffer == null)
return;
// TODO PERFORMANCE
// track world position while recursively iterating down through children.
var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform);
var mapTransform = new Transform(worldPos, worldRot);
foreach (var fixture in fixtures.Fixtures.Values)
{
for (var i = 0; i < fixture.ProxyCount; i++)
{
var proxy = fixture.Proxies[i];
newBuffer[proxy] = fixture.Shape.ComputeAABB(mapTransform, i);
}
}
DebugTools.Assert(HasComp<MapGridComponent>(args.Sender));
DebugTools.Assert(!newMap.IsValid() || HasComp<MapComponent>(newMap));
DebugTools.Assert(!oldMap.IsValid() || HasComp<MapComponent>(oldMap));
}
private void UpdateParent(EntityUid uid, TransformComponent xform)
{
BroadphaseComponent? oldBroadphase = null;
PhysicsMapComponent? oldPhysMap = null;
if (xform.Broadphase != null)
{
if (!xform.Broadphase.Value.IsValid())
return; // Entity is intentionally not on a broadphase (deferred updating?).
_physMapQuery.TryGetComponent(xform.Broadphase.Value.PhysicsMap, out oldPhysMap);
if (!_broadQuery.TryGetComponent(xform.Broadphase.Value.Uid, out oldBroadphase))
{
DebugTools.Assert("Encountered deleted broadphase.");
// broadphase was probably deleted.
@@ -661,24 +574,18 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (oldBroadphase != null && oldBroadphase != newBroadphase)
{
RemoveFromEntityTree(oldBroadphase.Owner, oldBroadphase, ref oldPhysMap, uid, xform);
RemoveFromEntityTree(oldBroadphase.Owner, oldBroadphase, uid, xform);
}
if (newBroadphase == null)
return;
var newBroadphaseXform = _xformQuery.GetComponent(newBroadphase.Owner);
if (!_physMapQuery.TryGetComponent(newBroadphaseXform.MapUid, out var physMap))
{
throw new InvalidOperationException(
$"Broadphase's map is missing a physics map comp. Broadphase: {ToPrettyString(newBroadphase.Owner)}");
}
AddOrUpdateEntityTree(
newBroadphase.Owner,
newBroadphase,
newBroadphaseXform,
physMap,
uid,
xform);
}
@@ -713,17 +620,11 @@ public sealed partial class EntityLookupSystem : EntitySystem
bool recursive = true)
{
var broadphaseXform = _xformQuery.GetComponent(broadphase.Owner);
if (!_physMapQuery.TryGetComponent(broadphaseXform.MapUid, out var physMap))
{
throw new InvalidOperationException(
$"Broadphase's map is missing a physics map comp. Broadphase: {ToPrettyString(broadphase.Owner)}");
}
AddOrUpdateEntityTree(
broadUid,
broadphase,
broadphaseXform,
physMap,
uid,
xform,
recursive);
@@ -733,7 +634,6 @@ public sealed partial class EntityLookupSystem : EntitySystem
EntityUid broadUid,
BroadphaseComponent broadphase,
TransformComponent broadphaseXform,
PhysicsMapComponent physicsMap,
EntityUid uid,
TransformComponent xform,
bool recursive = true)
@@ -758,7 +658,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
}
else
{
AddOrUpdatePhysicsTree(uid, broadUid, broadphase, broadphaseXform, physicsMap, xform, body, _fixturesQuery.GetComponent(uid));
AddOrUpdatePhysicsTree(uid, broadUid, broadphase, broadphaseXform, xform, body, _fixturesQuery.GetComponent(uid));
}
if (xform.ChildCount == 0 || !recursive)
@@ -771,7 +671,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
foreach (var child in xform._children)
{
var childXform = _xformQuery.GetComponent(child);
AddOrUpdateEntityTree(broadUid, broadphase, broadphaseXform, physicsMap, child, childXform, recursive);
AddOrUpdateEntityTree(broadUid, broadphase, broadphaseXform, child, childXform, recursive);
}
return;
}
@@ -782,7 +682,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
continue;
var childXform = _xformQuery.GetComponent(child);
AddOrUpdateEntityTree(broadUid, broadphase, broadphaseXform, physicsMap, child, childXform, recursive);
AddOrUpdateEntityTree(broadUid, broadphase, broadphaseXform, child, childXform, recursive);
}
}
@@ -794,16 +694,10 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (!TryGetCurrentBroadphase(xform, out var broadphase))
return;
DebugTools.Assert(!_gridQuery.HasComp(uid));
DebugTools.Assert(!_mapQuery.HasComp(uid));
PhysicsMapComponent? physMap = null;
if (xform.Broadphase!.Value.PhysicsMap is { Valid: true } map && !_physMapQuery.TryGetComponent(map, out physMap))
{
throw new InvalidOperationException(
$"Broadphase's map is missing a physics map comp. Broadphase: {ToPrettyString(broadphase.Owner)}");
}
DebugTools.Assert(!HasComp<MapGridComponent>(uid));
DebugTools.Assert(!HasComp<MapComponent>(uid));
RemoveFromEntityTree(broadphase.Owner, broadphase, ref physMap, uid, xform);
RemoveFromEntityTree(broadphase.Owner, broadphase, uid, xform);
}
/// <summary>
@@ -812,7 +706,6 @@ public sealed partial class EntityLookupSystem : EntitySystem
private void RemoveFromEntityTree(
EntityUid broadUid,
BroadphaseComponent broadphase,
ref PhysicsMapComponent? physicsMap,
EntityUid uid,
TransformComponent xform,
bool recursive = true)
@@ -833,16 +726,9 @@ public sealed partial class EntityLookupSystem : EntitySystem
broadUid = old.Uid;
}
if (old.PhysicsMap.IsValid() && physicsMap?.Owner != old.PhysicsMap)
{
if (!_physMapQuery.TryGetComponent(old.PhysicsMap, out physicsMap))
Log.Error($"Entity {ToPrettyString(uid)} has missing physics map?");
}
if (old.CanCollide)
{
DebugTools.Assert(old.PhysicsMap == (physicsMap?.Owner ?? default));
RemoveBroadTree(broadphase, _fixturesQuery.GetComponent(uid), old.Static, physicsMap);
RemoveBroadTree(broadphase, _fixturesQuery.GetComponent(uid), old.Static);
}
else if (old.Static)
broadphase.StaticSundriesTree.Remove(uid);
@@ -858,7 +744,6 @@ public sealed partial class EntityLookupSystem : EntitySystem
RemoveFromEntityTree(
broadUid,
broadphase,
ref physicsMap,
child,
_xformQuery.GetComponent(child));
}

View File

@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -165,10 +166,7 @@ public abstract partial class SharedMapSystem
gridTree.Tree.MoveProxy(component.MapProxy, in aabb);
}
if (TryComp<MovedGridsComponent>(xform.MapUid, out var movedGrids))
{
movedGrids.MovedGrids.Add(uid);
}
_physics.MovedGrids.Add(uid);
}
private void OnGridMove(EntityUid uid, MapGridComponent component, ref MoveEvent args)
@@ -190,10 +188,7 @@ public abstract partial class SharedMapSystem
gridTree.Tree.MoveProxy(component.MapProxy, in aabb);
}
if (TryComp<MovedGridsComponent>(xform.MapUid, out var movedGrids))
{
movedGrids.MovedGrids.Add(uid);
}
_physics.MovedGrids.Add(uid);
}
private void OnParentChange(EntityUid uid, MapGridComponent component, ref MoveEvent args)
@@ -218,23 +213,23 @@ public abstract partial class SharedMapSystem
if (xform.ParentUid != xform.MapUid && meta.EntityLifeStage < EntityLifeStage.Terminating && _netManager.IsServer)
{
Log.Error($"Grid {ToPrettyString(uid, meta)} is not parented to {ToPrettyString(xform._parent)} which is not a map. y'all need jesus. {Environment.StackTrace}");
Log.Error($"Grid {ToPrettyString(uid, meta)} is parented to {ToPrettyString(xform._parent)} which is not a map. y'all need jesus. {Environment.StackTrace}");
return;
}
// Make sure we cleanup old map for moved grid stuff.
var oldMap = _transform.ToMapCoordinates(args.OldPosition);
var oldMapUid = GetMapOrInvalid(oldMap.MapId);
if (component.MapProxy != DynamicTree.Proxy.Free && TryComp<MovedGridsComponent>(oldMapUid, out var oldMovedGrids))
if (component.MapProxy != DynamicTree.Proxy.Free)
{
oldMovedGrids.MovedGrids.Remove(uid);
_physics.MovedGrids.Remove(uid);
RemoveGrid(uid, component, oldMapUid);
}
DebugTools.Assert(component.MapProxy == DynamicTree.Proxy.Free);
if (TryComp<MovedGridsComponent>(xform.MapUid, out var newMovedGrids))
if (xform.MapUid != null)
{
newMovedGrids.MovedGrids.Add(uid);
_physics.MovedGrids.Add(uid);
AddGrid(uid, component);
}
}
@@ -343,6 +338,8 @@ public abstract partial class SharedMapSystem
chunk.SuppressCollisionRegeneration = true;
DebugTools.Assert(data.TileData.Any(x => !x.IsEmpty));
DebugTools.Assert(data.TileData.Length == component.ChunkSize * component.ChunkSize);
var changedEntry = new ValueList<TileChangedEntry>();
for (ushort x = 0; x < component.ChunkSize; x++)
{
for (ushort y = 0; y < component.ChunkSize; y++)
@@ -351,12 +348,15 @@ public abstract partial class SharedMapSystem
if (!chunk.TrySetTile(x, y, tile, out var oldTile, out _))
continue;
var gridIndices = chunk.ChunkTileToGridTile((x, y));
var newTileRef = new TileRef(uid, gridIndices, tile);
_mapInternal.RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index);
var chunkIndex = new Vector2i(x, y);
var gridIndices = chunk.ChunkTileToGridTile(chunkIndex);
changedEntry.Add(new TileChangedEntry(tile, oldTile, chunk.Indices, gridIndices));
}
}
var ev = new TileChangedEvent(gridEnt, changedEntry.ToArray());
EntityManager.EventBus.RaiseLocalEvent(gridEnt.Owner, ref ev, true);
DebugTools.Assert(chunk.Fixtures.SetEquals(data.Fixtures));
// These should never refer to the same object
@@ -518,9 +518,9 @@ public abstract partial class SharedMapSystem
component.MapProxy = proxy;
}
if (TryComp<MovedGridsComponent>(xform.MapUid, out var movedGrids))
if (xform.MapUid != null)
{
movedGrids.MovedGrids.Add(uid);
_physics.MovedGrids.Add(uid);
}
}
@@ -572,9 +572,9 @@ public abstract partial class SharedMapSystem
grid.MapProxy = proxy;
}
if (TryComp<MovedGridsComponent>(xform.MapUid, out var movedGrids))
if (xform.MapUid != null)
{
movedGrids.MovedGrids.Add(uid);
_physics.MovedGrids.Add(uid);
}
}
@@ -587,9 +587,9 @@ public abstract partial class SharedMapSystem
grid.MapProxy = DynamicTree.Proxy.Free;
if (TryComp<MovedGridsComponent>(mapUid, out var movedGrids))
if (mapUid.IsValid())
{
movedGrids.MovedGrids.Remove(uid);
_physics.MovedGrids.Remove(uid);
}
}
@@ -833,7 +833,7 @@ public abstract partial class SharedMapSystem
}
var offset = chunk.GridTileToChunkTile(gridIndices);
SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile);
SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile, out _);
}
public void SetTiles(EntityUid uid, MapGridComponent grid, List<(Vector2i GridIndices, Tile Tile)> tiles)
@@ -842,6 +842,11 @@ public abstract partial class SharedMapSystem
return;
var modified = new HashSet<MapChunk>(Math.Max(1, tiles.Count / grid.ChunkSize));
var tileChanges = new ValueList<TileChangedEntry>(tiles.Count);
// Suppress sending out events for each tile changed
// We're going to send them all out together at the end
MapManager.SuppressOnTileChanged = true;
foreach (var (gridIndices, tile) in tiles)
{
@@ -859,8 +864,11 @@ public abstract partial class SharedMapSystem
var offset = chunk.GridTileToChunkTile(gridIndices);
chunk.SuppressCollisionRegeneration = true;
if (SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile))
if (SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile, out var oldTile))
{
modified.Add(chunk);
tileChanges.Add(new TileChangedEntry(tile, oldTile, offset, gridIndices));
}
}
foreach (var chunk in modified)
@@ -869,6 +877,13 @@ public abstract partial class SharedMapSystem
}
RegenerateCollision(uid, grid, modified);
// Notify of all tile changes in one event
var ev = new TileChangedEvent((uid, grid), tileChanges.ToArray());
RaiseLocalEvent(uid, ref ev, true);
// Back to normal
MapManager.SuppressOnTileChanged = false;
}
public TilesEnumerator GetLocalTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2 aabb,

View File

@@ -14,9 +14,9 @@ public abstract partial class SharedMapSystem
/// <param name="xIndex">The X tile index relative to the chunk.</param>
/// <param name="yIndex">The Y tile index relative to the chunk.</param>
/// <param name="tile">The new tile to insert.</param>
internal bool SetChunkTile(EntityUid uid, MapGridComponent grid, MapChunk chunk, ushort xIndex, ushort yIndex, Tile tile)
internal bool SetChunkTile(EntityUid uid, MapGridComponent grid, MapChunk chunk, ushort xIndex, ushort yIndex, Tile tile, out Tile oldTile)
{
if (!chunk.TrySetTile(xIndex, yIndex, tile, out var oldTile, out var shapeChanged))
if (!chunk.TrySetTile(xIndex, yIndex, tile, out oldTile, out var shapeChanged))
return false;
var tileIndices = new Vector2i(xIndex, yIndex);

View File

@@ -116,9 +116,7 @@ public abstract partial class SharedMapSystem
private void OnComponentAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
// ordered startups when
EnsureComp<PhysicsMapComponent>(uid);
EnsureComp<GridTreeComponent>(uid);
EnsureComp<MovedGridsComponent>(uid);
}
internal void AssignMapId(Entity<MapComponent> map, MapId? id = null)

View File

@@ -173,45 +173,61 @@ namespace Robust.Shared.GameObjects
}
/// <summary>
/// Arguments for when a single tile on a grid is changed locally or remotely.
/// Raised directed at the grid when tiles are changed locally or remotely.
/// </summary>
[ByRefEvent]
public readonly record struct TileChangedEvent
{
/// <summary>
/// Creates a new instance of this class.
/// </summary>
/// <inheritdoc cref="TileChangedEvent(Entity{MapGridComponent}, Tile, Tile, Vector2i, Vector2i)"/>
public TileChangedEvent(Entity<MapGridComponent> entity, TileRef newTile, Tile oldTile, Vector2i chunkIndex)
: this(entity, newTile.Tile, oldTile, chunkIndex, newTile.GridIndices) { }
/// <summary>
/// Creates a new instance of this event for a single changed tile.
/// </summary>
/// <param name="entity">The grid entity containing the changed tile(s)</param>
/// <param name="newTile">New tile that replaced the old one.</param>
/// <param name="oldTile">Old tile that was replaced.</param>
/// <param name="chunkIndex">The index of the grid-chunk that this tile belongs to.</param>
/// <param name="gridIndices">The positional indices of this tile on the grid.</param>
public TileChangedEvent(Entity<MapGridComponent> entity, Tile newTile, Tile oldTile, Vector2i chunkIndex, Vector2i gridIndices)
{
Entity = entity;
NewTile = newTile;
OldTile = oldTile;
ChunkIndex = chunkIndex;
Changes = [new TileChangedEntry(newTile, oldTile, chunkIndex, gridIndices)];
}
/// <summary>
/// Was the tile previously empty or is it now empty.
/// Creates a new instance of this event for multiple changed tiles.
/// </summary>
public bool EmptyChanged => OldTile.IsEmpty != NewTile.Tile.IsEmpty;
public TileChangedEvent(Entity<MapGridComponent> entity, TileChangedEntry[] changes)
{
Entity = entity;
Changes = changes;
}
/// <summary>
/// Entity of the grid with the tile-change. TileRef stores the GridId.
/// Entity of the grid with the tile-change. TileRef stores the GridId.
/// </summary>
public readonly Entity<MapGridComponent> Entity;
/// <summary>
/// New tile that replaced the old one.
/// An array of all the tiles that were changed.
/// </summary>
public readonly TileRef NewTile;
public readonly TileChangedEntry[] Changes;
}
/// <summary>
/// Data about a single tile that was changed as part of a <see cref="TileChangedEvent"/>.
/// </summary>
/// <param name="NewTile">New tile that replaced the old one.</param>
/// <param name="OldTile">Old tile that was replaced.</param>
/// <param name="ChunkIndex">The index of the grid-chunk that this tile belongs to.</param>
/// <param name="GridIndices">The positional indices of this tile on the grid.</param>
public readonly record struct TileChangedEntry(Tile NewTile, Tile OldTile, Vector2i ChunkIndex, Vector2i GridIndices)
{
/// <summary>
/// Old tile that was replaced.
/// Was the tile previously empty or is it now empty.
/// </summary>
public readonly Tile OldTile;
/// <summary>
/// The index of the grid-chunk that this tile belongs to.
/// </summary>
public readonly Vector2i ChunkIndex;
public bool EmptyChanged => OldTile.IsEmpty != NewTile.IsEmpty;
}
}

View File

@@ -1,16 +1,16 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using Robust.Shared.Map.Components;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Robust.Shared.Containers;
namespace Robust.Shared.GameObjects;
@@ -217,9 +217,10 @@ public abstract partial class SharedTransformSystem
{
#if !EXCEPTION_TOLERANCE
throw new Exception("Transform is initialising before map ids have been assigned?");
#endif
#else
Log.Error($"Transform is initialising before map ids have been assigned?");
_map.AssignMapId((uid, mapComp));
#endif
}
xform.MapUid = uid;

View File

@@ -53,8 +53,14 @@ public abstract partial class SharedTransformSystem
return MapCoordinates.Nullspace;
}
var worldPos = Vector2.Transform(coordinates.Position, GetWorldMatrix(xform));
return new MapCoordinates(worldPos, xform.MapID);
Vector2 pos = xform._localRotation.RotateVec(coordinates.Position) + xform._localPosition;
while (xform.ParentUid != xform.MapUid && xform.ParentUid.IsValid())
{
xform = XformQuery.GetComponent(xform.ParentUid);
pos = xform._localRotation.RotateVec(pos) + xform._localPosition;
}
return new MapCoordinates(pos, xform.MapID);
}
/// <summary>
@@ -67,6 +73,41 @@ public abstract partial class SharedTransformSystem
return ToMapCoordinates(eCoords);
}
/// <summary>
/// Converts entity-local coordinates into map terms.
/// The same as ToMapCoordinates(coordinates, logError).Position, but doesn't have to construct the MapCoordinates first.
/// </summary>
public Vector2 ToWorldPosition(EntityCoordinates coordinates, bool logError = true)
{
if (!TryComp(coordinates.EntityId, out TransformComponent? xform))
{
if (logError)
Log.Error($"Attempted to convert coordinates with invalid entity: {coordinates}. Trace: {Environment.StackTrace}");
return Vector2.Zero;
}
Vector2 pos = xform._localRotation.RotateVec(coordinates.Position) + xform._localPosition;
while (xform.ParentUid != xform.MapUid && xform.ParentUid.IsValid())
{
xform = XformQuery.GetComponent(xform.ParentUid);
pos = xform._localRotation.RotateVec(pos) + xform._localPosition;
}
return pos;
}
/// <summary>
/// Converts entity-local coordinates into map terms.
/// The same as ToMapCoordinates(coordinates).Position, but doesn't have to construct the MapCoordinates first.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector2 ToWorldPosition(NetCoordinates coordinates)
{
var eCoords = GetCoordinates(coordinates);
return ToWorldPosition(eCoords);
}
/// <summary>
/// Creates EntityCoordinates given an entity and some MapCoordinates.
/// </summary>

View File

@@ -70,11 +70,14 @@ namespace Robust.Shared.GameObjects
private void MapManagerOnTileChanged(ref TileChangedEvent e)
{
if(e.NewTile.Tile != Tile.Empty)
return;
foreach (var change in e.Changes)
{
if(change.NewTile != Tile.Empty)
continue;
// TODO optimize this for when multiple tiles get empties simultaneously (e.g., explosions).
DeparentAllEntsOnTile(e.NewTile.GridUid, e.NewTile.GridIndices);
// TODO optimize this for when multiple tiles get empties simultaneously (e.g., explosions).
DeparentAllEntsOnTile(e.Entity, change.GridIndices);
}
}
/// <summary>
@@ -260,7 +263,7 @@ namespace Robust.Shared.GameObjects
return true;
}
public void RaiseMoveEvent(
internal void RaiseMoveEvent(
Entity<TransformComponent, MetaDataComponent> ent,
EntityUid oldParent,
Vector2 oldPosition,

View File

@@ -1,16 +0,0 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Map.Components;
/// <summary>
/// Stores what grids moved in a tick.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class MovedGridsComponent : Component
{
[ViewVariables]
public HashSet<EntityUid> MovedGrids = new();
}

View File

@@ -0,0 +1,64 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Robust.Shared.Collections;
using Robust.Shared.Maths;
namespace Robust.Shared.Map.Enumerators;
/// <summary>
/// Iterates chunk indices but prefers ones closer towards the center first.
/// </summary>
public record struct NearestChunkEnumerator
{
private readonly Vector2i _chunkLB;
private readonly Vector2i _chunkRT;
private ValueList<Vector2i> _chunks = new();
private int _n;
public NearestChunkEnumerator(Box2 localAABB, int chunkSize)
{
_chunkLB = (localAABB.BottomLeft / chunkSize).Floored();
_chunkRT = (localAABB.TopRight / chunkSize).Floored();
InitializeChunks(new Vector2i(chunkSize, chunkSize));
}
public NearestChunkEnumerator(Box2 localAABB, Vector2i chunkSize)
{
_chunkLB = (localAABB.BottomLeft / chunkSize).Floored();
_chunkRT = (localAABB.TopRight / chunkSize).Floored();
InitializeChunks(chunkSize);
}
private void InitializeChunks(Vector2i chunkSize)
{
var bl = (Vector2)_chunkLB * chunkSize;
var tr = (Vector2)_chunkRT * chunkSize;
var halfChunk = new Vector2(chunkSize.X / 2f, chunkSize.Y / 2f);
var center = (tr - bl) / 2 + bl;
for (var x = _chunkLB.X; x < _chunkRT.X; x++)
{
for (var y = _chunkLB.Y; y < _chunkRT.Y; y++)
{
_chunks.Add(new Vector2i(x, y) * chunkSize);
}
}
_chunks.Sort((c1, c2) => ((c1 + halfChunk) - center).LengthSquared().CompareTo(((c2 + halfChunk) - center).LengthSquared()));
}
public bool MoveNext([NotNullWhen(true)] out Vector2i? indices)
{
if (_n >= _chunks.Count)
{
indices = null;
return false;
}
indices = _chunks[_n++] ;
return true;
}
}

View File

@@ -20,11 +20,6 @@ namespace Robust.Shared.Map
/// </summary>
string Name { get; }
/// <summary>
/// Internal name of the definition.
/// </summary>
string ID { get; }
/// <summary>
/// The path of the sprite to draw.
/// </summary>

View File

@@ -37,7 +37,7 @@ using FNLfloat = System.Single;
//using FNLfloat = System.Double;
[DataDefinition, Serializable, NetSerializable]
public sealed partial class FastNoiseLite
public sealed partial class FastNoiseLite : ISerializationHooks
{
private const short INLINE = 256; // MethodImplOptions.AggressiveInlining;
private const short OPTIMISE = 512; // MethodImplOptions.AggressiveOptimization;
@@ -136,7 +136,6 @@ public sealed partial class FastNoiseLite
[DataField("pingPongStrength")]
private float mPingPongStrength = 2.0f;
[DataField("fractalBounding")]
private float mFractalBounding = 1 / 1.75f;
[DataField("cellularDistanceFunction")]
@@ -170,6 +169,11 @@ public sealed partial class FastNoiseLite
SetSeed(1337);
}
void ISerializationHooks.AfterDeserialization()
{
CalculateFractalBounding();
}
public int GetSeed() => mSeed;
/// <summary>

View File

@@ -1,18 +0,0 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Physics.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class Gravity2DComponent : Component
{
/// <summary>
/// Applies side-view gravity to the map.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("gravity")]
public Vector2 Gravity;
}

View File

@@ -154,11 +154,6 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
// ReSharper disable once InconsistentNaming
internal float _inertia;
/// <summary>
/// Indicates whether this body ignores gravity
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] public bool IgnoreGravity;
/// <summary>
/// Inverse moment of inertia (1 / I).
/// </summary>
@@ -264,6 +259,9 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
public BodyStatus BodyStatus { get; set; }
[DataField, Access(typeof(SharedPhysicsSystem))]
public bool IgnoreGravity;
[ViewVariables, Access(typeof(SharedPhysicsSystem))]
public bool Predict;
}

View File

@@ -1,106 +0,0 @@
using System;
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Controllers;
public sealed class Gravity2DController : VirtualController
{
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
private ISawmill _sawmill = default!;
public override void Initialize()
{
base.Initialize();
_sawmill = Logger.GetSawmill("physics");
SubscribeLocalEvent<Gravity2DComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<Gravity2DComponent, ComponentHandleState>(OnHandleState);
}
private void OnGetState(EntityUid uid, Gravity2DComponent component, ref ComponentGetState args)
{
args.State = new Gravity2DComponentState() { Gravity = component.Gravity };
}
private void OnHandleState(EntityUid uid, Gravity2DComponent component, ref ComponentHandleState args)
{
if (args.Current is not Gravity2DComponentState state)
return;
component.Gravity = state.Gravity;
}
public Vector2 GetGravity(EntityUid uid, Gravity2DComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return Vector2.Zero;
return component.Gravity;
}
public void SetGravity(EntityUid uid, Vector2 value)
{
if (!HasComp<MapComponent>(uid))
{
_sawmill.Error($"Tried to set 2D gravity for an entity that isn't a map?");
DebugTools.Assert(false);
return;
}
var gravity = EnsureComp<Gravity2DComponent>(uid);
if (gravity.Gravity.Equals(value))
return;
gravity.Gravity = value;
WakeBodiesRecursive(uid, GetEntityQuery<TransformComponent>(), GetEntityQuery<PhysicsComponent>());
Dirty(uid, gravity);
}
public void SetGravity(MapId mapId, Vector2 value)
{
var mapUid = _mapSystem.GetMap(mapId);
var gravity = EnsureComp<Gravity2DComponent>(mapUid);
if (gravity.Gravity.Equals(value))
return;
gravity.Gravity = value;
WakeBodiesRecursive(mapUid, GetEntityQuery<TransformComponent>(), GetEntityQuery<PhysicsComponent>());
Dirty(mapUid, gravity);
}
private void WakeBodiesRecursive(EntityUid uid, EntityQuery<TransformComponent> xformQuery, EntityQuery<PhysicsComponent> bodyQuery)
{
if (bodyQuery.TryGetComponent(uid, out var body) &&
body.BodyType == BodyType.Dynamic)
{
_physics.WakeBody(uid, body: body);
}
var xform = xformQuery.GetComponent(uid);
foreach (var child in xform._children)
{
WakeBodiesRecursive(child, xformQuery, bodyQuery);
}
}
[Serializable, NetSerializable]
private sealed class Gravity2DComponentState : ComponentState
{
public Vector2 Gravity;
}
}

View File

@@ -35,9 +35,6 @@ namespace Robust.Shared.Physics.Controllers
SubscribeLocalEvent<PhysicsUpdateBeforeSolveEvent>(OnBeforeSolve, updatesBefore, updatesAfter);
SubscribeLocalEvent<PhysicsUpdateAfterSolveEvent>(OnAfterSolve, updatesBefore, updatesAfter);
SubscribeLocalEvent<PhysicsUpdateBeforeMapSolveEvent>(OnBeforeMapSolve, updatesBefore, updatesAfter);
SubscribeLocalEvent<PhysicsUpdateAfterMapSolveEvent>(OnAfterMapSolve, updatesBefore, updatesAfter);
}
private void OnBeforeSolve(ref PhysicsUpdateBeforeSolveEvent ev)
@@ -62,16 +59,6 @@ namespace Robust.Shared.Physics.Controllers
AfterMonitor.Observe(Stopwatch.Elapsed.TotalSeconds);
}
private void OnBeforeMapSolve(ref PhysicsUpdateBeforeMapSolveEvent ev)
{
UpdateBeforeMapSolve(ev.Prediction, ev.MapComponent, ev.DeltaTime);
}
private void OnAfterMapSolve(ref PhysicsUpdateAfterMapSolveEvent ev)
{
UpdateAfterMapSolve(ev.Prediction, ev.MapComponent, ev.DeltaTime);
}
#endregion
/// <summary>
@@ -87,21 +74,5 @@ namespace Robust.Shared.Physics.Controllers
/// <param name="prediction"></param>
/// <param name="frameTime"></param>
public virtual void UpdateAfterSolve(bool prediction, float frameTime) {}
/// <summary>
/// Run before a particular map starts.
/// </summary>
/// <param name="prediction"></param>
/// <param name="mapComponent"></param>
/// <param name="frameTime"></param>
public virtual void UpdateBeforeMapSolve(bool prediction, PhysicsMapComponent mapComponent, float frameTime) {}
/// <summary>
/// Run after a particular map finishes.
/// </summary>
/// <param name="prediction"></param>
/// <param name="mapComponent"></param>
/// <param name="frameTime"></param>
public virtual void UpdateAfterMapSolve(bool prediction, PhysicsMapComponent mapComponent, float frameTime) {}
}
}

View File

@@ -80,6 +80,9 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
public PhysicsComponent? BodyA;
public PhysicsComponent? BodyB;
public TransformComponent? XformA;
public TransformComponent? XformB;
public Manifold Manifold;
internal ContactType Type;
@@ -253,6 +256,11 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
{
status = ContactStatus.EndTouching;
}
// Still touching
else
{
status = ContactStatus.Touching;
}
}
#if DEBUG
@@ -442,6 +450,17 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
throw new InvalidOperationException();
}
[Pure]
public TransformComponent OtherTransform(EntityUid uid)
{
if (uid == EntityA)
return XformB!;
else if (uid == EntityB)
return XformA!;
throw new InvalidOperationException();
}
}
[Flags]

View File

@@ -25,43 +25,45 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Physics.Dynamics
namespace Robust.Shared.Physics.Dynamics;
public sealed class FixtureProxy
{
public sealed class FixtureProxy
public EntityUid Entity;
public PhysicsComponent Body { get; internal set; }
public TransformComponent Xform { get; internal set; }
/// <summary>
/// Grid-based AABB of this proxy.
/// </summary>
[ViewVariables]
public Box2 AABB;
[ViewVariables]
public int ChildIndex;
public string FixtureId;
/// <summary>
/// Our parent fixture
/// </summary>
public Fixture Fixture;
/// <summary>
/// ID of this proxy in the broadphase dynamictree.
/// </summary>
[ViewVariables]
public DynamicTree.Proxy ProxyId = DynamicTree.Proxy.Free;
internal FixtureProxy(EntityUid uid, PhysicsComponent body, TransformComponent xform, Box2 aabb, string fixtureId, Fixture fixture, int childIndex)
{
public EntityUid Entity;
public PhysicsComponent Body;
/// <summary>
/// Grid-based AABB of this proxy.
/// </summary>
[ViewVariables]
public Box2 AABB;
[ViewVariables]
public int ChildIndex;
public string FixtureId;
/// <summary>
/// Our parent fixture
/// </summary>
public Fixture Fixture;
/// <summary>
/// ID of this proxy in the broadphase dynamictree.
/// </summary>
[ViewVariables]
public DynamicTree.Proxy ProxyId = DynamicTree.Proxy.Free;
public FixtureProxy(EntityUid uid, PhysicsComponent body, Box2 aabb, string fixtureId, Fixture fixture, int childIndex)
{
Entity = uid;
Body = body;
AABB = aabb;
FixtureId = fixtureId;
Fixture = fixture;
ChildIndex = childIndex;
}
Entity = uid;
Body = body;
Xform = xform;
AABB = aabb;
FixtureId = fixtureId;
Fixture = fixture;
ChildIndex = childIndex;
}
}

View File

@@ -1,90 +0,0 @@
/*
* Farseer Physics Engine:
* Copyright (c) 2012 Ian Qvist
*
* Original source Box2D:
* Copyright (c) 2006-2011 Erin Catto http://www.box2d.org
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
*/
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
using PhysicsComponent = Robust.Shared.Physics.Components.PhysicsComponent;
namespace Robust.Shared.Physics.Dynamics;
[RegisterComponent, NetworkedComponent]
public sealed partial class PhysicsMapComponent : Component
{
public bool AutoClearForces;
/// <summary>
/// When substepping the client needs to know about the first position to use for lerping.
/// </summary>
public readonly Dictionary<EntityUid, (EntityUid ParentUid, Vector2 LocalPosition, Angle LocalRotation)>
LerpData = new();
/// <summary>
/// Keep a buffer of everything that moved in a tick. This will be used to check for physics contacts.
/// </summary>
[ViewVariables]
public readonly Dictionary<FixtureProxy, Box2> MoveBuffer = new();
/// <summary>
/// All awake bodies on this map.
/// </summary>
[ViewVariables]
public readonly HashSet<PhysicsComponent> AwakeBodies = new();
/// <summary>
/// Store last tick's invDT
/// </summary>
internal float _invDt0;
}
[ByRefEvent]
public readonly struct PhysicsUpdateBeforeMapSolveEvent
{
public readonly bool Prediction;
public readonly PhysicsMapComponent MapComponent;
public readonly float DeltaTime;
public PhysicsUpdateBeforeMapSolveEvent(bool prediction, PhysicsMapComponent mapComponent, float deltaTime)
{
Prediction = prediction;
MapComponent = mapComponent;
DeltaTime = deltaTime;
}
}
[ByRefEvent]
public readonly struct PhysicsUpdateAfterMapSolveEvent
{
public readonly bool Prediction;
public readonly PhysicsMapComponent MapComponent;
public readonly float DeltaTime;
public PhysicsUpdateAfterMapSolveEvent(bool prediction, PhysicsMapComponent mapComponent, float deltaTime)
{
Prediction = prediction;
MapComponent = mapComponent;
DeltaTime = deltaTime;
}
}

View File

@@ -1,8 +1,8 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Events;
@@ -20,9 +20,16 @@ public readonly struct StartCollideEvent
public readonly Fixture OurFixture;
public readonly Fixture OtherFixture;
public readonly Vector2 WorldPoint;
public StartCollideEvent(
internal readonly FixedArray2<Vector2> _worldPoints;
public readonly int PointCount;
public readonly Vector2 WorldNormal;
public Vector2[] WorldPoints => _worldPoints.AsSpan[..PointCount].ToArray();
internal StartCollideEvent(
EntityUid ourEntity,
EntityUid otherEntity,
string ourFixtureId,
@@ -31,7 +38,9 @@ public readonly struct StartCollideEvent
Fixture otherFixture,
PhysicsComponent ourBody,
PhysicsComponent otherBody,
Vector2 worldPoint)
FixedArray2<Vector2> worldPoints,
int pointCount,
Vector2 worldNormal)
{
OurEntity = ourEntity;
OtherEntity = otherEntity;
@@ -39,8 +48,10 @@ public readonly struct StartCollideEvent
OtherFixtureId = otherFixtureId;
OurFixture = ourFixture;
OtherFixture = otherFixture;
WorldPoint = worldPoint;
OtherBody = otherBody;
OurBody = ourBody;
_worldPoints = worldPoints;
PointCount = pointCount;
WorldNormal = worldNormal;
}
}

View File

@@ -6,9 +6,9 @@ namespace Robust.Shared.Physics
{
internal sealed class IslandSolveMessage : EntityEventArgs
{
public List<PhysicsComponent> Bodies { get; }
public List<Entity<PhysicsComponent, TransformComponent>> Bodies { get; }
public IslandSolveMessage(List<PhysicsComponent> bodies)
public IslandSolveMessage(List<Entity<PhysicsComponent, TransformComponent>> bodies)
{
Bodies = bodies;
}

View File

@@ -21,13 +21,10 @@ namespace Robust.Shared.Physics.Systems
/// </summary>
public sealed partial class FixtureSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
#pragma warning disable CS0414
[Dependency] private readonly IGameTiming _timing = default!;
#pragma warning restore CS0414
private EntityQuery<PhysicsMapComponent> _mapQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<FixturesComponent> _fixtureQuery;
@@ -38,7 +35,6 @@ namespace Robust.Shared.Physics.Systems
SubscribeLocalEvent<FixturesComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<FixturesComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<FixturesComponent, ComponentHandleState>(OnHandleState);
_mapQuery = GetEntityQuery<PhysicsMapComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_fixtureQuery = GetEntityQuery<FixturesComponent>();
}
@@ -206,8 +202,7 @@ namespace Robust.Shared.Physics.Systems
if (_lookup.TryGetCurrentBroadphase(xform, out var broadphase))
{
DebugTools.Assert(xform.MapUid == Transform(broadphase.Owner).MapUid);
_mapQuery.TryGetComponent(xform.MapUid, out var physicsMap);
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase, physicsMap);
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase);
}
if (updates)

View File

@@ -34,10 +34,12 @@ namespace Robust.Shared.Physics.Systems
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
private EntityQuery<PhysicsMapComponent> _mapQuery;
private float _broadphaseExpand;
private readonly Dictionary<EntityUid, Matrix3x2> _broadMatrices = new();
private HashSet<FixtureProxy> _gridMoveBuffer = new();
/*
* Okay so Box2D has its own "MoveProxy" stuff so you can easily find new contacts when required.
* Our problem is that we have nested broadphases (rather than being on separate maps) which makes this
@@ -56,6 +58,8 @@ namespace Robust.Shared.Physics.Systems
_mapManager = _mapManager,
System = this,
BroadphaseExpand = _broadphaseExpand,
// TODO: EntityManager one isn't ready yet?
XformQuery = GetEntityQuery<TransformComponent>(),
};
_broadphaseQuery = GetEntityQuery<BroadphaseComponent>();
@@ -63,7 +67,6 @@ namespace Robust.Shared.Physics.Systems
_gridQuery = GetEntityQuery<MapGridComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
_mapQuery = GetEntityQuery<PhysicsMapComponent>();
UpdatesOutsidePrediction = true;
UpdatesAfter.Add(typeof(SharedTransformSystem));
@@ -98,87 +101,75 @@ namespace Robust.Shared.Physics.Systems
/// <summary>
/// Check the AABB for each moved broadphase fixture and add any colliding entities to the movebuffer in case.
/// </summary>
private void FindGridContacts(
PhysicsMapComponent component,
MapId mapId,
HashSet<EntityUid> movedGrids,
Dictionary<FixtureProxy, Box2> gridMoveBuffer)
private void FindGridContacts(HashSet<EntityUid> movedGrids)
{
// None moved this tick
if (movedGrids.Count == 0) return;
var mapBroadphase = _broadphaseQuery.GetComponent(_map.GetMapOrInvalid(mapId));
// This is so that if we're on a broadphase that's moving (e.g. a grid) we need to make sure anything
// we move over is getting checked for collisions, and putting it on the movebuffer is the easiest way to do so.
var moveBuffer = component.MoveBuffer;
var moveBuffer = _physicsSystem.MoveBuffer;
foreach (var gridUid in movedGrids)
{
var grid = _gridQuery.GetComponent(gridUid);
var xform = _xformQuery.GetComponent(gridUid);
DebugTools.Assert(xform.MapID == mapId);
// Moved to nullspace?
if (!_broadphaseQuery.TryComp(xform.MapUid, out var mapBroadphase))
continue;
var worldAABB = _transform.GetWorldMatrix(xform).TransformBox(grid.LocalAABB);
var enlargedAABB = worldAABB.Enlarged(_broadphaseExpand);
var state = (moveBuffer, gridMoveBuffer);
var state = (moveBuffer, _gridMoveBuffer);
QueryMapBroadphase(mapBroadphase.DynamicTree, ref state, enlargedAABB);
QueryMapBroadphase(mapBroadphase.StaticTree, ref state, enlargedAABB);
}
foreach (var (proxy, worldAABB) in gridMoveBuffer)
foreach (var proxy in _gridMoveBuffer)
{
moveBuffer[proxy] = worldAABB;
moveBuffer.Add(proxy);
// If something is in our AABB then try grid traversal for it
_traversal.CheckTraverse((proxy.Entity, _xformQuery.GetComponent(proxy.Entity)));
}
}
private void QueryMapBroadphase(IBroadPhase broadPhase,
ref (Dictionary<FixtureProxy, Box2>, Dictionary<FixtureProxy, Box2>) state,
ref (HashSet<FixtureProxy>, HashSet<FixtureProxy>) state,
Box2 enlargedAABB)
{
// Easier to just not go over each proxy as we already unioned the fixture's worldaabb.
broadPhase.QueryAabb(ref state, static (ref (
Dictionary<FixtureProxy, Box2> moveBuffer,
Dictionary<FixtureProxy, Box2> gridMoveBuffer) tuple,
HashSet<FixtureProxy> moveBuffer,
HashSet<FixtureProxy> gridMoveBuffer) tuple,
in FixtureProxy value) =>
{
// 99% of the time it's just going to be the broadphase (for now the grid) itself.
// hence this body check makes this run significantly better.
// Also check if it's not already on the movebuffer.
if (tuple.moveBuffer.ContainsKey(value))
if (tuple.moveBuffer.Contains(value))
return true;
// To avoid updating during iteration.
// Don't need to transform as it's already in map terms.
tuple.gridMoveBuffer[value] = value.AABB;
tuple.gridMoveBuffer.Add(value);
return true;
}, enlargedAABB, true);
}
[Obsolete("Use the overload with SharedPhysicsMapComponent")]
internal void FindNewContacts(MapId mapId)
{
if (!TryComp<PhysicsMapComponent>(_map.GetMapOrInvalid(mapId), out var physicsMap))
return;
FindNewContacts(physicsMap, mapId);
}
/// <summary>
/// Go through every single created, moved, or touched proxy on the map and try to find any new contacts that should be created.
/// </summary>
internal void FindNewContacts(PhysicsMapComponent component, MapId mapId)
internal void FindNewContacts()
{
var moveBuffer = component.MoveBuffer;
var mapUid = _map.GetMapOrInvalid(mapId);
var movedGrids = Comp<MovedGridsComponent>(mapUid).MovedGrids;
var gridMoveBuffer = new Dictionary<FixtureProxy, Box2>();
var moveBuffer = _physicsSystem.MoveBuffer;
var movedGrids = _physicsSystem.MovedGrids;
_gridMoveBuffer.Clear();
// Find any entities being driven over that might need to be considered
FindGridContacts(component, mapId, movedGrids, gridMoveBuffer);
FindGridContacts(movedGrids);
// There is some mariana trench levels of bullshit going on.
// We essentially need to re-create Box2D's FindNewContacts but in a way that allows us to check every
@@ -190,18 +181,29 @@ namespace Robust.Shared.Physics.Systems
// to cache a bunch of stuff to make up for it.
// Handle grids first as they're not stored on map broadphase at all.
HandleGridCollisions(mapId, movedGrids);
HandleGridCollisions(movedGrids);
// EZ
if (moveBuffer.Count == 0)
return;
_contactJob.MapUid = _mapManager.GetMapEntityIdOrThrow(mapId);
_contactJob.MoveBuffer.Clear();
foreach (var (proxy, aabb) in moveBuffer)
foreach (var proxy in moveBuffer)
{
_contactJob.MoveBuffer.Add((proxy, aabb));
DebugTools.Assert(_xformQuery.GetComponent(proxy.Entity).Broadphase?.Uid != null);
_contactJob.MoveBuffer.Add(proxy);
}
_broadMatrices.Clear();
var broadQuery = AllEntityQuery<BroadphaseComponent>();
// Cache broadphase matrices up front.
// We'll defer the proxy world AABBs until we get contacts rather than doing it on every single move.
// This is because contacts are run in parallel so we can spread the work a bit more and also don't duplicate it per tick.
while (broadQuery.MoveNext(out var bUid, out _))
{
_broadMatrices[bUid] = _transform.GetWorldMatrix(bUid);
}
for (var i = _contactJob.ContactBuffer.Count; i < _contactJob.MoveBuffer.Count; i++)
@@ -220,7 +222,7 @@ namespace Robust.Shared.Physics.Systems
if (proxies.Count == 0)
continue;
var proxyA = _contactJob.MoveBuffer[i].Proxy;
var proxyA = _contactJob.MoveBuffer[i];
var proxyABody = proxyA.Body;
_fixturesQuery.TryGetComponent(proxyA.Entity, out var manager);
@@ -234,7 +236,7 @@ namespace Robust.Shared.Physics.Systems
// This is because we generate a contact across 2 different broadphases where both bodies aren't
// moving locally but are moving in world-terms.
if (proxyA.Fixture.Hard && other.Fixture.Hard &&
(gridMoveBuffer.ContainsKey(proxyA) || gridMoveBuffer.ContainsKey(other)))
(_gridMoveBuffer.Contains(proxyA) || _gridMoveBuffer.Contains(other)))
{
_physicsSystem.WakeBody(proxyA.Entity, force: true, manager: manager, body: proxyABody);
_physicsSystem.WakeBody(other.Entity, force: true, body: otherBody);
@@ -248,15 +250,15 @@ namespace Robust.Shared.Physics.Systems
movedGrids.Clear();
}
private void HandleGridCollisions(
MapId mapId,
HashSet<EntityUid> movedGrids)
private void HandleGridCollisions(HashSet<EntityUid> movedGrids)
{
foreach (var gridUid in movedGrids)
{
var grid = _gridQuery.GetComponent(gridUid);
var xform = _xformQuery.GetComponent(gridUid);
DebugTools.Assert(xform.MapID == mapId);
if (xform.MapID == MapId.Nullspace)
continue;
var (worldPos, worldRot, worldMatrix, invWorldMatrix) = _transform.GetWorldPositionRotationMatrixWithInv(xform);
@@ -269,7 +271,7 @@ namespace Robust.Shared.Physics.Systems
var transform = _physicsSystem.GetPhysicsTransform(gridUid);
var state = (
new Entity<FixturesComponent, MapGridComponent, PhysicsComponent>(gridUid, fixture, grid, physics),
new Entity<FixturesComponent, MapGridComponent, PhysicsComponent, TransformComponent>(gridUid, fixture, grid, physics, xform),
transform,
worldMatrix,
invWorldMatrix,
@@ -280,9 +282,9 @@ namespace Robust.Shared.Physics.Systems
_physicsQuery,
_xformQuery);
_mapManager.FindGridsIntersecting(mapId, aabb, ref state,
_mapManager.FindGridsIntersecting(xform.MapID, aabb, ref state,
static (EntityUid uid, MapGridComponent component,
ref (Entity<FixturesComponent, MapGridComponent, PhysicsComponent> grid,
ref (Entity<FixturesComponent, MapGridComponent, PhysicsComponent, TransformComponent> grid,
Transform transform,
Matrix3x2 worldMatrix,
Matrix3x2 invWorldMatrix,
@@ -299,7 +301,7 @@ namespace Robust.Shared.Physics.Systems
return true;
}
var (_, _, otherGridMatrix, otherGridInvMatrix) = tuple.xformSystem.GetWorldPositionRotationMatrixWithInv(collidingXform, tuple.xformQuery);
var (_, _, otherGridMatrix, otherGridInvMatrix) = tuple.xformSystem.GetWorldPositionRotationMatrixWithInv(collidingXform);
var otherGridBounds = otherGridMatrix.TransformBox(component.LocalAABB);
var otherTransform = tuple._physicsSystem.GetPhysicsTransform(uid);
@@ -340,7 +342,10 @@ namespace Robust.Shared.Physics.Systems
var otherAABB = otherFixture.Shape.ComputeAABB(otherTransform, j);
if (!fixAABB.Intersects(otherAABB)) continue;
tuple._physicsSystem.AddPair(tuple.grid.Owner, uid,
tuple._physicsSystem.AddPair(
(tuple.grid.Owner, tuple.grid.Comp3, tuple.grid.Comp4),
(uid, physicsB, collidingXform),
ourId, otherId,
fixture, i,
otherFixture, j,
@@ -451,29 +456,24 @@ namespace Robust.Shared.Physics.Systems
_physicsSystem.SetAwake(entity!, true);
var matrix = _transform.GetWorldMatrix(broadphase);
foreach (var fixture in entity.Comp2.Fixtures.Values)
{
TouchProxies(entity.Comp3.MapUid.Value, matrix, fixture);
TouchProxies(fixture);
}
}
internal void TouchProxies(EntityUid mapId, Matrix3x2 broadphaseMatrix, Fixture fixture)
internal void TouchProxies(Fixture fixture)
{
foreach (var proxy in fixture.Proxies)
{
AddToMoveBuffer(mapId, proxy, broadphaseMatrix.TransformBox(proxy.AABB));
AddToMoveBuffer(proxy);
}
}
private void AddToMoveBuffer(EntityUid mapId, FixtureProxy proxy, Box2 aabb)
private void AddToMoveBuffer(FixtureProxy proxy)
{
if (!_mapQuery.TryGetComponent(mapId, out var physicsMap))
return;
DebugTools.Assert(proxy.Body.CanCollide);
physicsMap.MoveBuffer[proxy] = aabb;
_physicsSystem.MoveBuffer.Add(proxy);
}
public void Refilter(EntityUid uid, Fixture fixture, TransformComponent? xform = null)
@@ -492,8 +492,7 @@ namespace Robust.Shared.Physics.Systems
if (!_xformQuery.TryGetComponent(xform.Broadphase?.Uid, out var broadphase))
return;
var matrix = _transform.GetWorldMatrix(broadphase);
TouchProxies(xform.MapUid.Value, matrix, fixture);
TouchProxies(fixture);
}
internal void GetBroadphases(MapId mapId, Box2 aabb, BroadphaseCallback callback)
@@ -560,30 +559,35 @@ namespace Robust.Shared.Physics.Systems
private record struct BroadphaseContactJob() : IParallelRobustJob
{
public SharedBroadphaseSystem System = default!;
public SharedTransformSystem TransformSys = default!;
public IMapManager _mapManager = default!;
public float BroadphaseExpand;
public EntityUid MapUid;
public EntityQuery<TransformComponent> XformQuery;
public List<List<FixtureProxy>> ContactBuffer = new();
public List<(FixtureProxy Proxy, Box2 WorldAABB)> MoveBuffer = new();
public List<FixtureProxy> MoveBuffer = new();
public int BatchSize => 8;
public void Execute(int index)
{
var (proxy, worldAABB) = MoveBuffer[index];
var proxy = MoveBuffer[index];
var broadphaseUid = XformQuery.GetComponent(proxy.Entity).Broadphase?.Uid;
var worldAABB = System._broadMatrices[broadphaseUid!.Value].TransformBox(proxy.AABB);
var buffer = ContactBuffer[index];
buffer.Clear();
var mapUid = XformQuery.GetComponent(proxy.Entity).MapUid ?? EntityUid.Invalid;
var proxyBody = proxy.Body;
DebugTools.Assert(!proxyBody.Deleted);
var state = (System, proxy, worldAABB, buffer);
// Get every broadphase we may be intersecting.
_mapManager.FindGridsIntersecting(MapUid, worldAABB.Enlarged(BroadphaseExpand), ref state,
_mapManager.FindGridsIntersecting(mapUid, worldAABB.Enlarged(BroadphaseExpand), ref state,
static (EntityUid uid, MapGridComponent _, ref (
SharedBroadphaseSystem system,
FixtureProxy proxy,
@@ -599,7 +603,7 @@ namespace Robust.Shared.Physics.Systems
// Struct ref moment, I have no idea what's fastest.
buffer = state.buffer;
System.FindPairs(proxy, worldAABB, MapUid, buffer);
System.FindPairs(proxy, worldAABB, mapUid, buffer);
}
}
}

View File

@@ -23,10 +23,8 @@
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
@@ -314,13 +312,6 @@ public partial class SharedPhysicsSystem
body._inertia += data.I;
}
// Update this after re-calculating mass as content may want to use the sum of fixture masses instead.
if (((int) body.BodyType & (int) (BodyType.Kinematic | BodyType.Static)) != 0)
{
body._localCenter = Vector2.Zero;
return;
}
if (body._mass > 0.0f)
{
body._invMass = 1.0f / body._mass;
@@ -350,18 +341,23 @@ public partial class SharedPhysicsSystem
var oldCenter = body._localCenter;
body._localCenter = localCenter;
// Update center of mass velocity.
var comVelocityDiff = Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter);
if (((int) body.BodyType & (int) (BodyType.Kinematic | BodyType.Static)) == 0)
{
// Update center of mass velocity.
var comVelocityDiff = Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter);
if (comVelocityDiff != Vector2.Zero)
{
body.LinearVelocity += comVelocityDiff;
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
}
if (comVelocityDiff != Vector2.Zero)
{
body.LinearVelocity += comVelocityDiff;
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
}
}
if (body._mass == oldMass && body._inertia == oldInertia && oldCenter == localCenter)
return;
// we always do the full mass and COM calculation and raise this, even for static bodies as content may need the info
// examples are stations anchored with the station anchor, shuttles landed on planets, or grids getting an atmosphere above a certain mass threshold
var ev = new MassDataChangedEvent((uid, body, manager), oldMass, oldInertia, oldCenter);
RaiseLocalEvent(uid, ref ev);
}
@@ -488,14 +484,17 @@ public partial class SharedPhysicsSystem
body.Awake = true;
}
UpdateMapAwakeState(uid, body);
if (body.Awake)
AddAwakeBody((uid, body, Transform(uid)));
else
RemoveSleepBody((uid, body, Transform(uid)));
}
public void TrySetBodyType(EntityUid uid, BodyType value, FixturesComponent? manager = null, PhysicsComponent? body = null, TransformComponent? xform = null)
{
if (_fixturesQuery.Resolve(uid, ref manager, false) &&
PhysicsQuery.Resolve(uid, ref body, false) &&
_xformQuery.Resolve(uid, ref xform, false))
XformQuery.Resolve(uid, ref xform, false))
{
SetBodyType(uid, value, manager, body, xform);
}
@@ -511,7 +510,6 @@ public partial class SharedPhysicsSystem
var oldType = body.BodyType;
body.BodyType = value;
ResetMassData(uid, manager, body);
body.Force = Vector2.Zero;
body.Torque = 0f;
@@ -705,7 +703,7 @@ public partial class SharedPhysicsSystem
public Transform GetRelativePhysicsTransform(Transform worldTransform, Entity<TransformComponent?> relative)
{
if (!_xformQuery.Resolve(relative.Owner, ref relative.Comp))
if (!XformQuery.Resolve(relative.Owner, ref relative.Comp))
return Physics.Transform.Empty;
var (_, broadphaseRot, _, broadphaseInv) = _transform.GetWorldPositionRotationMatrixWithInv(relative.Comp);
@@ -721,8 +719,8 @@ public partial class SharedPhysicsSystem
Entity<TransformComponent?> entity,
Entity<TransformComponent?> relative)
{
if (!_xformQuery.Resolve(entity.Owner, ref entity.Comp) ||
!_xformQuery.Resolve(relative.Owner, ref relative.Comp))
if (!XformQuery.Resolve(entity.Owner, ref entity.Comp) ||
!XformQuery.Resolve(relative.Owner, ref relative.Comp))
{
return Physics.Transform.Empty;
}
@@ -738,7 +736,7 @@ public partial class SharedPhysicsSystem
/// </summary>
public Transform GetLocalPhysicsTransform(EntityUid uid, TransformComponent? xform = null)
{
if (!_xformQuery.Resolve(uid, ref xform) || xform.Broadphase == null)
if (!XformQuery.Resolve(uid, ref xform) || xform.Broadphase == null)
return Physics.Transform.Empty;
var broadphase = xform.Broadphase.Value.Uid;
@@ -753,7 +751,7 @@ public partial class SharedPhysicsSystem
public Transform GetPhysicsTransform(EntityUid uid, TransformComponent? xform = null)
{
if (!_xformQuery.Resolve(uid, ref xform))
if (!XformQuery.Resolve(uid, ref xform))
return Physics.Transform.Empty;
var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform);

View File

@@ -123,24 +123,26 @@ public abstract partial class SharedPhysicsSystem
DebugTools.Assert(obj.Flags is ContactFlags.None or ContactFlags.Deleted);
SetContact(obj,
false,
EntityUid.Invalid, EntityUid.Invalid,
new Entity<PhysicsComponent?, TransformComponent?>(EntityUid.Invalid, null, null),
new Entity<PhysicsComponent?, TransformComponent?>(EntityUid.Invalid, null, null),
string.Empty, string.Empty,
null, 0,
null, 0,
null, null);
null, 0);
return true;
}
}
private static void SetContact(Contact contact,
bool enabled,
EntityUid uidA, EntityUid uidB,
Entity<PhysicsComponent?, TransformComponent?> entA,
Entity<PhysicsComponent?, TransformComponent?> entB,
string fixtureAId, string fixtureBId,
Fixture? fixtureA, int indexA,
Fixture? fixtureB, int indexB,
PhysicsComponent? bodyA,
PhysicsComponent? bodyB)
Fixture? fixtureB, int indexB)
{
var uidA = entA.Owner;
var uidB = entB.Owner;
contact.Enabled = enabled;
contact.IsTouching = false;
DebugTools.Assert(contact.Flags is ContactFlags.None or ContactFlags.PreInit or ContactFlags.Deleted);
@@ -155,8 +157,11 @@ public abstract partial class SharedPhysicsSystem
contact.FixtureA = fixtureA;
contact.FixtureB = fixtureB;
contact.BodyA = bodyA;
contact.BodyB = bodyB;
contact.BodyA = entA.Comp1;
contact.BodyB = entB.Comp1;
contact.XformA = entA.Comp2;
contact.XformB = entB.Comp2;
contact.ChildIndexA = indexA;
contact.ChildIndexB = indexB;
@@ -213,11 +218,10 @@ public abstract partial class SharedPhysicsSystem
}
private Contact CreateContact(
EntityUid uidA, EntityUid uidB,
Entity<PhysicsComponent?, TransformComponent?> entA, Entity<PhysicsComponent?, TransformComponent?> entB,
string fixtureAId, string fixtureBId,
Fixture fixtureA, int indexA,
Fixture fixtureB, int indexB,
PhysicsComponent bodyA, PhysicsComponent bodyB)
Fixture fixtureB, int indexB)
{
var type1 = fixtureA.Shape.ShapeType;
var type2 = fixtureB.Shape.ShapeType;
@@ -233,11 +237,11 @@ public abstract partial class SharedPhysicsSystem
// Edge+Polygon is non-symmetrical due to the way Erin handles collision type registration.
if ((type1 >= type2 || (type1 == ShapeType.Edge && type2 == ShapeType.Polygon)) && !(type2 == ShapeType.Edge && type1 == ShapeType.Polygon))
{
SetContact(contact, true, uidA, uidB, fixtureAId, fixtureBId, fixtureA, indexA, fixtureB, indexB, bodyA, bodyB);
SetContact(contact, true, entA, entB, fixtureAId, fixtureBId, fixtureA, indexA, fixtureB, indexB);
}
else
{
SetContact(contact, true, uidB, uidA, fixtureBId, fixtureAId, fixtureB, indexB, fixtureA, indexA, bodyB, bodyA);
SetContact(contact, true, entB, entA, fixtureBId, fixtureAId, fixtureB, indexB, fixtureA, indexA);
}
contact.Type = _registers[(int)type1, (int)type2];
@@ -249,7 +253,7 @@ public abstract partial class SharedPhysicsSystem
/// Try to create a contact between these 2 fixtures.
/// </summary>
internal void AddPair(
EntityUid uidA, EntityUid uidB,
Entity<PhysicsComponent, TransformComponent> entA, Entity<PhysicsComponent, TransformComponent> entB,
string fixtureAId, string fixtureBId,
Fixture fixtureA, int indexA,
Fixture fixtureB, int indexB,
@@ -264,16 +268,15 @@ public abstract partial class SharedPhysicsSystem
return;
DebugTools.Assert(!fixtureB.Contacts.ContainsKey(fixtureA));
var xformA = _xformQuery.GetComponent(uidA);
var xformB = _xformQuery.GetComponent(uidB);
var xformA = entA.Comp2;
var xformB = entB.Comp2;
// Does a joint override collision? Is at least one body dynamic?
if (!ShouldCollide(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB))
if (!ShouldCollide(entA.Owner, entB.Owner, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB))
return;
// Call the factory.
var contact = CreateContact(uidA, uidB, fixtureAId, fixtureBId, fixtureA, indexA, fixtureB, indexB, bodyA, bodyB);
var contact = CreateContact((entA.Owner, entA.Comp1, entA.Comp2), (entB.Owner, entB.Comp1, entB.Comp2), fixtureAId, fixtureBId, fixtureA, indexA, fixtureB, indexB);
contact.Flags = flags;
// Contact creation may swap fixtures.
@@ -294,6 +297,14 @@ public abstract partial class SharedPhysicsSystem
DebugTools.Assert(!fixB.Contacts.ContainsKey(fixA));
fixB.Contacts.Add(fixA, contact);
bodB.Contacts.AddLast(contact.BodyBNode);
// If it's a spawned static ent then need to wake any contacting entities.
// The issue is that static ents can never be awake and if it spawns on an asleep entity never gets a contact.
// Checking only bodyA should be okay because if bodyA is the other ent (i.e. dynamic / kinematic) then it should already be awake.
if (bodyA.BodyType == BodyType.Static && !bodyB.Awake)
{
WakeBody(entB.Owner, body: bodyB);
}
}
/// <summary>
@@ -301,7 +312,8 @@ public abstract partial class SharedPhysicsSystem
/// </summary>
internal void AddPair(string fixtureAId, string fixtureBId, in FixtureProxy proxyA, in FixtureProxy proxyB)
{
AddPair(proxyA.Entity, proxyB.Entity,
AddPair((proxyA.Entity, proxyA.Body, proxyA.Xform),
(proxyB.Entity, proxyB.Body, proxyB.Xform),
fixtureAId, fixtureBId,
proxyA.Fixture, proxyA.ChildIndex,
proxyB.Fixture, proxyB.ChildIndex,
@@ -421,8 +433,8 @@ public abstract partial class SharedPhysicsSystem
continue;
}
var xformA = _xformQuery.GetComponent(uidA);
var xformB = _xformQuery.GetComponent(uidB);
var xformA = contact.XformA!;
var xformB = contact.XformB!;
if (xformA.MapID == MapId.Nullspace || xformB.MapID == MapId.Nullspace)
{
@@ -518,8 +530,8 @@ public abstract partial class SharedPhysicsSystem
// Instead of transforming both boxes (which enlarges both aabbs), maybe just transform one box.
// I.e. use (matrixA * invMatrixB).TransformBox(). Or (invMatrixB * matrixA), whichever is correct.
// Alternatively, maybe just directly construct the relative transform matrix?
var proxyAWorldAABB = _transform.GetWorldMatrix(_xformQuery.GetComponent(broadphaseA.Value), _xformQuery).TransformBox(proxyA.AABB);
var proxyBWorldAABB = _transform.GetWorldMatrix(_xformQuery.GetComponent(broadphaseB.Value), _xformQuery).TransformBox(proxyB.AABB);
var proxyAWorldAABB = _transform.GetWorldMatrix(XformQuery.GetComponent(broadphaseA.Value)).TransformBox(proxyA.AABB);
var proxyBWorldAABB = _transform.GetWorldMatrix(XformQuery.GetComponent(broadphaseB.Value)).TransformBox(proxyB.AABB);
overlap = proxyAWorldAABB.Intersects(proxyBWorldAABB);
}
}
@@ -543,7 +555,7 @@ public abstract partial class SharedPhysicsSystem
}
var status = ArrayPool<ContactStatus>.Shared.Rent(index);
var worldPoints = ArrayPool<Vector2>.Shared.Rent(index);
var worldPoints = ArrayPool<FixedArray4<Vector2>>.Shared.Rent(index);
// Update contacts all at once.
BuildManifolds(contacts, index, status, worldPoints);
@@ -578,9 +590,11 @@ public abstract partial class SharedPhysicsSystem
var uidA = contact.EntityA;
var uidB = contact.EntityB;
var worldPoint = worldPoints[i];
var points = new FixedArray2<Vector2>(worldPoint._00, worldPoint._01);
var worldNormal = worldPoint._02;
var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, worldPoint);
var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, worldPoint);
var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, points, contact.Manifold.PointCount, worldNormal);
var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, points, contact.Manifold.PointCount, worldNormal);
RaiseLocalEvent(uidA, ref ev1, true);
RaiseLocalEvent(uidB, ref ev2, true);
@@ -618,10 +632,10 @@ public abstract partial class SharedPhysicsSystem
ArrayPool<Contact>.Shared.Return(contacts);
ArrayPool<ContactStatus>.Shared.Return(status);
ArrayPool<Vector2>.Shared.Return(worldPoints);
ArrayPool<FixedArray4<Vector2>>.Shared.Return(worldPoints);
}
private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, Vector2[] worldPoints)
private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, FixedArray4<Vector2>[] worldPoints)
{
if (count == 0)
return;
@@ -664,7 +678,7 @@ public abstract partial class SharedPhysicsSystem
public Contact[] Contacts;
public ContactStatus[] Status;
public Vector2[] WorldPoints;
public FixedArray4<Vector2>[] WorldPoints;
public bool[] Wake;
public void Execute(int index)
@@ -673,7 +687,7 @@ public abstract partial class SharedPhysicsSystem
}
}
private void UpdateContact(Contact[] contacts, int index, ContactStatus[] status, bool[] wake, Vector2[] worldPoints)
private void UpdateContact(Contact[] contacts, int index, ContactStatus[] status, bool[] wake, FixedArray4<Vector2>[] worldPoints)
{
var contact = contacts[index];
@@ -698,7 +712,11 @@ public abstract partial class SharedPhysicsSystem
if (contactStatus == ContactStatus.StartTouching)
{
worldPoints[index] = Physics.Transform.Mul(bodyATransform, contacts[index].Manifold.LocalPoint);
var points = new FixedArray4<Vector2>();
contact.GetWorldManifold(bodyATransform, bodyBTransform, out var worldNormal, points.AsSpan);
// Use the 3rd Vector2 as the world normal, 4th is blank.
points._02 = worldNormal;
worldPoints[index] = points;
}
}

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
@@ -125,8 +126,8 @@ public abstract partial class SharedPhysicsSystem
*/
private const int MaxIslands = 256;
private readonly ObjectPool<List<PhysicsComponent>> _islandBodyPool =
new DefaultObjectPool<List<PhysicsComponent>>(new ListPolicy<PhysicsComponent>(), MaxIslands);
private readonly ObjectPool<List<Entity<PhysicsComponent, TransformComponent>>> _islandBodyPool =
new DefaultObjectPool<List<Entity<PhysicsComponent, TransformComponent>>>(new ListPolicy<Entity<PhysicsComponent, TransformComponent>>(), MaxIslands);
private readonly ObjectPool<List<Contact>> _islandContactPool =
new DefaultObjectPool<List<Contact>>(new ListPolicy<Contact>(), MaxIslands);
@@ -140,11 +141,16 @@ public abstract partial class SharedPhysicsSystem
internal record struct IslandData(
int Index,
bool LoneIsland,
List<PhysicsComponent> Bodies,
List<Entity<PhysicsComponent, TransformComponent>> Bodies,
List<Contact> Contacts,
List<(Joint Original, Joint Joint)> Joints,
List<(Joint Joint, float Error)> BrokenJoints)
{
/// <summary>
/// MapUid for the island.
/// </summary>
public EntityUid MapUid;
/// <summary>
/// Island index to be used for bodies to identify which island they're in.
/// </summary>
@@ -161,7 +167,7 @@ public abstract partial class SharedPhysicsSystem
/// </summary>
public int Offset = 0;
public readonly List<PhysicsComponent> Bodies = Bodies;
public readonly List<Entity<PhysicsComponent, TransformComponent>> Bodies = Bodies;
public readonly List<Contact> Contacts = Contacts;
public readonly List<(Joint Original, Joint Joint)> Joints = Joints;
public bool PositionSolved = false;
@@ -169,9 +175,9 @@ public abstract partial class SharedPhysicsSystem
}
// Caching for island generation.
private readonly HashSet<PhysicsComponent> _islandSet = new(64);
private readonly Stack<PhysicsComponent> _bodyStack = new(64);
private readonly List<PhysicsComponent> _awakeBodyList = new(256);
private readonly HashSet<Entity<PhysicsComponent, TransformComponent>> _islandSet = new(64);
private readonly Stack<Entity<PhysicsComponent, TransformComponent>> _bodyStack = new(64);
private readonly List<Entity<PhysicsComponent, TransformComponent>> _awakeBodyList = new(256);
// Config
private bool _warmStarting;
@@ -260,47 +266,50 @@ public abstract partial class SharedPhysicsSystem
/// <summary>
/// Where the magic happens.
/// </summary>
public void Step(EntityUid uid, PhysicsMapComponent component, float frameTime, bool prediction)
public void Step(float frameTime, bool prediction)
{
var invDt = frameTime > 0.0f ? 1.0f / frameTime : 0.0f;
var dtRatio = component._invDt0 * frameTime;
var dtRatio = _invDt0 * frameTime;
// Integrate velocities, solve velocity constraints, and do integration.
Solve(uid, component, frameTime, dtRatio, invDt, prediction);
Solve(frameTime, dtRatio, invDt, prediction);
// TODO: SolveTOI
var updateAfterSolve = new PhysicsUpdateAfterMapSolveEvent(prediction, component, frameTime);
RaiseLocalEvent(ref updateAfterSolve);
// Box2d recommends clearing (if you are) during fixed updates rather than variable if you are using it
if (!prediction && component.AutoClearForces)
ClearForces(component);
if (_autoClearForces)
ClearForces();
component._invDt0 = invDt;
_invDt0 = invDt;
}
private void ClearForces(PhysicsMapComponent component)
private void ClearForces()
{
foreach (var body in component.AwakeBodies)
foreach (var ent in AwakeBodies)
{
// TODO: Netsync
body.Force = Vector2.Zero;
body.Torque = 0.0f;
var uid = ent.Owner;
var body = ent.Comp1;
if (body.Force != Vector2.Zero)
{
body.Force = Vector2.Zero;
DirtyField(uid, body, nameof(PhysicsComponent.Force));
}
if (body.Torque != 0f)
{
body.Torque = 0f;
DirtyField(uid, body, nameof(PhysicsComponent.Torque));
}
}
}
private void Solve(EntityUid uid, PhysicsMapComponent component, float frameTime, float dtRatio, float invDt, bool prediction)
private void Solve(float frameTime, float dtRatio, float invDt, bool prediction)
{
// Build and simulated islands from awake bodies.
_bodyStack.EnsureCapacity(component.AwakeBodies.Count);
_islandSet.EnsureCapacity(component.AwakeBodies.Count);
_awakeBodyList.AddRange(component.AwakeBodies);
var bodyQuery = GetEntityQuery<PhysicsComponent>();
var metaQuery = GetEntityQuery<MetaDataComponent>();
var jointQuery = GetEntityQuery<JointComponent>();
var jointRelayQuery = GetEntityQuery<JointRelayTargetComponent>();
_bodyStack.EnsureCapacity(AwakeBodies.Count);
_islandSet.EnsureCapacity(AwakeBodies.Count);
_awakeBodyList.AddRange(AwakeBodies);
var islandIndex = 0;
var loneIsland = new IslandData(
@@ -315,18 +324,28 @@ public abstract partial class SharedPhysicsSystem
var islandJoints = new List<(Joint Original, Joint Joint)>();
// Build the relevant islands / graphs for all bodies.
foreach (var seed in _awakeBodyList)
foreach (var ent in _awakeBodyList)
{
var xform = ent.Comp2;
var seed = ent.Comp1;
// I tried not running prediction for non-contacted entities but unfortunately it looked like shit
// when contact broke so if you want to try that then GOOD LUCK.
if (seed.Island) continue;
var seedUid = seed.Owner;
var mapUid = xform.MapUid;
if (!metaQuery.TryGetComponent(seedUid, out var metadata))
// TODO: Handle this on client.
if (mapUid == null)
{
continue;
}
if (!EntityManager.MetaQuery.TryGetComponent(seedUid, out var metadata))
{
Log.Error($"Found deleted entity {ToPrettyString(seedUid)} on map!");
RemoveSleepBody(seedUid, seed, component);
RemoveSleepBody(ent);
continue;
}
@@ -342,16 +361,18 @@ public abstract partial class SharedPhysicsSystem
var bodies = _islandBodyPool.Get();
var contacts = _islandContactPool.Get();
var joints = _islandJointPool.Get();
_bodyStack.Push(seed);
_bodyStack.Push(ent);
seed.Island = true;
while (_bodyStack.TryPop(out var body))
while (_bodyStack.TryPop(out var bodyEnt))
{
var bodyUid = body.Owner;
var bodyUid = bodyEnt.Owner;
var body = bodyEnt.Comp1;
bodies.Add(body);
_islandSet.Add(body);
bodies.Add(bodyEnt);
_islandSet.Add(bodyEnt);
// Static bodies don't propagate islands
if (body.BodyType == BodyType.Static) continue;
@@ -377,24 +398,24 @@ public abstract partial class SharedPhysicsSystem
contacts.Add(contact);
contact.Flags |= ContactFlags.Island;
var bodyA = contact.BodyA!;
var bodyB = contact.BodyB!;
var other = bodyA == body ? bodyB : bodyA;
var other = contact.OtherBody(bodyUid);
// Was the other body already added to this island?
if (other.Island) continue;
_bodyStack.Push(other);
var otherEnt = contact.OtherEnt(bodyUid);
var otherXform = contact.OtherTransform(bodyUid);
// TODO: Store this transform on the component directly.
_bodyStack.Push(new Entity<PhysicsComponent, TransformComponent>(otherEnt, other, otherXform));
other.Island = true;
}
// Handle joints
if (jointRelayQuery.TryGetComponent(bodyUid, out var relayComp))
if (RelayTargetQuery.TryGetComponent(bodyUid, out var relayComp))
{
foreach (var relay in relayComp.Relayed)
{
if (!jointQuery.TryGetComponent(relay, out var jointComp))
if (!JointQuery.TryGetComponent(relay, out var jointComp))
continue;
foreach (var joint in jointComp.GetJoints.Values)
@@ -406,14 +427,14 @@ public abstract partial class SharedPhysicsSystem
var uidB = joint.BodyBUid;
DebugTools.AssertNotEqual(uidA, uidB);
if (jointQuery.TryGetComponent(uidA, out var jointCompA) &&
if (JointQuery.TryGetComponent(uidA, out var jointCompA) &&
jointCompA.Relay != null)
{
DebugTools.AssertNotEqual(uidB, jointCompA.Relay.Value);
uidA = jointCompA.Relay.Value;
}
if (jointQuery.TryGetComponent(uidB, out var jointCompB) &&
if (JointQuery.TryGetComponent(uidB, out var jointCompB) &&
jointCompB.Relay != null)
{
DebugTools.AssertNotEqual(uidA, jointCompB.Relay.Value);
@@ -427,7 +448,7 @@ public abstract partial class SharedPhysicsSystem
}
}
if (jointQuery.TryGetComponent(bodyUid, out var jointComponent) &&
if (JointQuery.TryGetComponent(bodyUid, out var jointComponent) &&
jointComponent.Relay == null)
{
foreach (var joint in jointComponent.Joints.Values)
@@ -438,13 +459,13 @@ public abstract partial class SharedPhysicsSystem
var uidA = joint.BodyAUid;
var uidB = joint.BodyBUid;
if (jointQuery.TryGetComponent(uidA, out var jointCompA) &&
if (JointQuery.TryGetComponent(uidA, out var jointCompA) &&
jointCompA.Relay != null)
{
uidA = jointCompA.Relay.Value;
}
if (jointQuery.TryGetComponent(uidB, out var jointCompB) &&
if (JointQuery.TryGetComponent(uidB, out var jointCompB) &&
jointCompB.Relay != null)
{
uidB = jointCompB.Relay.Value;
@@ -458,8 +479,9 @@ public abstract partial class SharedPhysicsSystem
foreach (var (original, joint) in islandJoints)
{
var bodyA = bodyQuery.GetComponent(joint.BodyAUid);
var bodyB = bodyQuery.GetComponent(joint.BodyBUid);
// TODO: Same here store physicscomp + transform on the joint, the savings are worth it.
var bodyA = PhysicsQuery.GetComponent(joint.BodyAUid);
var bodyB = PhysicsQuery.GetComponent(joint.BodyBUid);
if (!bodyA.CanCollide || !bodyB.CanCollide)
continue;
@@ -468,13 +490,13 @@ public abstract partial class SharedPhysicsSystem
if (!bodyA.Island)
{
_bodyStack.Push(bodyA);
_bodyStack.Push(new Entity<PhysicsComponent, TransformComponent>(joint.BodyAUid, bodyA, XformQuery.GetComponent(joint.BodyAUid)));
bodyA.Island = true;
}
if (!bodyB.Island)
{
_bodyStack.Push(bodyB);
_bodyStack.Push(new Entity<PhysicsComponent, TransformComponent>(joint.BodyBUid, bodyB, XformQuery.GetComponent(joint.BodyBUid)));
bodyB.Island = true;
}
}
@@ -487,13 +509,17 @@ public abstract partial class SharedPhysicsSystem
// Bodies not touching anything, hence we can just add it to the lone island.
if (contacts.Count == 0 && joints.Count == 0)
{
DebugTools.Assert(bodies.Count == 1 && bodies[0].BodyType != BodyType.Static);
DebugTools.Assert(bodies.Count == 1 && bodies[0].Comp1.BodyType != BodyType.Static);
loneIsland.MapUid = mapUid.Value;
loneIsland.Bodies.Add(bodies[0]);
idx = loneIsland.Index;
}
else
{
var data = new IslandData(islandIndex++, false, bodies, contacts, joints, new List<(Joint Joint, float Error)>());
var data = new IslandData(islandIndex++, false, bodies, contacts, joints, new List<(Joint Joint, float Error)>())
{
MapUid = mapUid.Value
};
islands.Add(data);
idx = data.Index;
}
@@ -501,7 +527,7 @@ public abstract partial class SharedPhysicsSystem
// Allow static bodies to be re-used in other islands
for (var i = 0; i < bodies.Count; i++)
{
var body = bodies[i];
var body = bodies[i].Comp1;
// Static bodies can participate in other islands
if (body.BodyType == BodyType.Static)
@@ -523,20 +549,21 @@ public abstract partial class SharedPhysicsSystem
ReturnIsland(loneIsland);
}
SolveIslands(uid, component, islands, frameTime, dtRatio, invDt, prediction);
SolveIslands(islands, frameTime, dtRatio, invDt, prediction);
foreach (var island in islands)
{
ReturnIsland(island);
}
Cleanup(component, frameTime);
Cleanup(frameTime);
}
private void ReturnIsland(IslandData island)
{
foreach (var body in island.Bodies)
foreach (var bodyEnt in island.Bodies)
{
var body = bodyEnt.Comp1;
DebugTools.Assert(body.IslandIndex.ContainsKey(island.Index));
body.IslandIndex.Remove(island.Index);
}
@@ -559,10 +586,12 @@ public abstract partial class SharedPhysicsSystem
island.BrokenJoints.Clear();
}
protected virtual void Cleanup(PhysicsMapComponent component, float frameTime)
protected virtual void Cleanup(float frameTime)
{
foreach (var body in _islandSet)
foreach (var bodyEnt in _islandSet)
{
var body = bodyEnt.Comp1;
if (!body.Island || body.Deleted)
{
continue;
@@ -574,15 +603,14 @@ public abstract partial class SharedPhysicsSystem
// So Box2D would update broadphase here buutttt we'll just wait until MoveEvent queue is used.
}
_islandSet.Clear();
_islandSet.Clear();
_awakeBodyList.Clear();
}
private void SolveIslands(EntityUid uid, PhysicsMapComponent component, List<IslandData> islands, float frameTime, float dtRatio, float invDt, bool prediction)
private void SolveIslands(List<IslandData> islands, float frameTime, float dtRatio, float invDt, bool prediction)
{
var iBegin = 0;
var gravity = _gravity.GetGravity(uid);
var data = new SolverData(
frameTime,
dtRatio,
@@ -609,13 +637,12 @@ public abstract partial class SharedPhysicsSystem
var totalBodies = 0;
var actualIslands = islands.ToArray();
var xformQuery = GetEntityQuery<TransformComponent>();
for (var i = 0; i < islands.Count; i++)
{
ref var island = ref actualIslands[i];
island.Offset = totalBodies;
UpdateLerpData(component, island.Bodies, xformQuery);
UpdateLerpData(island.Bodies);
#if DEBUG
RaiseLocalEvent(new IslandSolveMessage(island.Bodies));
@@ -648,14 +675,14 @@ public abstract partial class SharedPhysicsSystem
if (!InternalParallel(island))
break;
SolveIsland(ref island, in data, options, gravity, prediction, solvedPositions, solvedAngles, linearVelocities, angularVelocities, sleepStatus);
SolveIsland(ref island, in data, options, prediction, solvedPositions, solvedAngles, linearVelocities, angularVelocities, sleepStatus);
iBegin++;
}
Parallel.For(iBegin, actualIslands.Length, options, i =>
{
ref var island = ref actualIslands[i];
SolveIsland(ref island, in data, null, gravity, prediction, solvedPositions, solvedAngles, linearVelocities, angularVelocities, sleepStatus);
SolveIsland(ref island, in data, null, prediction, solvedPositions, solvedAngles, linearVelocities, angularVelocities, sleepStatus);
});
// Update data sequentially
@@ -663,7 +690,7 @@ public abstract partial class SharedPhysicsSystem
{
var island = actualIslands[i];
UpdateBodies(in island, solvedPositions, solvedAngles, linearVelocities, angularVelocities, xformQuery);
UpdateBodies(in island, solvedPositions, solvedAngles, linearVelocities, angularVelocities);
SleepBodies(in island, sleepStatus);
}
@@ -679,7 +706,7 @@ public abstract partial class SharedPhysicsSystem
/// If this is the first time a body has been updated this tick update its position for lerping.
/// Due to substepping we have to check it every time.
/// </summary>
protected virtual void UpdateLerpData(PhysicsMapComponent component, List<PhysicsComponent> bodies, EntityQuery<TransformComponent> xformQuery)
protected virtual void UpdateLerpData(List<Entity<PhysicsComponent, TransformComponent>> bodies)
{
}
@@ -702,7 +729,6 @@ public abstract partial class SharedPhysicsSystem
ref IslandData island,
in SolverData data,
ParallelOptions? options,
Vector2 gravity,
bool prediction,
Vector2[] solvedPositions,
float[] solvedAngles,
@@ -714,13 +740,16 @@ public abstract partial class SharedPhysicsSystem
var positions = ArrayPool<Vector2>.Shared.Rent(bodyCount);
var angles = ArrayPool<float>.Shared.Rent(bodyCount);
var offset = island.Offset;
var xformQuery = GetEntityQuery<TransformComponent>();
var gravity = Gravity;
for (var i = 0; i < island.Bodies.Count; i++)
{
var body = island.Bodies[i];
var bodyEnt = island.Bodies[i];
var body = bodyEnt.Comp1;
var xform = bodyEnt.Comp2;
var (worldPos, worldRot) =
_transform.GetWorldPositionRotation(xformQuery.GetComponent(body.Owner), xformQuery);
_transform.GetWorldPositionRotation(xform);
var transform = new Transform(worldPos, worldRot);
var position = Physics.Transform.Mul(transform, body.LocalCenter);
@@ -737,9 +766,13 @@ public abstract partial class SharedPhysicsSystem
if (body.BodyType == BodyType.Dynamic)
{
if (body.IgnoreGravity)
{
linearVelocity += body.Force * data.FrameTime * body.InvMass;
}
else
{
linearVelocity += (gravity + body.Force * body.InvMass) * data.FrameTime;
}
angularVelocity += body.InvI * body.Torque * data.FrameTime;
@@ -768,7 +801,6 @@ public abstract partial class SharedPhysicsSystem
}
var jointCount = island.Joints.Count;
var bodyQuery = GetEntityQuery<PhysicsComponent>();
if (jointCount > 0)
{
@@ -777,8 +809,8 @@ public abstract partial class SharedPhysicsSystem
var joint = island.Joints[i].Joint;
if (!joint.Enabled) continue;
var bodyA = bodyQuery.GetComponent(joint.BodyAUid);
var bodyB = bodyQuery.GetComponent(joint.BodyBUid);
var bodyA = PhysicsQuery.GetComponent(joint.BodyAUid);
var bodyB = PhysicsQuery.GetComponent(joint.BodyBUid);
joint.InitVelocityConstraints(in data, in island, bodyA, bodyB, positions, angles, linearVelocities, angularVelocities);
}
}
@@ -880,12 +912,12 @@ public abstract partial class SharedPhysicsSystem
var start = i * FinaliseBodies;
var end = Math.Min(bodyCount, start + FinaliseBodies);
FinalisePositions(start, end, offset, bodies, xformQuery, positions, angles, solvedPositions, solvedAngles);
FinalisePositions(start, end, offset, bodies, positions, angles, solvedPositions, solvedAngles);
});
}
else
{
FinalisePositions(0, bodyCount, offset, bodies,xformQuery, positions, angles, solvedPositions, solvedAngles);
FinalisePositions(0, bodyCount, offset, bodies, positions, angles, solvedPositions, solvedAngles);
}
// Check sleep status for all of the bodies
@@ -899,7 +931,7 @@ public abstract partial class SharedPhysicsSystem
{
for (var i = 0; i < bodyCount; i++)
{
var body = island.Bodies[i];
var body = island.Bodies[i].Comp1;
if (body.BodyType == BodyType.Static) continue;
@@ -930,7 +962,7 @@ public abstract partial class SharedPhysicsSystem
for (var i = 0; i < bodyCount; i++)
{
var body = island.Bodies[i];
var body = island.Bodies[i].Comp1;
if (body.BodyType == BodyType.Static) continue;
@@ -965,18 +997,18 @@ public abstract partial class SharedPhysicsSystem
ArrayPool<ContactPositionConstraint>.Shared.Return(positionConstraints);
}
private void FinalisePositions(int start, int end, int offset, List<PhysicsComponent> bodies, EntityQuery<TransformComponent> xformQuery, Vector2[] positions, float[] angles, Vector2[] solvedPositions, float[] solvedAngles)
private void FinalisePositions(int start, int end, int offset, List<Entity<PhysicsComponent, TransformComponent>> bodies, Vector2[] positions, float[] angles, Vector2[] solvedPositions, float[] solvedAngles)
{
for (var i = start; i < end; i++)
{
var body = bodies[i];
var ent = bodies[i];
var body = ent.Comp1;
if (body.BodyType == BodyType.Static)
continue;
var xform = xformQuery.GetComponent(body.Owner);
var parentXform = xformQuery.GetComponent(xform.ParentUid);
var (_, parentRot, parentInvMatrix) = parentXform.GetWorldPositionRotationInvMatrix(xformQuery);
var xform = ent.Comp2;
var (_, parentRot, parentInvMatrix) = _transform.GetWorldPositionRotationInvMatrix(xform.ParentUid);
var worldRot = (float) (parentRot + xform._localRotation);
var angle = angles[i];
@@ -999,8 +1031,7 @@ public abstract partial class SharedPhysicsSystem
Vector2[] positions,
float[] angles,
Vector2[] linearVelocities,
float[] angularVelocities,
EntityQuery<TransformComponent> xformQuery)
float[] angularVelocities)
{
foreach (var (joint, error) in island.BrokenJoints)
{
@@ -1015,16 +1046,17 @@ public abstract partial class SharedPhysicsSystem
for (var i = 0; i < island.Bodies.Count; i++)
{
var body = island.Bodies[i];
var bodyEnt = island.Bodies[i];
var body = bodyEnt.Comp1;
// So technically we don't /need/ to skip static bodies here but it saves us having to check for deferred updates so we'll do it anyway.
// Plus calcing worldpos can be costly so we skip that too which is nice.
if (body.BodyType == BodyType.Static) continue;
var uid = body.Owner;
var uid = bodyEnt.Owner;
var position = positions[offset + i];
var angle = angles[offset + i];
var xform = xformQuery.GetComponent(uid);
var xform = bodyEnt.Comp2;
var linVelocity = linearVelocities[offset + i];
var physicsDirtied = false;
@@ -1052,7 +1084,9 @@ public abstract partial class SharedPhysicsSystem
}
if (physicsDirtied)
{
Dirty(uid, body);
}
}
}

View File

@@ -9,45 +9,36 @@ public partial class SharedPhysicsSystem
{
#region AddRemove
internal void AddAwakeBody(EntityUid uid, PhysicsComponent body, PhysicsMapComponent? map = null)
internal void AddAwakeBody(Entity<PhysicsComponent, TransformComponent> ent)
{
if (map == null)
return;
var body = ent.Comp1;
if (!body.CanCollide)
{
Log.Error($"Tried to add non-colliding {ToPrettyString(uid)} as an awake body to map!");
Log.Error($"Tried to add non-colliding {ToPrettyString(ent)} as an awake body to map!");
DebugTools.Assert(false);
return;
}
if (body.BodyType == BodyType.Static)
{
Log.Error($"Tried to add static body {ToPrettyString(uid)} as an awake body to map!");
Log.Error($"Tried to add static body {ToPrettyString(ent)} as an awake body to map!");
DebugTools.Assert(false);
return;
}
DebugTools.Assert(body.Awake);
map.AwakeBodies.Add(body);
DebugTools.Assert(!AwakeBodies.Contains(ent));
AwakeBodies.Add(ent);
}
internal void AddAwakeBody(EntityUid uid, PhysicsComponent body, EntityUid mapUid, PhysicsMapComponent? map = null)
internal void RemoveSleepBody(Entity<PhysicsComponent, TransformComponent> ent)
{
PhysMapQuery.Resolve(mapUid, ref map, false);
AddAwakeBody(uid, body, map);
DebugTools.Assert(!ent.Comp1.Awake);
DebugTools.Assert(AwakeBodies.Contains(ent));
AwakeBodies.Remove(ent);
}
internal void RemoveSleepBody(EntityUid uid, PhysicsComponent body, PhysicsMapComponent? map = null)
{
map?.AwakeBodies.Remove(body);
}
internal void RemoveSleepBody(EntityUid uid, PhysicsComponent body, EntityUid mapUid, PhysicsMapComponent? map = null)
{
PhysMapQuery.Resolve(mapUid, ref map, false);
RemoveSleepBody(uid, body, map);
}
#endregion
}

View File

@@ -27,10 +27,9 @@ public abstract partial class SharedPhysicsSystem
shape.Radius = radius;
if (body.CanCollide &&
TryComp<BroadphaseComponent>(xform.Broadphase?.Uid, out var broadphase) &&
TryComp<PhysicsMapComponent>(xform.MapUid, out var physicsMap))
TryComp<BroadphaseComponent>(xform.Broadphase?.Uid, out var broadphase))
{
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase, physicsMap);
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase);
_lookup.CreateProxies(uid, fixtureId, fixture, xform, body);
}
@@ -58,10 +57,9 @@ public abstract partial class SharedPhysicsSystem
shape.Radius = radius;
if (body.CanCollide &&
TryComp<BroadphaseComponent>(xform.Broadphase?.Uid, out var broadphase) &&
TryComp<PhysicsMapComponent>(xform.MapUid, out var physicsMap))
TryComp<BroadphaseComponent>(xform.Broadphase?.Uid, out var broadphase))
{
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase, physicsMap);
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase);
_lookup.CreateProxies(uid, fixtureId, fixture, xform, body);
}
@@ -84,10 +82,9 @@ public abstract partial class SharedPhysicsSystem
circle.Position = position;
if (body.CanCollide &&
TryComp<BroadphaseComponent>(xform.Broadphase?.Uid, out var broadphase) &&
TryComp<PhysicsMapComponent>(xform.MapUid, out var physicsMap))
TryComp<BroadphaseComponent>(xform.Broadphase?.Uid, out var broadphase))
{
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase, physicsMap);
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase);
_lookup.CreateProxies(uid, fixtureId, fixture, xform, body);
}
@@ -120,10 +117,9 @@ public abstract partial class SharedPhysicsSystem
edge.Vertex3 = vertex3;
if (body.CanCollide &&
TryComp<BroadphaseComponent>(xform.Broadphase?.Uid, out var broadphase) &&
TryComp<PhysicsMapComponent>(xform.MapUid, out var physicsMap))
TryComp<BroadphaseComponent>(xform.Broadphase?.Uid, out var broadphase))
{
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase, physicsMap);
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase);
_lookup.CreateProxies(uid, fixtureId, fixture, xform, body);
}
@@ -150,10 +146,9 @@ public abstract partial class SharedPhysicsSystem
poly.Set(vertices, vertices.Length);
if (body.CanCollide &&
TryComp<BroadphaseComponent>(xform.Broadphase?.Uid, out var broadphase) &&
TryComp<PhysicsMapComponent>(xform.MapUid, out var physicsMap))
TryComp<BroadphaseComponent>(xform.Broadphase?.Uid, out var broadphase))
{
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase, physicsMap);
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase);
_lookup.CreateProxies(uid, fixtureId, fixture, xform, body);
}

View File

@@ -27,7 +27,7 @@ public abstract partial class SharedPhysicsSystem
if (!PhysicsQuery.Resolve(uid, ref component))
return Vector2.Zero;
if (!_xformQuery.Resolve(uid, ref xform))
if (!XformQuery.Resolve(uid, ref xform))
return Vector2.Zero;
var velocity = component.LinearVelocity;
@@ -56,7 +56,7 @@ public abstract partial class SharedPhysicsSystem
{
// Could make this a method with the below one but ehh
// then you get a method bigger than this block with a billion out args and who wants that.
var xform = _xformQuery.GetComponent(parent);
var xform = XformQuery.GetComponent(parent);
if (PhysicsQuery.TryGetComponent(parent, out var body))
{
@@ -86,9 +86,11 @@ public abstract partial class SharedPhysicsSystem
PhysicsComponent? component = null,
TransformComponent? xform = null)
{
if (!_xformQuery.Resolve(uid, ref xform))
if (!XformQuery.Resolve(uid, ref xform))
return Vector2.Zero;
PhysicsQuery.Resolve(uid, ref component, false);
var parent = xform.ParentUid;
var localPos = xform.LocalPosition;
@@ -97,7 +99,7 @@ public abstract partial class SharedPhysicsSystem
while (parent != xform.MapUid && parent.IsValid())
{
xform = _xformQuery.GetComponent(parent);
xform = XformQuery.GetComponent(parent);
if (PhysicsQuery.TryGetComponent(parent, out var body))
{
@@ -131,20 +133,19 @@ public abstract partial class SharedPhysicsSystem
PhysicsComponent? component = null,
TransformComponent? xform = null)
{
if (!PhysicsQuery.Resolve(uid, ref component))
return 0;
if (!_xformQuery.Resolve(uid, ref xform))
if (!XformQuery.Resolve(uid, ref xform))
return 0f;
var angularVelocity = component.AngularVelocity;
PhysicsQuery.Resolve(uid, ref component, false);
var angularVelocity = component?.AngularVelocity ?? 0;
while (xform.ParentUid != xform.MapUid && xform.ParentUid.IsValid())
{
if (PhysicsQuery.TryGetComponent(xform.ParentUid, out var body))
angularVelocity += body.AngularVelocity;
xform = _xformQuery.GetComponent(xform.ParentUid);
xform = XformQuery.GetComponent(xform.ParentUid);
}
return angularVelocity;
@@ -160,23 +161,22 @@ public abstract partial class SharedPhysicsSystem
PhysicsComponent? component = null,
TransformComponent? xform = null)
{
if (!PhysicsQuery.Resolve(uid, ref component))
if (!XformQuery.Resolve(uid, ref xform))
return (Vector2.Zero, 0);
if (!_xformQuery.Resolve(uid, ref xform))
return (Vector2.Zero, 0);
PhysicsQuery.Resolve(uid, ref component, false);
var parent = xform.ParentUid;
var localPos = xform.LocalPosition;
var linearVelocity = component.LinearVelocity;
var angularVelocity = component.AngularVelocity;
var linearVelocity = component?.LinearVelocity ?? Vector2.Zero;
var angularVelocity = component?.AngularVelocity ?? 0;
Vector2 linearVelocityAngularContribution = Vector2.Zero;
while (parent != xform.MapUid && parent.IsValid())
{
xform = _xformQuery.GetComponent(parent);
xform = XformQuery.GetComponent(parent);
if (PhysicsQuery.TryGetComponent(parent, out var body))
{
@@ -233,7 +233,7 @@ public abstract partial class SharedPhysicsSystem
}
var parent = oldParent;
TransformComponent? parentXform = _xformQuery.GetComponent(parent);
TransformComponent? parentXform = XformQuery.GetComponent(parent);
var localPos = Vector2.Transform(_transform.GetWorldPosition(xform), _transform.GetInvWorldMatrix(parentXform));
var oldLinear = physics.LinearVelocity;
@@ -258,7 +258,7 @@ public abstract partial class SharedPhysicsSystem
localPos = parentXform.LocalPosition + parentXform.LocalRotation.RotateVec(localPos);
parent = parentXform.ParentUid;
} while (parent.IsValid() && _xformQuery.TryGetComponent(parent, out parentXform));
} while (parent.IsValid() && XformQuery.TryGetComponent(parent, out parentXform));
oldLinear += linearAngularContribution;

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Physics.Systems;
public partial class SharedPhysicsSystem
{
[ViewVariables]
public Vector2 Gravity { get; private set; }
public void SetGravity(Vector2 value)
{
if (Gravity.Equals(value))
return;
Gravity = value;
// TODO: Network + datafield
}
// Box2D has a bunch of methods that work on worlds but in our case separate EntityManager instances are
// separate worlds so we can just treat the physics system as the world.
private bool _autoClearForces;
/// <summary>
/// When substepping the client needs to know about the first position to use for lerping.
/// </summary>
protected readonly Dictionary<EntityUid, EntityUid>
LerpData = new();
// TODO:
// - Add test that movebuffer removes entities moved to nullspace.
// Previously we stored the WorldAABB of the proxy being moved and tracked state.
// The issue is that if something moves multiple times in a tick it can add up, plus it's also done on hotpaths such as physics.
// As such we defer it until we actually try and get contacts, then we can run them in parallel.
/// <summary>
/// Keep a buffer of everything that moved in a tick. This will be used to check for physics contacts.
/// </summary>
[ViewVariables]
internal readonly HashSet<FixtureProxy> MoveBuffer = new();
/// <summary>
/// Track moved grids to know if we need to run checks for them driving over entities.
/// </summary>
[ViewVariables]
internal readonly HashSet<EntityUid> MovedGrids = new();
/// <summary>
/// All awake bodies in the game.
/// </summary>
[ViewVariables]
public readonly HashSet<Entity<PhysicsComponent, TransformComponent>> AwakeBodies = new();
/// <summary>
/// Store last tick's invDT
/// </summary>
private float _invDt0;
}

View File

@@ -4,13 +4,10 @@ using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
@@ -42,11 +39,9 @@ namespace Robust.Shared.Physics.Systems
Buckets = Histogram.ExponentialBuckets(0.000_001, 1.5, 25)
});
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IManifoldManager _manifoldManager = default!;
[Dependency] private readonly IParallelManager _parallel = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IDependencyCollection _deps = default!;
[Dependency] private readonly Gravity2DController _gravity = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
@@ -65,13 +60,14 @@ namespace Robust.Shared.Physics.Systems
public bool MetricsEnabled { get; protected set; }
private EntityQuery<FixturesComponent> _fixturesQuery;
protected EntityQuery<PhysicsComponent> PhysicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
private EntityQuery<CollideOnAnchorComponent> _anchorQuery;
protected EntityQuery<PhysicsMapComponent> PhysMapQuery;
private EntityQuery<FixturesComponent> _fixturesQuery;
private EntityQuery<JointComponent> JointQuery;
private EntityQuery<JointRelayTargetComponent> RelayTargetQuery;
private EntityQuery<MapGridComponent> _gridQuery;
protected EntityQuery<MapComponent> MapQuery;
protected EntityQuery<PhysicsComponent> PhysicsQuery;
protected EntityQuery<TransformComponent> XformQuery;
private ComponentRegistration _physicsReg = default!;
private byte _angularVelocityIndex;
@@ -106,18 +102,18 @@ namespace Robust.Shared.Physics.Systems
_angularVelocityIndex = 10;
_fixturesQuery = GetEntityQuery<FixturesComponent>();
PhysicsQuery = GetEntityQuery<PhysicsComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
_anchorQuery = GetEntityQuery<CollideOnAnchorComponent>();
PhysMapQuery = GetEntityQuery<PhysicsMapComponent>();
_gridQuery = GetEntityQuery<MapGridComponent>();
_fixturesQuery = GetEntityQuery<FixturesComponent>();
JointQuery = GetEntityQuery<JointComponent>();
RelayTargetQuery = GetEntityQuery<JointRelayTargetComponent>();
MapQuery = GetEntityQuery<MapComponent>();
_gridQuery = GetEntityQuery<MapGridComponent>();
PhysicsQuery = GetEntityQuery<PhysicsComponent>();
XformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<GridAddEvent>(OnGridAdd);
SubscribeLocalEvent<CollisionChangeEvent>(OnCollisionChange);
SubscribeLocalEvent<PhysicsComponent, EntGotRemovedFromContainerMessage>(HandleContainerRemoved);
SubscribeLocalEvent<PhysicsMapComponent, ComponentInit>(HandlePhysicsMapInit);
SubscribeLocalEvent<PhysicsComponent, ComponentInit>(OnPhysicsInit);
SubscribeLocalEvent<PhysicsComponent, ComponentShutdown>(OnPhysicsShutdown);
SubscribeLocalEvent<PhysicsComponent, ComponentGetState>(OnPhysicsGetState);
@@ -125,7 +121,7 @@ namespace Robust.Shared.Physics.Systems
InitializeIsland();
InitializeContacts();
Subs.CVar(_cfg, CVars.AutoClearForces, OnAutoClearChange);
Subs.CVar(_cfg, CVars.AutoClearForces, OnAutoClearChange, true);
Subs.CVar(_cfg, CVars.NetTickrate, UpdateSubsteps, true);
Subs.CVar(_cfg, CVars.TargetMinimumTickrate, UpdateSubsteps, true);
}
@@ -153,20 +149,9 @@ namespace Robust.Shared.Physics.Systems
}
}
private void HandlePhysicsMapInit(EntityUid uid, PhysicsMapComponent component, ComponentInit args)
{
_deps.InjectDependencies(component);
component.AutoClearForces = _cfg.GetCVar(CVars.AutoClearForces);
}
private void OnAutoClearChange(bool value)
{
var enumerator = AllEntityQuery<PhysicsMapComponent>();
while (enumerator.MoveNext(out var comp))
{
comp.AutoClearForces = value;
}
_autoClearForces = value;
}
private void UpdateSubsteps(int obj)
@@ -199,7 +184,7 @@ namespace Robust.Shared.Physics.Systems
if (oldMap != xform.MapUid)
{
// This will also handle broadphase updating & joint clearing.
HandleMapChange(uid, xform, body, oldMap, xform.MapUid);
HandleMapChange(uid, xform, body);
return;
}
@@ -210,11 +195,9 @@ namespace Robust.Shared.Physics.Systems
/// <summary>
/// Recursively add/remove from awake bodies, clear joints, remove from move buffer, and update broadphase.
/// </summary>
private void HandleMapChange(EntityUid uid, TransformComponent xform, PhysicsComponent? body, EntityUid? oldMapId, EntityUid? newMapId)
private void HandleMapChange(EntityUid uid, TransformComponent xform, PhysicsComponent? body)
{
PhysMapQuery.TryGetComponent(oldMapId, out var oldMap);
PhysMapQuery.TryGetComponent(newMapId, out var newMap);
RecursiveMapUpdate(uid, xform, body, newMap, oldMap);
RecursiveMapUpdate(uid, xform, body);
}
/// <summary>
@@ -223,33 +206,17 @@ namespace Robust.Shared.Physics.Systems
private void RecursiveMapUpdate(
EntityUid uid,
TransformComponent xform,
PhysicsComponent? body,
PhysicsMapComponent? newMap,
PhysicsMapComponent? oldMap)
PhysicsComponent? body)
{
DebugTools.Assert(!Deleted(uid));
// This entity may not have a body, but some of its children might:
if (body != null)
{
if (body.Awake)
{
RemoveSleepBody(uid, body, oldMap);
AddAwakeBody(uid, body, newMap);
DebugTools.Assert(body.Awake);
}
else
DebugTools.Assert(oldMap?.AwakeBodies.Contains(body) != true);
}
_joints.ClearJoints(uid);
foreach (var child in xform._children)
{
if (_xformQuery.TryGetComponent(child, out var childXform))
if (XformQuery.TryGetComponent(child, out var childXform))
{
PhysicsQuery.TryGetComponent(child, out var childBody);
RecursiveMapUpdate(child, childXform, childBody, newMap, oldMap);
RecursiveMapUpdate(child, childXform, childBody);
}
}
}
@@ -276,17 +243,6 @@ namespace Robust.Shared.Physics.Systems
ShutdownContacts();
}
private void UpdateMapAwakeState(EntityUid uid, PhysicsComponent body)
{
if (Transform(uid).MapUid is not {} map)
return;
if (body.Awake)
AddAwakeBody(uid, body, map);
else
RemoveSleepBody(uid, body, map);
}
private void HandleContainerRemoved(EntityUid uid, PhysicsComponent physics, EntGotRemovedFromContainerMessage message)
{
// If entity being deleted then the parent change will already be handled elsewhere and we don't want to re-add it to the map.
@@ -314,18 +270,11 @@ namespace Robust.Shared.Physics.Systems
var updateBeforeSolve = new PhysicsUpdateBeforeSolveEvent(prediction, frameTime);
RaiseLocalEvent(ref updateBeforeSolve);
var contactEnumerator = AllEntityQuery<PhysicsMapComponent, TransformComponent>();
// Find new contacts and (TODO: temporary) update any per-map virtual controllers
while (contactEnumerator.MoveNext(out var comp, out var xform))
{
// 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.
_broadphase.FindNewContacts(comp, xform.MapID);
var updateMapBeforeSolve = new PhysicsUpdateBeforeMapSolveEvent(prediction, comp, frameTime);
RaiseLocalEvent(ref updateMapBeforeSolve);
}
// 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.
_broadphase.FindNewContacts();
// TODO PHYSICS Fix Collision Mispredicts
// If a physics update induces a position update that brings fixtures into contact, the collision starts in the NEXT tick,
@@ -350,12 +299,8 @@ namespace Robust.Shared.Physics.Systems
// But that might be unnecessarily expensive for what are hopefully only infrequent mispredicts.
CollideContacts();
var enumerator = AllEntityQuery<PhysicsMapComponent>();
while (enumerator.MoveNext(out var uid, out var comp))
{
Step(uid, comp, frameTime, prediction);
}
Step(frameTime, prediction);
var updateAfterSolve = new PhysicsUpdateAfterSolveEvent(prediction, frameTime);
RaiseLocalEvent(ref updateAfterSolve);
@@ -363,12 +308,7 @@ namespace Robust.Shared.Physics.Systems
// On last substep (or main step where no substeps occured) we'll update all of the lerp data.
if (i == _substeps - 1)
{
enumerator = AllEntityQuery<PhysicsMapComponent>();
while (enumerator.MoveNext(out var comp))
{
FinalStep(comp);
}
FinalStep();
}
EffectiveCurTime = EffectiveCurTime.Value + TimeSpan.FromSeconds(frameTime);
@@ -377,7 +317,7 @@ namespace Robust.Shared.Physics.Systems
EffectiveCurTime = null;
}
protected virtual void FinalStep(PhysicsMapComponent component)
protected virtual void FinalStep()
{
}

View File

@@ -170,9 +170,9 @@ namespace Robust.Shared.Prototypes
[Obsolete("Pass in IComponentFactory")]
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component)
where T : IComponent
where T : IComponent, new()
{
var compName = IoCManager.Resolve<IComponentFactory>().GetComponentName(typeof(T));
var compName = IoCManager.Resolve<IComponentFactory>().GetComponentName<T>();
return TryGetComponent(compName, out component);
}
@@ -182,9 +182,9 @@ namespace Robust.Shared.Prototypes
return TryGetComponent(compName, out component);
}
public bool TryGetComponent<T>(string name, [NotNullWhen(true)] out T? component) where T : IComponent
public bool TryGetComponent<T>(string name, [NotNullWhen(true)] out T? component) where T : IComponent, new()
{
DebugTools.AssertEqual(IoCManager.Resolve<IComponentFactory>().GetComponentName(typeof(T)), name);
DebugTools.AssertEqual(IoCManager.Resolve<IComponentFactory>().GetComponentName<T>(), name);
if (!Components.TryGetValue(name, out var componentUnCast))
{

View File

@@ -45,8 +45,11 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Pro
IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<PrototypeFlags<T>>? instanceProvider = null)
{
if(instanceProvider != null)
Logger.Warning($"Provided value to a Read-call for a {nameof(PrototypeFlags<T>)}. Ignoring...");
if (instanceProvider != null)
{
var sawmill = dependencies.Resolve<ILogManager>().GetSawmill("szr");
sawmill.Warning($"Provided value to a Read-call for a {nameof(PrototypeFlags<T>)}. Ignoring...");
}
var flags = new List<string>(node.Sequence.Count);
@@ -78,8 +81,11 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Pro
IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<PrototypeFlags<T>>? instanceProvider = null)
{
if(instanceProvider != null)
Logger.Warning($"Provided value to a Read-call for a {nameof(PrototypeFlags<T>)}. Ignoring...");
if (instanceProvider != null)
{
var sawmill = dependencies.Resolve<ILogManager>().GetSawmill("szr");
sawmill.Warning($"Provided value to a Read-call for a {nameof(PrototypeFlags<T>)}. Ignoring...");
}
return new PrototypeFlags<T>(node.Value);
}

View File

@@ -153,7 +153,8 @@ public sealed class DictionarySerializer<TKey, TValue> :
{
if (instanceProvider != null)
{
Logger.Warning(
var sawmill = dependencies.Resolve<ILogManager>().GetSawmill("szr");
sawmill.Warning(
$"Provided value to a Read-call for a {nameof(FrozenDictionary<TKey, TValue>)}. Ignoring...");
}
@@ -180,7 +181,8 @@ public sealed class DictionarySerializer<TKey, TValue> :
{
if (instanceProvider != null)
{
Logger.Warning(
var sawmill = dependencies.Resolve<ILogManager>().GetSawmill("szr");
sawmill.Warning(
$"Provided value to a Read-call for a {nameof(IReadOnlyDictionary<TKey, TValue>)}. Ignoring...");
}

View File

@@ -45,7 +45,10 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic
SerializationHookContext hookCtx, ISerializationContext? context = null, ISerializationManager.InstantiationDelegate<FrozenSet<T>>? instanceProvider = null)
{
if (instanceProvider != null)
Logger.Warning($"Provided value to a Read-call for a {nameof(FrozenSet<T>)}. Ignoring...");
{
var sawmill = dependencies.Resolve<ILogManager>().GetSawmill("szr");
sawmill.Warning($"Provided value to a Read-call for a {nameof(FrozenSet<T>)}. Ignoring...");
}
var array = new T[node.Sequence.Count];
var i = 0;
@@ -65,7 +68,10 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic
ISerializationManager.InstantiationDelegate<ImmutableHashSet<T>>? instanceProvider)
{
if (instanceProvider != null)
Logger.Warning($"Provided value to a Read-call for a {nameof(ImmutableHashSet<T>)}. Ignoring...");
{
var sawmill = dependencies.Resolve<ILogManager>().GetSawmill("szr");
sawmill.Warning($"Provided value to a Read-call for a {nameof(ImmutableHashSet<T>)}. Ignoring...");
}
var set = ImmutableHashSet.CreateBuilder<T>();
foreach (var dataNode in node.Sequence)

View File

@@ -127,8 +127,11 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic
SerializationHookContext hookCtx, ISerializationContext? context,
ISerializationManager.InstantiationDelegate<IReadOnlyList<T>>? instanceProvider)
{
if(instanceProvider != null)
Logger.Warning($"Provided value to a Read-call for a {nameof(IReadOnlySet<T>)}. Ignoring...");
if (instanceProvider != null)
{
var sawmill = dependencies.Resolve<ILogManager>().GetSawmill("szr");
sawmill.Warning($"Provided value to a Read-call for a {nameof(IReadOnlySet<T>)}. Ignoring...");
}
var list = new List<T>();
@@ -146,8 +149,11 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic
SerializationHookContext hookCtx, ISerializationContext? context,
ISerializationManager.InstantiationDelegate<IReadOnlyCollection<T>>? instanceProvider)
{
if(instanceProvider != null)
Logger.Warning($"Provided value to a Read-call for a {nameof(IReadOnlyCollection<T>)}. Ignoring...");
if (instanceProvider != null)
{
var sawmill = dependencies.Resolve<ILogManager>().GetSawmill("szr");
sawmill.Warning($"Provided value to a Read-call for a {nameof(IReadOnlyCollection<T>)}. Ignoring...");
}
var list = new List<T>();
@@ -165,8 +171,11 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic
SerializationHookContext hookCtx, ISerializationContext? context,
ISerializationManager.InstantiationDelegate<ImmutableList<T>>? instanceProvider)
{
if(instanceProvider != null)
Logger.Warning($"Provided value to a Read-call for a {nameof(ImmutableList<T>)}. Ignoring...");
if (instanceProvider != null)
{
var sawmill = dependencies.Resolve<ILogManager>().GetSawmill("szr");
sawmill.Warning($"Provided value to a Read-call for a {nameof(ImmutableList<T>)}. Ignoring...");
}
var list = ImmutableList.CreateBuilder<T>();

View File

@@ -46,8 +46,9 @@ public sealed class PipedArgumentAttribute : Attribute;
[MeansImplicitUse]
public sealed class CommandArgumentAttribute : Attribute
{
public CommandArgumentAttribute(Type? customParser = null)
public CommandArgumentAttribute(Type? customParser = null, bool unparseable = false)
{
Unparseable = unparseable;
if (customParser == null)
return;
@@ -56,6 +57,13 @@ public sealed class CommandArgumentAttribute : Attribute
$"Custom parser {customParser.PrettyName()} does not inherit from {typeof(CustomTypeParser<>).PrettyName()}");
}
/// <summary>
/// Command initialization will validate that all of a command's arguments are parseable (i.e., have a type parser).
/// In some niche situations you might want to have a command that accepts unparseable arguments that must be
/// supplied via a toolshed variable or command block, in which case the check can be disabled via this property.
/// </summary>
public bool Unparseable { get; }
public Type? CustomParser { get; }
}

View File

@@ -11,8 +11,11 @@ internal sealed class TpCommand : ToolshedCommand
{
private SharedTransformSystem? _xform;
// TODO TOOLSHED
// add EntityCoordinates parser
[CommandImplementation("coords")]
public EntityUid TpCoords([PipedArgument] EntityUid teleporter, EntityCoordinates target)
public EntityUid TpCoords([PipedArgument] EntityUid teleporter, [CommandArgument(unparseable:true)] EntityCoordinates target)
{
_xform ??= GetSys<SharedTransformSystem>();
_xform.SetCoordinates(teleporter, target);
@@ -20,7 +23,7 @@ internal sealed class TpCommand : ToolshedCommand
}
[CommandImplementation("coords")]
public IEnumerable<EntityUid> TpCoords([PipedArgument] IEnumerable<EntityUid> teleporters, EntityCoordinates target)
public IEnumerable<EntityUid> TpCoords([PipedArgument] IEnumerable<EntityUid> teleporters, [CommandArgument(unparseable:true)] EntityCoordinates target)
=> teleporters.Select(x => TpCoords(x, target));
[CommandImplementation("to")]

View File

@@ -124,12 +124,12 @@ public abstract partial class ToolshedCommand
{
var hasAnyAttribute = false;
if (param.HasCustomAttribute<CommandArgumentAttribute>())
if (param.GetCustomAttribute<CommandArgumentAttribute>() is {} cmdAttr)
{
if (param.Name == null || !argNames.Add(param.Name))
throw new InvalidCommandImplementation($"Command arguments must have a unique name");
hasAnyAttribute = true;
ValidateArg(param);
ValidateArg(param, cmdAttr);
}
if (param.HasCustomAttribute<PipedArgumentAttribute>())
@@ -232,8 +232,18 @@ public abstract partial class ToolshedCommand
}
}
private void ValidateArg(ParameterInfo arg)
private void ValidateArg(ParameterInfo arg, CommandArgumentAttribute? cmdAttr = null)
{
if (cmdAttr == null || cmdAttr.CustomParser == null && !cmdAttr.Unparseable)
{
// This checks that each argument has a corresponding type parser, as people have sometimes created a command
// without realising that the type is unparseable.
var t = Nullable.GetUnderlyingType(arg.ParameterType) ?? arg.ParameterType;
var ignore = t.IsGenericType || t.IsArray || t.ContainsGenericParameters;
if (!ignore && Toolshed.GetParserForType(t) == null)
throw new InvalidCommandImplementation($"{Name} command argument of type {t.PrettyName()} has no type parser. You either need to add a type parser or explicitly mark the argument as unparseable.");
}
var isParams = arg.HasCustomAttribute<ParamArrayAttribute>();
if (!isParams)
return;

View File

@@ -231,7 +231,10 @@ public sealed partial class ToolshedManager
/// <returns>Success.</returns>
public bool TryParse<T>(ParserContext parserContext, [NotNullWhen(true)] out T? parsed)
{
var res = TryParse(parserContext, typeof(T), out var p);
var t = typeof(T);
t = Nullable.GetUnderlyingType(t) ?? t;
var res = TryParse(parserContext, t, out var p);
if (p is not null)
parsed = (T?) p;
else

Some files were not shown because too many files have changed in this diff Show More