mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
21 Commits
serializat
...
v243.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a336d236b | ||
|
|
fc55c8e0d3 | ||
|
|
2719b9f0c8 | ||
|
|
bd69d51d36 | ||
|
|
1bf0687671 | ||
|
|
bdef9e3401 | ||
|
|
43648201ce | ||
|
|
2b2d08ba47 | ||
|
|
d7f6a9ba43 | ||
|
|
15f81751f7 | ||
|
|
033c52751a | ||
|
|
51edceae4d | ||
|
|
5d7720755a | ||
|
|
acc7bf7595 | ||
|
|
de55d1bc52 | ||
|
|
d5e6e91b58 | ||
|
|
da2bfdaa10 | ||
|
|
af6cac14d6 | ||
|
|
3f37846731 | ||
|
|
d9bf1d1afb | ||
|
|
b9b80192e7 |
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -54,6 +54,80 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 243.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* RemoveChild is called after OnClose for BaseWindow.
|
||||
|
||||
### New features
|
||||
|
||||
* BUIs now have their positions saved when closed and re-used when opened when using the `CreateWindow<T>` helper or via manually registering it via RegisterControl.
|
||||
|
||||
### Other
|
||||
|
||||
* Ensure grid fixtures get updated in client state handling even if exceptions occur.
|
||||
|
||||
|
||||
## 242.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed prototype reloading/hotloading not properly handling data-fields with the `AlwaysPushInheritanceAttribute`
|
||||
* Fix the pooled polygons using incorrect vertices for EntityLookup and MapManager.
|
||||
|
||||
### Internal
|
||||
|
||||
* Avoid normalizing angles constructed from vectors.
|
||||
|
||||
|
||||
## 242.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The order in which the client initialises networked entities has changed. It will now always apply component states, initialise, and start an entity's parent before processing any children. This might break anything that was relying on the old behaviour where all component states were applied before any entities were initialised & started.
|
||||
* `IClydeViewport` overlay rendering methods now take in an `IRenderHandle` instead of a world/screen handle.
|
||||
* The `OverlayDrawArgs` struct now has an internal constructor.
|
||||
|
||||
### New features
|
||||
|
||||
* Controls can now be manually restyled via `Control.InvalidateStyleSheet()` and `Control.DoStyleUpdate()`
|
||||
* Added `IUserInterfaceManager.RenderControl()` for manually drawing controls.
|
||||
* `OverlayDrawArgs` struct now has an `IRenderHandle` field such that overlays can use the new `RenderControl()` methods.
|
||||
* TileSpawnWindow will now take focus when opened.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed a client-side bug where `TransformComponent.GridUid` does not get set properly when an existing entity is attached to a new entity outside of the player's PVS range.
|
||||
* EntityPrototypeView will only create entities when it's on the UI tree and not when the prototype is set.
|
||||
* Make CollisionWake not log errors if it can't resolve.
|
||||
|
||||
### Other
|
||||
|
||||
* Replace IPhysShape API with generics on IMapManager and EntityLookupSystem.
|
||||
|
||||
### Internal
|
||||
|
||||
* Significantly reduce allocations for Box2 / Box2Rotated queries.
|
||||
|
||||
|
||||
## 241.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Remove DeferredClose from BUIs.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `EntityManager.DirtyFields()`, which allows components with delta states to simultaneously mark several fields as dirty at the same time.
|
||||
* Add `CloserUserUIs<T>` to close keys of a specific key.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed `RaisePredictiveEvent()` not properly re-raising events during prediction for event handlers that did not take an `EntitySessionEventArgs` argument.
|
||||
* BUI openings are now deferred to avoid having slight desync between deferred closes and opens occurring in the same tick.
|
||||
|
||||
|
||||
## 240.1.2
|
||||
|
||||
|
||||
|
||||
@@ -101,11 +101,33 @@ namespace Robust.Client.GameObjects
|
||||
/// <inheritdoc />
|
||||
public override void Dirty<T>(Entity<T> ent, MetaDataComponent? meta = null)
|
||||
{
|
||||
// Client only dirties during prediction
|
||||
// Client only dirties during prediction
|
||||
if (_gameTiming.InPrediction)
|
||||
base.Dirty(ent, meta);
|
||||
}
|
||||
|
||||
public override void DirtyField<T>(EntityUid uid, T comp, string fieldName, MetaDataComponent? metadata = null)
|
||||
{
|
||||
// TODO Prediction
|
||||
// does the client actually need to dirty the field?
|
||||
// I.e., can't it just dirty the whole component to trigger a reset?
|
||||
|
||||
// Client only dirties during prediction
|
||||
if (_gameTiming.InPrediction)
|
||||
base.DirtyField(uid, comp, fieldName, metadata);
|
||||
}
|
||||
|
||||
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
|
||||
{
|
||||
// TODO Prediction
|
||||
// does the client actually need to dirty the field?
|
||||
// I.e., can't it just dirty the whole component to trigger a reset?
|
||||
|
||||
// Client only dirties during prediction
|
||||
if (_gameTiming.InPrediction)
|
||||
base.DirtyFields(uid, comp, meta, fields);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dirty<T1, T2>(Entity<T1, T2> ent, MetaDataComponent? meta = null)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
{
|
||||
private Dictionary<EntityUid, Dictionary<Enum, Vector2>> _savedPositions = new();
|
||||
private Dictionary<BoundUserInterface, Control> _registeredControls = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -17,6 +25,59 @@ public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
ProtoManager.PrototypesReloaded -= OnProtoReload;
|
||||
}
|
||||
|
||||
protected override void OnUserInterfaceShutdown(Entity<UserInterfaceComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
base.OnUserInterfaceShutdown(ent, ref args);
|
||||
_savedPositions.Remove(ent.Owner);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OpenUi(Entity<UserInterfaceComponent?> entity, Enum key, bool predicted = false)
|
||||
{
|
||||
var player = Player.LocalEntity;
|
||||
|
||||
if (player == null)
|
||||
return;
|
||||
|
||||
OpenUi(entity, key, player.Value, predicted);
|
||||
}
|
||||
|
||||
protected override void SavePosition(BoundUserInterface bui)
|
||||
{
|
||||
if (!_registeredControls.Remove(bui, out var control))
|
||||
return;
|
||||
|
||||
var keyed = _savedPositions[bui.Owner];
|
||||
keyed[bui.UiKey] = control.Position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a control so it will later have its position stored by <see cref="SavePosition"/> when the BUI is closed.
|
||||
/// </summary>
|
||||
public void RegisterControl(BoundUserInterface bui, Control control)
|
||||
{
|
||||
DebugTools.Assert(!_registeredControls.ContainsKey(bui));
|
||||
_registeredControls[bui] = control;
|
||||
_savedPositions.GetOrNew(bui.Owner);
|
||||
}
|
||||
|
||||
public override bool TryGetPosition(Entity<UserInterfaceComponent?> entity, Enum key, out Vector2 position)
|
||||
{
|
||||
position = default;
|
||||
|
||||
if (!_savedPositions.TryGetValue(entity.Owner, out var keyed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!keyed.TryGetValue(key, out position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnProtoReload(PrototypesReloadedEventArgs obj)
|
||||
{
|
||||
var player = Player.LocalEntity;
|
||||
|
||||
@@ -23,7 +23,6 @@ using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Profiling;
|
||||
@@ -42,13 +41,13 @@ namespace Robust.Client.GameStates
|
||||
private uint _nextInputCmdSeq = 1;
|
||||
private readonly Queue<FullInputCmdMessage> _pendingInputs = new();
|
||||
|
||||
private readonly Queue<(uint sequence, GameTick sourceTick, EntityEventArgs msg, object sessionMsg)>
|
||||
private readonly Queue<(uint sequence, GameTick sourceTick, object msg, object sessionMsg)>
|
||||
_pendingSystemMessages
|
||||
= new();
|
||||
|
||||
// Game state dictionaries that get used every tick.
|
||||
private readonly Dictionary<EntityUid, (NetEntity NetEntity, MetaDataComponent Meta, bool EnteringPvs, GameTick LastApplied, EntityState? curState, EntityState? nextState)> _toApply = new();
|
||||
private readonly Dictionary<NetEntity, EntityState> _toCreate = new();
|
||||
private readonly Dictionary<EntityUid, StateData> _toApply = new();
|
||||
private StateData[] _toApplySorted = default!;
|
||||
private readonly Dictionary<ushort, (IComponent Component, IComponentState? curState, IComponentState? nextState)> _compStateWork = new();
|
||||
private readonly Dictionary<EntityUid, HashSet<Type>> _pendingReapplyNetStates = new();
|
||||
private readonly HashSet<NetEntity> _stateEnts = new();
|
||||
@@ -56,15 +55,29 @@ namespace Robust.Client.GameStates
|
||||
private readonly List<IComponent> _toRemove = new();
|
||||
private readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState?>> _outputData = new();
|
||||
private readonly List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
|
||||
private readonly HashSet<EntityUid> _sorted = new();
|
||||
private readonly List<NetEntity> _created = new();
|
||||
private readonly List<NetEntity> _detached = new();
|
||||
|
||||
private readonly record struct StateData(
|
||||
EntityUid Uid,
|
||||
NetEntity NetEntity,
|
||||
MetaDataComponent Meta,
|
||||
bool Created,
|
||||
bool EnteringPvs,
|
||||
GameTick LastApplied,
|
||||
EntityState? CurState,
|
||||
EntityState? NextState,
|
||||
HashSet<Type>? PendingReapply);
|
||||
|
||||
private readonly ObjectPool<Dictionary<ushort, IComponentState?>> _compDataPool =
|
||||
new DefaultObjectPool<Dictionary<ushort, IComponentState?>>(new DictPolicy<ushort, IComponentState?>(), 256);
|
||||
|
||||
private uint _metaCompNetId;
|
||||
private uint _xformCompNetId;
|
||||
|
||||
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
||||
[Dependency] private readonly IComponentFactory _compFactory = default!;
|
||||
[Dependency] private readonly IClientEntityManagerInternal _entities = default!;
|
||||
[Dependency] private readonly IPlayerManager _players = default!;
|
||||
[Dependency] private readonly IClientNetManager _network = default!;
|
||||
[Dependency] private readonly IBaseClient _client = default!;
|
||||
@@ -72,7 +85,7 @@ namespace Robust.Client.GameStates
|
||||
[Dependency] private readonly INetConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IConsoleHost _conHost = default!;
|
||||
[Dependency] private readonly ClientEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly ClientEntityManager _entities = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly ProfManager _prof = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
@@ -126,7 +139,6 @@ namespace Robust.Client.GameStates
|
||||
|
||||
private bool _resettingPredictedEntities;
|
||||
private readonly List<EntityUid> _brokenEnts = new();
|
||||
private readonly List<(EntityUid, NetEntity)> _toStart = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
@@ -172,6 +184,12 @@ namespace Robust.Client.GameStates
|
||||
throw new InvalidOperationException("MetaDataComponent does not have a NetId.");
|
||||
|
||||
_metaCompNetId = metaId.Value;
|
||||
|
||||
var xformId = _compFactory.GetRegistration(typeof(TransformComponent)).NetID;
|
||||
if (!xformId.HasValue)
|
||||
throw new InvalidOperationException("TransformComponent does not have a NetId.");
|
||||
|
||||
_xformCompNetId = xformId.Value;
|
||||
}
|
||||
|
||||
private void OnComponentAdded(AddedComponentEventArgs args)
|
||||
@@ -183,11 +201,11 @@ namespace Robust.Client.GameStates
|
||||
if (comp.NetID == null)
|
||||
return;
|
||||
|
||||
if (_entityManager.IsClientSide(args.BaseArgs.Owner))
|
||||
if (_entities.IsClientSide(args.BaseArgs.Owner))
|
||||
return;
|
||||
|
||||
_sawmill.Error($"""
|
||||
Added component {comp.Name} to entity {_entityManager.ToPrettyString(args.BaseArgs.Owner)} while resetting predicted entities.
|
||||
Added component {comp.Name} to entity {_entities.ToPrettyString(args.BaseArgs.Owner)} while resetting predicted entities.
|
||||
Stack trace:
|
||||
{Environment.StackTrace}
|
||||
""");
|
||||
@@ -385,7 +403,7 @@ namespace Robust.Client.GameStates
|
||||
try
|
||||
{
|
||||
#endif
|
||||
createdEntities = ApplyGameState(curState, nextState);
|
||||
ApplyGameState(curState, nextState);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (MissingMetadataException e)
|
||||
@@ -399,7 +417,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
using (_prof.Group("MergeImplicitData"))
|
||||
{
|
||||
GenerateImplicitStates(createdEntities);
|
||||
MergeImplicitData();
|
||||
}
|
||||
|
||||
if (_lastProcessedInput < curState.LastProcessedInput)
|
||||
@@ -456,7 +474,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
using (_prof.Group("Tick"))
|
||||
{
|
||||
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled);
|
||||
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled, histogram: null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,9 +522,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= _timing.CurTick)
|
||||
{
|
||||
var msg = pendingMessagesEnumerator.Current.msg;
|
||||
|
||||
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
|
||||
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.msg);
|
||||
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
|
||||
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
|
||||
}
|
||||
@@ -545,7 +561,7 @@ namespace Robust.Client.GameStates
|
||||
PredictionNeedsResetting = false;
|
||||
var countReset = 0;
|
||||
var system = _entitySystemManager.GetEntitySystem<ClientDirtySystem>();
|
||||
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
|
||||
var metaQuery = _entities.GetEntityQuery<MetaDataComponent>();
|
||||
RemQueue<IComponent> toRemove = new();
|
||||
|
||||
foreach (var entity in system.DirtyEntities)
|
||||
@@ -632,7 +648,7 @@ namespace Robust.Client.GameStates
|
||||
if (!last.TryGetValue(netId, out var state))
|
||||
continue;
|
||||
|
||||
var comp = _entityManager.AddComponent(entity, netId, meta);
|
||||
var comp = _entities.AddComponent(entity, netId, meta);
|
||||
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was removed: {comp.GetType()}");
|
||||
@@ -652,7 +668,7 @@ namespace Robust.Client.GameStates
|
||||
meta.EntityLastModifiedTick = _timing.LastRealTick;
|
||||
}
|
||||
|
||||
_entityManager.System<PhysicsSystem>().ResetContacts();
|
||||
_entities.System<PhysicsSystem>().ResetContacts();
|
||||
|
||||
// TODO maybe reset more of physics?
|
||||
// E.g., warm impulses for warm starting?
|
||||
@@ -671,21 +687,21 @@ namespace Robust.Client.GameStates
|
||||
/// initial server state for any newly created entity. It does this by simply using the standard <see
|
||||
/// cref="IEntityManager.GetComponentState"/>.
|
||||
/// </remarks>
|
||||
public void GenerateImplicitStates(IEnumerable<NetEntity> createdEntities)
|
||||
public void MergeImplicitData()
|
||||
{
|
||||
var bus = _entityManager.EventBus;
|
||||
var bus = _entities.EventBus;
|
||||
|
||||
foreach (var netEntity in createdEntities)
|
||||
foreach (var netEntity in _created)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
if (!_entityManager.TryGetEntityData(netEntity, out _, out var meta))
|
||||
if (!_entities.TryGetEntityData(netEntity, out _, out var meta))
|
||||
{
|
||||
_sawmill.Error($"Encountered deleted entity while merging implicit data! NetEntity: {netEntity}");
|
||||
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw new KeyNotFoundException();
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
#else
|
||||
var (_, meta) = _entityManager.GetEntityData(netEntity);
|
||||
#endif
|
||||
|
||||
var compData = _compDataPool.Get();
|
||||
_outputData.Add(netEntity, compData);
|
||||
@@ -694,12 +710,13 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
DebugTools.Assert(component.NetSyncEnabled);
|
||||
|
||||
var state = _entityManager.GetComponentState(bus, component, null, GameTick.Zero);
|
||||
var state = _entities.GetComponentState(bus, component, null, GameTick.Zero);
|
||||
DebugTools.Assert(state is not IComponentDeltaState);
|
||||
compData.Add(netId, state);
|
||||
}
|
||||
}
|
||||
|
||||
_created.Clear();
|
||||
_processor.MergeImplicitData(_outputData);
|
||||
|
||||
foreach (var data in _outputData.Values)
|
||||
@@ -735,10 +752,9 @@ namespace Robust.Client.GameStates
|
||||
_config.TickProcessMessages();
|
||||
}
|
||||
|
||||
(IEnumerable<NetEntity> Created, List<NetEntity> Detached) output;
|
||||
using (_prof.Group("Entity"))
|
||||
{
|
||||
output = ApplyEntityStates(curState, nextState);
|
||||
ApplyEntityStates(curState, nextState);
|
||||
}
|
||||
|
||||
using (_prof.Group("Player"))
|
||||
@@ -748,13 +764,13 @@ namespace Robust.Client.GameStates
|
||||
|
||||
using (_prof.Group("Callback"))
|
||||
{
|
||||
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, output.Detached));
|
||||
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, _detached));
|
||||
}
|
||||
|
||||
return output.Created;
|
||||
return _created;
|
||||
}
|
||||
|
||||
private (IEnumerable<NetEntity> Created, List<NetEntity> Detached) ApplyEntityStates(GameState curState, GameState? nextState)
|
||||
private void ApplyEntityStates(GameState curState, GameState? nextState)
|
||||
{
|
||||
var metas = _entities.GetEntityQuery<MetaDataComponent>();
|
||||
var xforms = _entities.GetEntityQuery<TransformComponent>();
|
||||
@@ -762,90 +778,74 @@ namespace Robust.Client.GameStates
|
||||
|
||||
var enteringPvs = 0;
|
||||
_toApply.Clear();
|
||||
_toCreate.Clear();
|
||||
_created.Clear();
|
||||
_pendingReapplyNetStates.Clear();
|
||||
var curSpan = curState.EntityStates.Span;
|
||||
|
||||
// Create new entities
|
||||
// This is done BEFORE state application to ensure any new parents exist before existing children have their states applied, otherwise, we may have issues with entity transforms!
|
||||
{
|
||||
using var _ = _prof.Group("Create uninitialized entities");
|
||||
var count = 0;
|
||||
|
||||
using (_prof.Group("Create uninitialized entities"))
|
||||
{
|
||||
var created = 0;
|
||||
foreach (var es in curSpan)
|
||||
{
|
||||
if (_entityManager.TryGetEntity(es.NetEntity, out var nUid))
|
||||
if (_entities.TryGetEntity(es.NetEntity, out var nUid))
|
||||
{
|
||||
DebugTools.Assert(_entityManager.EntityExists(nUid));
|
||||
DebugTools.Assert(_entities.EntityExists(nUid));
|
||||
continue;
|
||||
}
|
||||
|
||||
count++;
|
||||
var metaState = (MetaDataComponentState?)es.ComponentChanges.Value?.FirstOrDefault(c => c.NetID == _metaCompNetId).State;
|
||||
if (metaState == null)
|
||||
throw new MissingMetadataException(es.NetEntity);
|
||||
|
||||
var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta);
|
||||
_toCreate.Add(es.NetEntity, es);
|
||||
_toApply.Add(uid, (es.NetEntity, newMeta, false, GameTick.Zero, es, null));
|
||||
|
||||
// Client creates a client-side net entity for the newly created entity.
|
||||
// We need to clear this mapping before assigning the real net id.
|
||||
// TODO NetEntity Jank: prevent the client from creating this in the first place.
|
||||
_entityManager.ClearNetEntity(newMeta.NetEntity);
|
||||
|
||||
_entityManager.SetNetEntity(uid, es.NetEntity, newMeta);
|
||||
newMeta.LastStateApplied = curState.ToSequence;
|
||||
|
||||
// Check if there's any component states awaiting this entity.
|
||||
if (_entityManager.PendingNetEntityStates.Remove(es.NetEntity, out var value))
|
||||
{
|
||||
foreach (var (type, owner) in value)
|
||||
{
|
||||
var pending = _pendingReapplyNetStates.GetOrNew(owner);
|
||||
pending.Add(type);
|
||||
}
|
||||
}
|
||||
created++;
|
||||
CreateNewEntity(es, curState.ToSequence);
|
||||
}
|
||||
|
||||
_prof.WriteValue("Count", ProfData.Int32(count));
|
||||
_prof.WriteValue("Count", ProfData.Int32(created));
|
||||
}
|
||||
|
||||
// Add entity entities that aren't new to _toCreate.
|
||||
// In the process, we also check if these entities are re-entering PVS range.
|
||||
foreach (var es in curSpan)
|
||||
{
|
||||
if (_toCreate.ContainsKey(es.NetEntity))
|
||||
if (!_entities.TryGetEntityData(es.NetEntity, out var uid, out var meta))
|
||||
continue;
|
||||
|
||||
if (!_entityManager.TryGetEntityData(es.NetEntity, out var uid, out var meta))
|
||||
continue;
|
||||
|
||||
bool isEnteringPvs = (meta.Flags & MetaDataFlags.Detached) != 0;
|
||||
var isEnteringPvs = (meta.Flags & MetaDataFlags.Detached) != 0;
|
||||
if (isEnteringPvs)
|
||||
{
|
||||
// _toApply already contains newly created entities, but these should never be "entering PVS"
|
||||
DebugTools.Assert(!_toApply.ContainsKey(uid.Value));
|
||||
|
||||
meta.Flags &= ~MetaDataFlags.Detached;
|
||||
enteringPvs++;
|
||||
}
|
||||
else if (meta.LastStateApplied >= es.EntityLastModified && meta.LastStateApplied != GameTick.Zero)
|
||||
{
|
||||
// _toApply already contains newly created entities, but for those this set should have no effect
|
||||
DebugTools.Assert(!_toApply.ContainsKey(uid.Value) || meta.LastStateApplied == curState.ToSequence);
|
||||
|
||||
meta.LastStateApplied = curState.ToSequence;
|
||||
continue;
|
||||
}
|
||||
|
||||
_toApply.Add(uid.Value, (es.NetEntity, meta, isEnteringPvs, meta.LastStateApplied, es, null));
|
||||
// Any newly created entities already added to _toApply should've already been caught by the previous continue
|
||||
DebugTools.Assert(!_toApply.ContainsKey(uid.Value));
|
||||
|
||||
_toApply.Add(uid.Value, new(uid.Value, es.NetEntity, meta, false, isEnteringPvs, meta.LastStateApplied, es, null, null));
|
||||
meta.LastStateApplied = curState.ToSequence;
|
||||
}
|
||||
|
||||
// Detach entities to null space
|
||||
var containerSys = _entitySystemManager.GetEntitySystem<ContainerSystem>();
|
||||
var lookupSys = _entitySystemManager.GetEntitySystem<EntityLookupSystem>();
|
||||
var detached = ProcessPvsDeparture(curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
|
||||
ProcessPvsDeparture(curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
|
||||
|
||||
// Check next state (AFTER having created new entities introduced in curstate)
|
||||
if (nextState != null)
|
||||
{
|
||||
foreach (var es in nextState.EntityStates.Span)
|
||||
{
|
||||
if (!_entityManager.TryGetEntityData(es.NetEntity, out var uid, out var meta))
|
||||
if (!_entities.TryGetEntityData(es.NetEntity, out var uid, out var meta))
|
||||
continue;
|
||||
|
||||
// Does the next state actually have any future information about this entity that could be used for interpolation?
|
||||
@@ -854,15 +854,14 @@ namespace Robust.Client.GameStates
|
||||
|
||||
ref var state = ref CollectionsMarshal.GetValueRefOrAddDefault(_toApply, uid.Value, out var exists);
|
||||
|
||||
if (exists)
|
||||
state = (es.NetEntity, meta, state.EnteringPvs, state.LastApplied, state.curState, es);
|
||||
else
|
||||
state = (es.NetEntity, meta, false, GameTick.Zero, null, es);
|
||||
state = exists
|
||||
? state with {NextState = es}
|
||||
: new(uid.Value, es.NetEntity, meta, false, false, GameTick.Zero, null, es, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Check pending states and see if we need to force any entities to re-run component states.
|
||||
foreach (var uid in _pendingReapplyNetStates.Keys)
|
||||
foreach (var (uid, pending) in _pendingReapplyNetStates)
|
||||
{
|
||||
// Original entity referencing the NetEntity may have been deleted.
|
||||
if (!metas.TryGetComponent(uid, out var meta))
|
||||
@@ -879,51 +878,30 @@ namespace Robust.Client.GameStates
|
||||
|
||||
DebugTools.Assert(!curState.EntityDeletions.Value.Contains(meta.NetEntity));
|
||||
|
||||
// State already being re-applied so don't bulldoze it.
|
||||
ref var state = ref CollectionsMarshal.GetValueRefOrAddDefault(_toApply, uid, out var exists);
|
||||
|
||||
if (exists)
|
||||
continue;
|
||||
|
||||
state = (meta.NetEntity, meta, false, GameTick.Zero, null, null);
|
||||
state = exists
|
||||
? state with {PendingReapply = pending}
|
||||
: new(uid, meta.NetEntity, meta, false, false, GameTick.Zero, null, null, pending);
|
||||
}
|
||||
|
||||
_queuedBroadphaseUpdates.Clear();
|
||||
|
||||
using (_prof.Group("Sort States"))
|
||||
{
|
||||
SortStates(_toApply);
|
||||
}
|
||||
|
||||
// Apply entity states.
|
||||
using (_prof.Group("Apply States"))
|
||||
{
|
||||
foreach (var (entity, data) in _toApply)
|
||||
var span = _toApplySorted.AsSpan(0, _toApply.Count);
|
||||
foreach (ref var data in span)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
HandleEntityState(entity, data.NetEntity, data.Meta, _entities.EventBus, data.curState,
|
||||
data.nextState, data.LastApplied, curState.ToSequence, data.EnteringPvs);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Caught exception while applying entity state. Entity: {_entities.ToPrettyString(entity)}. Exception: {e}");
|
||||
_entityManager.DeleteEntity(entity);
|
||||
RequestFullState();
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
if (!data.EnteringPvs)
|
||||
continue;
|
||||
|
||||
// Now that things like collision data, fixtures, and positions have been updated, we queue a
|
||||
// broadphase update. However, if this entity is parented to some other entity also re-entering PVS,
|
||||
// we only need to update it's parent (as it recursively updates children anyways).
|
||||
var xform = xforms.GetComponent(entity);
|
||||
DebugTools.Assert(xform.Broadphase == BroadphaseData.Invalid);
|
||||
xform.Broadphase = null;
|
||||
if (!_toApply.TryGetValue(xform.ParentUid, out var parent) || !parent.EnteringPvs)
|
||||
_queuedBroadphaseUpdates.Add((entity, xform));
|
||||
ApplyEntState(data, curState.ToSequence);
|
||||
}
|
||||
|
||||
Array.Clear(_toApplySorted, 0, _toApply.Count);
|
||||
_prof.WriteValue("Count", ProfData.Int32(_toApply.Count));
|
||||
}
|
||||
|
||||
@@ -958,14 +936,166 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and start the newly created entities.
|
||||
if (_toCreate.Count > 0)
|
||||
InitializeAndStart(_toCreate, metas, xforms);
|
||||
// Delete any entities that failed to properly initialize/start
|
||||
foreach (var entity in _brokenEnts)
|
||||
{
|
||||
_entities.DeleteEntity(entity);
|
||||
}
|
||||
|
||||
_brokenEnts.Clear();
|
||||
|
||||
_prof.WriteValue("State Size", ProfData.Int32(curSpan.Length));
|
||||
_prof.WriteValue("Entered PVS", ProfData.Int32(enteringPvs));
|
||||
}
|
||||
|
||||
return (_toCreate.Keys, detached);
|
||||
private void ApplyEntState(in StateData data, GameTick toTick)
|
||||
{
|
||||
try
|
||||
{
|
||||
HandleEntityState(data, _entities.EventBus, toTick);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Caught exception while applying entity state. Entity: {_entities.ToPrettyString(data.Uid)}. Exception: {e}");
|
||||
_brokenEnts.Add(data.Uid);
|
||||
RequestFullState();
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.Created)
|
||||
{
|
||||
try
|
||||
{
|
||||
_entities.InitializeEntity(data.Uid, data.Meta);
|
||||
_entities.StartEntity(data.Uid);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error(
|
||||
$"Caught exception while initializing or starting entity: {_entities.ToPrettyString(data.Uid)}. Exception: {e}");
|
||||
_brokenEnts.Add(data.Uid);
|
||||
RequestFullState();
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.EnteringPvs)
|
||||
return;
|
||||
|
||||
// Now that things like collision data, fixtures, and positions have been updated, we queue a
|
||||
// broadphase update. However, if this entity is parented to some other entity also re-entering PVS,
|
||||
// we only need to update it's parent (as it recursively updates children anyways).
|
||||
var xform = _entities.TransformQuery.Comp(data.Uid);
|
||||
DebugTools.Assert(xform.Broadphase == BroadphaseData.Invalid);
|
||||
xform.Broadphase = null;
|
||||
if (!_toApply.TryGetValue(xform.ParentUid, out var parent) || !parent.EnteringPvs)
|
||||
_queuedBroadphaseUpdates.Add((data.Uid, xform));
|
||||
}
|
||||
|
||||
private void CreateNewEntity(EntityState state, GameTick toTick)
|
||||
{
|
||||
// TODO GAME STATE
|
||||
// store MetaData & Transform information separately.
|
||||
var metaState =
|
||||
(MetaDataComponentState?) state.ComponentChanges.Value?.FirstOrDefault(c => c.NetID == _metaCompNetId)
|
||||
.State;
|
||||
|
||||
if (metaState == null)
|
||||
throw new MissingMetadataException(state.NetEntity);
|
||||
|
||||
var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta);
|
||||
_toApply.Add(uid, new(uid, state.NetEntity, newMeta, true, false, GameTick.Zero, state, null, null));
|
||||
_created.Add(state.NetEntity);
|
||||
|
||||
// Client creates a client-side net entity for the newly created entity.
|
||||
// We need to clear this mapping before assigning the real net id.
|
||||
// TODO NetEntity Jank: prevent the client from creating this in the first place.
|
||||
_entities.ClearNetEntity(newMeta.NetEntity);
|
||||
|
||||
_entities.SetNetEntity(uid, state.NetEntity, newMeta);
|
||||
newMeta.LastStateApplied = toTick;
|
||||
|
||||
// Check if there's any component states awaiting this entity.
|
||||
if (!_entities.PendingNetEntityStates.Remove(state.NetEntity, out var value))
|
||||
return;
|
||||
|
||||
foreach (var (type, owner) in value)
|
||||
{
|
||||
var pending = _pendingReapplyNetStates.GetOrNew(owner);
|
||||
pending.Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sort states to ensure that we always apply states, initialize, and start parent entities before any of their
|
||||
/// children.
|
||||
/// </summary>
|
||||
private void SortStates(Dictionary<EntityUid, StateData> toApply)
|
||||
{
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
if (_toApplySorted == null || _toApplySorted.Length < toApply.Count)
|
||||
Array.Resize(ref _toApplySorted, toApply.Count);
|
||||
|
||||
_sorted.Clear();
|
||||
|
||||
var i = 0;
|
||||
foreach (var (ent, data) in toApply)
|
||||
{
|
||||
AddToSorted(ent, data, ref i);
|
||||
}
|
||||
|
||||
DebugTools.AssertEqual(i, toApply.Count);
|
||||
}
|
||||
|
||||
private void AddToSorted(EntityUid ent, in StateData data, ref int i)
|
||||
{
|
||||
if (!_sorted.Add(ent))
|
||||
return;
|
||||
|
||||
EnsureParentsSorted(ent, data, ref i);
|
||||
_toApplySorted[i++] = data;
|
||||
}
|
||||
|
||||
private void EnsureParentsSorted(EntityUid ent, in StateData data, ref int i)
|
||||
{
|
||||
var parent = GetStateParent(ent, data);
|
||||
|
||||
while (parent != EntityUid.Invalid)
|
||||
{
|
||||
if (_toApply.TryGetValue(parent, out var parentData))
|
||||
{
|
||||
AddToSorted(parent, parentData, ref i);
|
||||
// The above method will handle the rest of the transform hierarchy, so we can just return early.
|
||||
return;
|
||||
}
|
||||
|
||||
parent = _entities.TransformQuery.GetComponent(parent).ParentUid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the entity's parent in the game state that is being applies. I.e., if the state contains a new
|
||||
/// transform state, get the parent from that. Otherwise, return the entity's current parent.
|
||||
/// </summary>
|
||||
private EntityUid GetStateParent(EntityUid uid, in StateData data)
|
||||
{
|
||||
// TODO GAME STATE
|
||||
// store MetaData & Transform information separately.
|
||||
if (data.CurState != null
|
||||
&& data.CurState.ComponentChanges.Value
|
||||
.TryFirstOrNull(c => c.NetID == _xformCompNetId, out var found))
|
||||
{
|
||||
var state = (TransformComponentState) found.Value.State!;
|
||||
return _entities.GetEntity(state.ParentID);
|
||||
}
|
||||
|
||||
return _entities.TransformQuery.GetComponent(uid).ParentUid;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -1000,7 +1130,7 @@ namespace Robust.Client.GameStates
|
||||
_toDelete.Clear();
|
||||
|
||||
// Client side entities won't need the transform, but that should always be a tiny minority of entities
|
||||
var metaQuery = _entityManager.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent>();
|
||||
var metaQuery = _entities.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent>();
|
||||
|
||||
while (metaQuery.MoveNext(out var ent, out var metadata, out var xform))
|
||||
{
|
||||
@@ -1067,9 +1197,9 @@ namespace Robust.Client.GameStates
|
||||
foreach (var netEntity in delSpan)
|
||||
{
|
||||
// Don't worry about this for later.
|
||||
_entityManager.PendingNetEntityStates.Remove(netEntity);
|
||||
_entities.PendingNetEntityStates.Remove(netEntity);
|
||||
|
||||
if (!_entityManager.TryGetEntity(netEntity, out var id))
|
||||
if (!_entities.TryGetEntity(netEntity, out var id))
|
||||
continue;
|
||||
|
||||
if (!xforms.TryGetComponent(id, out var xform))
|
||||
@@ -1099,9 +1229,10 @@ namespace Robust.Client.GameStates
|
||||
var containerSys = _entitySystemManager.GetEntitySystem<ContainerSystem>();
|
||||
var lookupSys = _entitySystemManager.GetEntitySystem<EntityLookupSystem>();
|
||||
Detach(GameTick.MaxValue, null, entities, metas, xforms, xformSys, containerSys, lookupSys);
|
||||
_detached.Clear();
|
||||
}
|
||||
|
||||
private List<NetEntity> ProcessPvsDeparture(
|
||||
private void ProcessPvsDeparture(
|
||||
GameTick toTick,
|
||||
EntityQuery<MetaDataComponent> metas,
|
||||
EntityQuery<TransformComponent> xforms,
|
||||
@@ -1110,25 +1241,23 @@ namespace Robust.Client.GameStates
|
||||
EntityLookupSystem lookupSys)
|
||||
{
|
||||
var toDetach = _processor.GetEntitiesToDetach(toTick, _pvsDetachBudget);
|
||||
var detached = new List<NetEntity>();
|
||||
|
||||
if (toDetach.Count == 0)
|
||||
return detached;
|
||||
return;
|
||||
|
||||
// TODO optimize
|
||||
// If an entity is leaving PVS, so are all of its children. If we can preserve the hierarchy we can avoid
|
||||
// things like container insertion and ejection.
|
||||
|
||||
using var _ = _prof.Group("Leave PVS");
|
||||
detached.EnsureCapacity(toDetach.Count);
|
||||
|
||||
_detached.Clear();
|
||||
foreach (var (tick, ents) in toDetach)
|
||||
{
|
||||
Detach(tick, toTick, ents, metas, xforms, xformSys, containerSys, lookupSys, detached);
|
||||
Detach(tick, toTick, ents, metas, xforms, xformSys, containerSys, lookupSys);
|
||||
}
|
||||
|
||||
_prof.WriteValue("Count", ProfData.Int32(detached.Count));
|
||||
return detached;
|
||||
_prof.WriteValue("Count", ProfData.Int32(_detached.Count));
|
||||
}
|
||||
|
||||
private void Detach(GameTick maxTick,
|
||||
@@ -1138,12 +1267,11 @@ namespace Robust.Client.GameStates
|
||||
EntityQuery<TransformComponent> xforms,
|
||||
SharedTransformSystem xformSys,
|
||||
ContainerSystem containerSys,
|
||||
EntityLookupSystem lookupSys,
|
||||
List<NetEntity>? detached = null)
|
||||
EntityLookupSystem lookupSys)
|
||||
{
|
||||
foreach (var netEntity in entities)
|
||||
{
|
||||
if (!_entityManager.TryGetEntityData(netEntity, out var ent, out var meta))
|
||||
if (!_entities.TryGetEntityData(netEntity, out var ent, out var meta))
|
||||
continue;
|
||||
|
||||
if (meta.LastStateApplied > maxTick)
|
||||
@@ -1184,159 +1312,75 @@ namespace Robust.Client.GameStates
|
||||
containerSys.AddExpectedEntity(netEntity, container);
|
||||
}
|
||||
|
||||
detached?.Add(netEntity);
|
||||
_detached.Add(netEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeAndStart(
|
||||
Dictionary<NetEntity, EntityState> toCreate,
|
||||
EntityQuery<MetaDataComponent> metas,
|
||||
EntityQuery<TransformComponent> xforms)
|
||||
{
|
||||
_toStart.Clear();
|
||||
|
||||
using (_prof.Group("Initialize Entity"))
|
||||
{
|
||||
EntityUid entity = default;
|
||||
foreach (var netEntity in toCreate.Keys)
|
||||
{
|
||||
(entity, var meta) = _entityManager.GetEntityData(netEntity);
|
||||
InitializeRecursive(entity, meta, metas, xforms);
|
||||
}
|
||||
}
|
||||
|
||||
using (_prof.Group("Start Entity"))
|
||||
{
|
||||
foreach (var (entity, netEntity) in _toStart)
|
||||
{
|
||||
try
|
||||
{
|
||||
_entities.StartEntity(entity);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Server entity threw in Start: nent={netEntity}, ent={_entityManager.ToPrettyString(entity)}");
|
||||
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
|
||||
_toCreate.Remove(netEntity);
|
||||
_brokenEnts.Add(entity);
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entity in _brokenEnts)
|
||||
{
|
||||
_entityManager.DeleteEntity(entity);
|
||||
}
|
||||
_brokenEnts.Clear();
|
||||
}
|
||||
|
||||
private void InitializeRecursive(
|
||||
EntityUid entity,
|
||||
MetaDataComponent meta,
|
||||
EntityQuery<MetaDataComponent> metas,
|
||||
EntityQuery<TransformComponent> xforms)
|
||||
{
|
||||
var xform = xforms.GetComponent(entity);
|
||||
if (xform.ParentUid is {Valid: true} parent)
|
||||
{
|
||||
var parentMeta = metas.GetComponent(parent);
|
||||
if (parentMeta.EntityLifeStage < EntityLifeStage.Initialized)
|
||||
InitializeRecursive(parent, parentMeta, metas, xforms);
|
||||
}
|
||||
|
||||
if (meta.EntityLifeStage >= EntityLifeStage.Initialized)
|
||||
{
|
||||
// Was probably already initialized because one of its children appeared earlier in the list.
|
||||
DebugTools.AssertEqual(_toStart.Count(x => x.Item1 == entity), 1);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_entities.InitializeEntity(entity, meta);
|
||||
_toStart.Add((entity, meta.NetEntity));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Server entity threw in Init: nent={meta.NetEntity}, ent={_entities.ToPrettyString(entity)}");
|
||||
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
|
||||
_toCreate.Remove(meta.NetEntity);
|
||||
_brokenEnts.Add(entity);
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataComponent meta, IEventBus bus, EntityState? curState,
|
||||
EntityState? nextState, GameTick lastApplied, GameTick toTick, bool enteringPvs)
|
||||
private void HandleEntityState(in StateData data, IEventBus bus, GameTick toTick)
|
||||
{
|
||||
_compStateWork.Clear();
|
||||
|
||||
// First remove any deleted components
|
||||
if (curState?.NetComponents != null)
|
||||
if (data.CurState?.NetComponents is {} netComps)
|
||||
{
|
||||
_toRemove.Clear();
|
||||
|
||||
foreach (var (id, comp) in meta.NetComponents)
|
||||
foreach (var (id, comp) in data.Meta.NetComponents)
|
||||
{
|
||||
DebugTools.Assert(comp.NetSyncEnabled);
|
||||
|
||||
if (!curState.NetComponents.Contains(id))
|
||||
if (!netComps.Contains(id))
|
||||
_toRemove.Add(comp);
|
||||
}
|
||||
|
||||
foreach (var comp in _toRemove)
|
||||
{
|
||||
_entities.RemoveComponent(uid, comp, meta);
|
||||
_entities.RemoveComponent(data.Uid, comp, data.Meta);
|
||||
}
|
||||
}
|
||||
|
||||
if (enteringPvs)
|
||||
if (data.EnteringPvs)
|
||||
{
|
||||
// last-server state has already been updated with new information from curState
|
||||
// --> simply reset to the most recent server state.
|
||||
//
|
||||
// as to why we need to reset: because in the process of detaching to null-space, we will have dirtied
|
||||
// the entity. most notably, all entities will have been ejected from their containers.
|
||||
foreach (var (id, state) in _processor.GetLastServerStates(netEntity))
|
||||
foreach (var (id, state) in _processor.GetLastServerStates(data.NetEntity))
|
||||
{
|
||||
if (!meta.NetComponents.TryGetValue(id, out var comp))
|
||||
if (!data.Meta.NetComponents.TryGetValue(id, out var comp))
|
||||
{
|
||||
comp = _compFactory.GetComponent(id);
|
||||
_entityManager.AddComponent(uid, comp, true, metadata: meta);
|
||||
_entities.AddComponent(data.Uid, comp, true, metadata: data.Meta);
|
||||
}
|
||||
|
||||
_compStateWork[id] = (comp, state, null);
|
||||
}
|
||||
}
|
||||
else if (curState != null)
|
||||
else if (data.CurState != null)
|
||||
{
|
||||
foreach (var compChange in curState.ComponentChanges.Span)
|
||||
foreach (var compChange in data.CurState.ComponentChanges.Span)
|
||||
{
|
||||
if (!meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
|
||||
if (!data.Meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
|
||||
{
|
||||
comp = _compFactory.GetComponent(compChange.NetID);
|
||||
_entityManager.AddComponent(uid, comp, true, metadata:meta);
|
||||
_entities.AddComponent(data.Uid, comp, true, metadata: data.Meta);
|
||||
}
|
||||
else if (compChange.LastModifiedTick <= lastApplied && lastApplied != GameTick.Zero)
|
||||
else if (compChange.LastModifiedTick <= data.LastApplied && data.LastApplied != GameTick.Zero)
|
||||
continue;
|
||||
|
||||
_compStateWork[compChange.NetID] = (comp, compChange.State, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextState != null)
|
||||
if (data.NextState != null)
|
||||
{
|
||||
foreach (var compState in nextState.ComponentChanges.Span)
|
||||
foreach (var compState in data.NextState.ComponentChanges.Span)
|
||||
{
|
||||
if (compState.LastModifiedTick != toTick + 1)
|
||||
continue;
|
||||
|
||||
if (!meta.NetComponents.TryGetValue(compState.NetID, out var comp))
|
||||
if (!data.Meta.NetComponents.TryGetValue(compState.NetID, out var comp))
|
||||
{
|
||||
// The component can be null here due to interp, because the NEXT state will have a new
|
||||
// component, but the component does not yet exist.
|
||||
@@ -1354,9 +1398,10 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
|
||||
// If we have a NetEntity we reference come in then apply their state.
|
||||
if (_pendingReapplyNetStates.TryGetValue(uid, out var reapplyTypes))
|
||||
DebugTools.Assert(_pendingReapplyNetStates.ContainsKey(data.Uid) == (data.PendingReapply != null));
|
||||
if (data.PendingReapply is {} reapplyTypes)
|
||||
{
|
||||
var lastState = _processor.GetLastServerStates(netEntity);
|
||||
var lastState = _processor.GetLastServerStates(data.NetEntity);
|
||||
|
||||
foreach (var type in reapplyTypes)
|
||||
{
|
||||
@@ -1366,7 +1411,7 @@ namespace Robust.Client.GameStates
|
||||
if (netId == null)
|
||||
continue;
|
||||
|
||||
if (!meta.NetComponents.TryGetValue(netId.Value, out var comp) ||
|
||||
if (!data.Meta.NetComponents.TryGetValue(netId.Value, out var comp) ||
|
||||
!lastState.TryGetValue(netId.Value, out var lastCompState))
|
||||
{
|
||||
continue;
|
||||
@@ -1388,7 +1433,7 @@ namespace Robust.Client.GameStates
|
||||
continue;
|
||||
|
||||
var handleState = new ComponentHandleState(cur, next);
|
||||
bus.RaiseComponentEvent(uid, comp, ref handleState);
|
||||
bus.RaiseComponentEvent(data.Uid, comp, ref handleState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1510,7 +1555,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
using var _ = _timing.StartStateApplicationArea();
|
||||
|
||||
var query = _entityManager.AllEntityQueryEnumerator<MetaDataComponent>();
|
||||
var query = _entities.AllEntityQueryEnumerator<MetaDataComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var meta))
|
||||
{
|
||||
@@ -1536,14 +1581,14 @@ namespace Robust.Client.GameStates
|
||||
if (!meta.NetComponents.TryGetValue(id, out var comp))
|
||||
{
|
||||
comp = _compFactory.GetComponent(id);
|
||||
_entityManager.AddComponent(uid, comp, true, meta);
|
||||
_entities.AddComponent(uid, comp, true, meta);
|
||||
}
|
||||
|
||||
if (state == null)
|
||||
continue;
|
||||
|
||||
var handleState = new ComponentHandleState(state, null);
|
||||
_entityManager.EventBus.RaiseComponentEvent(uid, comp, ref handleState);
|
||||
_entities.EventBus.RaiseComponentEvent(uid, comp, ref handleState);
|
||||
}
|
||||
|
||||
// ensure we don't have any extra components
|
||||
|
||||
@@ -82,11 +82,7 @@ namespace Robust.Client.GameStates
|
||||
/// </summary>
|
||||
IEnumerable<NetEntity> ApplyGameState(GameState curState, GameState? nextState);
|
||||
|
||||
/// <summary>
|
||||
/// Generates implicit component states for newly created entities.
|
||||
/// This should always be called after running <see cref="ApplyGameState(GameState, GameState)"/>.
|
||||
/// </summary>
|
||||
void GenerateImplicitStates(IEnumerable<NetEntity> states);
|
||||
void MergeImplicitData();
|
||||
|
||||
/// <summary>
|
||||
/// Resets any entities that have changed while predicting future ticks.
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
|
||||
|
||||
var args = new OverlayDrawArgs(space, null, vp, _renderHandle.DrawingHandleWorld, new UIBox2i((0, 0), vp.Size), vp.Eye!.Position.MapId, worldBox, worldBounds);
|
||||
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), vp.Eye!.Position.MapId, worldBox, worldBounds);
|
||||
|
||||
if (!overlay.BeforeDraw(args))
|
||||
return;
|
||||
@@ -165,7 +165,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private void RenderOverlaysDirect(
|
||||
Viewport vp,
|
||||
IViewportControl vpControl,
|
||||
DrawingHandleBase handle,
|
||||
IRenderHandle handle,
|
||||
OverlaySpace space,
|
||||
in UIBox2i bounds)
|
||||
{
|
||||
|
||||
@@ -171,7 +171,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
public void RenderScreenOverlaysBelow(
|
||||
DrawingHandleScreen handle,
|
||||
IRenderHandle handle,
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds)
|
||||
{
|
||||
@@ -179,7 +179,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
public void RenderScreenOverlaysAbove(
|
||||
DrawingHandleScreen handle,
|
||||
IRenderHandle handle,
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds)
|
||||
{
|
||||
|
||||
@@ -497,7 +497,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
public void RenderScreenOverlaysBelow(
|
||||
DrawingHandleScreen handle,
|
||||
IRenderHandle handle,
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds)
|
||||
{
|
||||
@@ -505,7 +505,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
public void RenderScreenOverlaysAbove(
|
||||
DrawingHandleScreen handle,
|
||||
IRenderHandle handle,
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds)
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Robust.Client.Graphics
|
||||
/// Not relative to the current transform of <see cref="handle"/>.
|
||||
/// </param>
|
||||
public void RenderScreenOverlaysBelow(
|
||||
DrawingHandleScreen handle,
|
||||
IRenderHandle handle,
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds);
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace Robust.Client.Graphics
|
||||
/// Not relative to the current transform of <see cref="handle"/>.
|
||||
/// </param>
|
||||
public void RenderScreenOverlaysAbove(
|
||||
DrawingHandleScreen handle,
|
||||
IRenderHandle handle,
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds);
|
||||
}
|
||||
|
||||
@@ -54,23 +54,29 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
public readonly Box2Rotated WorldBounds;
|
||||
|
||||
public readonly IRenderHandle RenderHandle;
|
||||
|
||||
public DrawingHandleScreen ScreenHandle => (DrawingHandleScreen) DrawingHandle;
|
||||
public DrawingHandleWorld WorldHandle => (DrawingHandleWorld) DrawingHandle;
|
||||
|
||||
public OverlayDrawArgs(
|
||||
internal OverlayDrawArgs(
|
||||
OverlaySpace space,
|
||||
IViewportControl? viewportControl,
|
||||
IClydeViewport viewport,
|
||||
DrawingHandleBase drawingHandle,
|
||||
IRenderHandle renderHandle,
|
||||
in UIBox2i viewportBounds,
|
||||
in MapId mapId,
|
||||
in Box2 worldAabb,
|
||||
in Box2Rotated worldBounds)
|
||||
{
|
||||
DrawingHandle = space is OverlaySpace.ScreenSpace or OverlaySpace.ScreenSpaceBelowWorld
|
||||
? renderHandle.DrawingHandleScreen
|
||||
: renderHandle.DrawingHandleWorld;
|
||||
|
||||
Space = space;
|
||||
ViewportControl = viewportControl;
|
||||
Viewport = viewport;
|
||||
DrawingHandle = drawingHandle;
|
||||
RenderHandle = renderHandle;
|
||||
ViewportBounds = viewportBounds;
|
||||
MapId = mapId;
|
||||
WorldAABB = worldAabb;
|
||||
|
||||
@@ -93,7 +93,7 @@ public sealed partial class PhysicsSystem
|
||||
var maps = new HashSet<EntityUid>();
|
||||
|
||||
var enumerator = AllEntityQuery<PredictedPhysicsComponent, PhysicsComponent, TransformComponent>();
|
||||
while (enumerator.MoveNext(out var _, out var physics, out var xform))
|
||||
while (enumerator.MoveNext(out _, out var physics, out var xform))
|
||||
{
|
||||
DebugTools.Assert(physics.Predict);
|
||||
|
||||
|
||||
@@ -352,6 +352,9 @@ public sealed partial class ReplayLoadManager
|
||||
// prototype changes when jumping around in time. This also requires reworking how the initial
|
||||
// implicit state data is generated, because we can't simply cache it anymore.
|
||||
// Also, does reloading prototypes in release mode modify existing entities?
|
||||
// Yes, yes it does. See PrototypeReloadSystem.UpdateEntity()
|
||||
// Its just not supported ATM.
|
||||
// TBH it'd be easier if overriding existing prototypes in release mode was just forbidden.
|
||||
|
||||
var msg = $"Overwriting an existing prototype! Kind: {kind.Name}. Ids: {string.Join(", ", ids)}";
|
||||
if (_confMan.GetCVar(CVars.ReplayIgnoreErrors))
|
||||
@@ -361,7 +364,6 @@ public sealed partial class ReplayLoadManager
|
||||
}
|
||||
}
|
||||
|
||||
_protoMan.ResolveResults();
|
||||
_protoMan.ReloadPrototypes(changed);
|
||||
_locMan.ReloadLocalizations();
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ internal sealed partial class ReplayPlaybackManager
|
||||
_gameState.UpdateFullRep(state, cloneDelta: true);
|
||||
var next = Replay.NextState;
|
||||
BeforeApplyState?.Invoke((state, next));
|
||||
var created = _gameState.ApplyGameState(state, next);
|
||||
_gameState.GenerateImplicitStates(created);
|
||||
_gameState.ApplyGameState(state, next);
|
||||
_gameState.MergeImplicitData();
|
||||
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
|
||||
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
|
||||
Replay.LastApplied = state.ToSequence;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
@@ -6,14 +7,47 @@ namespace Robust.Client.UserInterface;
|
||||
|
||||
public static class BoundUserInterfaceExt
|
||||
{
|
||||
private static T GetWindow<T>(BoundUserInterface bui) where T : BaseWindow, new()
|
||||
{
|
||||
var window = bui.CreateDisposableControl<T>();
|
||||
window.OnClose += bui.Close;
|
||||
var system = bui.EntMan.System<UserInterfaceSystem>();
|
||||
system.RegisterControl(bui, window);
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to create a window and also handle closing the BUI when it's closed.
|
||||
/// </summary>
|
||||
public static T CreateWindow<T>(this BoundUserInterface bui) where T : BaseWindow, new()
|
||||
{
|
||||
var window = bui.CreateDisposableControl<T>();
|
||||
window.OpenCentered();
|
||||
window.OnClose += bui.Close;
|
||||
var window = GetWindow<T>(bui);
|
||||
|
||||
if (bui.EntMan.System<UserInterfaceSystem>().TryGetPosition(bui.Owner, bui.UiKey, out var position))
|
||||
{
|
||||
window.Open(position);
|
||||
}
|
||||
else
|
||||
{
|
||||
window.OpenCentered();
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
public static T CreateWindowCenteredLeft<T>(this BoundUserInterface bui) where T : BaseWindow, new()
|
||||
{
|
||||
var window = GetWindow<T>(bui);
|
||||
|
||||
if (bui.EntMan.System<UserInterfaceSystem>().TryGetPosition(bui.Owner, bui.UiKey, out var position))
|
||||
{
|
||||
window.Open(position);
|
||||
}
|
||||
else
|
||||
{
|
||||
window.OpenCenteredLeft();
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace Robust.Client.UserInterface
|
||||
UserInterfaceManagerInternal.QueueStyleUpdate(this);
|
||||
}
|
||||
|
||||
internal void StyleSheetUpdate()
|
||||
public void InvalidateStyleSheet()
|
||||
{
|
||||
_stylesheetUpdateNeeded = true;
|
||||
|
||||
@@ -137,7 +137,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
internal void StylesheetUpdateRecursive()
|
||||
{
|
||||
StyleSheetUpdate();
|
||||
InvalidateStyleSheet();
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
@@ -149,7 +149,7 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
internal void DoStyleUpdate()
|
||||
public void DoStyleUpdate()
|
||||
{
|
||||
_styleProperties.Clear();
|
||||
|
||||
|
||||
@@ -549,7 +549,7 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
}
|
||||
|
||||
internal virtual void DrawInternal(IRenderHandle renderHandle)
|
||||
protected internal virtual void Draw(IRenderHandle renderHandle)
|
||||
{
|
||||
Draw(renderHandle.DrawingHandleScreen);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ public sealed class TileSpawningUIController : UIController
|
||||
return;
|
||||
_window = UIManager.CreateWindow<TileSpawnWindow>();
|
||||
LayoutContainer.SetAnchorPreset(_window,LayoutContainer.LayoutPreset.CenterLeft);
|
||||
_window.SearchBar.GrabKeyboardFocus();
|
||||
_window.ClearButton.OnPressed += OnTileClearPressed;
|
||||
_window.SearchBar.OnTextChanged += OnTileSearchChanged;
|
||||
_window.TileList.OnItemSelected += OnTileItemSelected;
|
||||
|
||||
@@ -22,8 +22,6 @@ public class EntityPrototypeView : SpriteView
|
||||
|
||||
public void SetPrototype(EntProtoId? entProto)
|
||||
{
|
||||
SpriteSystem ??= EntMan.System<SpriteSystem>();
|
||||
|
||||
if (entProto == _currentPrototype
|
||||
&& EntMan.TryGetComponent(Entity?.Owner, out MetaDataComponent? meta)
|
||||
&& meta.EntityPrototype?.ID == _currentPrototype)
|
||||
@@ -32,14 +30,10 @@ public class EntityPrototypeView : SpriteView
|
||||
}
|
||||
|
||||
_currentPrototype = entProto;
|
||||
SetEntity(null);
|
||||
EntMan.DeleteEntity(_ourEntity);
|
||||
|
||||
if (_currentPrototype != null)
|
||||
if (_ourEntity != null)
|
||||
{
|
||||
_ourEntity = EntMan.Spawn(_currentPrototype);
|
||||
SpriteSystem.ForceUpdate(_ourEntity.Value);
|
||||
SetEntity(_ourEntity);
|
||||
UpdateEntity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,12 +42,34 @@ public class EntityPrototypeView : SpriteView
|
||||
base.EnteredTree();
|
||||
|
||||
if (_currentPrototype != null)
|
||||
SetPrototype(_currentPrototype);
|
||||
{
|
||||
UpdateEntity();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
EntMan.TryQueueDeleteEntity(_ourEntity);
|
||||
_ourEntity = null;
|
||||
}
|
||||
|
||||
private void UpdateEntity()
|
||||
{
|
||||
SetEntity(null);
|
||||
EntMan.DeleteEntity(_ourEntity);
|
||||
|
||||
if (_currentPrototype != null)
|
||||
{
|
||||
SpriteSystem ??= EntMan.System<SpriteSystem>();
|
||||
|
||||
_ourEntity = EntMan.Spawn(_currentPrototype);
|
||||
SpriteSystem.ForceUpdate(_ourEntity.Value);
|
||||
SetEntity(_ourEntity);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ourEntity = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_spriteSize = new Vector2(longestRotatedSide, longestRotatedSide);
|
||||
}
|
||||
|
||||
internal override void DrawInternal(IRenderHandle renderHandle)
|
||||
protected internal override void Draw(IRenderHandle renderHandle)
|
||||
{
|
||||
if (!ResolveEntity(out var uid, out var sprite, out var xform))
|
||||
return;
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -36,8 +37,8 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
return;
|
||||
}
|
||||
|
||||
Parent.RemoveChild(this);
|
||||
OnClose?.Invoke();
|
||||
Parent.RemoveChild(this);
|
||||
}
|
||||
|
||||
protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
@@ -229,6 +230,16 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
OnOpen?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the window and places it at the specified position.
|
||||
/// </summary>
|
||||
public void Open(Vector2 position)
|
||||
{
|
||||
Measure(Vector2Helpers.Infinity);
|
||||
Open();
|
||||
LayoutContainer.SetPosition(this, position);
|
||||
}
|
||||
|
||||
public void OpenCentered() => OpenCenteredAt(new Vector2(0.5f, 0.5f));
|
||||
|
||||
public void OpenToLeft() => OpenCenteredAt(new Vector2(0, 0.5f));
|
||||
|
||||
@@ -10,4 +10,11 @@ public sealed partial class TileSpawnWindow : DefaultWindow
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
protected override void Opened()
|
||||
{
|
||||
base.Opened();
|
||||
|
||||
SearchBar.GrabKeyboardFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +65,13 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
// -- Handlers: Out --
|
||||
|
||||
protected internal override void Draw(DrawingHandleScreen handle)
|
||||
protected internal override void Draw(IRenderHandle handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (Viewport == null)
|
||||
{
|
||||
handle.DrawRect(UIBox2.FromDimensions(new Vector2(0, 0), Size * UIScale), Color.Red);
|
||||
handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(new Vector2(0, 0), Size * UIScale), Color.Red);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -82,7 +82,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
Viewport.RenderScreenOverlaysBelow(handle, this, viewportBounds);
|
||||
|
||||
Viewport.Render();
|
||||
handle.DrawTextureRect(Viewport.RenderTarget.Texture,
|
||||
handle.DrawingHandleScreen.DrawTextureRect(Viewport.RenderTarget.Texture,
|
||||
UIBox2.FromDimensions(new Vector2(0, 0), (Vector2i) (Viewport.Size / _viewportResolution)));
|
||||
|
||||
Viewport.RenderScreenOverlaysAbove(handle, this, viewportBounds);
|
||||
|
||||
@@ -7,6 +7,7 @@ using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
@@ -146,6 +147,16 @@ namespace Robust.Client.UserInterface
|
||||
/// but not necessarily a new or existing control is rearranged.
|
||||
/// </summary>
|
||||
void UpdateHovered();
|
||||
|
||||
/// <summary>
|
||||
/// Render a control and all of its children.
|
||||
/// </summary>
|
||||
void RenderControl(in Control.ControlRenderArguments args, Control control);
|
||||
|
||||
/// <summary>
|
||||
/// Render a control and all of its children.
|
||||
/// </summary>
|
||||
void RenderControl(IRenderHandle handle, Control control, Vector2i position);
|
||||
}
|
||||
|
||||
public readonly struct PostDrawUIRootEventArgs
|
||||
|
||||
@@ -313,7 +313,10 @@ internal partial class UserInterfaceManager
|
||||
|
||||
private void _clearTooltip()
|
||||
{
|
||||
if (!_showingTooltip) return;
|
||||
_resetTooltipTimer();
|
||||
|
||||
if (!_showingTooltip)
|
||||
return;
|
||||
|
||||
if (_suppliedTooltip != null)
|
||||
{
|
||||
@@ -322,7 +325,6 @@ internal partial class UserInterfaceManager
|
||||
}
|
||||
|
||||
CurrentlyHovered?.PerformHideTooltip();
|
||||
_resetTooltipTimer();
|
||||
_showingTooltip = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ internal sealed partial class UserInterfaceManager
|
||||
_roots.Add(newRoot);
|
||||
_windowsToRoot.Add(window.Id, newRoot);
|
||||
|
||||
newRoot.StyleSheetUpdate();
|
||||
newRoot.InvalidateStyleSheet();
|
||||
newRoot.InvalidateMeasure();
|
||||
QueueMeasureUpdate(newRoot);
|
||||
QueueArrangeUpdate(newRoot);
|
||||
|
||||
@@ -335,6 +335,30 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
public void RenderControl(in Control.ControlRenderArguments args, Control control)
|
||||
{
|
||||
var _ = 0;
|
||||
RenderControl(args.Handle,
|
||||
ref _,
|
||||
control,
|
||||
args.Position,
|
||||
args.Modulate,
|
||||
args.ScissorBox,
|
||||
args.CoordinateTransform);
|
||||
}
|
||||
|
||||
public void RenderControl(IRenderHandle handle, Control control, Vector2i position)
|
||||
{
|
||||
var _ = 0;
|
||||
RenderControl(handle,
|
||||
ref _,
|
||||
control,
|
||||
position,
|
||||
Color.White,
|
||||
null,
|
||||
Matrix3x2.Identity);
|
||||
}
|
||||
|
||||
public void RenderControl(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
|
||||
UIBox2i? scissorBox, Matrix3x2 coordinateTransform)
|
||||
{
|
||||
@@ -393,7 +417,7 @@ namespace Robust.Client.UserInterface
|
||||
// Handle modulation with care.
|
||||
var oldMod = handle.Modulate;
|
||||
handle.Modulate = modulate * control.ActualModulateSelf;
|
||||
control.DrawInternal(renderHandle);
|
||||
control.Draw(renderHandle);
|
||||
handle.Modulate = oldMod;
|
||||
handle.UseShader(null);
|
||||
}
|
||||
@@ -409,10 +433,11 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
control.PreRenderChildren(ref args);
|
||||
|
||||
foreach (var child in control.Children)
|
||||
for (var index = 0; index < control.ChildCount; index++)
|
||||
{
|
||||
var child = control.GetChild(index);
|
||||
var pos = position + (Vector2i)Vector2.Transform(child.PixelPosition, coordinateTransform);
|
||||
control.RenderChildOverride(ref args, child.GetPositionInParent(), pos);
|
||||
control.RenderChildOverride(ref args, index, pos);
|
||||
}
|
||||
|
||||
control.PostRenderChildren(ref args);
|
||||
|
||||
@@ -20,6 +20,10 @@ namespace Robust.Server.GameStates;
|
||||
/// </summary>
|
||||
internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<PvsData> memoryRegion)
|
||||
{
|
||||
#if DEBUG
|
||||
public HashSet<NetEntity> ToSendSet = new();
|
||||
#endif
|
||||
|
||||
public readonly ICommonSession Session = session;
|
||||
|
||||
public readonly ResizableMemoryRegion<PvsData> DataMemory = memoryRegion;
|
||||
@@ -197,7 +201,7 @@ internal struct PvsMetadata
|
||||
{
|
||||
DebugTools.AssertEqual(NetEntity, comp.NetEntity);
|
||||
DebugTools.AssertEqual(VisMask, comp.VisibilityMask);
|
||||
DebugTools.AssertEqual(LifeStage, comp.EntityLifeStage);
|
||||
DebugTools.Assert(LifeStage == comp.EntityLifeStage);
|
||||
DebugTools.Assert(LastModifiedTick == comp.EntityLastModifiedTick || LastModifiedTick.Value == 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,9 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
// Process all entities in visible PVS chunks
|
||||
AddPvsChunks(session);
|
||||
|
||||
#if DEBUG
|
||||
VerifySessionData(session);
|
||||
#endif
|
||||
|
||||
var toSend = session.ToSend!;
|
||||
session.ToSend = null;
|
||||
@@ -332,11 +334,12 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
session.Overflow = oldEntry.Value;
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
#if DEBUG
|
||||
private void VerifySessionData(PvsSession pvsSession)
|
||||
{
|
||||
var toSend = pvsSession.ToSend;
|
||||
var toSendSet = new HashSet<NetEntity>(toSend!.Count);
|
||||
var toSend = pvsSession.ToSend!;
|
||||
var toSendSet = pvsSession.ToSendSet;
|
||||
toSendSet.Clear();
|
||||
|
||||
foreach (var intPtr in toSend)
|
||||
{
|
||||
@@ -360,6 +363,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|| data.LastSeen == _gameTiming.CurTick - 1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private (Vector2 worldPos, float range, EntityUid? map) CalcViewBounds(Entity<TransformComponent, EyeComponent?> eye)
|
||||
{
|
||||
|
||||
@@ -39,7 +39,6 @@ namespace Robust.Shared.Maths
|
||||
/// <param name="dir"></param>
|
||||
public Angle(Vector2 dir)
|
||||
{
|
||||
dir = dir.Normalized();
|
||||
Theta = Math.Atan2(dir.Y, dir.X);
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,10 @@ public abstract partial class SharedContainerSystem
|
||||
{
|
||||
if (entity.Comp2 is { } physics)
|
||||
{
|
||||
// TODO CONTAINER
|
||||
// Is this actually needed?
|
||||
// I.e., shouldn't this just do a if (_timing.ApplyingState) return
|
||||
|
||||
// Here we intentionally don't dirty the physics comp. Client-side state handling will apply these same
|
||||
// changes. This also ensures that the server doesn't have to send the physics comp state to every
|
||||
// player for any entity inside of a container during init.
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
public abstract class BoundUserInterface : IDisposable
|
||||
{
|
||||
[Dependency] protected readonly IEntityManager EntMan = default!;
|
||||
[Dependency] protected internal readonly IEntityManager EntMan = default!;
|
||||
[Dependency] protected readonly ISharedPlayerManager PlayerManager = default!;
|
||||
protected readonly SharedUserInterfaceSystem UiSystem;
|
||||
|
||||
@@ -28,13 +28,6 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
protected internal BoundUserInterfaceState? State { get; internal set; }
|
||||
|
||||
// Bandaid just for storage :)
|
||||
/// <summary>
|
||||
/// Defers state handling
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public virtual bool DeferredClose { get; } = true;
|
||||
|
||||
protected BoundUserInterface(EntityUid owner, Enum uiKey)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
@@ -38,11 +39,14 @@ public abstract partial class EntityManager
|
||||
Dirty(uid, comp, metadata);
|
||||
}
|
||||
|
||||
public void DirtyField<T>(EntityUid uid, T comp, string fieldName, MetaDataComponent? metadata = null)
|
||||
public virtual void DirtyField<T>(EntityUid uid, T comp, string fieldName, MetaDataComponent? metadata = null)
|
||||
where T : IComponentDelta
|
||||
{
|
||||
var compReg = ComponentFactory.GetRegistration(CompIdx.Index<T>());
|
||||
|
||||
// TODO
|
||||
// consider storing this on MetaDataComponent?
|
||||
// We alsready store other dirtying information there anyways, and avoids having to fetch the registration.
|
||||
if (!compReg.NetworkedFieldLookup.TryGetValue(fieldName, out var idx))
|
||||
{
|
||||
_sawmill.Error($"Tried to dirty delta field {fieldName} on {ToPrettyString(uid)} that isn't implemented.");
|
||||
@@ -54,6 +58,24 @@ public abstract partial class EntityManager
|
||||
comp.LastModifiedFields[idx] = curTick;
|
||||
Dirty(uid, comp, metadata);
|
||||
}
|
||||
|
||||
public virtual void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
|
||||
where T : IComponentDelta
|
||||
{
|
||||
var compReg = ComponentFactory.GetRegistration(CompIdx.Index<T>());
|
||||
|
||||
var curTick = _gameTiming.CurTick;
|
||||
foreach (var field in fields)
|
||||
{
|
||||
if (!compReg.NetworkedFieldLookup.TryGetValue(field, out var idx))
|
||||
_sawmill.Error($"Tried to dirty delta field {field} on {ToPrettyString(uid)} that isn't implemented.");
|
||||
else
|
||||
comp.LastModifiedFields[idx] = curTick;
|
||||
}
|
||||
|
||||
comp.LastFieldUpdate = curTick;
|
||||
Dirty(uid, comp, meta);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1694,7 +1694,6 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp1? component, bool logMissing = true)
|
||||
{
|
||||
if (component != null)
|
||||
@@ -1717,7 +1716,7 @@ namespace Robust.Shared.GameObjects
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Resolve(ref Entity<TComp1?> entity, bool logMissing = true)
|
||||
{
|
||||
return Resolve(entity.Owner, ref entity.Comp, logMissing);
|
||||
|
||||
@@ -171,6 +171,13 @@ public partial class EntitySystem
|
||||
EntityManager.DirtyField(uid, component, fieldName, meta);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
|
||||
where T : IComponentDelta
|
||||
{
|
||||
EntityManager.DirtyFields(uid, comp, meta);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a component as dirty. This also implicitly dirties the entity this component belongs to.
|
||||
/// </summary>
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
public void SetEnabled(EntityUid uid, bool enabled, CollisionWakeComponent? component = null)
|
||||
{
|
||||
if (!_query.Resolve(uid, ref component) || component.Enabled == enabled)
|
||||
if (!_query.Resolve(uid, ref component, false) || component.Enabled == enabled)
|
||||
return;
|
||||
|
||||
component.Enabled = enabled;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
@@ -11,7 +9,6 @@ using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -77,14 +74,14 @@ public sealed partial class EntityLookupSystem
|
||||
/// <summary>
|
||||
/// Wrapper around the per-grid version.
|
||||
/// </summary>
|
||||
private void AddEntitiesIntersecting(MapId mapId,
|
||||
private void AddEntitiesIntersecting<T>(MapId mapId,
|
||||
HashSet<EntityUid> intersecting,
|
||||
IPhysShape shape,
|
||||
T shape,
|
||||
Transform shapeTransform,
|
||||
LookupFlags flags)
|
||||
LookupFlags flags) where T : IPhysShape
|
||||
{
|
||||
var worldAABB = shape.ComputeAABB(shapeTransform, 0);
|
||||
var state = new EntityQueryState(intersecting,
|
||||
var state = new EntityQueryState<T>(intersecting,
|
||||
shape,
|
||||
shapeTransform,
|
||||
_fixtures,
|
||||
@@ -96,7 +93,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
// Need to include maps
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
|
||||
static (EntityUid uid, MapGridComponent _, ref EntityQueryState state) =>
|
||||
static (EntityUid uid, MapGridComponent _, ref EntityQueryState<T> state) =>
|
||||
{
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
@@ -112,19 +109,19 @@ public sealed partial class EntityLookupSystem
|
||||
AddContained(intersecting, flags);
|
||||
}
|
||||
|
||||
private void AddEntitiesIntersecting(
|
||||
private void AddEntitiesIntersecting<T>(
|
||||
EntityUid lookupUid,
|
||||
HashSet<EntityUid> intersecting,
|
||||
IPhysShape shape,
|
||||
T shape,
|
||||
Box2 localAABB,
|
||||
Transform localShapeTransform,
|
||||
LookupFlags flags,
|
||||
BroadphaseComponent? lookup = null)
|
||||
BroadphaseComponent? lookup = null) where T : IPhysShape
|
||||
{
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return;
|
||||
|
||||
var state = new EntityQueryState(
|
||||
var state = new EntityQueryState<T>(
|
||||
intersecting,
|
||||
shape,
|
||||
localShapeTransform,
|
||||
@@ -157,7 +154,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
return;
|
||||
|
||||
static bool PhysicsQuery(ref EntityQueryState state, in FixtureProxy value)
|
||||
static bool PhysicsQuery(ref EntityQueryState<T> state, in FixtureProxy value)
|
||||
{
|
||||
var sensors = (state.Flags & LookupFlags.Sensors) != 0x0;
|
||||
|
||||
@@ -179,7 +176,7 @@ public sealed partial class EntityLookupSystem
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SundriesQuery(ref EntityQueryState state, in EntityUid value)
|
||||
static bool SundriesQuery(ref EntityQueryState<T> state, in EntityUid value)
|
||||
{
|
||||
var approx = (state.Flags & LookupFlags.Approximate) != 0x0;
|
||||
|
||||
@@ -227,14 +224,14 @@ public sealed partial class EntityLookupSystem
|
||||
/// <summary>
|
||||
/// Wrapper around the per-grid version.
|
||||
/// </summary>
|
||||
private bool AnyEntitiesIntersecting(MapId mapId,
|
||||
IPhysShape shape,
|
||||
private bool AnyEntitiesIntersecting<T>(MapId mapId,
|
||||
T shape,
|
||||
Transform shapeTransform,
|
||||
LookupFlags flags,
|
||||
EntityUid? ignored = null)
|
||||
EntityUid? ignored = null) where T : IPhysShape
|
||||
{
|
||||
var worldAABB = shape.ComputeAABB(shapeTransform, 0);
|
||||
var state = new AnyEntityQueryState(false,
|
||||
var state = new AnyEntityQueryState<T>(false,
|
||||
ignored,
|
||||
shape,
|
||||
shapeTransform,
|
||||
@@ -247,7 +244,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
// Need to include maps
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
|
||||
static (EntityUid uid, MapGridComponent _, ref AnyEntityQueryState state) =>
|
||||
static (EntityUid uid, MapGridComponent _, ref AnyEntityQueryState<T> state) =>
|
||||
{
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
@@ -272,18 +269,18 @@ public sealed partial class EntityLookupSystem
|
||||
return state.Found;
|
||||
}
|
||||
|
||||
private bool AnyEntitiesIntersecting(EntityUid lookupUid,
|
||||
IPhysShape shape,
|
||||
private bool AnyEntitiesIntersecting<T>(EntityUid lookupUid,
|
||||
T shape,
|
||||
Box2 localAABB,
|
||||
Transform shapeTransform,
|
||||
LookupFlags flags,
|
||||
EntityUid? ignored = null,
|
||||
BroadphaseComponent? lookup = null)
|
||||
BroadphaseComponent? lookup = null) where T : IPhysShape
|
||||
{
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return false;
|
||||
|
||||
var state = new AnyEntityQueryState(false,
|
||||
var state = new AnyEntityQueryState<T>(false,
|
||||
ignored,
|
||||
shape,
|
||||
shapeTransform,
|
||||
@@ -325,7 +322,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
return state.Found;
|
||||
|
||||
static bool PhysicsQuery(ref AnyEntityQueryState state, in FixtureProxy value)
|
||||
static bool PhysicsQuery(ref AnyEntityQueryState<T> state, in FixtureProxy value)
|
||||
{
|
||||
if (state.Ignored == value.Entity)
|
||||
return true;
|
||||
@@ -350,7 +347,7 @@ public sealed partial class EntityLookupSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool SundriesQuery(ref AnyEntityQueryState state, in EntityUid value)
|
||||
static bool SundriesQuery(ref AnyEntityQueryState<T> state, in EntityUid value)
|
||||
{
|
||||
if (state.Ignored == value)
|
||||
return true;
|
||||
@@ -409,9 +406,16 @@ public sealed partial class EntityLookupSystem
|
||||
var broadphaseInv = _transform.GetInvWorldMatrix(lookupUid);
|
||||
|
||||
var localBounds = broadphaseInv.TransformBounds(worldBounds);
|
||||
var shape = new Polygon(localBounds);
|
||||
var polygon = _physics.GetPooled(localBounds);
|
||||
var result = AnyEntitiesIntersecting(lookupUid,
|
||||
polygon,
|
||||
localBounds.CalcBoundingBox(),
|
||||
Physics.Transform.Empty,
|
||||
flags,
|
||||
ignored);
|
||||
|
||||
return AnyEntitiesIntersecting(lookupUid, shape, localBounds.CalcBoundingBox(), Physics.Transform.Empty, flags, ignored);
|
||||
_physics.ReturnPooled(polygon);
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -454,8 +458,10 @@ public sealed partial class EntityLookupSystem
|
||||
{
|
||||
if (mapId == MapId.Nullspace) return false;
|
||||
|
||||
var shape = new Polygon(worldAABB);
|
||||
return AnyEntitiesIntersecting(mapId, shape, Physics.Transform.Empty, flags);
|
||||
var polygon = _physics.GetPooled(worldAABB);
|
||||
var result = AnyEntitiesIntersecting(mapId, polygon, Physics.Transform.Empty, flags);
|
||||
_physics.ReturnPooled(polygon);
|
||||
return result;
|
||||
}
|
||||
|
||||
public HashSet<EntityUid> GetEntitiesIntersecting(MapId mapId, Box2 worldAABB, LookupFlags flags = DefaultFlags)
|
||||
@@ -469,8 +475,9 @@ public sealed partial class EntityLookupSystem
|
||||
{
|
||||
if (mapId == MapId.Nullspace) return;
|
||||
|
||||
var shape = new Polygon(worldAABB);
|
||||
AddEntitiesIntersecting(mapId, intersecting, shape, Physics.Transform.Empty, flags);
|
||||
var polygon = _physics.GetPooled(worldAABB);
|
||||
AddEntitiesIntersecting(mapId, intersecting, polygon, Physics.Transform.Empty, flags);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -480,15 +487,18 @@ public sealed partial class EntityLookupSystem
|
||||
public bool AnyEntitiesIntersecting(MapId mapId, Box2Rotated worldBounds, LookupFlags flags = DefaultFlags)
|
||||
{
|
||||
// Don't need to check contained entities as they have the same bounds as the parent.
|
||||
var shape = new Polygon(worldBounds);
|
||||
return AnyEntitiesIntersecting(mapId, shape, Physics.Transform.Empty, flags);
|
||||
var polygon = _physics.GetPooled(worldBounds);
|
||||
var result = AnyEntitiesIntersecting(mapId, polygon, Physics.Transform.Empty, flags);
|
||||
_physics.ReturnPooled(polygon);
|
||||
return result;
|
||||
}
|
||||
|
||||
public HashSet<EntityUid> GetEntitiesIntersecting(MapId mapId, Box2Rotated worldBounds, LookupFlags flags = DefaultFlags)
|
||||
{
|
||||
var intersecting = new HashSet<EntityUid>();
|
||||
var shape = new Polygon(worldBounds);
|
||||
AddEntitiesIntersecting(mapId, intersecting, shape, Physics.Transform.Empty, flags);
|
||||
var polygon = _physics.GetPooled(worldBounds);
|
||||
AddEntitiesIntersecting(mapId, intersecting, polygon, Physics.Transform.Empty, flags);
|
||||
_physics.ReturnPooled(polygon);
|
||||
return intersecting;
|
||||
}
|
||||
|
||||
@@ -703,12 +713,12 @@ public sealed partial class EntityLookupSystem
|
||||
return entities;
|
||||
}
|
||||
|
||||
public void GetEntitiesIntersecting(
|
||||
public void GetEntitiesIntersecting<T>(
|
||||
MapId mapId,
|
||||
IPhysShape shape,
|
||||
T shape,
|
||||
Transform transform,
|
||||
HashSet<EntityUid> entities,
|
||||
LookupFlags flags = LookupFlags.All)
|
||||
LookupFlags flags = LookupFlags.All) where T : IPhysShape
|
||||
{
|
||||
if (mapId == MapId.Nullspace)
|
||||
return;
|
||||
@@ -751,10 +761,11 @@ public sealed partial class EntityLookupSystem
|
||||
return;
|
||||
|
||||
var localAABB = _transform.GetInvWorldMatrix(gridId).TransformBox(worldAABB);
|
||||
var shape = new Polygon(localAABB);
|
||||
var polygon = _physics.GetPooled(localAABB);
|
||||
|
||||
AddEntitiesIntersecting(gridId, intersecting, shape, localAABB, Physics.Transform.Empty, flags, lookup);
|
||||
AddEntitiesIntersecting(gridId, intersecting, polygon, localAABB, Physics.Transform.Empty, flags, lookup);
|
||||
AddContained(intersecting, flags);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
public void GetEntitiesIntersecting(EntityUid gridId, Box2Rotated worldBounds, HashSet<EntityUid> intersecting, LookupFlags flags = DefaultFlags)
|
||||
@@ -763,10 +774,11 @@ public sealed partial class EntityLookupSystem
|
||||
return;
|
||||
|
||||
var localBounds = _transform.GetInvWorldMatrix(gridId).TransformBounds(worldBounds);
|
||||
var shape = new Polygon(localBounds);
|
||||
var polygon = _physics.GetPooled(localBounds);
|
||||
|
||||
AddEntitiesIntersecting(gridId, intersecting, shape, localBounds.CalcBoundingBox(), Physics.Transform.Empty, flags, lookup);
|
||||
AddEntitiesIntersecting(gridId, intersecting, polygon, localBounds.CalcBoundingBox(), Physics.Transform.Empty, flags, lookup);
|
||||
AddContained(intersecting, flags);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -832,10 +844,10 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
#endregion
|
||||
|
||||
private record struct AnyEntityQueryState(
|
||||
private record struct AnyEntityQueryState<T>(
|
||||
bool Found,
|
||||
EntityUid? Ignored,
|
||||
IPhysShape Shape,
|
||||
T Shape,
|
||||
Transform Transform,
|
||||
FixtureSystem Fixtures,
|
||||
EntityLookupSystem Lookup,
|
||||
@@ -843,11 +855,11 @@ public sealed partial class EntityLookupSystem
|
||||
IManifoldManager Manifolds,
|
||||
EntityQuery<FixturesComponent> FixturesQuery,
|
||||
LookupFlags Flags
|
||||
);
|
||||
) where T : IPhysShape;
|
||||
|
||||
private readonly record struct EntityQueryState(
|
||||
private readonly record struct EntityQueryState<T>(
|
||||
HashSet<EntityUid> Intersecting,
|
||||
IPhysShape Shape,
|
||||
T Shape,
|
||||
Transform Transform,
|
||||
FixtureSystem Fixtures,
|
||||
EntityLookupSystem Lookup,
|
||||
@@ -855,5 +867,5 @@ public sealed partial class EntityLookupSystem
|
||||
IManifoldManager Manifolds,
|
||||
EntityQuery<FixturesComponent> FixturesQuery,
|
||||
LookupFlags Flags
|
||||
);
|
||||
) where T : IPhysShape;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public sealed partial class EntityLookupSystem
|
||||
/// <summary>
|
||||
/// Common method to determine if an entity overlaps the specified shape.
|
||||
/// </summary>
|
||||
private bool IsIntersecting(MapId mapId, EntityUid uid, TransformComponent xform, IPhysShape shape, Transform shapeTransform, Box2 worldAABB, LookupFlags flags)
|
||||
private bool IsIntersecting<TShape>(MapId mapId, EntityUid uid, TransformComponent xform, TShape shape, Transform shapeTransform, Box2 worldAABB, LookupFlags flags) where TShape : IPhysShape
|
||||
{
|
||||
var (entPos, entRot) = _transform.GetWorldPositionRotation(xform);
|
||||
|
||||
@@ -121,25 +121,27 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return;
|
||||
|
||||
var lookupPoly = new Polygon(localAABB);
|
||||
|
||||
AddEntitiesIntersecting(lookupUid, intersecting, lookupPoly, localAABB, Physics.Transform.Empty, flags, query, lookup);
|
||||
var polygon = _physics.GetPooled(localAABB);
|
||||
AddEntitiesIntersecting(lookupUid, intersecting, polygon, localAABB, Physics.Transform.Empty, flags, query, lookup);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
private void AddEntitiesIntersecting<T>(
|
||||
private void AddEntitiesIntersecting<T, TShape>(
|
||||
EntityUid lookupUid,
|
||||
HashSet<Entity<T>> intersecting,
|
||||
IPhysShape shape,
|
||||
TShape shape,
|
||||
Box2 localAABB,
|
||||
Transform localTransform,
|
||||
LookupFlags flags,
|
||||
EntityQuery<T> query,
|
||||
BroadphaseComponent? lookup = null) where T : IComponent
|
||||
BroadphaseComponent? lookup = null)
|
||||
where T : IComponent
|
||||
where TShape : IPhysShape
|
||||
{
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return;
|
||||
|
||||
var state = new QueryState<T>(
|
||||
var state = new QueryState<T, TShape>(
|
||||
intersecting,
|
||||
shape,
|
||||
localTransform,
|
||||
@@ -174,7 +176,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
return;
|
||||
|
||||
static bool PhysicsQuery(ref QueryState<T> state, in FixtureProxy value)
|
||||
static bool PhysicsQuery(ref QueryState<T, TShape> state, in FixtureProxy value)
|
||||
{
|
||||
if (!state.Sensors && !value.Fixture.Hard)
|
||||
return true;
|
||||
@@ -195,7 +197,7 @@ public sealed partial class EntityLookupSystem
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SundriesQuery(ref QueryState<T> state, in EntityUid value)
|
||||
static bool SundriesQuery(ref QueryState<T, TShape> state, in EntityUid value)
|
||||
{
|
||||
if (!state.Query.TryGetComponent(value, out var comp))
|
||||
return true;
|
||||
@@ -250,22 +252,26 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return false;
|
||||
|
||||
var shape = new Polygon(localAABB);
|
||||
var polygon = _physics.GetPooled(localAABB);
|
||||
var (lookupPos, lookupRot) = _transform.GetWorldPositionRotation(lookupUid);
|
||||
var transform = new Transform(lookupPos, lookupRot);
|
||||
var result = AnyComponentsIntersecting(lookupUid, polygon, localAABB, transform, flags, query, ignored, lookup);
|
||||
_physics.ReturnPooled(polygon);
|
||||
|
||||
return AnyComponentsIntersecting(lookupUid, shape, localAABB, transform, flags, query, ignored, lookup);
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool AnyComponentsIntersecting<T>(
|
||||
private bool AnyComponentsIntersecting<T, TShape>(
|
||||
EntityUid lookupUid,
|
||||
IPhysShape shape,
|
||||
TShape shape,
|
||||
Box2 localAABB,
|
||||
Transform shapeTransform,
|
||||
LookupFlags flags,
|
||||
EntityQuery<T> query,
|
||||
EntityUid? ignored = null,
|
||||
BroadphaseComponent? lookup = null) where T : IComponent
|
||||
BroadphaseComponent? lookup = null)
|
||||
where T : IComponent
|
||||
where TShape : IPhysShape
|
||||
{
|
||||
/*
|
||||
* Unfortunately this is split from the other query as we can short-circuit here, hence the code duplication.
|
||||
@@ -275,7 +281,7 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.Resolve(lookupUid, ref lookup))
|
||||
return false;
|
||||
|
||||
var state = new AnyQueryState<T>(false,
|
||||
var state = new AnyQueryState<T, TShape>(false,
|
||||
ignored,
|
||||
shape,
|
||||
shapeTransform,
|
||||
@@ -317,7 +323,7 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
return state.Found;
|
||||
|
||||
static bool PhysicsQuery(ref AnyQueryState<T> state, in FixtureProxy value)
|
||||
static bool PhysicsQuery(ref AnyQueryState<T, TShape> state, in FixtureProxy value)
|
||||
{
|
||||
if (value.Entity == state.Ignored)
|
||||
return true;
|
||||
@@ -342,7 +348,7 @@ public sealed partial class EntityLookupSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool SundriesQuery(ref AnyQueryState<T> state, in EntityUid value)
|
||||
static bool SundriesQuery(ref AnyQueryState<T, TShape> state, in EntityUid value)
|
||||
{
|
||||
if (state.Ignored == value)
|
||||
return true;
|
||||
@@ -421,13 +427,14 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
public bool AnyComponentsIntersecting(Type type, MapId mapId, Box2 worldAABB, EntityUid? ignored = null, LookupFlags flags = DefaultFlags)
|
||||
{
|
||||
var shape = new Polygon(worldAABB);
|
||||
var polygon = _physics.GetPooled(worldAABB);
|
||||
var transform = Physics.Transform.Empty;
|
||||
|
||||
return AnyComponentsIntersecting(type, mapId, shape, transform, ignored, flags);
|
||||
var result = AnyComponentsIntersecting(type, mapId, polygon, transform, ignored, flags);
|
||||
_physics.ReturnPooled(polygon);
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool AnyComponentsIntersecting(Type type, MapId mapId, IPhysShape shape, Transform shapeTransform, EntityUid? ignored = null, LookupFlags flags = DefaultFlags)
|
||||
public bool AnyComponentsIntersecting<T>(Type type, MapId mapId, T shape, Transform shapeTransform, EntityUid? ignored = null, LookupFlags flags = DefaultFlags) where T : IPhysShape
|
||||
{
|
||||
DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type));
|
||||
if (mapId == MapId.Nullspace)
|
||||
@@ -489,37 +496,40 @@ public sealed partial class EntityLookupSystem
|
||||
if (mapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
var shape = new Polygon(worldAABB);
|
||||
var polygon = _physics.GetPooled(worldAABB);
|
||||
var transform = Physics.Transform.Empty;
|
||||
|
||||
GetEntitiesIntersecting(type, mapId, shape, transform, intersecting, flags);
|
||||
GetEntitiesIntersecting(type, mapId, polygon, transform, intersecting, flags);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
public void GetEntitiesIntersecting<T>(MapId mapId, Box2Rotated worldBounds, HashSet<Entity<T>> entities, LookupFlags flags = DefaultFlags) where T : IComponent
|
||||
{
|
||||
if (mapId == MapId.Nullspace) return;
|
||||
|
||||
var shape = new Polygon(worldBounds);
|
||||
var polygon = _physics.GetPooled(worldBounds);
|
||||
var shapeTransform = Physics.Transform.Empty;
|
||||
|
||||
GetEntitiesIntersecting(mapId, shape, shapeTransform, entities, flags);
|
||||
GetEntitiesIntersecting(mapId, polygon, shapeTransform, entities, flags);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
public void GetEntitiesIntersecting<T>(MapId mapId, Box2 worldAABB, HashSet<Entity<T>> entities, LookupFlags flags = DefaultFlags) where T : IComponent
|
||||
{
|
||||
if (mapId == MapId.Nullspace) return;
|
||||
|
||||
var shape = new Polygon(worldAABB);
|
||||
var polygon = _physics.GetPooled(worldAABB);
|
||||
var shapeTransform = Physics.Transform.Empty;
|
||||
|
||||
GetEntitiesIntersecting(mapId, shape, shapeTransform, entities, flags);
|
||||
GetEntitiesIntersecting(mapId, polygon, shapeTransform, entities, flags);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPhysShape
|
||||
|
||||
public void GetEntitiesIntersecting(Type type, MapId mapId, IPhysShape shape, Transform shapeTransform, HashSet<Entity<IComponent>> intersecting, LookupFlags flags = DefaultFlags)
|
||||
public void GetEntitiesIntersecting<T>(Type type, MapId mapId, T shape, Transform shapeTransform, HashSet<Entity<IComponent>> intersecting, LookupFlags flags = DefaultFlags) where T : IPhysShape
|
||||
{
|
||||
DebugTools.Assert(typeof(IComponent).IsAssignableFrom(type));
|
||||
if (mapId == MapId.Nullspace)
|
||||
@@ -544,10 +554,10 @@ public sealed partial class EntityLookupSystem
|
||||
var query = EntityManager.GetEntityQuery(type);
|
||||
|
||||
// Get grid entities
|
||||
var state = new GridQueryState<IComponent>(intersecting, shape, shapeTransform, this, _physics, flags, query);
|
||||
var state = new GridQueryState<IComponent, T>(intersecting, shape, shapeTransform, this, _physics, flags, query);
|
||||
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
|
||||
static (EntityUid uid, MapGridComponent grid, ref GridQueryState<IComponent> state) =>
|
||||
static (EntityUid uid, MapGridComponent grid, ref GridQueryState<IComponent, T> state) =>
|
||||
{
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
@@ -565,7 +575,9 @@ public sealed partial class EntityLookupSystem
|
||||
}
|
||||
}
|
||||
|
||||
public void GetEntitiesIntersecting<T>(MapId mapId, IPhysShape shape, Transform shapeTransform, HashSet<Entity<T>> entities, LookupFlags flags = DefaultFlags) where T : IComponent
|
||||
public void GetEntitiesIntersecting<T, TShape>(MapId mapId, TShape shape, Transform shapeTransform, HashSet<Entity<T>> entities, LookupFlags flags = DefaultFlags)
|
||||
where T : IComponent
|
||||
where TShape : IPhysShape
|
||||
{
|
||||
if (mapId == MapId.Nullspace) return;
|
||||
|
||||
@@ -588,10 +600,10 @@ public sealed partial class EntityLookupSystem
|
||||
var query = GetEntityQuery<T>();
|
||||
|
||||
// Get grid entities
|
||||
var state = new GridQueryState<T>(entities, shape, shapeTransform, this, _physics, flags, query);
|
||||
var state = new GridQueryState<T, TShape>(entities, shape, shapeTransform, this, _physics, flags, query);
|
||||
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
|
||||
static (EntityUid uid, MapGridComponent grid, ref GridQueryState<T> state) =>
|
||||
static (EntityUid uid, MapGridComponent grid, ref GridQueryState<T, TShape> state) =>
|
||||
{
|
||||
var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid);
|
||||
var localAabb = state.Shape.ComputeAABB(localTransform, 0);
|
||||
@@ -679,7 +691,9 @@ public sealed partial class EntityLookupSystem
|
||||
GetEntitiesInRange(mapId, shape, transform, entities, flags);
|
||||
}
|
||||
|
||||
public void GetEntitiesInRange<T>(MapId mapId, IPhysShape shape, Transform transform, HashSet<Entity<T>> entities, LookupFlags flags = DefaultFlags) where T : IComponent
|
||||
public void GetEntitiesInRange<T, TShape>(MapId mapId, TShape shape, Transform transform, HashSet<Entity<T>> entities, LookupFlags flags = DefaultFlags)
|
||||
where T : IComponent
|
||||
where TShape : IPhysShape
|
||||
{
|
||||
DebugTools.Assert(shape.Radius > 0, "Range must be a positive float");
|
||||
|
||||
@@ -829,20 +843,21 @@ public sealed partial class EntityLookupSystem
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct GridQueryState<T>(
|
||||
private readonly record struct GridQueryState<T, TShape>(
|
||||
HashSet<Entity<T>> Intersecting,
|
||||
IPhysShape Shape,
|
||||
TShape Shape,
|
||||
Transform Transform,
|
||||
EntityLookupSystem Lookup,
|
||||
SharedPhysicsSystem Physics,
|
||||
LookupFlags Flags,
|
||||
EntityQuery<T> Query
|
||||
) where T : IComponent;
|
||||
) where T : IComponent
|
||||
where TShape : IPhysShape;
|
||||
|
||||
private record struct AnyQueryState<T>(
|
||||
private record struct AnyQueryState<T, TShape>(
|
||||
bool Found,
|
||||
EntityUid? Ignored,
|
||||
IPhysShape Shape,
|
||||
TShape Shape,
|
||||
Transform Transform,
|
||||
FixtureSystem Fixtures,
|
||||
SharedPhysicsSystem Physics,
|
||||
@@ -850,11 +865,12 @@ public sealed partial class EntityLookupSystem
|
||||
EntityQuery<T> Query,
|
||||
EntityQuery<FixturesComponent> FixturesQuery,
|
||||
LookupFlags Flags
|
||||
) where T : IComponent;
|
||||
) where T : IComponent
|
||||
where TShape : IPhysShape;
|
||||
|
||||
private readonly record struct QueryState<T>(
|
||||
private readonly record struct QueryState<T, TShape>(
|
||||
HashSet<Entity<T>> Intersecting,
|
||||
IPhysShape Shape,
|
||||
TShape Shape,
|
||||
Transform Transform,
|
||||
FixtureSystem Fixtures,
|
||||
SharedPhysicsSystem Physics,
|
||||
@@ -863,5 +879,6 @@ public sealed partial class EntityLookupSystem
|
||||
EntityQuery<FixturesComponent> FixturesQuery,
|
||||
bool Sensors,
|
||||
bool Approximate
|
||||
) where T : IComponent;
|
||||
) where T : IComponent
|
||||
where TShape : IPhysShape;
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
if (data.IsDeleted())
|
||||
{
|
||||
if (!component.Chunks.TryGetValue(index, out var deletedChunk))
|
||||
if (!component.Chunks.Remove(index, out var deletedChunk))
|
||||
return;
|
||||
|
||||
// Deleted chunks still need to raise tile-changed events.
|
||||
@@ -330,11 +330,13 @@ public abstract partial class SharedMapSystem
|
||||
}
|
||||
}
|
||||
|
||||
component.Chunks.Remove(index);
|
||||
return;
|
||||
}
|
||||
|
||||
var chunk = GetOrAddChunk(uid, component, index);
|
||||
chunk.Fixtures.Clear();
|
||||
chunk.Fixtures.UnionWith(data.Fixtures);
|
||||
|
||||
chunk.SuppressCollisionRegeneration = true;
|
||||
DebugTools.Assert(data.TileData.Any(x => !x.IsEmpty));
|
||||
DebugTools.Assert(data.TileData.Length == component.ChunkSize * component.ChunkSize);
|
||||
@@ -355,12 +357,6 @@ public abstract partial class SharedMapSystem
|
||||
// These should never refer to the same object
|
||||
DebugTools.AssertNotEqual(chunk.Fixtures, data.Fixtures);
|
||||
|
||||
if (!chunk.Fixtures.SetEquals(data.Fixtures))
|
||||
{
|
||||
chunk.Fixtures.Clear();
|
||||
chunk.Fixtures.UnionWith(data.Fixtures);
|
||||
}
|
||||
|
||||
chunk.CachedBounds = data.CachedBounds!.Value;
|
||||
chunk.SuppressCollisionRegeneration = false;
|
||||
}
|
||||
|
||||
@@ -289,18 +289,26 @@ public abstract partial class SharedTransformSystem
|
||||
EntityUid uid,
|
||||
TransformComponent xform)
|
||||
{
|
||||
// Dont set pre-init, as the map grid component might not have been added yet.
|
||||
if (xform._gridInitialized || xform.LifeStage < ComponentLifeStage.Initializing)
|
||||
if (xform._gridInitialized)
|
||||
return;
|
||||
|
||||
xform._gridInitialized = true;
|
||||
DebugTools.Assert(xform.GridUid == null);
|
||||
if (_gridQuery.HasComponent(uid))
|
||||
{
|
||||
xform._gridUid = uid;
|
||||
xform._gridInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't set _gridInitialized to true unless the transform (and hence entity) is already being initialized,
|
||||
// as otherwise the current entity's grid component might just not have been added yet.
|
||||
//
|
||||
// We don't just return early, on the off chance that what is happening here is some convoluted entity
|
||||
// initialization pasta, where an an entity has been attached to an un-initialized entity on an already
|
||||
// initialized grid. In that case, the newly attached entity needs to be able to figure out the new grid id.
|
||||
// AFAIK this shouldn't happen anymore, but might as well keep this just in case.
|
||||
if (xform.LifeStage >= ComponentLifeStage.Initializing)
|
||||
xform._gridInitialized = true;
|
||||
|
||||
if (!xform._parent.IsValid())
|
||||
return;
|
||||
|
||||
@@ -726,6 +734,11 @@ public abstract partial class SharedTransformSystem
|
||||
{
|
||||
if (args.Current is TransformComponentState newState)
|
||||
{
|
||||
// TODO Delta-states
|
||||
// If the transform component ever gets delta states, then the client state manager needs to be updated.
|
||||
// Currently it explicitly looks for a "TransformComponentState" when determining an entity's parent for the
|
||||
// sake of sorting the states that need to be applied base on the transform hierarchy.
|
||||
|
||||
var parent = EnsureEntity<TransformComponent>(newState.ParentID, uid);
|
||||
var oldAnchored = xform.Anchored;
|
||||
|
||||
@@ -899,11 +912,11 @@ public abstract partial class SharedTransformSystem
|
||||
_mapManager.TryFindGridAt(mapUid, coordinates.Position, out var targetGrid, out _))
|
||||
{
|
||||
var invWorldMatrix = GetInvWorldMatrix(targetGrid);
|
||||
SetCoordinates(entity, new EntityCoordinates(targetGrid, Vector2.Transform(coordinates.Position, invWorldMatrix)));
|
||||
SetCoordinates((entity.Owner, entity.Comp, MetaData(entity.Owner)), new EntityCoordinates(targetGrid, Vector2.Transform(coordinates.Position, invWorldMatrix)));
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCoordinates(entity, new EntityCoordinates(mapUid, coordinates.Position));
|
||||
SetCoordinates((entity.Owner, entity.Comp, MetaData(entity.Owner)), new EntityCoordinates(mapUid, coordinates.Position));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,6 +316,10 @@ namespace Robust.Shared.GameObjects
|
||||
/// Current parent entity of this entity.
|
||||
/// </summary>
|
||||
public readonly NetEntity ParentID;
|
||||
// TODO Delta-states
|
||||
// If the transform component ever gets delta states, then the client state manager needs to be updated.
|
||||
// Currently it explicitly looks for a "TransformComponentState" when determining an entity's parent for the
|
||||
// sake of sorting the states that need to be applied base on the transform hierarchy.
|
||||
|
||||
/// <summary>
|
||||
/// Current position offset of the entity.
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Collections;
|
||||
@@ -36,9 +37,16 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
private ActorRangeCheckJob _rangeJob;
|
||||
|
||||
/// <summary>
|
||||
/// Defer closing BUIs during state handling so client doesn't spam a BUI constantly during prediction.
|
||||
/// Defer BUIs during state handling so client doesn't spam a BUI constantly during prediction.
|
||||
/// </summary>
|
||||
private readonly List<BoundUserInterface> _queuedCloses = new();
|
||||
private readonly List<(BoundUserInterface Bui, bool value)> _queuedBuis = new();
|
||||
|
||||
/// <summary>
|
||||
/// Temporary storage for BUI keys
|
||||
/// </summary>
|
||||
private ValueList<Enum> _keys = new();
|
||||
|
||||
private ValueList<EntityUid> _entList = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -81,6 +89,11 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentShutdown>(OnActorShutdown);
|
||||
}
|
||||
|
||||
private void AddQueued(BoundUserInterface bui, bool value)
|
||||
{
|
||||
_queuedBuis.Add((bui, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the received message, and then pass it onto systems/components
|
||||
/// </summary>
|
||||
@@ -227,13 +240,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
|
||||
if (ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
|
||||
{
|
||||
if (cBui.DeferredClose)
|
||||
_queuedCloses.Add(cBui);
|
||||
else
|
||||
{
|
||||
ent.Comp.ClientOpenInterfaces.Remove(key);
|
||||
cBui.Dispose();
|
||||
}
|
||||
AddQueued(cBui, false);
|
||||
}
|
||||
|
||||
if (ent.Comp.Actors.Count == 0)
|
||||
@@ -275,24 +282,17 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
// PlayerAttachedEvent will catch some of these.
|
||||
foreach (var (key, bui) in ent.Comp.ClientOpenInterfaces)
|
||||
{
|
||||
bui.Open();
|
||||
|
||||
if (ent.Comp.States.TryGetValue(key, out var state))
|
||||
{
|
||||
bui.UpdateState(state);
|
||||
bui.Update();
|
||||
}
|
||||
AddQueued(bui, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUserInterfaceShutdown(Entity<UserInterfaceComponent> ent, ref ComponentShutdown args)
|
||||
protected virtual void OnUserInterfaceShutdown(Entity<UserInterfaceComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
var actors = new List<EntityUid>();
|
||||
foreach (var (key, acts) in ent.Comp.Actors)
|
||||
{
|
||||
actors.Clear();
|
||||
actors.AddRange(acts);
|
||||
foreach (var actor in actors)
|
||||
_entList.Clear();
|
||||
_entList.AddRange(acts);
|
||||
foreach (var actor in _entList)
|
||||
{
|
||||
CloseUiInternal(ent!, key, actor);
|
||||
DebugTools.Assert(!acts.Contains(actor));
|
||||
@@ -301,7 +301,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
DebugTools.Assert(!ent.Comp.Actors.ContainsKey(key));
|
||||
}
|
||||
|
||||
DebugTools.Assert(ent.Comp.ClientOpenInterfaces.Values.All(x => _queuedCloses.Contains(x)));
|
||||
DebugTools.Assert(ent.Comp.ClientOpenInterfaces.Values.All(x => _queuedBuis.Contains((x, false))));
|
||||
}
|
||||
|
||||
private void OnUserInterfaceGetState(Entity<UserInterfaceComponent> ent, ref ComponentGetState args)
|
||||
@@ -463,14 +463,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
}
|
||||
|
||||
var bui = ent.Comp.ClientOpenInterfaces[key];
|
||||
|
||||
if (bui.DeferredClose)
|
||||
_queuedCloses.Add(bui);
|
||||
else
|
||||
{
|
||||
ent.Comp.ClientOpenInterfaces.Remove(key);
|
||||
bui.Dispose();
|
||||
}
|
||||
AddQueued(bui, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,9 +520,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
// Existing BUI just keep it.
|
||||
if (entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var existing))
|
||||
{
|
||||
if (existing.DeferredClose)
|
||||
_queuedCloses.Remove(existing);
|
||||
|
||||
_queuedBuis.Remove((existing, false));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -545,10 +536,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
// Try-catch to try prevent error loops / bricked clients that constantly throw exceptions while applying game
|
||||
// states. E.g., stripping UI used to throw NREs in some instances while fetching the identity of unknown
|
||||
// entities.
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
var type = _reflection.LooseGetType(data.ClientType);
|
||||
var boundUserInterface = (BoundUserInterface) _factory.CreateInstance(type, [entity.Owner, key]);
|
||||
entity.Comp.ClientOpenInterfaces[key] = boundUserInterface;
|
||||
@@ -557,23 +544,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (!open)
|
||||
return;
|
||||
|
||||
boundUserInterface.Open();
|
||||
|
||||
if (entity.Comp.States.TryGetValue(key, out var buiState))
|
||||
{
|
||||
boundUserInterface.State = buiState;
|
||||
boundUserInterface.UpdateState(buiState);
|
||||
boundUserInterface.Update();
|
||||
}
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(
|
||||
$"Caught exception while attempting to create a BUI {key} with type {data.ClientType} on entity {ToPrettyString(entity.Owner)}. Exception: {e}");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
AddQueued(boundUserInterface, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -689,6 +660,14 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the UI for the local client. Does nothing on server.
|
||||
/// </summary>
|
||||
public virtual void OpenUi(Entity<UserInterfaceComponent?> entity, Enum key, bool predicted = false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OpenUi(Entity<UserInterfaceComponent?> entity, Enum key, EntityUid? actor, bool predicted = false)
|
||||
{
|
||||
if (actor == null || !UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
|
||||
@@ -726,6 +705,23 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
OpenUi(entity, key, actorEnt.Value, predicted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to return the saved position of a user interface.
|
||||
/// </summary>
|
||||
public virtual bool TryGetPosition(Entity<UserInterfaceComponent?> entity, Enum key, out Vector2 position)
|
||||
{
|
||||
position = Vector2.Zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a position for the BUI.
|
||||
/// </summary>
|
||||
protected virtual void SavePosition(BoundUserInterface bui)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a BUI state and networks it to all clients.
|
||||
/// </summary>
|
||||
@@ -887,6 +883,32 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the user's UIs that match the specified key.
|
||||
/// </summary>
|
||||
public void CloseUserUis<T>(Entity<UserInterfaceUserComponent?> actor) where T: Enum
|
||||
{
|
||||
if (!UserQuery.Resolve(actor.Owner, ref actor.Comp, false))
|
||||
return;
|
||||
|
||||
if (actor.Comp.OpenInterfaces.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var (uid, enums) in actor.Comp.OpenInterfaces)
|
||||
{
|
||||
_keys.Clear();
|
||||
_keys.AddRange(enums);
|
||||
|
||||
foreach (var weh in _keys)
|
||||
{
|
||||
if (weh is not T)
|
||||
continue;
|
||||
|
||||
CloseUiInternal(uid, weh, actor.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes all Uis for the actor.
|
||||
/// </summary>
|
||||
@@ -898,13 +920,12 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (actor.Comp.OpenInterfaces.Count == 0)
|
||||
return;
|
||||
|
||||
var enumCopy = new ValueList<Enum>();
|
||||
foreach (var (uid, enums) in actor.Comp.OpenInterfaces)
|
||||
{
|
||||
enumCopy.Clear();
|
||||
enumCopy.AddRange(enums);
|
||||
_keys.Clear();
|
||||
_keys.AddRange(enums);
|
||||
|
||||
foreach (var key in enumCopy)
|
||||
foreach (var key in _keys)
|
||||
{
|
||||
CloseUiInternal(uid, key, actor.Owner);
|
||||
}
|
||||
@@ -1039,17 +1060,48 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
{
|
||||
if (_timing.IsFirstTimePredicted)
|
||||
{
|
||||
foreach (var bui in _queuedCloses)
|
||||
foreach (var (bui, open) in _queuedBuis)
|
||||
{
|
||||
if (UIQuery.TryComp(bui.Owner, out var uiComp))
|
||||
if (open)
|
||||
{
|
||||
uiComp.ClientOpenInterfaces.Remove(bui.UiKey);
|
||||
}
|
||||
bui.Open();
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
|
||||
bui.Dispose();
|
||||
if (UIQuery.TryComp(bui.Owner, out var uiComp))
|
||||
{
|
||||
if (uiComp.States.TryGetValue(bui.UiKey, out var buiState))
|
||||
{
|
||||
bui.State = buiState;
|
||||
bui.UpdateState(buiState);
|
||||
bui.Update();
|
||||
}
|
||||
}
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(
|
||||
$"Caught exception while attempting to create a BUI {bui.UiKey} with type {bui.GetType()} on entity {ToPrettyString(bui.Owner)}. Exception: {e}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// Close BUI
|
||||
else
|
||||
{
|
||||
if (UIQuery.TryComp(bui.Owner, out var uiComp))
|
||||
{
|
||||
uiComp.ClientOpenInterfaces.Remove(bui.UiKey);
|
||||
}
|
||||
|
||||
SavePosition(bui);
|
||||
bui.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_queuedCloses.Clear();
|
||||
_queuedBuis.Clear();
|
||||
}
|
||||
|
||||
var query = AllEntityQuery<ActiveUserInterfaceComponent, UserInterfaceComponent>();
|
||||
|
||||
@@ -77,11 +77,11 @@ namespace Robust.Shared.Map
|
||||
|
||||
#region MapId
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, IPhysShape shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = Approximate, bool includeMap = IncludeMap);
|
||||
public void FindGridsIntersecting<T>(MapId mapId, T shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape;
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, IPhysShape shape, Transform transform, GridCallback callback,
|
||||
bool approx = Approximate, bool includeMap = IncludeMap);
|
||||
public void FindGridsIntersecting<T>(MapId mapId, T shape, Transform transform, GridCallback callback,
|
||||
bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape;
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = Approximate,
|
||||
bool includeMap = IncludeMap);
|
||||
@@ -107,11 +107,11 @@ namespace Robust.Shared.Map
|
||||
|
||||
#region MapEnt
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, IPhysShape shape, Transform transform, GridCallback callback,
|
||||
bool approx = Approximate, bool includeMap = IncludeMap);
|
||||
public void FindGridsIntersecting<T>(EntityUid mapEnt, T shape, Transform transform, GridCallback callback,
|
||||
bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape;
|
||||
|
||||
public void FindGridsIntersecting<TState>(EntityUid mapEnt, IPhysShape shape, Transform transform,
|
||||
ref TState state, GridCallback<TState> callback, bool approx = Approximate, bool includeMap = IncludeMap);
|
||||
public void FindGridsIntersecting<T, TState>(EntityUid mapEnt, T shape, Transform transform,
|
||||
ref TState state, GridCallback<TState> callback, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if any grids overlap the specified shapes.
|
||||
@@ -119,8 +119,8 @@ namespace Robust.Shared.Map
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, List<IPhysShape> shapes, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> entities, bool approx = Approximate, bool includeMap = IncludeMap);
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, IPhysShape shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = Approximate, bool includeMap = IncludeMap);
|
||||
public void FindGridsIntersecting<T>(EntityUid mapEnt, T shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape;
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback,
|
||||
bool approx = Approximate, bool includeMap = IncludeMap);
|
||||
|
||||
@@ -8,17 +8,16 @@ using Robust.Shared.Map.Enumerators;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
|
||||
namespace Robust.Shared.Map;
|
||||
|
||||
internal partial class MapManager
|
||||
{
|
||||
private bool IsIntersecting(
|
||||
private bool IsIntersecting<T>(
|
||||
ChunkEnumerator enumerator,
|
||||
IPhysShape shape,
|
||||
T shape,
|
||||
Transform shapeTransform,
|
||||
Entity<FixturesComponent> grid)
|
||||
Entity<FixturesComponent> grid) where T : IPhysShape
|
||||
{
|
||||
var gridTransform = _physics.GetPhysicsTransform(grid);
|
||||
|
||||
@@ -43,14 +42,14 @@ internal partial class MapManager
|
||||
|
||||
#region MapId
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, IPhysShape shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
public void FindGridsIntersecting<T>(MapId mapId, T shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt, shape, transform, ref grids, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, IPhysShape shape, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
public void FindGridsIntersecting<T>(MapId mapId, T shape, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt, shape, transform, callback, includeMap, approx);
|
||||
@@ -100,18 +99,18 @@ internal partial class MapManager
|
||||
|
||||
#region MapEnt
|
||||
|
||||
public void FindGridsIntersecting(
|
||||
public void FindGridsIntersecting<T>(
|
||||
EntityUid mapEnt,
|
||||
IPhysShape shape,
|
||||
T shape,
|
||||
Transform transform,
|
||||
GridCallback callback,
|
||||
bool approx = IMapManager.Approximate,
|
||||
bool includeMap = IMapManager.IncludeMap)
|
||||
bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
|
||||
{
|
||||
FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
private void FindGridsIntersecting(EntityUid mapEnt, IPhysShape shape, Box2 worldAABB, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
private void FindGridsIntersecting<T>(EntityUid mapEnt, T shape, Box2 worldAABB, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
|
||||
{
|
||||
// This is here so we don't double up on code.
|
||||
var state = callback;
|
||||
@@ -121,20 +120,27 @@ internal partial class MapManager
|
||||
approx: approx, includeMap: includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting<TState>(
|
||||
public void FindGridsIntersecting<T, TState>(
|
||||
EntityUid mapEnt,
|
||||
IPhysShape shape,
|
||||
T shape,
|
||||
Transform transform,
|
||||
ref TState state,
|
||||
GridCallback<TState> callback,
|
||||
bool approx = IMapManager.Approximate,
|
||||
bool includeMap = IMapManager.IncludeMap)
|
||||
bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
|
||||
{
|
||||
FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref state, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
private void FindGridsIntersecting<TState>(EntityUid mapEnt, IPhysShape shape, Box2 worldAABB, Transform transform,
|
||||
ref TState state, GridCallback<TState> callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
private void FindGridsIntersecting<T, TState>(
|
||||
EntityUid mapEnt,
|
||||
T shape,
|
||||
Box2 worldAABB,
|
||||
Transform transform,
|
||||
ref TState state,
|
||||
GridCallback<TState> callback,
|
||||
bool approx = IMapManager.Approximate,
|
||||
bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
|
||||
{
|
||||
if (!_gridTreeQuery.TryGetComponent(mapEnt, out var gridTree))
|
||||
return;
|
||||
@@ -144,7 +150,7 @@ internal partial class MapManager
|
||||
callback(mapEnt, mapGrid, ref state);
|
||||
}
|
||||
|
||||
var gridState = new GridQueryState<TState>(
|
||||
var gridState = new GridQueryState<T, TState>(
|
||||
callback,
|
||||
state,
|
||||
worldAABB,
|
||||
@@ -156,8 +162,7 @@ internal partial class MapManager
|
||||
_transformSystem,
|
||||
approx);
|
||||
|
||||
|
||||
gridTree.Tree.Query(ref gridState, static (ref GridQueryState<TState> state, DynamicTree.Proxy proxy) =>
|
||||
gridTree.Tree.Query(ref gridState, static (ref GridQueryState<T, TState> state, DynamicTree.Proxy proxy) =>
|
||||
{
|
||||
// Even for approximate we'll check if any chunks roughly overlap.
|
||||
var data = state.Tree.GetUserData(proxy);
|
||||
@@ -198,14 +203,14 @@ internal partial class MapManager
|
||||
}
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, IPhysShape shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
public void FindGridsIntersecting<T>(EntityUid mapEnt, T shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
|
||||
{
|
||||
FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref grids, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, IPhysShape shape, Box2 worldAABB, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
public void FindGridsIntersecting<T>(EntityUid mapEnt, T shape, Box2 worldAABB, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
|
||||
{
|
||||
var state = grids;
|
||||
|
||||
@@ -219,39 +224,48 @@ internal partial class MapManager
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
FindGridsIntersecting(mapEnt, new Polygon(worldAABB), worldAABB, Transform.Empty, callback, approx, includeMap);
|
||||
var polygon = _physics.GetPooled(worldAABB);
|
||||
FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, callback, approx, includeMap);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting<TState>(EntityUid mapEnt, Box2 worldAABB, ref TState state, GridCallback<TState> callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
FindGridsIntersecting(mapEnt, new Polygon(worldAABB), worldAABB, Transform.Empty, ref state, callback, approx, includeMap);
|
||||
var polygon = _physics.GetPooled(worldAABB);
|
||||
FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, ref state, callback, approx, includeMap);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref List<Entity<MapGridComponent>> grids,
|
||||
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
FindGridsIntersecting(mapEnt, new Polygon(worldAABB), worldAABB, Transform.Empty, ref grids, approx, includeMap);
|
||||
var polygon = _physics.GetPooled(worldAABB);
|
||||
FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, ref grids, approx, includeMap);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate,
|
||||
bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
var shape = new Polygon(worldBounds);
|
||||
FindGridsIntersecting(mapEnt, shape, worldBounds.CalcBoundingBox(), Transform.Empty, callback, approx, includeMap);
|
||||
var polygon = _physics.GetPooled(worldBounds);
|
||||
FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, callback, approx, includeMap);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting<TState>(EntityUid mapEnt, Box2Rotated worldBounds, ref TState state, GridCallback<TState> callback,
|
||||
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
var shape = new Polygon(worldBounds);
|
||||
FindGridsIntersecting(mapEnt, shape, worldBounds.CalcBoundingBox(), Transform.Empty, ref state, callback, approx, includeMap);
|
||||
var polygon = _physics.GetPooled(worldBounds);
|
||||
FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, ref state, callback, approx, includeMap);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref List<Entity<MapGridComponent>> grids,
|
||||
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
var shape = new Polygon(worldBounds);
|
||||
FindGridsIntersecting(mapEnt, shape, worldBounds.CalcBoundingBox(), Transform.Empty, ref grids, approx, includeMap);
|
||||
var polygon = _physics.GetPooled(worldBounds);
|
||||
FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, ref grids, approx, includeMap);
|
||||
_physics.ReturnPooled(polygon);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -342,22 +356,11 @@ internal partial class MapManager
|
||||
|
||||
#endregion
|
||||
|
||||
private readonly record struct GridQueryState(
|
||||
GridCallback Callback,
|
||||
Box2 WorldAABB,
|
||||
IPhysShape Shape,
|
||||
Transform Transform,
|
||||
B2DynamicTree<(EntityUid Uid, FixturesComponent Fixtures, MapGridComponent Grid)> Tree,
|
||||
SharedMapSystem MapSystem,
|
||||
MapManager MapManager,
|
||||
SharedTransformSystem TransformSystem,
|
||||
bool Approximate);
|
||||
|
||||
private record struct GridQueryState<TState>(
|
||||
private record struct GridQueryState<T, TState>(
|
||||
GridCallback<TState> Callback,
|
||||
TState State,
|
||||
Box2 WorldAABB,
|
||||
IPhysShape Shape,
|
||||
T Shape,
|
||||
Transform Transform,
|
||||
B2DynamicTree<(EntityUid Uid, FixturesComponent Fixtures, MapGridComponent Grid)> Tree,
|
||||
SharedMapSystem MapSystem,
|
||||
|
||||
@@ -12,12 +12,12 @@ internal sealed partial class CollisionManager
|
||||
/// <param name="xfA">The transform for the first shape.</param>
|
||||
/// <param name="xfB">The transform for the seconds shape.</param>
|
||||
/// <returns></returns>
|
||||
public bool TestOverlap(IPhysShape shapeA, int childIndexA, IPhysShape shapeB, int childIndexB, in Transform xfA, in Transform xfB)
|
||||
public bool TestOverlap<T, U>(T shapeA, int indexA, U shapeB, int indexB, in Transform xfA, in Transform xfB) where T : IPhysShape where U : IPhysShape
|
||||
{
|
||||
var input = new DistanceInput();
|
||||
|
||||
input.ProxyA.Set(shapeA, childIndexA);
|
||||
input.ProxyB.Set(shapeB, childIndexB);
|
||||
input.ProxyA.Set(shapeA, indexA);
|
||||
input.ProxyB.Set(shapeB, indexB);
|
||||
input.TransformA = xfA;
|
||||
input.TransformB = xfB;
|
||||
input.UseRadii = true;
|
||||
|
||||
@@ -24,6 +24,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -56,12 +57,12 @@ internal ref struct DistanceProxy
|
||||
/// must remain in scope while the proxy is in use.
|
||||
/// </summary>
|
||||
/// <param name="shape">The shape.</param>
|
||||
public void Set(IPhysShape shape, int index)
|
||||
internal void Set<T>(T shape, int index) where T : IPhysShape
|
||||
{
|
||||
switch (shape.ShapeType)
|
||||
{
|
||||
case ShapeType.Circle:
|
||||
PhysShapeCircle circle = (PhysShapeCircle) shape;
|
||||
var circle = Unsafe.As<PhysShapeCircle>(shape);
|
||||
Buffer._00 = circle.Position;
|
||||
Vertices = Buffer.AsSpan[..1];
|
||||
Radius = circle.Radius;
|
||||
@@ -70,20 +71,20 @@ internal ref struct DistanceProxy
|
||||
case ShapeType.Polygon:
|
||||
if (shape is Polygon poly)
|
||||
{
|
||||
Vertices = poly.Vertices;
|
||||
Vertices = poly.Vertices.AsSpan()[..poly.VertexCount];
|
||||
Radius = poly.Radius;
|
||||
}
|
||||
else
|
||||
{
|
||||
var polyShape = (PolygonShape) shape;
|
||||
Vertices = polyShape.Vertices;
|
||||
var polyShape = Unsafe.As<PolygonShape>(shape);
|
||||
Vertices = polyShape.Vertices.AsSpan()[..polyShape.VertexCount];
|
||||
Radius = polyShape.Radius;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case ShapeType.Chain:
|
||||
ChainShape chain = (ChainShape) shape;
|
||||
var chain = Unsafe.As<ChainShape>(shape);
|
||||
Debug.Assert(0 <= index && index < chain.Vertices.Length);
|
||||
|
||||
Buffer._00 = chain.Vertices[index];
|
||||
@@ -93,7 +94,7 @@ internal ref struct DistanceProxy
|
||||
Radius = chain.Radius;
|
||||
break;
|
||||
case ShapeType.Edge:
|
||||
EdgeShape edge = (EdgeShape) shape;
|
||||
var edge = Unsafe.As<EdgeShape>(shape);
|
||||
|
||||
Buffer._00 = edge.Vertex1;
|
||||
Buffer._01 = edge.Vertex2;
|
||||
|
||||
@@ -12,7 +12,9 @@ internal interface IManifoldManager
|
||||
|
||||
void ReturnEdge(EdgeShape edge);
|
||||
|
||||
bool TestOverlap(IPhysShape shapeA, int indexA, IPhysShape shapeB, int indexB, in Transform xfA, in Transform xfB);
|
||||
bool TestOverlap<T, U>(T shapeA, int indexA, U shapeB, int indexB, in Transform xfA, in Transform xfB)
|
||||
where T : IPhysShape
|
||||
where U : IPhysShape;
|
||||
|
||||
void CollideCircles(ref Manifold manifold, PhysShapeCircle circleA, in Transform xfA,
|
||||
PhysShapeCircle circleB, in Transform xfB);
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -131,6 +132,14 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
|
||||
/// </summary>
|
||||
public float TangentSpeed { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public bool Deleting => (Flags & ContactFlags.Deleting) == ContactFlags.Deleting;
|
||||
|
||||
/// <summary>
|
||||
/// If either fixture is hard then it's a hard contact.
|
||||
/// </summary>
|
||||
public bool Hard => FixtureA != null && FixtureB != null && (FixtureA.Hard && FixtureB.Hard);
|
||||
|
||||
public void ResetRestitution()
|
||||
{
|
||||
Restitution = MathF.Max(FixtureA?.Restitution ?? 0.0f, FixtureB?.Restitution ?? 0.0f);
|
||||
@@ -353,9 +362,21 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
|
||||
return HashCode.Combine(EntityA, EntityB);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public EntityUid OurEnt(EntityUid uid)
|
||||
{
|
||||
if (uid == EntityA)
|
||||
return EntityA;
|
||||
else if (uid == EntityB)
|
||||
return EntityB;
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the other ent for this contact.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public EntityUid OtherEnt(EntityUid uid)
|
||||
{
|
||||
if (uid == EntityA)
|
||||
@@ -366,6 +387,18 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
[Pure, PublicAPI]
|
||||
public (string Id, Fixture) OurFixture(EntityUid uid)
|
||||
{
|
||||
if (uid == EntityA)
|
||||
return (FixtureAId, FixtureA!);
|
||||
else if (uid == EntityB)
|
||||
return (FixtureBId, FixtureB!);
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
[Pure, PublicAPI]
|
||||
public (string Id, Fixture) OtherFixture(EntityUid uid)
|
||||
{
|
||||
if (uid == EntityA)
|
||||
|
||||
@@ -16,7 +16,7 @@ internal record struct Polygon : IPhysShape
|
||||
[DataField]
|
||||
public Vector2[] Vertices;
|
||||
|
||||
public int VertexCount => Vertices.Length;
|
||||
public byte VertexCount;
|
||||
|
||||
public Vector2[] Normals;
|
||||
|
||||
@@ -42,6 +42,19 @@ internal record struct Polygon : IPhysShape
|
||||
|
||||
Array.Copy(polyShape.Vertices, Vertices, Vertices.Length);
|
||||
Array.Copy(polyShape.Normals, Normals, Vertices.Length);
|
||||
VertexCount = (byte) Vertices.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually constructed polygon for internal use to take advantage of pooling.
|
||||
/// </summary>
|
||||
internal Polygon(Vector2[] vertices, Vector2[] normals, Vector2 centroid, byte count)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
Vertices = vertices;
|
||||
Normals = normals;
|
||||
Centroid = centroid;
|
||||
VertexCount = count;
|
||||
}
|
||||
|
||||
public Polygon(Box2 aabb)
|
||||
@@ -61,22 +74,25 @@ internal record struct Polygon : IPhysShape
|
||||
Normals[2] = new Vector2(0.0f, 1.0f);
|
||||
Normals[3] = new Vector2(-1.0f, 0.0f);
|
||||
|
||||
VertexCount = 4;
|
||||
Centroid = aabb.Center;
|
||||
}
|
||||
|
||||
public Polygon(Box2Rotated bounds)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
Vertices = new Vector2[4];
|
||||
Normals = new Vector2[4];
|
||||
Radius = 0f;
|
||||
Span<Vector2> verts = stackalloc Vector2[4];
|
||||
verts[0] = bounds.BottomLeft;
|
||||
verts[1] = bounds.BottomRight;
|
||||
verts[2] = bounds.TopRight;
|
||||
verts[3] = bounds.TopLeft;
|
||||
|
||||
var hull = new PhysicsHull(verts, 4);
|
||||
Set(hull);
|
||||
Vertices[0] = bounds.BottomLeft;
|
||||
Vertices[1] = bounds.BottomRight;
|
||||
Vertices[2] = bounds.TopRight;
|
||||
Vertices[3] = bounds.TopLeft;
|
||||
|
||||
CalculateNormals(Normals, 4);
|
||||
|
||||
VertexCount = 4;
|
||||
Centroid = bounds.Center;
|
||||
}
|
||||
|
||||
@@ -87,11 +103,13 @@ internal record struct Polygon : IPhysShape
|
||||
|
||||
if (hull.Count < 3)
|
||||
{
|
||||
VertexCount = 0;
|
||||
Vertices = [];
|
||||
Normals = [];
|
||||
return;
|
||||
}
|
||||
|
||||
VertexCount = (byte) vertices.Length;
|
||||
Vertices = vertices;
|
||||
Normals = new Vector2[vertices.Length];
|
||||
Set(hull);
|
||||
@@ -116,9 +134,14 @@ internal record struct Polygon : IPhysShape
|
||||
}
|
||||
|
||||
// Compute normals. Ensure the edges have non-zero length.
|
||||
for (var i = 0; i < vertexCount; i++)
|
||||
CalculateNormals(Normals, vertexCount);
|
||||
}
|
||||
|
||||
internal void CalculateNormals(Span<Vector2> normals, int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var next = i + 1 < vertexCount ? i + 1 : 0;
|
||||
var next = i + 1 < count ? i + 1 : 0;
|
||||
var edge = Vertices[next] - Vertices[i];
|
||||
DebugTools.Assert(edge.LengthSquared() > float.Epsilon * float.Epsilon);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,7 +7,6 @@ using Robust.Shared.Collections;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -413,24 +411,34 @@ namespace Robust.Shared.Physics.Systems
|
||||
}, aabb, true);
|
||||
}
|
||||
|
||||
[Obsolete("Use Entity<T> variant")]
|
||||
public void RegenerateContacts(EntityUid uid, PhysicsComponent body, FixturesComponent? fixtures = null, TransformComponent? xform = null)
|
||||
{
|
||||
_physicsSystem.DestroyContacts(body);
|
||||
if (!Resolve(uid, ref xform, ref fixtures))
|
||||
RegenerateContacts((uid, body, fixtures, xform));
|
||||
}
|
||||
|
||||
public void RegenerateContacts(Entity<PhysicsComponent?, FixturesComponent?, TransformComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity.Owner, ref entity.Comp1))
|
||||
return;
|
||||
|
||||
if (xform.MapUid == null)
|
||||
_physicsSystem.DestroyContacts(entity.Comp1);
|
||||
|
||||
if (!Resolve(entity.Owner, ref entity.Comp2 , ref entity.Comp3))
|
||||
return;
|
||||
|
||||
if (!_xformQuery.TryGetComponent(xform.Broadphase?.Uid, out var broadphase))
|
||||
if (entity.Comp3.MapUid == null)
|
||||
return;
|
||||
|
||||
_physicsSystem.SetAwake((uid, body), true);
|
||||
if (!_xformQuery.TryGetComponent(entity.Comp3.Broadphase?.Uid, out var broadphase))
|
||||
return;
|
||||
|
||||
_physicsSystem.SetAwake(entity!, true);
|
||||
|
||||
var matrix = _transform.GetWorldMatrix(broadphase);
|
||||
foreach (var fixture in fixtures.Fixtures.Values)
|
||||
foreach (var fixture in entity.Comp2.Fixtures.Values)
|
||||
{
|
||||
TouchProxies(xform.MapUid.Value, matrix, fixture);
|
||||
TouchProxies(entity.Comp3.MapUid.Value, matrix, fixture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -141,26 +141,26 @@ public partial class SharedPhysicsSystem
|
||||
|
||||
if (args.Current is PhysicsLinearVelocityDeltaState linearState)
|
||||
{
|
||||
SetLinearVelocity(uid, linearState.LinearVelocity, body: component, manager: manager);
|
||||
SetLinearVelocity(uid, linearState.LinearVelocity, dirty: false, body: component, manager: manager);
|
||||
}
|
||||
else if (args.Current is PhysicsVelocityDeltaState velocityState)
|
||||
{
|
||||
SetLinearVelocity(uid, velocityState.LinearVelocity, body: component, manager: manager);
|
||||
SetAngularVelocity(uid, velocityState.AngularVelocity, body: component, manager: manager);
|
||||
SetLinearVelocity(uid, velocityState.LinearVelocity, dirty: false, body: component, manager: manager);
|
||||
SetAngularVelocity(uid, velocityState.AngularVelocity, dirty: false, body: component, manager: manager);
|
||||
}
|
||||
else if (args.Current is PhysicsComponentState newState)
|
||||
{
|
||||
SetSleepingAllowed(uid, component, newState.SleepingAllowed);
|
||||
SetFixedRotation(uid, newState.FixedRotation, body: component);
|
||||
SetCanCollide(uid, newState.CanCollide, body: component);
|
||||
SetSleepingAllowed(uid, component, newState.SleepingAllowed, dirty: false);
|
||||
SetFixedRotation(uid, newState.FixedRotation, body: component, dirty: false);
|
||||
SetCanCollide(uid, newState.CanCollide, body: component, dirty: false);
|
||||
component.BodyStatus = newState.Status;
|
||||
|
||||
SetLinearVelocity(uid, newState.LinearVelocity, body: component, manager: manager);
|
||||
SetAngularVelocity(uid, newState.AngularVelocity, body: component, manager: manager);
|
||||
SetLinearVelocity(uid, newState.LinearVelocity, dirty: false, body: component, manager: manager);
|
||||
SetAngularVelocity(uid, newState.AngularVelocity, dirty: false, body: component, manager: manager);
|
||||
SetBodyType(uid, newState.BodyType, manager, component);
|
||||
SetFriction(uid, component, newState.Friction);
|
||||
SetLinearDamping(uid, component, newState.LinearDamping);
|
||||
SetAngularDamping(uid, component, newState.AngularDamping);
|
||||
SetFriction(uid, component, newState.Friction, dirty: false);
|
||||
SetLinearDamping(uid, component, newState.LinearDamping, dirty: false);
|
||||
SetAngularDamping(uid, component, newState.AngularDamping, dirty: false);
|
||||
component.Force = newState.Force;
|
||||
component.Torque = newState.Torque;
|
||||
}
|
||||
@@ -276,29 +276,12 @@ public partial class SharedPhysicsSystem
|
||||
/// </summary>
|
||||
public void ResetDynamics(EntityUid uid, PhysicsComponent body, bool dirty = true)
|
||||
{
|
||||
if (body.Torque != 0f)
|
||||
{
|
||||
body.Torque = 0f;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.Torque));
|
||||
}
|
||||
|
||||
if (body.AngularVelocity != 0f)
|
||||
{
|
||||
body.AngularVelocity = 0f;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
|
||||
}
|
||||
|
||||
if (body.Force != Vector2.Zero)
|
||||
{
|
||||
body.Force = Vector2.Zero;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.Force));
|
||||
}
|
||||
|
||||
if (body.LinearVelocity != Vector2.Zero)
|
||||
{
|
||||
body.LinearVelocity = Vector2.Zero;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
|
||||
}
|
||||
body.Torque = 0f;
|
||||
body.AngularVelocity = 0f;
|
||||
body.Force = Vector2.Zero;
|
||||
body.LinearVelocity = Vector2.Zero;
|
||||
if (dirty)
|
||||
DirtyFields(uid, body, null, nameof(PhysicsComponent.Torque), nameof(PhysicsComponent.AngularVelocity), nameof(PhysicsComponent.Force), nameof(PhysicsComponent.LinearVelocity));
|
||||
}
|
||||
|
||||
public void ResetMassData(EntityUid uid, FixturesComponent? manager = null, PhysicsComponent? body = null)
|
||||
@@ -403,7 +386,8 @@ public partial class SharedPhysicsSystem
|
||||
return false;
|
||||
|
||||
body.AngularVelocity = value;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
|
||||
if (dirty)
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -429,7 +413,9 @@ public partial class SharedPhysicsSystem
|
||||
return false;
|
||||
|
||||
body.LinearVelocity = velocity;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
|
||||
if (dirty)
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -439,7 +425,8 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body.AngularDamping = value;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularDamping));
|
||||
if (dirty)
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularDamping));
|
||||
}
|
||||
|
||||
public void SetLinearDamping(EntityUid uid, PhysicsComponent body, float value, bool dirty = true)
|
||||
@@ -448,7 +435,8 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body.LinearDamping = value;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearDamping));
|
||||
if (dirty)
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearDamping));
|
||||
}
|
||||
|
||||
[Obsolete("Use SetAwake with EntityUid<PhysicsComponent>")]
|
||||
@@ -524,32 +512,27 @@ public partial class SharedPhysicsSystem
|
||||
body.BodyType = value;
|
||||
ResetMassData(uid, manager, body);
|
||||
|
||||
body.Force = Vector2.Zero;
|
||||
body.Torque = 0f;
|
||||
|
||||
if (body.BodyType == BodyType.Static)
|
||||
{
|
||||
SetAwake((uid, body), false);
|
||||
|
||||
if (body.LinearVelocity != Vector2.Zero)
|
||||
{
|
||||
body.LinearVelocity = Vector2.Zero;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
|
||||
}
|
||||
body.LinearVelocity = Vector2.Zero;
|
||||
body.AngularVelocity = 0f;
|
||||
|
||||
if (body.AngularVelocity != 0f)
|
||||
{
|
||||
body.AngularVelocity = 0f;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
|
||||
}
|
||||
DirtyFields(uid, body, null,
|
||||
nameof(PhysicsComponent.LinearVelocity),
|
||||
nameof(PhysicsComponent.AngularVelocity),
|
||||
nameof(PhysicsComponent.Force),
|
||||
nameof(PhysicsComponent.Torque));
|
||||
}
|
||||
// Even if it's dynamic if it can't collide then don't force it awake.
|
||||
else if (body.CanCollide)
|
||||
{
|
||||
SetAwake((uid, body), true);
|
||||
}
|
||||
|
||||
if (body.Torque != 0f)
|
||||
{
|
||||
body.Torque = 0f;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.Torque));
|
||||
DirtyFields(uid, body, null, nameof(PhysicsComponent.Force), nameof(PhysicsComponent.Torque));
|
||||
}
|
||||
|
||||
_broadphase.RegenerateContacts(uid, body, manager, xform);
|
||||
@@ -567,7 +550,8 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body.BodyStatus = status;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.BodyStatus));
|
||||
if (dirty)
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.BodyStatus));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -618,7 +602,10 @@ public partial class SharedPhysicsSystem
|
||||
var ev = new CollisionChangeEvent(uid, body, value);
|
||||
RaiseLocalEvent(ref ev);
|
||||
}
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.CanCollide));
|
||||
|
||||
if (dirty)
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.CanCollide));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -628,13 +615,10 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body.FixedRotation = value;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.FixedRotation));
|
||||
body.AngularVelocity = 0.0f;
|
||||
|
||||
if (body.AngularVelocity != 0f)
|
||||
{
|
||||
body.AngularVelocity = 0.0f;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
|
||||
}
|
||||
if (dirty)
|
||||
DirtyFields(uid, body, null, nameof(PhysicsComponent.FixedRotation), nameof(PhysicsComponent.AngularVelocity));
|
||||
|
||||
ResetMassData(uid, manager: manager, body: body);
|
||||
}
|
||||
@@ -645,7 +629,8 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body._friction = value;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.Friction));
|
||||
if (dirty)
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.Friction));
|
||||
}
|
||||
|
||||
public void SetInertia(EntityUid uid, PhysicsComponent body, float value, bool dirty = true)
|
||||
@@ -684,7 +669,8 @@ public partial class SharedPhysicsSystem
|
||||
SetAwake((uid, body), true);
|
||||
|
||||
body.SleepingAllowed = value;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.SleepingAllowed));
|
||||
if (dirty)
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.SleepingAllowed));
|
||||
}
|
||||
|
||||
public void SetSleepTime(PhysicsComponent body, float value)
|
||||
|
||||
@@ -809,10 +809,10 @@ public abstract partial class SharedPhysicsSystem
|
||||
/// Returns all of this entity's contacts.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public ContactEnumerator GetContacts(Entity<FixturesComponent?> entity)
|
||||
public ContactEnumerator GetContacts(Entity<FixturesComponent?> entity, bool includeDeleting = false)
|
||||
{
|
||||
_fixturesQuery.Resolve(entity.Owner, ref entity.Comp);
|
||||
return new ContactEnumerator(entity.Comp);
|
||||
return new ContactEnumerator(entity.Comp, includeDeleting);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -823,8 +823,16 @@ public record struct ContactEnumerator
|
||||
private Dictionary<string, Fixture>.ValueCollection.Enumerator _fixtureEnumerator;
|
||||
private Dictionary<Fixture, Contact>.ValueCollection.Enumerator _contactEnumerator;
|
||||
|
||||
public ContactEnumerator(FixturesComponent? fixtures)
|
||||
/// <summary>
|
||||
/// Also include deleting contacts.
|
||||
/// This typically includes the current contact if you're invoking this in the eventbus for an EndCollideEvent.
|
||||
/// </summary>
|
||||
public bool IncludeDeleting;
|
||||
|
||||
public ContactEnumerator(FixturesComponent? fixtures, bool includeDeleting = false)
|
||||
{
|
||||
IncludeDeleting = includeDeleting;
|
||||
|
||||
if (fixtures == null || fixtures.Fixtures.Count == 0)
|
||||
{
|
||||
this = Empty;
|
||||
@@ -851,6 +859,10 @@ public record struct ContactEnumerator
|
||||
}
|
||||
|
||||
contact = _contactEnumerator.Current;
|
||||
|
||||
if (!IncludeDeleting && contact.Deleting)
|
||||
return MoveNext(out contact);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
55
Robust.Shared/Physics/Systems/SharedPhysicsSystem.Pool.cs
Normal file
55
Robust.Shared/Physics/Systems/SharedPhysicsSystem.Pool.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
|
||||
namespace Robust.Shared.Physics.Systems;
|
||||
|
||||
public abstract partial class SharedPhysicsSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a polygon with pooled arrays backing it.
|
||||
/// </summary>
|
||||
internal Polygon GetPooled(Box2 box)
|
||||
{
|
||||
var vertices = ArrayPool<Vector2>.Shared.Rent(4);
|
||||
var normals = ArrayPool<Vector2>.Shared.Rent(4);
|
||||
var centroid = box.Center;
|
||||
|
||||
vertices[0] = box.BottomLeft;
|
||||
vertices[1] = box.BottomRight;
|
||||
vertices[2] = box.TopRight;
|
||||
vertices[3] = box.TopLeft;
|
||||
|
||||
normals[0] = new Vector2(0.0f, -1.0f);
|
||||
normals[1] = new Vector2(1.0f, 0.0f);
|
||||
normals[2] = new Vector2(0.0f, 1.0f);
|
||||
normals[3] = new Vector2(-1.0f, 0.0f);
|
||||
|
||||
return new Polygon(vertices, normals, centroid, 4);
|
||||
}
|
||||
|
||||
internal Polygon GetPooled(Box2Rotated box)
|
||||
{
|
||||
var vertices = ArrayPool<Vector2>.Shared.Rent(4);
|
||||
var normals = ArrayPool<Vector2>.Shared.Rent(4);
|
||||
var centroid = box.Center;
|
||||
|
||||
vertices[0] = box.BottomLeft;
|
||||
vertices[1] = box.BottomRight;
|
||||
vertices[2] = box.TopRight;
|
||||
vertices[3] = box.TopLeft;
|
||||
|
||||
var polygon = new Polygon(vertices, normals, centroid, 4);
|
||||
polygon.CalculateNormals(normals, 4);
|
||||
|
||||
return polygon;
|
||||
}
|
||||
|
||||
internal void ReturnPooled(Polygon polygon)
|
||||
{
|
||||
ArrayPool<Vector2>.Shared.Return(polygon.Vertices);
|
||||
ArrayPool<Vector2>.Shared.Return(polygon.Normals);
|
||||
}
|
||||
}
|
||||
@@ -202,27 +202,27 @@ namespace Robust.Shared.Physics.Systems
|
||||
return bodies;
|
||||
}
|
||||
|
||||
public HashSet<EntityUid> GetContactingEntities(EntityUid uid, PhysicsComponent? body = null, bool approximate = false)
|
||||
public void GetContactingEntities(Entity<PhysicsComponent?> ent, HashSet<EntityUid> contacting, bool approximate = false)
|
||||
{
|
||||
// HashSet to ensure that we only return each entity once, instead of once per colliding fixture.
|
||||
var result = new HashSet<EntityUid>();
|
||||
if (!Resolve(ent.Owner, ref ent.Comp))
|
||||
return;
|
||||
|
||||
if (!Resolve(uid, ref body))
|
||||
return result;
|
||||
|
||||
var node = body.Contacts.First;
|
||||
var node = ent.Comp.Contacts.First;
|
||||
|
||||
while (node != null)
|
||||
{
|
||||
var contact = node.Value;
|
||||
node = node.Next;
|
||||
|
||||
if (!approximate && !contact.IsTouching)
|
||||
continue;
|
||||
|
||||
result.Add(uid == contact.EntityA ? contact.EntityB : contact.EntityA);
|
||||
if (approximate || contact.IsTouching)
|
||||
contacting.Add(ent.Owner == contact.EntityA ? contact.EntityB : contact.EntityA);
|
||||
}
|
||||
}
|
||||
|
||||
public HashSet<EntityUid> GetContactingEntities(EntityUid uid, PhysicsComponent? body = null, bool approximate = false)
|
||||
{
|
||||
var result = new HashSet<EntityUid>();
|
||||
GetContactingEntities((uid, body), result, approximate);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ namespace Robust.Shared.Physics.Systems
|
||||
/*
|
||||
* TODO:
|
||||
|
||||
* Raycasts for non-box shapes.
|
||||
* TOI Solver (continuous collision detection)
|
||||
* Poly cutting
|
||||
*/
|
||||
@@ -83,6 +82,13 @@ namespace Robust.Shared.Physics.Systems
|
||||
|
||||
_physicsReg = EntityManager.ComponentFactory.GetRegistration(CompIdx.Index<PhysicsComponent>());
|
||||
|
||||
// TODO PHYSICS STATE
|
||||
// Consider condensing the possible fields into just Linear velocity, angular velocity, and "Other"
|
||||
// Or maybe even just "velocity" & "other"
|
||||
// Then get-state doesn't have to iterate over a 10-element array.
|
||||
// And it simplifies the DirtyField calls.
|
||||
// Though I guess combining fixtures & physics will complicate it a bit more again.
|
||||
|
||||
// If you update this then update the delta state + GetState + HandleState!
|
||||
EntityManager.ComponentFactory.RegisterNetworkedFields(_physicsReg,
|
||||
nameof(PhysicsComponent.CanCollide),
|
||||
@@ -320,6 +326,28 @@ namespace Robust.Shared.Physics.Systems
|
||||
RaiseLocalEvent(ref updateMapBeforeSolve);
|
||||
}
|
||||
|
||||
// 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,
|
||||
// as positions are updated after CollideContacts() gets called.
|
||||
//
|
||||
// If a player input induces a position update that brings fixtures into contact, the collision happens on the SAME tick,
|
||||
// as inputs are handled before system updates.
|
||||
//
|
||||
// When applying a server's game state with new positions, the client won't know what caused the positions to update,
|
||||
// and thus can't know whether the collision already occurred (i.e., whether its effects are already contained within the
|
||||
// game state currently being applied), or whether it should start on the next tick and needs to predict the start of
|
||||
// the collision.
|
||||
//
|
||||
// Currently the client assumes that any position updates happened due to physics steps. I.e., positions are reset, then
|
||||
// contacts are reset via ResetContacts(), then positions are updated using the new game state. Alternatively, we could
|
||||
// call ResetContacts() AFTER applying the server state, which would correspond to assuming that the collisions have
|
||||
// already "started" in the state, and we don't want to re-raise the events.
|
||||
//
|
||||
// Currently there is no way to avoid mispredicts from happening. E.g., a simple collision-counter component will always
|
||||
// either mispredict on physics induced position changes, or on player/input induced updates. The easiest way I can think
|
||||
// of to fix this would be to always call `CollideContacts` again at the very end of a physics update.
|
||||
// But that might be unnecessarily expensive for what are hopefully only infrequent mispredicts.
|
||||
|
||||
CollideContacts();
|
||||
var enumerator = AllEntityQuery<PhysicsMapComponent>();
|
||||
|
||||
|
||||
@@ -284,15 +284,21 @@ public interface IPrototypeManager
|
||||
void LoadDefaultPrototypes(Dictionary<Type, HashSet<string>>? loaded = null);
|
||||
|
||||
/// <summary>
|
||||
/// Syncs all inter-prototype data. Call this when operations adding new prototypes are done.
|
||||
/// Call this when operations adding new prototypes are done.
|
||||
/// This will handle prototype inheritance, instance creation, and update entity categories.
|
||||
/// When loading extra prototypes, or reloading a subset of existing prototypes, you should probably use
|
||||
/// <see cref="ReloadPrototypes"/> instead.
|
||||
/// </summary>
|
||||
void ResolveResults();
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <see cref="PrototypesReloaded"/> with information about the modified prototypes.
|
||||
/// When built with development tools, this will also push inheritance for reloaded prototypes/
|
||||
/// This should be called after new or updated prototypes ahve been loaded.
|
||||
/// This will handle prototype inheritance, instance creation, and update entity categories.
|
||||
/// It will also invoke <see cref="PrototypesReloaded"/> and raise a <see cref="PrototypesReloadedEventArgs"/>
|
||||
/// event with information about the modified prototypes.
|
||||
/// </summary>
|
||||
void ReloadPrototypes(Dictionary<Type, HashSet<string>> modified,
|
||||
void ReloadPrototypes(
|
||||
Dictionary<Type, HashSet<string>> modified,
|
||||
Dictionary<Type, HashSet<string>>? removed = null);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -209,10 +209,10 @@ public partial class PrototypeManager
|
||||
|
||||
var kindData = _kinds[kind];
|
||||
|
||||
if (!overwrite && kindData.Results.ContainsKey(id))
|
||||
if (!overwrite && kindData.RawResults.ContainsKey(id))
|
||||
throw new PrototypeLoadException($"Duplicate ID: '{id}' for kind '{kind}");
|
||||
|
||||
kindData.Results[id] = data;
|
||||
kindData.RawResults[id] = data;
|
||||
|
||||
if (kindData.Inheritance is { } inheritance)
|
||||
{
|
||||
@@ -295,6 +295,7 @@ public partial class PrototypeManager
|
||||
kindData.UnfrozenInstances ??= kindData.Instances.ToDictionary();
|
||||
kindData.UnfrozenInstances.Remove(id);
|
||||
kindData.Results.Remove(id);
|
||||
kindData.RawResults.Remove(id);
|
||||
modified.Add(kindData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,98 +335,117 @@ namespace Robust.Shared.Prototypes
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReloadPrototypes(Dictionary<Type, HashSet<string>> modified,
|
||||
public void ReloadPrototypes(
|
||||
Dictionary<Type, HashSet<string>> modified,
|
||||
Dictionary<Type, HashSet<string>>? removed = null)
|
||||
{
|
||||
#if TOOLS
|
||||
var prototypeTypeOrder = modified.Keys.ToList();
|
||||
prototypeTypeOrder.Sort(SortPrototypesByPriority);
|
||||
|
||||
var pushed = new Dictionary<Type, HashSet<string>>();
|
||||
var byType = new Dictionary<Type, PrototypesReloadedEventArgs.PrototypeChangeSet>();
|
||||
var modifiedKinds = new HashSet<KindData>();
|
||||
var toProcess = new HashSet<string>();
|
||||
var processQueue = new Queue<string>();
|
||||
|
||||
foreach (var kind in prototypeTypeOrder)
|
||||
{
|
||||
var modifiedInstances = new Dictionary<string, IPrototype>();
|
||||
var kindData = _kinds[kind];
|
||||
if (!kind.IsAssignableTo(typeof(IInheritingPrototype)))
|
||||
{
|
||||
foreach (var id in modified[kind])
|
||||
{
|
||||
var prototype = (IPrototype)_serializationManager.Read(kind, kindData.Results[id])!;
|
||||
kindData.UnfrozenInstances ??= kindData.Instances.ToDictionary();
|
||||
kindData.UnfrozenInstances[id] = prototype;
|
||||
modifiedKinds.Add(kindData);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
var tree = kindData.Inheritance;
|
||||
toProcess.Clear();
|
||||
processQueue.Clear();
|
||||
|
||||
DebugTools.AssertEqual(kind.IsAssignableTo(typeof(IInheritingPrototype)), tree != null);
|
||||
DebugTools.Assert(tree != null || kindData.RawResults == kindData.Results);
|
||||
|
||||
var tree = kindData.Inheritance!;
|
||||
var processQueue = new Queue<string>();
|
||||
foreach (var id in modified[kind])
|
||||
{
|
||||
AddToQueue(id);
|
||||
}
|
||||
|
||||
void AddToQueue(string id)
|
||||
{
|
||||
if (!toProcess.Add(id))
|
||||
return;
|
||||
processQueue.Enqueue(id);
|
||||
|
||||
if (tree == null)
|
||||
return;
|
||||
|
||||
if (!tree.TryGetChildren(id, out var children))
|
||||
return;
|
||||
|
||||
foreach (var child in children!)
|
||||
{
|
||||
AddToQueue(child);
|
||||
}
|
||||
}
|
||||
|
||||
while (processQueue.TryDequeue(out var id))
|
||||
{
|
||||
var pushedSet = pushed.GetOrNew(kind);
|
||||
|
||||
if (tree.TryGetParents(id, out var parents))
|
||||
DebugTools.Assert(toProcess.Contains(id));
|
||||
if (tree != null)
|
||||
{
|
||||
var nonPushedParent = false;
|
||||
foreach (var parent in parents)
|
||||
if (tree.TryGetParents(id, out var parents))
|
||||
{
|
||||
//our parent has been reloaded and has not been added to the pushedSet yet
|
||||
if (modified[kind].Contains(parent) && !pushedSet.Contains(parent))
|
||||
DebugTools.Assert(parents.Length > 0);
|
||||
var nonPushedParent = false;
|
||||
foreach (var parent in parents)
|
||||
{
|
||||
//we re-queue ourselves at the end of the queue
|
||||
if (!toProcess.Contains(parent))
|
||||
continue;
|
||||
|
||||
// our parent has been modified, but has not yet been processed.
|
||||
// we re-queue ourselves at the end of the queue.
|
||||
DebugTools.Assert(processQueue.Contains(parent));
|
||||
processQueue.Enqueue(id);
|
||||
nonPushedParent = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (nonPushedParent)
|
||||
continue;
|
||||
|
||||
var parentMaps = new MappingDataNode[parents.Length];
|
||||
for (var i = 0; i < parentMaps.Length; i++)
|
||||
{
|
||||
parentMaps[i] = kindData.Results[parents[i]];
|
||||
}
|
||||
|
||||
kindData.Results[id] = _serializationManager.PushCompositionWithGenericNode(
|
||||
kind,
|
||||
parentMaps,
|
||||
kindData.RawResults[id]);
|
||||
}
|
||||
|
||||
if (nonPushedParent)
|
||||
continue;
|
||||
|
||||
var parentMaps = new MappingDataNode[parents.Length];
|
||||
for (var i = 0; i < parentMaps.Length; i++)
|
||||
else
|
||||
{
|
||||
parentMaps[i] = kindData.Results[parents[i]];
|
||||
kindData.Results[id] = kindData.RawResults[id];
|
||||
}
|
||||
|
||||
kindData.Results[id] = _serializationManager.PushCompositionWithGenericNode(
|
||||
kind,
|
||||
parentMaps,
|
||||
kindData.Results[id]);
|
||||
}
|
||||
|
||||
toProcess.Remove(id);
|
||||
|
||||
var prototype = TryReadPrototype(kind, id, kindData.Results[id], SerializationHookContext.DontSkipHooks);
|
||||
if (prototype != null)
|
||||
{
|
||||
kindData.UnfrozenInstances ??= kindData.Instances.ToDictionary();
|
||||
kindData.UnfrozenInstances[id] = prototype;
|
||||
modifiedKinds.Add(kindData);
|
||||
}
|
||||
if (prototype == null)
|
||||
continue;
|
||||
|
||||
pushedSet.Add(id);
|
||||
kindData.UnfrozenInstances ??= kindData.Instances.ToDictionary();
|
||||
kindData.UnfrozenInstances[id] = prototype;
|
||||
modifiedInstances.Add(id, prototype);
|
||||
}
|
||||
|
||||
if (modifiedInstances.Count == 0)
|
||||
continue;
|
||||
|
||||
byType.Add(kindData.Type, new(modifiedInstances));
|
||||
modifiedKinds.Add(kindData);
|
||||
}
|
||||
|
||||
Freeze(modifiedKinds);
|
||||
|
||||
if (modifiedKinds.Any(x => x.Type == typeof(EntityPrototype) || x.Type == typeof(EntityCategoryPrototype)))
|
||||
UpdateCategories();
|
||||
#endif
|
||||
|
||||
//todo paul i hate it but i am not opening that can of worms in this refactor
|
||||
var byType = modified
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => new PrototypesReloadedEventArgs.PrototypeChangeSet(
|
||||
g.Value.Where(x => _kinds[g.Key].Instances.ContainsKey(x))
|
||||
.ToDictionary(a => a, a => _kinds[g.Key].Instances[a])));
|
||||
|
||||
var modifiedTypes = new HashSet<Type>(byType.Keys);
|
||||
if (removed != null)
|
||||
@@ -592,11 +611,9 @@ namespace Robust.Shared.Prototypes
|
||||
|
||||
// var sw = RStopwatch.StartNew();
|
||||
|
||||
var results = new Dictionary<string, InheritancePushDatum>(
|
||||
data.Results.Select(k => new KeyValuePair<string, InheritancePushDatum>(
|
||||
k.Key,
|
||||
new InheritancePushDatum(k.Value, tree.GetParentsCount(k.Key))))
|
||||
);
|
||||
var results = data.RawResults.ToDictionary(
|
||||
k => k.Key,
|
||||
k => new InheritancePushDatum(k.Value, tree.GetParentsCount(k.Key)));
|
||||
|
||||
using var countDown = new CountdownEvent(results.Count);
|
||||
|
||||
@@ -1015,6 +1032,8 @@ namespace Robust.Shared.Prototypes
|
||||
|
||||
if (kind.IsAssignableTo(typeof(IInheritingPrototype)))
|
||||
kindData.Inheritance = new MultiRootInheritanceGraph<string>();
|
||||
else
|
||||
kindData.Results = kindData.RawResults;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -1026,7 +1045,13 @@ namespace Robust.Shared.Prototypes
|
||||
|
||||
public FrozenDictionary<string, IPrototype> Instances = FrozenDictionary<string, IPrototype>.Empty;
|
||||
|
||||
public readonly Dictionary<string, MappingDataNode> Results = new();
|
||||
public Dictionary<string, MappingDataNode> Results = new();
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="Results"/> prior to inheritance pushing. If the kind does not have inheritance,
|
||||
/// then this is just the same dictionary.
|
||||
/// </summary>
|
||||
public readonly Dictionary<string, MappingDataNode> RawResults = new();
|
||||
|
||||
public readonly Type Type = kind;
|
||||
public readonly string Name = name;
|
||||
|
||||
@@ -26,14 +26,27 @@ public partial class SerializationManager
|
||||
|
||||
public DataNode PushComposition(Type type, DataNode[] parents, DataNode child, ISerializationContext? context = null)
|
||||
{
|
||||
if (parents.Length == 0)
|
||||
return child.Copy();
|
||||
|
||||
DebugTools.Assert(parents.All(x => x.GetType() == child.GetType()));
|
||||
|
||||
// TODO SERIALIZATION
|
||||
// Change inheritance pushing so that it modifies the passed in child.
|
||||
// I.e., move the child.Clone() statement to the beginning here, then make the delegate modify the clone.
|
||||
// Currently pusing more than one parent requires multiple unnecessary clones.
|
||||
|
||||
var pusher = GetOrCreatePushCompositionDelegate(type, child);
|
||||
|
||||
var node = child;
|
||||
for (int i = 0; i < parents.Length; i++)
|
||||
foreach (var parent in parents)
|
||||
{
|
||||
node = pusher(type, parents[i], node, context);
|
||||
var newNode = pusher(type, parent, node, context);
|
||||
|
||||
// Currently delegate pusher should be returning a new instance, and not modifying the passed in child.
|
||||
DebugTools.Assert(!ReferenceEquals(newNode, node));
|
||||
|
||||
node = newNode;
|
||||
}
|
||||
|
||||
return node;
|
||||
@@ -95,7 +108,7 @@ public partial class SerializationManager
|
||||
Expression.Convert(parentParam, nodeType)),
|
||||
MappingDataNode => Expression.Call(
|
||||
instanceConst,
|
||||
nameof(PushInheritanceMapping),
|
||||
nameof(CombineMappings),
|
||||
Type.EmptyTypes,
|
||||
Expression.Convert(childParam, nodeType),
|
||||
Expression.Convert(parentParam, nodeType)),
|
||||
@@ -117,32 +130,26 @@ public partial class SerializationManager
|
||||
//todo implement different inheritancebehaviours for yamlfield
|
||||
// I have NFI what this comment means.
|
||||
|
||||
var result = new SequenceDataNode(child.Count + parent.Count);
|
||||
var result = child.Copy();
|
||||
foreach (var entry in parent)
|
||||
{
|
||||
result.Add(entry);
|
||||
}
|
||||
foreach (var entry in child)
|
||||
{
|
||||
result.Add(entry);
|
||||
result.Add(entry.Copy());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private MappingDataNode PushInheritanceMapping(MappingDataNode child, MappingDataNode parent)
|
||||
public MappingDataNode CombineMappings(MappingDataNode child, MappingDataNode parent)
|
||||
{
|
||||
//todo implement different inheritancebehaviours for yamlfield
|
||||
// I have NFI what this comment means.
|
||||
// I still don't know what it means, but if it's talking about the always/never push inheritance attributes,
|
||||
// make sure it doesn't break entity serialization.
|
||||
|
||||
var result = new MappingDataNode(child.Count + parent.Count);
|
||||
var result = child.Copy();
|
||||
foreach (var (k, v) in parent)
|
||||
{
|
||||
result[k] = v;
|
||||
}
|
||||
foreach (var (k, v) in child)
|
||||
{
|
||||
result[k] = v;
|
||||
result.TryAddCopy(k, v);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -396,5 +397,20 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
|
||||
|
||||
public int Count => _children.Count;
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public bool TryAdd(DataNode key, DataNode value)
|
||||
{
|
||||
return _children.TryAdd(key, value);
|
||||
}
|
||||
|
||||
public bool TryAddCopy(DataNode key, DataNode value)
|
||||
{
|
||||
ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_children, key, out var exists);
|
||||
if (exists)
|
||||
return false;
|
||||
|
||||
entry = value.Copy();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ public abstract class SharedPrototypeLoadManager : IGamePrototypeLoadManager
|
||||
|
||||
var changed = new Dictionary<Type, HashSet<string>>();
|
||||
_prototypeManager.LoadString(data, true, changed);
|
||||
_prototypeManager.ResolveResults();
|
||||
_prototypeManager.ReloadPrototypes(changed);
|
||||
_localizationManager.ReloadLocalizations();
|
||||
|
||||
|
||||
446
Robust.UnitTesting/Server/GameStates/DetachedParentTest.cs
Normal file
446
Robust.UnitTesting/Server/GameStates/DetachedParentTest.cs
Normal file
@@ -0,0 +1,446 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.UnitTesting.Server.GameStates;
|
||||
|
||||
public sealed class DetachedParentTest : RobustIntegrationTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Check that the client can handle an entity getting attached to an entity that is outside of their PVS range, or
|
||||
/// that they have never seen. Previously this could result in entities with improperly assigned GridUids due to
|
||||
/// an existing/initialized entity being attached to an un-initialized entity on an already initialized grid.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestDetachedParent()
|
||||
{
|
||||
var server = StartServer();
|
||||
var client = StartClient();
|
||||
|
||||
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
|
||||
|
||||
var mapSys = server.System<SharedMapSystem>();
|
||||
var xformSys = server.System<SharedTransformSystem>();
|
||||
var mapMan = server.ResolveDependency<IMapManager>();
|
||||
var sEntMan = server.ResolveDependency<IEntityManager>();
|
||||
var confMan = server.ResolveDependency<IConfigurationManager>();
|
||||
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
|
||||
var netMan = client.ResolveDependency<IClientNetManager>();
|
||||
|
||||
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
|
||||
client.Post(() => netMan.ClientConnect(null!, 0, null!));
|
||||
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
// Ensure client & server ticks are synced.
|
||||
// Client runs 1 tick ahead
|
||||
{
|
||||
var sTick = (int)server.Timing.CurTick.Value;
|
||||
var cTick = (int)client.Timing.CurTick.Value;
|
||||
var delta = cTick - sTick;
|
||||
|
||||
if (delta > 1)
|
||||
await server.WaitRunTicks(delta - 1);
|
||||
else if (delta < 1)
|
||||
await client.WaitRunTicks(1 - delta);
|
||||
|
||||
sTick = (int)server.Timing.CurTick.Value;
|
||||
cTick = (int)client.Timing.CurTick.Value;
|
||||
delta = cTick - sTick;
|
||||
Assert.That(delta, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
// Set up map and spawn player
|
||||
MapId mapId = default;
|
||||
EntityUid map = default;
|
||||
EntityUid grid = default;
|
||||
EntityUid parent = default;
|
||||
EntityUid player = default;
|
||||
EntityUid child = default;
|
||||
EntityCoordinates gridCoords = default;
|
||||
EntityCoordinates mapCoords = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
// Cycle through some EntityUids to avoid server-side and client-side uids accidentally matching up.
|
||||
// I made a mistake earlier in this test where I used a server-side uid on the client
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
server.EntMan.DeleteEntity(server.EntMan.SpawnEntity(null, MapCoordinates.Nullspace));
|
||||
}
|
||||
|
||||
map = mapSys.CreateMap(out mapId);
|
||||
|
||||
var gridEnt = mapMan.CreateGridEntity(mapId);
|
||||
mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1));
|
||||
gridCoords = new EntityCoordinates(gridEnt, .5f, .5f);
|
||||
mapCoords = new EntityCoordinates(map, 200, 200);
|
||||
grid = gridEnt.Owner;
|
||||
|
||||
parent = sEntMan.SpawnEntity(null, gridCoords);
|
||||
player = sEntMan.SpawnEntity(null, gridCoords);
|
||||
child = sEntMan.SpawnEntity(null, mapCoords);
|
||||
|
||||
// Attach player.
|
||||
var session = sPlayerMan.Sessions.First();
|
||||
server.PlayerMan.SetAttachedEntity(session, player);
|
||||
sPlayerMan.JoinGame(session);
|
||||
});
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
// Check that transforms are as expected.
|
||||
var childX = server.Transform(child);
|
||||
var parentX = server.Transform(parent);
|
||||
var playerX = server.Transform(player);
|
||||
var gridX = server.Transform(grid);
|
||||
|
||||
Assert.That(childX.MapID, Is.EqualTo(mapId));
|
||||
Assert.That(parentX.MapID, Is.EqualTo(mapId));
|
||||
Assert.That(playerX.MapID, Is.EqualTo(mapId));
|
||||
Assert.That(gridX.MapID, Is.EqualTo(mapId));
|
||||
|
||||
Assert.That(childX.ParentUid, Is.EqualTo(map));
|
||||
Assert.That(parentX.ParentUid, Is.EqualTo(grid));
|
||||
Assert.That(playerX.ParentUid, Is.EqualTo(grid));
|
||||
Assert.That(gridX.ParentUid, Is.EqualTo(map));
|
||||
|
||||
Assert.That(childX.GridUid, Is.Null);
|
||||
Assert.That(parentX.GridUid, Is.EqualTo(grid));
|
||||
Assert.That(playerX.GridUid, Is.EqualTo(grid));
|
||||
Assert.That(gridX.GridUid, Is.EqualTo(grid));
|
||||
|
||||
// Check that the player received the entities, and that their transforms are as expected.
|
||||
// Note that the child entity should be outside of PVS range.
|
||||
|
||||
var cMap = client.EntMan.GetEntity(server.EntMan.GetNetEntity(map));
|
||||
var cGrid = client.EntMan.GetEntity(server.EntMan.GetNetEntity(grid));
|
||||
var cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player));
|
||||
var cParent = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent));
|
||||
var cChild = client.EntMan.GetEntity(server.EntMan.GetNetEntity(child));
|
||||
|
||||
Assert.That(cMap, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
Assert.That(cGrid, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
Assert.That(cPlayer, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
Assert.That(cParent, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
Assert.That(cChild, Is.EqualTo(EntityUid.Invalid));
|
||||
|
||||
var cParentX = client.Transform(cParent);
|
||||
var cPlayerX = client.Transform(cPlayer);
|
||||
var cGridX = client.Transform(cGrid);
|
||||
|
||||
Assert.That(cParentX.MapID, Is.EqualTo(mapId));
|
||||
Assert.That(cPlayerX.MapID, Is.EqualTo(mapId));
|
||||
Assert.That(cGridX.MapID, Is.EqualTo(mapId));
|
||||
|
||||
Assert.That(cParentX.ParentUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cPlayerX.ParentUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cGridX.ParentUid, Is.EqualTo(cMap));
|
||||
|
||||
Assert.That(cParentX.GridUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cPlayerX.GridUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cGridX.GridUid, Is.EqualTo(cGrid));
|
||||
|
||||
// Move the player into pvs range of the child, which will move them outside of the grid & parent's PVS range.
|
||||
await server.WaitPost(() => xformSys.SetCoordinates(player, mapCoords));
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
// the client now knows about the child.
|
||||
cChild = client.EntMan.GetEntity(server.EntMan.GetNetEntity(child));
|
||||
Assert.That(cChild, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
var cChildX = client.Transform(cChild);
|
||||
Assert.That(childX.MapID, Is.EqualTo(mapId));
|
||||
Assert.That(cChildX.ParentUid, Is.EqualTo(cMap));
|
||||
Assert.That(cChildX.GridUid, Is.Null);
|
||||
|
||||
// Player transform has updated
|
||||
Assert.That(cPlayerX.GridUid, Is.Null);
|
||||
Assert.That(cPlayerX.MapID, Is.EqualTo(mapId));
|
||||
Assert.That(cPlayerX.ParentUid, Is.EqualTo(cMap));
|
||||
|
||||
// But the other entities have left PVS range
|
||||
Assert.That(cParentX.ParentUid, Is.EqualTo(EntityUid.Invalid));
|
||||
Assert.That(cParentX.MapID, Is.EqualTo(MapId.Nullspace));
|
||||
Assert.That(cParentX.GridUid, Is.Null);
|
||||
Assert.That((client.MetaData(cParent).Flags & MetaDataFlags.Detached) != 0);
|
||||
|
||||
// Attach the child & player entities to the parent
|
||||
// This is the main step that the test is actually checking
|
||||
|
||||
var parentCoords = new EntityCoordinates(parent, Vector2.Zero);
|
||||
await server.WaitPost(() => xformSys.SetCoordinates(player, parentCoords));
|
||||
await server.WaitPost(() => xformSys.SetCoordinates(child, parentCoords));
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
// Check that server-side transforms are as expected
|
||||
Assert.That(childX.ParentUid, Is.EqualTo(parent));
|
||||
Assert.That(parentX.ParentUid, Is.EqualTo(grid));
|
||||
Assert.That(playerX.ParentUid, Is.EqualTo(parent));
|
||||
Assert.That(gridX.ParentUid, Is.EqualTo(map));
|
||||
|
||||
Assert.That(childX.GridUid, Is.EqualTo(grid));
|
||||
Assert.That(parentX.GridUid, Is.EqualTo(grid));
|
||||
Assert.That(playerX.GridUid, Is.EqualTo(grid));
|
||||
Assert.That(gridX.GridUid, Is.EqualTo(grid));
|
||||
|
||||
// Next check the client-side transforms
|
||||
Assert.That((client.MetaData(cParent).Flags & MetaDataFlags.Detached) == 0);
|
||||
|
||||
Assert.That(cChildX.ParentUid, Is.EqualTo(cParent));
|
||||
Assert.That(cParentX.ParentUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cPlayerX.ParentUid, Is.EqualTo(cParent));
|
||||
Assert.That(cGridX.ParentUid, Is.EqualTo(cMap));
|
||||
|
||||
Assert.That(cChildX.GridUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cParentX.GridUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cPlayerX.GridUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cGridX.GridUid, Is.EqualTo(cGrid));
|
||||
|
||||
// Repeat the previous test, but this time attaching to an entity that gets spawned outside of PVS range, that
|
||||
// the client never new about previously.
|
||||
await server.WaitPost(() => xformSys.SetCoordinates(player, mapCoords));
|
||||
await server.WaitPost(() => xformSys.SetCoordinates(child, mapCoords));
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
// Child transform has updated.
|
||||
Assert.That(childX.MapID, Is.EqualTo(mapId));
|
||||
Assert.That(cChildX.ParentUid, Is.EqualTo(cMap));
|
||||
Assert.That(cChildX.GridUid, Is.Null);
|
||||
|
||||
// Player transform has updated
|
||||
Assert.That(cPlayerX.GridUid, Is.Null);
|
||||
Assert.That(cPlayerX.MapID, Is.EqualTo(mapId));
|
||||
Assert.That(cPlayerX.ParentUid, Is.EqualTo(cMap));
|
||||
|
||||
// The other entities have left PVS range
|
||||
Assert.That(cParentX.ParentUid, Is.EqualTo(EntityUid.Invalid));
|
||||
Assert.That(cParentX.MapID, Is.EqualTo(MapId.Nullspace));
|
||||
Assert.That(cParentX.GridUid, Is.Null);
|
||||
Assert.That((client.MetaData(cParent).Flags & MetaDataFlags.Detached) != 0);
|
||||
|
||||
// Create a new parent entity
|
||||
EntityUid parent2 = default;
|
||||
await server.WaitPost(() => parent2 = sEntMan.SpawnEntity(null, gridCoords));
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
var parent2X = server.Transform(parent2);
|
||||
Assert.That(parent2X.MapID, Is.EqualTo(mapId));
|
||||
Assert.That(parent2X.ParentUid, Is.EqualTo(grid));
|
||||
Assert.That(parent2X.GridUid, Is.EqualTo(grid));
|
||||
|
||||
// Client does not know that parent2 exists yet.
|
||||
var cParent2 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent2));
|
||||
Assert.That(cParent2, Is.EqualTo(EntityUid.Invalid));
|
||||
|
||||
// Attach player & child to the new parent.
|
||||
var parent2Coords = new EntityCoordinates(parent2, Vector2.Zero);
|
||||
await server.WaitPost(() => xformSys.SetCoordinates(player, parent2Coords));
|
||||
await server.WaitPost(() => xformSys.SetCoordinates(child, parent2Coords));
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
// Check all the transforms
|
||||
cParent2 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent2));
|
||||
Assert.That(cParent2, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
var cParent2X = client.Transform(cParent2);
|
||||
|
||||
Assert.That(cChildX.ParentUid, Is.EqualTo(cParent2));
|
||||
Assert.That(cParent2X.ParentUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cPlayerX.ParentUid, Is.EqualTo(cParent2));
|
||||
Assert.That(cGridX.ParentUid, Is.EqualTo(cMap));
|
||||
|
||||
Assert.That(cParent2X.GridUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cChildX.GridUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cPlayerX.GridUid, Is.EqualTo(cGrid));
|
||||
Assert.That(cGridX.GridUid, Is.EqualTo(cGrid));
|
||||
|
||||
// Repeat again, but with a new map.
|
||||
// Set up map and spawn player
|
||||
MapId mapId2 = default;
|
||||
EntityUid map2 = default;
|
||||
EntityUid grid2 = default;
|
||||
EntityUid parent3 = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
map2 = mapSys.CreateMap(out mapId2);
|
||||
var gridEnt = mapMan.CreateGridEntity(mapId2);
|
||||
mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1));
|
||||
var grid2Coords = new EntityCoordinates(gridEnt, .5f, .5f);
|
||||
grid2 = gridEnt.Owner;
|
||||
parent3 = sEntMan.SpawnEntity(null, grid2Coords);
|
||||
});
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
// Check server-side transforms
|
||||
var grid2X = server.Transform(grid2);
|
||||
var parent3X = server.Transform(parent3);
|
||||
|
||||
Assert.That(parent3X.MapID, Is.EqualTo(mapId2));
|
||||
Assert.That(grid2X.MapID, Is.EqualTo(mapId2));
|
||||
|
||||
Assert.That(parent3X.ParentUid, Is.EqualTo(grid2));
|
||||
Assert.That(grid2X.ParentUid, Is.EqualTo(map2));
|
||||
|
||||
Assert.That(parent3X.GridUid, Is.EqualTo(grid2));
|
||||
Assert.That(grid2X.GridUid, Is.EqualTo(grid2));
|
||||
|
||||
// Client does not know that parent3 exists, but (at least for now) clients always know about all maps and grids.
|
||||
var cParent3 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent3));
|
||||
var cGrid2 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(grid2));
|
||||
var cMap2 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(map2));
|
||||
Assert.That(cMap2, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
Assert.That(cGrid2, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
Assert.That(cParent3, Is.EqualTo(EntityUid.Invalid));
|
||||
|
||||
// Attach the entities to the parent on the new map.
|
||||
var parent3Coords = new EntityCoordinates(parent3, Vector2.Zero);
|
||||
await server.WaitPost(() => xformSys.SetCoordinates(player, parent3Coords));
|
||||
await server.WaitPost(() => xformSys.SetCoordinates(child, parent3Coords));
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
// Check all the transforms
|
||||
cParent3 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent3));
|
||||
Assert.That(cParent3, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
|
||||
var cParent3X = client.Transform(cParent3);
|
||||
var cGrid2X = client.Transform(cGrid2);
|
||||
|
||||
Assert.That(cChildX.ParentUid, Is.EqualTo(cParent3));
|
||||
Assert.That(cParent3X.ParentUid, Is.EqualTo(cGrid2));
|
||||
Assert.That(cPlayerX.ParentUid, Is.EqualTo(cParent3));
|
||||
Assert.That(cGrid2X.ParentUid, Is.EqualTo(cMap2));
|
||||
|
||||
Assert.That(cParent3X.GridUid, Is.EqualTo(cGrid2));
|
||||
Assert.That(cChildX.GridUid, Is.EqualTo(cGrid2));
|
||||
Assert.That(cPlayerX.GridUid, Is.EqualTo(cGrid2));
|
||||
Assert.That(cGrid2X.GridUid, Is.EqualTo(cGrid2));
|
||||
|
||||
Assert.That(cParent3X.MapID, Is.EqualTo(mapId2));
|
||||
Assert.That(cChildX.MapID, Is.EqualTo(mapId2));
|
||||
Assert.That(cPlayerX.MapID, Is.EqualTo(mapId2));
|
||||
Assert.That(cGrid2X.MapID, Is.EqualTo(mapId2));
|
||||
|
||||
Assert.That(cParent3X.MapUid, Is.EqualTo(cMap2));
|
||||
Assert.That(cChildX.MapUid, Is.EqualTo(cMap2));
|
||||
Assert.That(cPlayerX.MapUid, Is.EqualTo(cMap2));
|
||||
Assert.That(cGrid2X.MapUid, Is.EqualTo(cMap2));
|
||||
|
||||
|
||||
// Create a new map & grid and move entities in the same tick
|
||||
MapId mapId3 = default;
|
||||
EntityUid map3 = default;
|
||||
EntityUid grid3 = default;
|
||||
EntityUid parent4 = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
map3 = mapSys.CreateMap(out mapId3);
|
||||
var gridEnt = mapMan.CreateGridEntity(mapId3);
|
||||
mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1));
|
||||
var grid3Coords = new EntityCoordinates(gridEnt, .5f, .5f);
|
||||
grid3 = gridEnt.Owner;
|
||||
parent4 = sEntMan.SpawnEntity(null, grid3Coords);
|
||||
|
||||
var parent4Coords = new EntityCoordinates(parent4, Vector2.Zero);
|
||||
|
||||
// Move existing entity to new parent
|
||||
xformSys.SetCoordinates(player, parent4Coords);
|
||||
|
||||
// Move existing parent & child combination to new grid
|
||||
xformSys.SetCoordinates(parent3, grid3Coords);
|
||||
});
|
||||
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
// Check all the transforms
|
||||
var cParent4 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent4));
|
||||
var cMap3 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(map3));
|
||||
var cGrid3 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(grid3));
|
||||
|
||||
Assert.That(cParent4, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
Assert.That(cMap3, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
Assert.That(cGrid3, Is.Not.EqualTo(EntityUid.Invalid));
|
||||
|
||||
var cParent4X = client.Transform(cParent4);
|
||||
var cGrid3X = client.Transform(cGrid3);
|
||||
|
||||
Assert.That(cChildX.ParentUid, Is.EqualTo(cParent3));
|
||||
Assert.That(cPlayerX.ParentUid, Is.EqualTo(cParent4));
|
||||
Assert.That(cParent3X.ParentUid, Is.EqualTo(cGrid3));
|
||||
Assert.That(cParent4X.ParentUid, Is.EqualTo(cGrid3));
|
||||
Assert.That(cGrid3X.ParentUid, Is.EqualTo(cMap3));
|
||||
|
||||
Assert.That(cChildX.GridUid, Is.EqualTo(cGrid3));
|
||||
Assert.That(cPlayerX.GridUid, Is.EqualTo(cGrid3));
|
||||
Assert.That(cParent3X.GridUid, Is.EqualTo(cGrid3));
|
||||
Assert.That(cParent4X.GridUid, Is.EqualTo(cGrid3));
|
||||
Assert.That(cGrid3X.GridUid, Is.EqualTo(cGrid3));
|
||||
|
||||
Assert.That(cChildX.MapID, Is.EqualTo(mapId3));
|
||||
Assert.That(cPlayerX.MapID, Is.EqualTo(mapId3));
|
||||
Assert.That(cParent3X.MapID, Is.EqualTo(mapId3));
|
||||
Assert.That(cParent4X.MapID, Is.EqualTo(mapId3));
|
||||
Assert.That(cGrid3X.MapID, Is.EqualTo(mapId3));
|
||||
|
||||
Assert.That(cChildX.MapUid, Is.EqualTo(cMap3));
|
||||
Assert.That(cPlayerX.MapUid, Is.EqualTo(cMap3));
|
||||
Assert.That(cParent3X.MapUid, Is.EqualTo(cMap3));
|
||||
Assert.That(cParent4X.MapUid, Is.EqualTo(cMap3));
|
||||
Assert.That(cGrid3X.MapUid, Is.EqualTo(cMap3));
|
||||
}
|
||||
}
|
||||
|
||||
515
Robust.UnitTesting/Shared/Physics/CollisionPredictionTest.cs
Normal file
515
Robust.UnitTesting/Shared/Physics/CollisionPredictionTest.cs
Normal file
@@ -0,0 +1,515 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.UnitTesting.Shared.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// This test is meant to check that collision start & stop events are raised correctly by the client.
|
||||
/// The expectation is that start & stop events are only raised if the client predicts that two entities will move into
|
||||
/// contact. They do not get raised as a result of applying component states received from the server.
|
||||
/// I.e., the assumption is that if a collision results in changes to data on a component, then that data will already
|
||||
/// have been sent to clients in the component's state, so we don't want to "double count" collisions.
|
||||
/// </summary>
|
||||
public sealed class CollisionPredictionTest : RobustIntegrationTest
|
||||
{
|
||||
private static readonly string Prototypes = @"
|
||||
- type: entity
|
||||
id: CollisionTest1
|
||||
components:
|
||||
- type: CollisionPredictionTest
|
||||
- type: Physics
|
||||
bodyType: Dynamic
|
||||
sleepingAllowed: false
|
||||
|
||||
- type: entity
|
||||
id: CollisionTest2
|
||||
components:
|
||||
- type: Physics
|
||||
bodyType: Dynamic
|
||||
sleepingAllowed: false
|
||||
";
|
||||
|
||||
[Test]
|
||||
[TestCase(true, true)]
|
||||
[TestCase(true, false)]
|
||||
[TestCase(false, true)]
|
||||
[TestCase(false, false)]
|
||||
public async Task TestCollisionPrediction(bool hard1, bool hard2)
|
||||
{
|
||||
var serverOpts = new ServerIntegrationOptions { Pool = false, ExtraPrototypes = Prototypes };
|
||||
var clientOpts = new ClientIntegrationOptions { Pool = false, ExtraPrototypes = Prototypes };
|
||||
var server = StartServer(serverOpts);
|
||||
var client = StartClient(clientOpts);
|
||||
|
||||
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
|
||||
var netMan = client.ResolveDependency<IClientNetManager>();
|
||||
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
|
||||
await server.WaitPost(() => server.CfgMan.SetCVar(CVars.NetPVS, false));
|
||||
await client.WaitPost(() => netMan.ClientConnect(null!, 0, null!));
|
||||
|
||||
var sFix = server.System<FixtureSystem>();
|
||||
var sPhys = server.System<SharedPhysicsSystem>();
|
||||
var sSys = server.System<CollisionPredictionTestSystem>();
|
||||
|
||||
// Set up entities
|
||||
EntityUid map = default;
|
||||
EntityUid sEntity1 = default;
|
||||
EntityUid sEntity2 = default;
|
||||
MapCoordinates coords1 = default;
|
||||
MapCoordinates coords2 = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var radius = 0.25f;
|
||||
map = server.System<SharedMapSystem>().CreateMap(out var mapId);
|
||||
coords1 = new(default, mapId);
|
||||
coords2 = new(Vector2.One, mapId);
|
||||
sEntity1 = server.EntMan.Spawn("CollisionTest1", coords1);
|
||||
sEntity2 = server.EntMan.Spawn("CollisionTest2", new MapCoordinates(coords2.Position + new Vector2(0, radius), mapId));
|
||||
sFix.CreateFixture(sEntity1, "a", new Fixture(new PhysShapeCircle(radius), 1, 1, hard1));
|
||||
sFix.CreateFixture(sEntity2, "a", new Fixture(new PhysShapeCircle(radius), 1, 1, hard2));
|
||||
sPhys.SetCanCollide(sEntity1, true);
|
||||
sPhys.SetCanCollide(sEntity2, true);
|
||||
sPhys.SetAwake((sEntity1, server.EntMan.GetComponent<PhysicsComponent>(sEntity1)), true);
|
||||
sPhys.SetAwake((sEntity2, server.EntMan.GetComponent<PhysicsComponent>(sEntity2)), true);
|
||||
});
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
await server.WaitPost(() => server.PlayerMan.JoinGame(server.PlayerMan.Sessions.First()));
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
// Ensure client & server ticks are synced.
|
||||
// Client runs 2 tick ahead
|
||||
{
|
||||
var targetDelta = 2;
|
||||
var sTick = (int)server.Timing.CurTick.Value;
|
||||
var cTick = (int)client.Timing.CurTick.Value;
|
||||
var delta = cTick - sTick;
|
||||
|
||||
if (delta > targetDelta)
|
||||
await server.WaitRunTicks(delta - targetDelta);
|
||||
else if (delta < targetDelta)
|
||||
await client.WaitRunTicks(targetDelta - delta);
|
||||
|
||||
sTick = (int)server.Timing.CurTick.Value;
|
||||
cTick = (int)client.Timing.CurTick.Value;
|
||||
delta = cTick - sTick;
|
||||
Assert.That(delta, Is.EqualTo(targetDelta));
|
||||
}
|
||||
|
||||
var cPhys = client.System<SharedPhysicsSystem>();
|
||||
var cSys = client.System<CollisionPredictionTestSystem>();
|
||||
|
||||
void ResetSystem()
|
||||
{
|
||||
sSys.CollisionEnded = false;
|
||||
sSys.CollisionStarted = false;
|
||||
|
||||
cSys.CollisionEnded = false;
|
||||
cSys.CollisionStarted = false;
|
||||
}
|
||||
|
||||
async Task Tick()
|
||||
{
|
||||
ResetSystem();
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
var nEntity1 = server.EntMan.GetNetEntity(sEntity1);
|
||||
var nEntity2 = server.EntMan.GetNetEntity(sEntity2);
|
||||
|
||||
var cEntity1 = client.EntMan.GetEntity(nEntity1);
|
||||
var cEntity2 = client.EntMan.GetEntity(nEntity2);
|
||||
|
||||
var sComp = server.EntMan.GetComponent<CollisionPredictionTestComponent>(sEntity1);
|
||||
var cComp = client.EntMan.GetComponent<CollisionPredictionTestComponent>(cEntity1);
|
||||
|
||||
cPhys.UpdateIsPredicted(cEntity1);
|
||||
|
||||
// Initially, the objects are not colliding.
|
||||
{
|
||||
Assert.That(sComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.WasTouching, Is.False);
|
||||
Assert.That(cComp.LastState, Is.False);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.False);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.False);
|
||||
}
|
||||
|
||||
// We now simulate a predictive event that gets raised due to some client-side input, causing the entities to
|
||||
// move and start colliding. Instead of setting up a proper input / keybind handler, The predictive event will
|
||||
// just be raised in the system update method, which updates before the physics system does.
|
||||
{
|
||||
cSys.Ev = new CollisionTestMoveEvent(nEntity1, coords2);
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.WasTouching, Is.False);
|
||||
Assert.That(cComp.LastState, Is.False);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.True);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.False);
|
||||
}
|
||||
|
||||
// Run another tick. Client should reset states, and re-predict the event.
|
||||
{
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.WasTouching, Is.True);
|
||||
Assert.That(cComp.LastState, Is.False);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.True);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.False);
|
||||
}
|
||||
|
||||
// Next tick the server should raise the event received from the client, which will raise a serve-side
|
||||
// collide-start event.
|
||||
{
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
|
||||
Assert.That(cComp.WasTouching, Is.True);
|
||||
Assert.That(cComp.LastState, Is.False);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
|
||||
Assert.That(sSys.CollisionStarted, Is.True);
|
||||
Assert.That(cSys.CollisionStarted, Is.True);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.False);
|
||||
}
|
||||
|
||||
// The client will have received the server-state, but will take some time for it to leave the state buffer.
|
||||
// In the meantime, the client will keep predicting that the collision will "starts"
|
||||
for (var i = 0; i < 2; i ++)
|
||||
{
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
|
||||
Assert.That(cComp.WasTouching, Is.True);
|
||||
Assert.That(cComp.LastState, Is.False);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.True);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.False);
|
||||
}
|
||||
|
||||
// Then in the next tick the client should apply the new server state, wherein the contacts were already touching.
|
||||
// I.e., the contact start event never actually gets raised.
|
||||
{
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
|
||||
Assert.That(cComp.WasTouching, Is.False); // IsTouching gets resets to false before server state is applied
|
||||
Assert.That(cComp.LastState, Is.True);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.False);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.False);
|
||||
}
|
||||
|
||||
// for the next few ticks, nothing should change
|
||||
for (var i = 0; i < 10; i ++)
|
||||
{
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
|
||||
Assert.That(cComp.WasTouching, Is.False);
|
||||
Assert.That(cComp.LastState, Is.True);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.False);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.False);
|
||||
}
|
||||
|
||||
// Next we move the entity away again, so the contact should stop
|
||||
{
|
||||
cSys.Ev = new CollisionTestMoveEvent(nEntity1, coords1);
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
|
||||
Assert.That(cComp.WasTouching, Is.False);
|
||||
Assert.That(cComp.LastState, Is.True);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.False);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.True);
|
||||
}
|
||||
|
||||
// Next tick, the client should reset to a state where the entities were touching, and then re-predict the stop-collide events
|
||||
{
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.True);
|
||||
Assert.That(cComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
|
||||
Assert.That(cComp.WasTouching, Is.False);
|
||||
Assert.That(cComp.LastState, Is.True);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.False);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.True);
|
||||
}
|
||||
|
||||
// Next, the server should receive the networked event
|
||||
{
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
|
||||
Assert.That(cComp.StopTick, Is.EqualTo(sComp.StopTick));
|
||||
Assert.That(cComp.WasTouching, Is.False);
|
||||
Assert.That(cComp.LastState, Is.True);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.False);
|
||||
Assert.That(sSys.CollisionEnded, Is.True);
|
||||
Assert.That(cSys.CollisionEnded, Is.True);
|
||||
}
|
||||
|
||||
// nothing changes while waiting for the client to apply the new server state
|
||||
for (var i = 0; i < 2; i ++)
|
||||
{
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
|
||||
Assert.That(cComp.StopTick, Is.EqualTo(sComp.StopTick));
|
||||
Assert.That(cComp.WasTouching, Is.False);
|
||||
Assert.That(cComp.LastState, Is.True);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.False);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.True);
|
||||
}
|
||||
|
||||
// And then the client should apply the new server state
|
||||
{
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
|
||||
Assert.That(cComp.StopTick, Is.EqualTo(sComp.StopTick));
|
||||
Assert.That(cComp.WasTouching, Is.True);
|
||||
Assert.That(cComp.LastState, Is.False);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.False);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.False);
|
||||
}
|
||||
|
||||
// Nothing should change in the next few ticks
|
||||
for (var i = 0; i < 10; i ++)
|
||||
{
|
||||
await Tick();
|
||||
Assert.That(sComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.IsTouching, Is.False);
|
||||
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
|
||||
Assert.That(cComp.StopTick, Is.EqualTo(sComp.StopTick));
|
||||
Assert.That(cComp.WasTouching, Is.True);
|
||||
Assert.That(cComp.LastState, Is.False);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
|
||||
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
|
||||
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
|
||||
Assert.That(sSys.CollisionStarted, Is.False);
|
||||
Assert.That(cSys.CollisionStarted, Is.False);
|
||||
Assert.That(sSys.CollisionEnded, Is.False);
|
||||
Assert.That(cSys.CollisionEnded, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class CollisionPredictionTestComponent : Component
|
||||
{
|
||||
public bool IsTouching;
|
||||
public bool WasTouching;
|
||||
public bool LastState;
|
||||
public GameTick StartTick;
|
||||
public GameTick StopTick;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class State(bool isTouching) : ComponentState
|
||||
{
|
||||
public bool IsTouching = isTouching;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class CollisionTestMoveEvent(NetEntity ent, MapCoordinates coords) : EntityEventArgs
|
||||
{
|
||||
public NetEntity Ent = ent;
|
||||
public MapCoordinates Coords = coords;
|
||||
}
|
||||
|
||||
public sealed class CollisionPredictionTestSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedTransformSystem _xform = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public bool CollisionStarted;
|
||||
public bool CollisionEnded;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<CollisionPredictionTestComponent, ComponentHandleState>(OnHandleState);
|
||||
SubscribeLocalEvent<CollisionPredictionTestComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<CollisionPredictionTestComponent, StartCollideEvent>(OnStartCollide);
|
||||
SubscribeLocalEvent<CollisionPredictionTestComponent, EndCollideEvent>(OnEndCollide);
|
||||
SubscribeLocalEvent<CollisionPredictionTestComponent, UpdateIsPredictedEvent>(OnIsPredicted);
|
||||
SubscribeAllEvent<CollisionTestMoveEvent>(OnMove);
|
||||
|
||||
// Updates before physics to simulate input events.
|
||||
// inputs are processed before systems update, but I CBF setting up a proper input / keybinding.
|
||||
UpdatesBefore.Add(typeof(SharedPhysicsSystem));
|
||||
}
|
||||
|
||||
public CollisionTestMoveEvent? Ev;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
if (Ev == null || !_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
RaisePredictiveEvent(Ev);
|
||||
Ev = null;
|
||||
}
|
||||
|
||||
private void OnIsPredicted(Entity<CollisionPredictionTestComponent> ent, ref UpdateIsPredictedEvent args)
|
||||
{
|
||||
args.IsPredicted = true;
|
||||
}
|
||||
|
||||
private void OnMove(CollisionTestMoveEvent ev)
|
||||
{
|
||||
_xform.SetMapCoordinates(GetEntity(ev.Ent), ev.Coords);
|
||||
}
|
||||
|
||||
private void OnEndCollide(Entity<CollisionPredictionTestComponent> ent, ref EndCollideEvent args)
|
||||
{
|
||||
// TODO PHYSICS Collision Mispredicts
|
||||
// Currently the client will raise collision start/stop events multiple times for each collision
|
||||
// If this ever gets fixed, re-add the assert:
|
||||
// Assert.That(ent.Comp.IsTouching, Is.True);
|
||||
if (!ent.Comp.IsTouching)
|
||||
return;
|
||||
|
||||
Assert.That(CollisionEnded, Is.False);
|
||||
ent.Comp.StopTick = _timing.CurTick;
|
||||
ent.Comp.IsTouching = false;
|
||||
CollisionEnded = true;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnStartCollide(Entity<CollisionPredictionTestComponent> ent, ref StartCollideEvent args)
|
||||
{
|
||||
// TODO PHYSICS Collision Mispredicts
|
||||
// Currently the client will raise collision start/stop events multiple times for each collision
|
||||
// If this ever gets fixed, re-add the assert:
|
||||
// Assert.That(ent.Comp.IsTouching, Is.False);
|
||||
if (ent.Comp.IsTouching)
|
||||
return;
|
||||
|
||||
Assert.That(CollisionStarted, Is.False);
|
||||
ent.Comp.StartTick = _timing.CurTick;
|
||||
ent.Comp.IsTouching = true;
|
||||
CollisionStarted = true;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnGetState(Entity<CollisionPredictionTestComponent> ent, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new CollisionPredictionTestComponent.State(ent.Comp.IsTouching);
|
||||
}
|
||||
|
||||
private void OnHandleState(Entity<CollisionPredictionTestComponent> ent, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not CollisionPredictionTestComponent.State state)
|
||||
return;
|
||||
|
||||
ent.Comp.WasTouching = ent.Comp.IsTouching;
|
||||
ent.Comp.LastState = state.IsTouching;
|
||||
ent.Comp.IsTouching = state.IsTouching;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user