Files
RobustToolbox/Robust.Shared/Physics/Systems/FixtureSystem.cs
T
83c2a1be11 [Dependency] source generator part 2 (#6550)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Apply dependencies generator changes to all code

* Release notes

* Preprocessor got hands

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:33 +02:00

414 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Systems
{
/// <summary>
/// Manages physics fixtures.
/// </summary>
public sealed partial class FixtureSystem : EntitySystem
{
#if DEBUG
[Dependency] private IGameTiming _timing = default!;
#endif
[Dependency] private EntityLookupSystem _lookup = default!;
[Dependency] private SharedBroadphaseSystem _broadphase = default!;
[Dependency] private SharedPhysicsSystem _physics = default!;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<FixturesComponent> _fixtureQuery;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FixturesComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<FixturesComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<FixturesComponent, ComponentHandleState>(OnHandleState);
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_fixtureQuery = GetEntityQuery<FixturesComponent>();
}
private void OnShutdown(EntityUid uid, FixturesComponent component, ComponentShutdown args)
{
// TODO: Need a better solution to this because the only reason I don't throw is that allcomponents test
// Yes it is actively making the game buggier but I would essentially double the size of this PR trying to fix it
// my best solution rn is move the broadphase property onto FixturesComponent and then refactor
// SharedBroadphaseSystem a LOT.
if (!_physicsQuery.TryGetComponent(uid, out var body))
return;
// Can't just get physicscomp on shutdown as it may be touched completely independently.
_physics.DestroyContacts(body);
}
#region Public
public bool TryCreateFixture(
EntityUid uid,
IPhysShape shape,
string id,
float density = PhysicsConstants.DefaultDensity,
bool hard = true,
int collisionLayer = 0,
int collisionMask = 0,
float friction = PhysicsConstants.DefaultContactFriction,
float restitution = PhysicsConstants.DefaultRestitution,
bool updates = true,
FixturesComponent? manager = null,
PhysicsComponent? body = null,
TransformComponent? xform = null)
{
if (!_physicsQuery.Resolve(uid, ref body) || !_fixtureQuery.Resolve(uid, ref manager))
return false;
if (manager.Fixtures.ContainsKey(id))
return false;
var fixture = new Fixture(shape, collisionLayer, collisionMask, hard, density, friction, restitution);
CreateFixture(uid, id, fixture, updates, manager, body, xform);
return true;
}
internal void CreateFixture(
EntityUid uid,
string fixtureId,
Fixture fixture,
bool updates = true,
FixturesComponent? manager = null,
PhysicsComponent? body = null,
TransformComponent? xform = null)
{
DebugTools.Assert(MetaData(uid).EntityLifeStage < EntityLifeStage.Terminating);
if (!_physicsQuery.Resolve(uid, ref body) || !_fixtureQuery.Resolve(uid, ref manager))
{
DebugTools.Assert(false);
return;
}
if (string.IsNullOrEmpty(fixtureId))
{
throw new InvalidOperationException($"Tried to create a fixture without an ID!");
}
manager.Fixtures.Add(fixtureId, fixture);
fixture.Owner = uid;
if (body.CanCollide && Resolve(uid, ref xform))
{
_lookup.CreateProxies(uid, fixtureId, fixture, xform, body);
}
// Supposed to be wrapped in density but eh
if (updates)
{
// Don't need to dirty here as we'll just manually call it after (we 100% need to call it).
FixtureUpdate(uid, false, manager: manager, body: body);
// Don't need to ResetMassData as FixtureUpdate already does it.
Dirty(uid, manager);
}
// TODO: Set newcontacts to true.
}
/// <summary>
/// Attempts to get the <see cref="Fixture"/> with the specified ID for this body.
/// </summary>
public Fixture? GetFixtureOrNull(EntityUid uid, string id, FixturesComponent? manager = null)
{
if (!_fixtureQuery.Resolve(uid, ref manager))
return null;
return manager.Fixtures.TryGetValue(id, out var fixture) ? fixture : null;
}
/// <summary>
/// Destroys the specified <see cref="Fixture"/> attached to the body.
/// </summary>
/// <param name="body">The specified body</param>
/// <param name="id">The fixture ID</param>
/// <param name="updates">Whether to update mass etc. Set false if you're doing a bulk operation</param>
public void DestroyFixture(
EntityUid uid,
string id,
bool updates = true,
PhysicsComponent? body = null,
FixturesComponent? manager = null,
TransformComponent? xform = null)
{
if (!_fixtureQuery.Resolve(uid, ref manager))
return;
var fixture = GetFixtureOrNull(uid, id, manager);
if (fixture != null)
DestroyFixture(uid, id, fixture, updates, body, manager, xform);
}
/// <summary>
/// Destroys the specified <see cref="Fixture"/>
/// </summary>
/// <param name="updates">Whether to update mass etc. Set false if you're doing a bulk operation</param>
public void DestroyFixture(
EntityUid uid,
string fixtureId,
Fixture fixture,
bool updates = true,
PhysicsComponent? body = null,
FixturesComponent? manager = null,
TransformComponent? xform = null)
{
if (!Resolve(uid, ref body, ref manager, ref xform))
{
return;
}
// TODO: Assert world locked
DebugTools.Assert(manager.FixtureCount > 0);
if (!manager.Fixtures.Remove(fixtureId))
{
Log.Error($"Tried to remove fixture from {ToPrettyString(uid)} that was already removed.");
return;
}
// Temporary debug block for trying to help catch a bug where grid fixtures disappear without the chunk's
// fixture set being updated
#if DEBUG
if (TryComp(uid, out MapGridComponent? grid) && !_timing.ApplyingState)
{
foreach (var chunk in grid.Chunks.Values)
{
DebugTools.Assert(!chunk.Fixtures.Contains(fixtureId), $"A grid fixture is being deleted without first removing it from the chunk. Please report this bug.");
}
}
#endif
foreach (var contact in fixture.Contacts.Values.ToArray())
{
_physics.DestroyContact(contact);
}
if (_lookup.TryGetCurrentBroadphase(xform, out var broadphase))
{
DebugTools.Assert(xform.MapUid == Transform(broadphase.Owner).MapUid);
_lookup.DestroyProxies(uid, fixtureId, fixture, xform, broadphase);
}
if (updates)
{
var resetMass = fixture.Density > 0f;
FixtureUpdate(uid, resetMass: resetMass, manager: manager, body: body);
}
}
#endregion
internal void OnPhysicsInit(EntityUid uid, FixturesComponent component, PhysicsComponent? body = null)
{
// Can't ACTUALLY add it to the broadphase here because transform is still in a transient dimension on the 5th plane
// hence we'll just make sure its body is set and SharedBroadphaseSystem will deal with it later.
if (Resolve(uid, ref body, false))
{
foreach (var (id, fixture) in component.Fixtures)
{
if (string.IsNullOrEmpty(id))
{
throw new InvalidOperationException($"Tried to setup fixture on init for {ToPrettyString(uid)} with no ID!");
}
fixture.Owner = uid;
}
// Make sure all the right stuff is set on the body
FixtureUpdate(uid, dirty: false, manager: component, body: body);
}
}
private void OnGetState(EntityUid uid, FixturesComponent component, ref ComponentGetState args)
{
var copied = new Dictionary<string, Fixture>(component.Fixtures.Count);
foreach (var (id, fixture) in component.Fixtures)
{
var copy = new Fixture();
fixture.CopyTo(copy);
copied[id] = copy;
}
args.State = new FixtureManagerComponentState()
{
Fixtures = copied,
};
}
private void OnHandleState(EntityUid uid, FixturesComponent component, ref ComponentHandleState args)
{
if (args.Current is not FixtureManagerComponentState state)
return;
if (!TryComp(uid, out PhysicsComponent? physics))
{
Log.Error($"Tried to apply fixture state for an entity without physics: {ToPrettyString(uid)}");
return;
}
var toAddFixtures = new ValueList<(string Id, Fixture Fixture)>();
var toRemoveFixtures = new ValueList<(string Id, Fixture Fixture)>();
var computeProperties = false;
// Given a bunch of data isn't serialized need to sort of re-initialise it
var newFixtures = new Dictionary<string, Fixture>(state.Fixtures.Count);
foreach (var (id, fixture) in state.Fixtures)
{
var newFixture = new Fixture();
fixture.CopyTo(newFixture);
newFixtures.Add(id, newFixture);
newFixture.Owner = uid;
}
TransformComponent? xform = null;
var regenerate = false;
// Add / update new fixtures
// FUTURE SLOTH
// Do not touch this or I WILL GLASS YOU.
// Updating fixtures in place causes prediction issues with contacts.
// See PR #3431 for when this started.
foreach (var (id, fixture) in newFixtures)
{
if (!component.Fixtures.TryGetValue(id, out var existing))
{
toAddFixtures.Add((id, fixture));
}
// Retained fixture but new data
else if (!existing.Equivalent(fixture))
{
fixture.CopyTo(existing);
computeProperties = true;
regenerate = true;
}
}
// Remove old fixtures
foreach (var (existingId, existing) in component.Fixtures)
{
if (!newFixtures.ContainsKey(existingId))
{
toRemoveFixtures.Add((existingId, existing));
}
}
// TODO add a DestroyFixture() override that takes in a list.
// reduced broadphase lookups
foreach (var (id, fixture) in toRemoveFixtures.Span)
{
computeProperties = true;
DestroyFixture(uid, id, fixture, false, physics, component);
}
// TODO: We also still need event listeners for shapes (Probably need C# events)
// Or we could just make it so shapes can only be updated via fixturesystem which handles it
// automagically (friends or something?)
foreach (var (id, fixture) in toAddFixtures.Span)
{
computeProperties = true;
CreateFixture(uid, id, fixture, false, component, physics, xform);
}
if (computeProperties)
{
FixtureUpdate(uid, manager: component, body: physics);
}
if (regenerate)
{
_broadphase.RegenerateContacts((uid, physics, component, xform));
}
}
#region Restitution
public void SetRestitution(EntityUid uid, string fixtureId, Fixture fixture, float value, bool update = true, FixturesComponent? manager = null)
{
fixture.Restitution = value;
if (update && Resolve(uid, ref manager))
FixtureUpdate(uid, manager: manager);
}
#endregion
/// <summary>
/// Updates all of the cached physics information on the body derived from fixtures.
/// </summary>
public void FixtureUpdate(EntityUid uid, bool dirty = true, bool resetMass = true, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!_physicsQuery.Resolve(uid, ref body) || !_fixtureQuery.Resolve(uid, ref manager))
return;
var mask = 0;
var layer = 0;
var hard = false;
foreach (var fixture in manager.Fixtures.Values)
{
mask |= fixture.CollisionMask;
layer |= fixture.CollisionLayer;
hard |= fixture.Hard;
}
if (resetMass)
_physics.ResetMassData(uid, manager, body);
// Save the old layer to see if an event should be raised later.
var oldLayer = body.CollisionLayer;
// Normally this method is called when fixtures need to be dirtied anyway so no point in returning early I think
body.CollisionMask = mask;
body.CollisionLayer = layer;
body.Hard = hard;
if (manager.FixtureCount == 0)
_physics.SetCanCollide(uid, false, manager: manager, body: body);
if (oldLayer != layer)
{
var ev = new CollisionLayerChangeEvent((uid, body));
RaiseLocalEvent(ref ev);
}
if (dirty)
Dirty(uid, manager);
}
public int GetFixtureCount(EntityUid uid, FixturesComponent? manager = null)
{
if (!_fixtureQuery.Resolve(uid, ref manager))
{
return 0;
}
return manager.FixtureCount;
}
[Serializable, NetSerializable]
private sealed class FixtureManagerComponentState : ComponentState
{
public Dictionary<string, Fixture> Fixtures = default!;
}
}
}