Compare commits

...

45 Commits

Author SHA1 Message Date
DrSmugleaf
318548a4d6 Make setter internal 2024-05-07 20:51:01 -07:00
DrSmugleaf
dde4dd06f6 Fix never setting BoundUserInterface.State 2024-05-07 19:15:48 -07:00
metalgearsloth
7cb3aeccc2 Version: 221.2.0 2024-05-02 12:20:54 +10:00
metalgearsloth
ae83e606d6 Add SetWorldRotNoLerp method (#5091)
* Add SetWorldRotNoLerp method

I neeeeed it.

* Also this one

* dum
2024-05-02 12:14:27 +10:00
metalgearsloth
d9d5ef7471 Add audio helpers for map-based audio (#5086)
Doesn't need to be a flag because we just set it as global, whereas gridaudio cares about stuff every frame.
2024-05-02 09:51:14 +10:00
Pieter-Jan Briers
0f97f366a6 Copy CopyToShaderParameters in SpriteComponent.CopyFrom.
Fixes dragging displacement-mapped mobs in SS14 making the displacement map visible.
2024-05-01 23:45:58 +02:00
Jezithyr
35ab0b8cc8 Version: 221.1.0 2024-04-30 12:51:22 -07:00
metalgearsloth
5a14e939bf TileChangedEvent bool (#5089)
Shows whether IsEmpty is different, useful in circumstances.

Also NotNullWhen null handling consistency.
2024-04-30 07:28:29 -07:00
T-Stalker
ccba6b5d1c Reduce default sound range to 15 (#5085) 2024-04-30 15:41:00 +10:00
DrSmugleaf
254a5987c7 Fix Array.Resize sandbox signature (#5084) 2024-04-30 02:14:56 +02:00
metalgearsloth
8550056e68 Version: 221.0.0 2024-04-29 18:46:57 +10:00
Leon Friedrich
25211e3781 Improve transform & state handling exception tolerance (#5081)
* Improve transform & state exception tolerance

* release notes

* Fix pvs assert

* Fix velocity conservation
2024-04-29 18:42:05 +10:00
Leon Friedrich
3500abfd47 Add IUserInterfaceManager.UpdateHovered() (#5083)
* Add `IUserInterfaceManager.UpdateHovered()`

* Try fix tests
2024-04-29 18:37:52 +10:00
Leon Friedrich
7d1915096a Use more entity queries in physics systems & entity manager (#5082) 2024-04-29 13:46:10 +10:00
Nemanja
4504731588 Add a method in SharedTransformSystem for swapping the position of two entities (#4988)
* swap pos method

* no forcing

* Sluth review

* weh
2024-04-29 13:45:12 +10:00
Leon Friedrich
701fa95a82 Temporarily disable macos tests (#5079) 2024-04-29 02:35:04 +10:00
ShadowCommander
40a9048704 Add margin input value order as a comment (#5067)
* Add margin input value order as a comment

* Make a better comment and move value to remark
2024-04-27 20:17:11 +02:00
Leon Friedrich
cee8d42776 Improve MergeImplicitData exception tolerance (#5075) 2024-04-28 02:23:56 +10:00
metalgearsloth
3330d96177 Version: 220.2.0 2024-04-27 16:05:51 +10:00
Pieter-Jan Briers
4033d96327 Engine changes for displacement maps. (#5023)
* Add load parameter support to RSIs.

Currently only supports turning sRGB off. RSIs with custom load parameters are not thrown into the meta-atlas.

As part of this, TextureLoadParameters and TextureSampleParameters has been made to support equality.

* Add UV2 channel to vertices.

This is a bad hack to make displacement maps work in Robust. The UV2 channel goes from 0 -> 1 across the draw and can therefore be used by displacement maps to map a separate displacement map layer on top of the regular meta-atlas RSI texture.

This creates float inaccuracy issues but they weren't bad enough to completely void the feature. I'm thinking I learn from this experience and completely re-do how UVs work with the renderer rewrite, so that hopefully won't happen anymore.

This required dumping the optimized PadVerticesV2 because the changed struct size made it impractical. RIP.

I don't like this approach at all but the renderer is slated for a rewrite anyways, and all shaders will need to be rewritten regardless.

* Add CopyToShaderParameters for sprite layers.

This effectively allows copying the parameters of a sprite layer into another layer's shader parameters. The use case is to copy texture coordinates for displacement maps, as the exact map used changes depending on orientation. It also enables animations to be used though I didn't use that personally.
2024-04-27 16:03:35 +10:00
metalgearsloth
6e0205d1a8 Version: 220.1.0 2024-04-27 12:33:45 +10:00
ShadowCommander
7cd95351c3 Remove IP address and HWId from ViewVariables (#5062)
* Remove IP address from ViewVariables

* Remove HWId from ViewVariables
2024-04-27 12:30:34 +10:00
Leon Friedrich
2a102f048f Fix client-side replay exception (#5068)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-04-27 12:30:00 +10:00
metalgearsloth
16bab1bc03 Close UIs on disconnect (#5071)
Engine handles it fine but content does not as the state gets handled before all comps are initialized.
2024-04-27 12:21:46 +10:00
metalgearsloth
123d0ae6ac Version: 220.0.0 2024-04-26 18:15:47 +10:00
metalgearsloth
d72de032fa Predicted BUIs (#5059)
* Add TryGetOpenBUI

Avoids having to get the component and openinterfaces separately.

* Couple more helpers

* entityquery

* reviews

* Shared BUIs

* zawehdo

* More boilerplate

* Bunch more work

* Building

* Stuff

* More state handling

* API cleanup

* Slight tweak

* Tweaks

* gabriel

* Disposies

* Active UI support

* Lots of fixes

- Fix states not applying properly, fix predicted messages, remove redundant message type, add RaiseUiMessage for an easy way to do it from shared, add the old BUI state change events back.

* Fix test failures

* weh

* Remove unncessary closes.

* release note
2024-04-26 18:12:55 +10:00
Tayrtahn
0fdba836ee Remove debug assert for Fixture.Owner equality (#5066)
* Removed debug assert for Fixture owner equality

* Blah
2024-04-26 14:47:09 +10:00
metalgearsloth
eb63809999 Version: 219.2.0 2024-04-25 00:31:04 +10:00
Leon Friedrich
4c3c74865c Fix yaml linter & improve validation of static fields (#5056) 2024-04-25 00:27:02 +10:00
Nemanja
b624f5b70f Add SetMapCoordinates (#5065)
* add set map coordinates function

* mmmmmmm yes

* Update Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2024-04-24 22:55:58 +10:00
Pieter-Jan Briers
6566a7658a Fix DebugCoordsPanel freezing when hovering a UI control.
It would bail out of the entire update logic if you aren't hovering over a map position, which isn't great when the control displays far more than in-simulation mouse position info.
2024-04-23 20:34:29 +02:00
metalgearsloth
9e3e1cc929 Optimise physics networking a lot (#5064)
Avoids unnecessarily dirtying every single tick when a mob is moving. Also avoids the getcomponent every time which is nice.
2024-04-23 22:36:40 +10:00
ElectroJr
4e87d93009 Version: 219.1.3 2024-04-23 00:26:17 -04:00
Leon Friedrich
1031ae4cc5 Fix mapping not pausing maps (#5063) 2024-04-23 14:25:40 +10:00
ElectroJr
73da147b88 Version: 219.1.2 2024-04-21 04:58:44 -04:00
Leon Friedrich
0ab59d70b1 More mapinit fixes (#5058) 2024-04-21 18:57:20 +10:00
ElectroJr
8e8470ac7e Version: 219.1.1 2024-04-21 02:50:34 -04:00
Leon Friedrich
15f94bd094 Fix mapinit persistence when overwriting existing maps (#5057)
* Fix mapinit persistence when overwriting existing maps

* Hours wasted chasing fucking chickens in a crate
2024-04-21 16:48:39 +10:00
DrSmugleaf
68888c4370 Make remaining IPrototypes partial (#5053) 2024-04-21 07:39:54 +10:00
ElectroJr
19f87dfbb3 Version: 219.1.0 2024-04-20 16:52:47 -04:00
Leon Friedrich
68e5b6924d Add ComponentRegistry overrides to more entity spawn methods (#5051) 2024-04-19 13:16:17 +10:00
Leon Friedrich
9f913cd2d9 Fix RecursiveMapInit (#5052)
* Fix RecursiveMapInit

* re-use sawmill
2024-04-19 13:15:42 +10:00
Vasilis
ec37d1c137 Auth is now required by default (#5050) 2024-04-18 17:51:25 +02:00
metalgearsloth
ea58924495 Audio stuff (#5048)
Better overlay debug and uses the adjusted distance for cutoff instead.
2024-04-18 14:36:29 +10:00
keronshb
c5aa735506 Adds rotation to Map Position spawns (#5047)
* add angle to rotation

* fixes inheritance

* proxy

* Fixes maprot

* changes angle to default and uses set coords overload
2024-04-18 14:25:07 +10:00
97 changed files with 2340 additions and 1411 deletions

View File

@@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-latest, windows-latest ] # , macos-latest] - temporarily disabled due to libfreetype.dll errors.
runs-on: ${{ matrix.os }}

View File

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

View File

@@ -54,6 +54,131 @@ END TEMPLATE-->
*None yet*
## 221.2.0
### New features
* Add SetMapAudio helper to SharedAudioSystem to setup map-wide audio entities.
* Add SetWorldRotNoLerp method to SharedTransformSystem to avoid client lerping.
### Bugfixes
* `SpriteComponent.CopyFrom` now copies `CopyToShaderParameters` configuration.
## 221.1.0
## 221.0.0
### Breaking changes
* `EntParentChangedMessage.OldMapId` is now an `EntityUid` instead of `MapId`
* `TransformSystem.DetachParentToNull()` is being renamed to `DetachEntity`
* The order in which `MoveEvent` handlers are invoked has been changed to prioritise engine subscriptions
### New features
* Added `UpdateHovered()` and `SetHovered()` to `IUserInterfaceManager`, for updating or modifying the currently hovered control.
* Add SwapPositions to TransformSystem to swap two entity's transforms.
### Bugfixes
* Improve client gamestate exception tolerance.
### Other
* If the currently hovered control is disposed, `UserInterfaceManager` will now look for a new control, rather than just setting the hovered control to null.
### Internal
* Use more `EntityQuery<T>` internally in EntityManager and PhysicsSystem.
## 220.2.0
### New features
* RSIs can now specify load parameters, mimicking the ones from `.png.yml`. Currently only disabling sRGB is supported.
* Added a second UV channel to Clyde's vertex format. On regular batched sprite draws, this goes 0 -> 1 across the sprite quad.
* Added a new `CopyToShaderParameters` system for `SpriteComponent` layers.
## 220.1.0
### Bugfixes
* Fix client-side replay exceptions due to dropped states when recording.
### Other
* Remove IP + HWId from ViewVariables.
* Close BUIs upon disconnect.
## 220.0.0
### Breaking changes
* Refactor UserInterfaceSystem.
- The API has been significantly cleaned up and standardised, most noticeably callers don't need to worry about TryGetUi and can rely on either HasUi, SetUiState, CloseUi, or OpenUi to handle their code as appropriate.
- Interface data is now stored via key rather than as a flat list which is a breaking change for YAML.
- BoundUserInterfaces can now be completely handled via Shared code. Existing Server-side callers will behave similarly to before.
- BoundUserInterfaces now properly close in many more situations, additionally they are now attached to the entity so reconnecting can re-open them and they can be serialized properly.
## 219.2.0
### New features
* Add SetMapCoordinates to TransformSystem.
* Improve YAML Linter and validation of static fields.
### Bugfixes
* Fix DebugCoordsPanel freezing when hovering a control.
### Other
* Optimise physics networking to not dirty every tick of movement.
## 219.1.3
### Bugfixes
* Fix map-loader not pausing pre-init maps when not actively overwriting an existing map.
## 219.1.2
### Bugfixes
* Fix map-loader not map-initialising grids when loading into a post-init map.
## 219.1.1
### Bugfixes
* Fix map-loader not map-initialising maps when overwriting a post-init map.
## 219.1.0
### New features
* Added a new optional arguments to various entity spawning methods, including a new argument to set the entity's rotation.
### Bugfixes
* Fixes map initialisation not always initialising all entities on a map.
### Other
* The default value of the `auth.mode` cvar has changed
## 219.0.0
### Breaking changes

View File

@@ -74,11 +74,13 @@ public sealed class AudioOverlay : Overlay
output.Clear();
output.AppendLine("Audio Source");
output.AppendLine("Runtime:");
output.AppendLine($"- Distance: {_audio.GetAudioDistance(distance.Length()):0.00}");
output.AppendLine($"- Occlusion: {posOcclusion:0.0000}");
output.AppendLine("Params:");
output.AppendLine($"- RolloffFactor: {comp.RolloffFactor:0.0000}");
output.AppendLine($"- Volume: {comp.Volume:0.0000}");
output.AppendLine($"- Reference distance: {comp.ReferenceDistance}");
output.AppendLine($"- Max distance: {comp.MaxDistance}");
output.AppendLine($"- Reference distance: {comp.ReferenceDistance:0.00}");
output.AppendLine($"- Max distance: {comp.MaxDistance:0.00}");
var outputText = output.ToString().Trim();
var dimensions = screenHandle.GetDimensions(_font, outputText, 1f);
var buffer = new Vector2(3f, 3f);

View File

@@ -388,7 +388,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
var distance = delta.Length();
// Out of range so just clip it for us.
if (distance > component.MaxDistance)
if (GetAudioDistance(distance) > component.MaxDistance)
{
// Still keeps the source playing, just with no volume.
component.Gain = 0f;

View File

@@ -285,6 +285,7 @@ namespace Robust.Client
/// <summary>
/// Enumeration of the run levels of the BaseClient.
/// </summary>
/// <seealso cref="ClientRunLevelExt"/>
public enum ClientRunLevel : byte
{
Error = 0,
@@ -315,6 +316,21 @@ namespace Robust.Client
SinglePlayerGame,
}
/// <summary>
/// Helper functions for working with <see cref="ClientRunLevel"/>.
/// </summary>
public static class ClientRunLevelExt
{
/// <summary>
/// Check if a <see cref="ClientRunLevel"/> is <see cref="ClientRunLevel.InGame"/>
/// or <see cref="ClientRunLevel.SinglePlayerGame"/>.
/// </summary>
public static bool IsInGameLike(this ClientRunLevel runLevel)
{
return runLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame;
}
}
/// <summary>
/// Event arguments for when something changed with the player.
/// </summary>

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Numerics;
using System.Text;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.Animations;
@@ -28,6 +29,7 @@ using static Robust.Client.ComponentTrees.SpriteTreeSystem;
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer;
using Direction = Robust.Shared.Maths.Direction;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.GameObjects
{
@@ -770,15 +772,7 @@ namespace Robust.Client.GameObjects
{
foreach (var keyString in layerDatum.MapKeys)
{
object key;
if (reflection.TryParseEnumReference(keyString, out var @enum))
{
key = @enum;
}
else
{
key = keyString;
}
var key = ParseKey(keyString);
if (LayerMap.TryGetValue(key, out var mappedIndex))
{
@@ -804,9 +798,30 @@ namespace Robust.Client.GameObjects
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = layerDatum.Visible ?? layer.Visible;
if (layerDatum.CopyToShaderParameters is { } copyParameters)
{
layer.CopyToShaderParameters = new CopyToShaderParameters(ParseKey(copyParameters.LayerKey))
{
ParameterTexture = copyParameters.ParameterTexture,
ParameterUV = copyParameters.ParameterUV
};
}
else
{
layer.CopyToShaderParameters = null;
}
RebuildBounds();
}
private object ParseKey(string keyString)
{
if (reflection.TryParseEnumReference(keyString, out var @enum))
return @enum;
return keyString;
}
public void LayerSetData(object layerKey, PrototypeLayerData data)
{
if (!LayerMapTryGet(layerKey, out var layer, true))
@@ -1635,6 +1650,9 @@ namespace Robust.Client.GameObjects
[ViewVariables]
public LayerRenderingStrategy RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy;
[ViewVariables(VVAccess.ReadWrite)]
public CopyToShaderParameters? CopyToShaderParameters;
public Layer(SpriteComponent parent)
{
_parent = parent;
@@ -1663,6 +1681,8 @@ namespace Robust.Client.GameObjects
DirOffset = toClone.DirOffset;
_autoAnimated = toClone._autoAnimated;
RenderingStrategy = toClone.RenderingStrategy;
if (toClone.CopyToShaderParameters is { } copyToShaderParameters)
CopyToShaderParameters = new CopyToShaderParameters(copyToShaderParameters);
}
void ISerializationHooks.AfterDeserialization()
@@ -2007,8 +2027,6 @@ namespace Robust.Client.GameObjects
// Set the drawing transform for this layer
GetLayerDrawMatrix(dir, out var layerMatrix);
Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
drawingHandle.SetTransform(in transformMatrix);
// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
// due to direction overrides or offsets.
@@ -2018,7 +2036,41 @@ namespace Robust.Client.GameObjects
// Get the correct directional texture from the state, and draw it!
var texture = GetRenderTexture(_actualState, dir);
RenderTexture(drawingHandle, texture);
if (CopyToShaderParameters == null)
{
// Set the drawing transform for this layer
Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
drawingHandle.SetTransform(in transformMatrix);
RenderTexture(drawingHandle, texture);
}
else
{
// Multiple atrocities to god being committed right here.
var otherLayerIdx = _parent.LayerMap[CopyToShaderParameters.LayerKey!];
var otherLayer = _parent.Layers[otherLayerIdx];
if (otherLayer.Shader is not { } shader)
{
// No shader set apparently..?
return;
}
if (!shader.Mutable)
otherLayer.Shader = shader = shader.Duplicate();
var clydeTexture = Clyde.RenderHandle.ExtractTexture(texture, null, out var csr);
var sr = Clyde.RenderHandle.WorldTextureBoundsToUV(clydeTexture, csr);
if (CopyToShaderParameters.ParameterTexture is { } paramTexture)
shader.SetParameter(paramTexture, clydeTexture);
if (CopyToShaderParameters.ParameterUV is { } paramUV)
{
var uv = new Vector4(sr.Left, sr.Bottom, sr.Right, sr.Top);
shader.SetParameter(paramUV, uv);
}
}
}
private void RenderTexture(DrawingHandleWorld drawingHandle, Texture texture)
@@ -2096,6 +2148,23 @@ namespace Robust.Client.GameObjects
}
}
/// <summary>
/// Instantiated version of <see cref="PrototypeCopyToShaderParameters"/>.
/// Has <see cref="LayerKey"/> actually resolved to a a real key.
/// </summary>
public sealed class CopyToShaderParameters(object layerKey)
{
public object LayerKey = layerKey;
public string? ParameterTexture;
public string? ParameterUV;
public CopyToShaderParameters(CopyToShaderParameters toClone) : this(toClone.LayerKey)
{
ParameterTexture = toClone.ParameterTexture;
ParameterUV = toClone.ParameterUV;
}
}
void IAnimationProperties.SetAnimatableProperty(string name, object value)
{
if (!name.StartsWith("layer/"))

View File

@@ -1,84 +1,8 @@
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using System;
using UserInterfaceComponent = Robust.Shared.GameObjects.UserInterfaceComponent;
namespace Robust.Client.GameObjects
namespace Robust.Client.GameObjects;
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
[Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<BoundUIWrapMessage>(MessageReceived);
}
private void MessageReceived(BoundUIWrapMessage ev)
{
var uid = GetEntity(ev.Entity);
if (!TryComp<UserInterfaceComponent>(uid, out var cmp))
return;
var uiKey = ev.UiKey;
var message = ev.Message;
message.Session = _playerManager.LocalSession!;
message.Entity = GetNetEntity(uid);
message.UiKey = uiKey;
// Raise as object so the correct type is used.
RaiseLocalEvent(uid, (object)message, true);
switch (message)
{
case OpenBoundInterfaceMessage _:
TryOpenUi(uid, uiKey, cmp);
break;
case CloseBoundInterfaceMessage _:
TryCloseUi(message.Session, uid, uiKey, remoteCall: true, uiComp: cmp);
break;
default:
if (cmp.OpenInterfaces.TryGetValue(uiKey, out var bui))
bui.InternalReceiveMessage(message);
break;
}
}
private bool TryOpenUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? uiComp = null)
{
if (!Resolve(uid, ref uiComp))
return false;
if (uiComp.OpenInterfaces.ContainsKey(uiKey))
return false;
var data = uiComp.MappedInterfaceData[uiKey];
// TODO: This type should be cached, but I'm too lazy.
var type = _reflectionManager.LooseGetType(data.ClientType);
var boundInterface =
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new object[] {uid, uiKey});
boundInterface.Open();
uiComp.OpenInterfaces[uiKey] = boundInterface;
if (_playerManager.LocalSession is { } playerSession)
{
uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);
}
return true;
}
}
}

View File

@@ -125,6 +125,8 @@ namespace Robust.Client.GameStates
#endif
private bool _resettingPredictedEntities;
private readonly List<EntityUid> _brokenEnts = new();
private readonly List<(EntityUid, NetEntity)> _toStart = new();
/// <inheritdoc />
public void Initialize()
@@ -667,7 +669,16 @@ namespace Robust.Client.GameStates
foreach (var netEntity in createdEntities)
{
#if EXCEPTION_TOLERANCE
if (!_entityManager.TryGetEntityData(netEntity, out _, out var meta))
{
_sawmill.Error($"Encountered deleted entity while merging implicit data! NetEntity: {netEntity}");
continue;
}
#else
var (_, meta) = _entityManager.GetEntityData(netEntity);
#endif
var compData = _compDataPool.Get();
_outputData.Add(netEntity, compData);
@@ -876,9 +887,22 @@ namespace Robust.Client.GameStates
{
foreach (var (entity, data) in _toApply)
{
#if EXCEPTION_TOLERANCE
try
{
#endif
HandleEntityState(entity, data.NetEntity, data.Meta, _entities.EventBus, data.curState,
data.nextState, data.LastApplied, curState.ToSequence, data.EnteringPvs);
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;
@@ -917,7 +941,7 @@ namespace Robust.Client.GameStates
{
try
{
ProcessDeletions(delSpan, xforms, xformSys);
ProcessDeletions(delSpan, xforms, metas, xformSys);
}
catch (Exception e)
{
@@ -962,6 +986,7 @@ namespace Robust.Client.GameStates
}
var xforms = _entities.GetEntityQuery<TransformComponent>();
var metas = _entities.GetEntityQuery<MetaDataComponent>();
var xformSys = _entitySystemManager.GetEntitySystem<SharedTransformSystem>();
_toDelete.Clear();
@@ -990,12 +1015,12 @@ namespace Robust.Client.GameStates
// This entity is going to get deleted, but maybe some if its children won't be, so lets detach them to
// null. First we will detach the parent in order to reduce the number of broadphase/lookup updates.
xformSys.DetachParentToNull(ent, xform);
xformSys.DetachEntity(ent, xform);
// Then detach all children.
foreach (var child in xform._children)
{
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
xformSys.DetachEntity(child, xforms.Get(child), metas.Get(child), xform);
if (deleteClientChildren
&& !deleteClientEntities // don't add duplicates
@@ -1014,9 +1039,9 @@ namespace Robust.Client.GameStates
}
}
private void ProcessDeletions(
ReadOnlySpan<NetEntity> delSpan,
private void ProcessDeletions(ReadOnlySpan<NetEntity> delSpan,
EntityQuery<TransformComponent> xforms,
EntityQuery<MetaDataComponent> metas,
SharedTransformSystem xformSys)
{
// Processing deletions is non-trivial, because by default deletions will also delete all child entities.
@@ -1043,13 +1068,13 @@ namespace Robust.Client.GameStates
continue; // Already deleted? or never sent to us?
// First, a single recursive map change
xformSys.DetachParentToNull(id.Value, xform);
xformSys.DetachEntity(id.Value, xform);
// Then detach all children.
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
xformSys.DetachEntity(child, xforms.Get(child), metas.Get(child), xform);
}
// Finally, delete the entity.
@@ -1144,7 +1169,7 @@ namespace Robust.Client.GameStates
}
meta._flags |= MetaDataFlags.Detached;
xformSys.DetachParentToNull(ent.Value, xform);
xformSys.DetachEntity(ent.Value, xform);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
if (container != null)
@@ -1157,63 +1182,58 @@ namespace Robust.Client.GameStates
private void InitializeAndStart(Dictionary<NetEntity, EntityState> toCreate)
{
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
_toStart.Clear();
#if EXCEPTION_TOLERANCE
var brokenEnts = new List<EntityUid>();
#endif
using (_prof.Group("Initialize Entity"))
{
EntityUid entity = default;
foreach (var netEntity in toCreate.Keys)
{
var entity = _entityManager.GetEntity(netEntity);
#if EXCEPTION_TOLERANCE
try
{
#endif
_entities.InitializeEntity(entity, metaQuery.GetComponent(entity));
#if EXCEPTION_TOLERANCE
(entity, var meta) = _entityManager.GetEntityData(netEntity);
_entities.InitializeEntity(entity, meta);
_toStart.Add((entity, netEntity));
}
catch (Exception e)
{
_sawmill.Error($"Server entity threw in Init: ent={_entities.ToPrettyString(entity)}");
_sawmill.Error($"Server entity threw in Init: nent={netEntity}, ent={_entities.ToPrettyString(entity)}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
brokenEnts.Add(entity);
toCreate.Remove(netEntity);
}
_toCreate.Remove(netEntity);
_brokenEnts.Add(entity);
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}
}
using (_prof.Group("Start Entity"))
{
foreach (var netEntity in toCreate.Keys)
foreach (var (entity, netEntity) in _toStart)
{
var entity = _entityManager.GetEntity(netEntity);
#if EXCEPTION_TOLERANCE
try
{
#endif
_entities.StartEntity(entity);
#if EXCEPTION_TOLERANCE
_entities.StartEntity(entity);
}
catch (Exception e)
{
_sawmill.Error($"Server entity threw in Start: ent={_entityManager.ToPrettyString(entity)}");
_sawmill.Error($"Server entity threw in Start: nent={netEntity}, ent={_entityManager.ToPrettyString(entity)}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
brokenEnts.Add(entity);
toCreate.Remove(netEntity);
}
_toCreate.Remove(netEntity);
_brokenEnts.Add(entity);
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}
}
#if EXCEPTION_TOLERANCE
foreach (var entity in brokenEnts)
foreach (var entity in _brokenEnts)
{
_entityManager.DeleteEntity(entity);
}
#endif
_brokenEnts.Clear();
}
private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataComponent meta, IEventBus bus, EntityState? curState,
@@ -1402,7 +1422,7 @@ namespace Robust.Client.GameStates
containerSys.TryGetContainingContainer(xform.ParentUid, uid, out container);
}
_entities.EntitySysManager.GetEntitySystem<TransformSystem>().DetachParentToNull(uid, xform);
_entities.EntitySysManager.GetEntitySystem<TransformSystem>().DetachEntity(uid, xform);
if (container != null)
containerSys.AddExpectedEntity(_entities.GetNetEntity(uid), container);

View File

@@ -6,7 +6,8 @@ namespace Robust.Client.Graphics.Clyde
{
("aPos", 0),
("tCoord", 1),
("modulate", 2)
("tCoord2", 2),
("modulate", 3)
};
private const int UniIModUV = 0;

View File

@@ -23,9 +23,12 @@ namespace Robust.Client.Graphics.Clyde
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
// Colour Modulation.
GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
// Texture Coords (2).
GL.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
GL.EnableVertexAttribArray(2);
// Colour Modulation.
GL.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 6 * sizeof(float));
GL.EnableVertexAttribArray(3);
}
// NOTE: This is:
@@ -37,6 +40,7 @@ namespace Robust.Client.Graphics.Clyde
{
public readonly Vector2 Position;
public readonly Vector2 TextureCoordinates;
public readonly Vector2 TextureCoordinates2;
// Note that this color is in linear space.
public readonly Color Modulate;
@@ -48,6 +52,15 @@ namespace Robust.Client.Graphics.Clyde
Modulate = modulate;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vertex2D(Vector2 position, Vector2 textureCoordinates, Vector2 textureCoordinates2, Color modulate)
{
Position = position;
TextureCoordinates = textureCoordinates;
TextureCoordinates2 = textureCoordinates2;
Modulate = modulate;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vertex2D(float x, float y, float u, float v, float r, float g, float b, float a)
: this(new Vector2(x, y), new Vector2(u, v), new Color(r, g, b, a))

View File

@@ -15,7 +15,7 @@ namespace Robust.Client.Graphics.Clyde
{
private RenderHandle _renderHandle = default!;
private sealed class RenderHandle : IRenderHandle
internal sealed class RenderHandle : IRenderHandle
{
private readonly Clyde _clyde;
private readonly IEntityManager _entities;
@@ -88,16 +88,21 @@ namespace Robust.Client.Graphics.Clyde
{
var clydeTexture = ExtractTexture(texture, in subRegion, out var csr);
var (w, h) = clydeTexture.Size;
var sr = new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
var sr = WorldTextureBoundsToUV(clydeTexture, csr);
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
}
internal static Box2 WorldTextureBoundsToUV(ClydeTexture texture, UIBox2 csr)
{
var (w, h) = texture.Size;
return new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
}
/// <summary>
/// Converts a subRegion (px) into texture coords (0-1) of a given texture (cells of the textureAtlas).
/// </summary>
private static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr)
internal static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr)
{
if (texture is AtlasTexture atlas)
{

View File

@@ -578,10 +578,10 @@ namespace Robust.Client.Graphics.Clyde
// TODO: split batch if necessary.
var vIdx = BatchVertexIndex;
BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, modulate);
BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, modulate);
BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, modulate);
BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, modulate);
BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, new Vector2(0, 0), modulate);
BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, new Vector2(1, 0), modulate);
BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, new Vector2(1, 1), modulate);
BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, new Vector2(0, 1), modulate);
BatchVertexIndex += 4;
QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx);

View File

@@ -601,7 +601,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
private sealed class ClydeTexture : OwnedTexture
internal sealed class ClydeTexture : OwnedTexture
{
private readonly Clyde _clyde;
public readonly bool IsSrgb;

View File

@@ -1,4 +1,5 @@
varying highp vec2 UV;
varying highp vec2 UV2;
varying highp vec2 Pos;
varying highp vec4 VtxModulate;

View File

@@ -2,10 +2,12 @@
/*layout (location = 0)*/ attribute vec2 aPos;
// Texture coordinates.
/*layout (location = 1)*/ attribute vec2 tCoord;
/*layout (location = 2)*/ attribute vec2 tCoord2;
// Colour modulation.
/*layout (location = 2)*/ attribute vec4 modulate;
/*layout (location = 3)*/ attribute vec4 modulate;
varying vec2 UV;
varying vec2 UV2;
varying vec2 Pos;
varying vec4 VtxModulate;
@@ -36,5 +38,6 @@ void main()
gl_Position = vec4(VERTEX, 0.0, 1.0);
Pos = (VERTEX + 1.0) / 2.0;
UV = mix(modifyUV.xy, modifyUV.zw, tCoord);
UV2 = tCoord2;
VtxModulate = zFromSrgb(modulate);
}

View File

@@ -1,4 +1,5 @@
varying highp vec2 UV;
varying highp vec2 UV2;
uniform sampler2D lightMap;

View File

@@ -2,10 +2,12 @@
/*layout (location = 0)*/ attribute vec2 aPos;
// Texture coordinates.
/*layout (location = 1)*/ attribute vec2 tCoord;
/*layout (location = 2)*/ attribute vec2 tCoord2;
// Colour modulation.
/*layout (location = 2)*/ attribute vec4 modulate;
/*layout (location = 3)*/ attribute vec4 modulate;
varying vec2 UV;
varying vec2 UV2;
// Maybe we should merge these CPU side.
// idk yet.
@@ -40,6 +42,7 @@ void main()
vec2 VERTEX = aPos;
UV = tCoord;
UV2 = tCoord2;
// [SHADER_CODE]

View File

@@ -114,43 +114,12 @@ namespace Robust.Client.Graphics
DrawPrimitives(primitiveTopology, White, indices, drawVertices);
}
private static void PadVerticesV2(ReadOnlySpan<Vector2> input, Span<DrawVertexUV2DColor> output, Color color)
private void PadVerticesV2(ReadOnlySpan<Vector2> input, Span<DrawVertexUV2DColor> output, Color color)
{
if (input.Length == 0)
return;
if (input.Length != output.Length)
Color colorLinear = Color.FromSrgb(color);
for (var i = 0; i < output.Length; i++)
{
throw new InvalidOperationException("Invalid lengths!");
}
var colorLinear = Color.FromSrgb(color);
var colorVec = Unsafe.As<Color, Vector128<float>>(ref colorLinear);
var uvVec = Vector128.Create(0, 0, 0.5f, 0.5f);
var maskVec = Vector128.Create(0xFFFFFFFF, 0xFFFFFFFF, 0, 0).AsSingle();
var simdVectors = (nuint)(input.Length / 2);
ref readonly var srcBase = ref Unsafe.As<Vector2, float>(ref Unsafe.AsRef(in input[0]));
ref var dstBase = ref Unsafe.As<DrawVertexUV2DColor, float>(ref output[0]);
for (nuint i = 0; i < simdVectors; i++)
{
var positions = Vector128.LoadUnsafe(in srcBase, i * 4);
var posColorLower = (positions & maskVec) | uvVec;
var posColorUpper = (Vector128.Shuffle(positions, Vector128.Create(2, 3, 0, 0)) & maskVec) | uvVec;
posColorLower.StoreUnsafe(ref dstBase, i * 16);
colorVec.StoreUnsafe(ref dstBase, i * 16 + 4);
posColorUpper.StoreUnsafe(ref dstBase, i * 16 + 8);
colorVec.StoreUnsafe(ref dstBase, i * 16 + 12);
}
var lastPos = (int)simdVectors * 2;
if (lastPos != output.Length)
{
// Odd number of vertices. Handle the last manually.
output[lastPos] = new DrawVertexUV2DColor(input[lastPos], new Vector2(0.5f, 0.5f), colorLinear);
output[i] = new DrawVertexUV2DColor(input[i], new Vector2(0.5f, 0.5f), colorLinear);
}
}
@@ -268,6 +237,8 @@ namespace Robust.Client.Graphics
{
public Vector2 Position;
public Vector2 UV;
public Vector2 UV2;
/// <summary>
/// Modulation colour for this vertex.
/// Note that this color is in linear space.

View File

@@ -17,7 +17,7 @@ using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Graphics
{
[Prototype("shader")]
public sealed class ShaderPrototype : IPrototype, ISerializationHooks
public sealed partial class ShaderPrototype : IPrototype, ISerializationHooks
{
[ViewVariables]
[IdDataField]

View File

@@ -88,7 +88,6 @@ public sealed partial class ReplayLoadManager
if (initMessages != null)
UpdateMessages(initMessages, uploadedFiles, prototypes, cvars, detachQueue, ref timeBase, true);
UpdateMessages(messages[0], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase, true);
ProcessQueue(GameTick.MaxValue, detachQueue, detached);
var entSpan = state0.EntityStates.Value;
Dictionary<NetEntity, EntityState> entStates = new(entSpan.Count);
@@ -98,6 +97,8 @@ public sealed partial class ReplayLoadManager
entStates.Add(entState.NetEntity, modifiedState);
}
ProcessQueue(GameTick.MaxValue, detachQueue, detached, entStates);
await callback(0, states.Count, LoadingState.ProcessingFiles, true);
var playerSpan = state0.PlayerStates.Value;
Dictionary<NetUserId, SessionState> playerStates = new(playerSpan.Count);
@@ -144,7 +145,7 @@ public sealed partial class ReplayLoadManager
UpdatePlayerStates(curState.PlayerStates.Span, playerStates);
UpdateEntityStates(curState.EntityStates.Span, entStates, ref spawnedTracker, ref stateTracker, detached);
UpdateMessages(messages[i], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase);
ProcessQueue(curState.ToSequence, detachQueue, detached);
ProcessQueue(curState.ToSequence, detachQueue, detached, entStates);
UpdateDeletions(curState.EntityDeletions, entStates, detached);
serverTime[i] = GetTime(curState.ToSequence) - initialTime;
ticksSinceLastCheckpoint++;
@@ -176,14 +177,28 @@ public sealed partial class ReplayLoadManager
private void ProcessQueue(
GameTick curTick,
Dictionary<GameTick, List<NetEntity>> detachQueue,
HashSet<NetEntity> detached)
HashSet<NetEntity> detached,
Dictionary<NetEntity, EntityState> entStates)
{
foreach (var (tick, ents) in detachQueue)
{
if (tick > curTick)
continue;
detachQueue.Remove(tick);
detached.UnionWith(ents);
foreach (var e in ents)
{
if (entStates.ContainsKey(e))
detached.Add(e);
else
{
// AFAIK this should only happen if the client skipped over some ticks, probably due to packet loss
// I.e., entity was created on tick n, then leaves PVS range on the tick n+1
// If the n-th tick gets dropped, the client only ever receives the pvs-leave message.
// In that case we should just ignore it.
_sawmill.Debug($"Received a PVS detach msg for entity {e} before it was received?");
}
}
}
}

View File

@@ -79,13 +79,14 @@ internal sealed partial class ReplayPlaybackManager
if (checkpoint.DetachedStates == null)
return;
DebugTools.Assert(checkpoint.Detached.Count == checkpoint.DetachedStates.Length); ;
var metas = _entMan.GetEntityQuery<MetaDataComponent>();
DebugTools.Assert(checkpoint.Detached.Count == checkpoint.DetachedStates.Length);
foreach (var es in checkpoint.DetachedStates)
{
var uid = _entMan.GetEntity(es.NetEntity);
if (metas.TryGetComponent(uid, out var meta) && !meta.EntityDeleted)
if (_entMan.TryGetEntityData(es.NetEntity, out var uid, out var meta))
{
DebugTools.Assert(!meta.EntityDeleted);
continue;
}
var metaState = (MetaDataComponentState?)es.ComponentChanges.Value?
.FirstOrDefault(c => c.NetID == _metaId).State;
@@ -93,18 +94,16 @@ internal sealed partial class ReplayPlaybackManager
if (metaState == null)
throw new MissingMetadataException(es.NetEntity);
_entMan.CreateEntityUninitialized(metaState.PrototypeId, uid);
meta = metas.GetComponent(uid);
uid = _entMan.CreateEntity(metaState.PrototypeId, out meta);
// 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.
_entMan.ClearNetEntity(meta.NetEntity);
_entMan.SetNetEntity(uid.Value, es.NetEntity, meta);
_entMan.SetNetEntity(uid, es.NetEntity, meta);
_entMan.InitializeEntity(uid, meta);
_entMan.StartEntity(uid);
_entMan.InitializeEntity(uid.Value, meta);
_entMan.StartEntity(uid.Value);
meta.LastStateApplied = checkpoint.Tick;
}
}

View File

@@ -10,6 +10,7 @@ using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
@@ -142,6 +143,26 @@ namespace Robust.Client.ResourceManagement
}
});
// Do not meta-atlas RSIs with custom load parameters.
var atlasList = rsiList.Where(x => x.LoadParameters == TextureLoadParameters.Default).ToArray();
var nonAtlasList = rsiList.Where(x => x.LoadParameters != TextureLoadParameters.Default).ToArray();
foreach (var data in nonAtlasList)
{
if (data.Bad)
continue;
try
{
RSIResource.LoadTexture(Clyde, data);
}
catch (Exception e)
{
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
}
}
// This combines individual RSI atlases into larger atlases to reduce draw batches. currently this is a VERY
// lazy bundling and is not at all compact, its basically an atlas of RSI atlases. Really what this should
// try to do is to have each RSI write directly to the atlas, rather than having each RSI write to its own
@@ -155,7 +176,7 @@ namespace Robust.Client.ResourceManagement
// TODO allow RSIs to opt out (useful for very big & rare RSIs)
// TODO combine with (non-rsi) texture atlas?
Array.Sort(rsiList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
Array.Sort(atlasList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
// Each RSI sub atlas has a different size.
// Even if we iterate through them once to estimate total area, I have NFI how to sanely estimate an optimal square-texture size.
@@ -167,9 +188,9 @@ namespace Robust.Client.ResourceManagement
Vector2i offset = default;
int finalized = -1;
int atlasCount = 0;
for (int i = 0; i < rsiList.Length; i++)
for (int i = 0; i < atlasList.Length; i++)
{
var rsi = rsiList[i];
var rsi = atlasList[i];
if (rsi.Bad)
continue;
@@ -200,14 +221,14 @@ namespace Robust.Client.ResourceManagement
var height = offset.Y + deltaY;
var croppedSheet = new Image<Rgba32>(maxSize, height);
sheet.Blit(new UIBox2i(0, 0, maxSize, height), croppedSheet, default);
FinalizeMetaAtlas(rsiList.Length - 1, croppedSheet);
FinalizeMetaAtlas(atlasList.Length - 1, croppedSheet);
void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
{
var atlas = Clyde.LoadTextureFromImage(sheet);
for (int i = finalized + 1; i <= toIndex; i++)
{
var rsi = rsiList[i];
var rsi = atlasList[i];
rsi.AtlasTexture = atlas;
}
@@ -255,9 +276,10 @@ namespace Robust.Client.ResourceManagement
}
sawmill.Debug(
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountErrored} errored) in {LoadTime}",
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountNotAtlas} not atlassed, {CountErrored} errored) in {LoadTime}",
rsiList.Length,
atlasCount,
nonAtlasList.Length,
errors,
sw.Elapsed);

View File

@@ -40,17 +40,21 @@ namespace Robust.Client.ResourceManagement
var loadStepData = new LoadStepData {Path = path};
var manager = dependencies.Resolve<IResourceManager>();
LoadPreTexture(manager, loadStepData);
loadStepData.AtlasTexture = dependencies.Resolve<IClyde>().LoadTextureFromImage(
loadStepData.AtlasSheet,
loadStepData.Path.ToString());
LoadTexture(dependencies.Resolve<IClyde>(), loadStepData);
LoadPostTexture(loadStepData);
LoadFinish(dependencies.Resolve<IResourceCacheInternal>(), loadStepData);
loadStepData.AtlasSheet.Dispose();
}
internal static void LoadTexture(IClyde clyde, LoadStepData loadStepData)
{
loadStepData.AtlasTexture = clyde.LoadTextureFromImage(
loadStepData.AtlasSheet,
loadStepData.Path.ToString(),
loadStepData.LoadParameters);
}
internal static void LoadPreTexture(IResourceManager manager, LoadStepData data)
{
var manifestPath = data.Path / "meta.json";
@@ -178,6 +182,7 @@ namespace Robust.Client.ResourceManagement
data.FrameSize = frameSize;
data.DimX = dimensionX;
data.CallbackOffsets = callbackOffsets;
data.LoadParameters = metadata.LoadParameters;
}
internal static void LoadPostTexture(LoadStepData data)
@@ -380,6 +385,7 @@ namespace Robust.Client.ResourceManagement
public Texture AtlasTexture = default!;
public Vector2i AtlasOffset;
public RSI Rsi = default!;
public TextureLoadParameters LoadParameters;
}
internal struct StateReg

View File

@@ -52,6 +52,10 @@ namespace Robust.Client.UserInterface
[ViewVariables] public bool IsMeasureValid { get; private set; }
[ViewVariables] public bool IsArrangeValid { get; private set; }
/// <summary>
/// Controls the amount of empty space in virtual pixels around the control.
/// </summary>
/// <remarks>Values can be provided as "All" or "Horizontal, Vertical" or "Left, Top, Right, Bottom"</remarks>
[ViewVariables]
public Thickness Margin
{

View File

@@ -20,6 +20,7 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClyde _displayManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IBaseClient _baseClient = default!;
private readonly StringBuilder _textBuilder = new();
private readonly char[] _textBuffer = new char[1024];
@@ -58,30 +59,36 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
_textBuilder.Clear();
var isInGame = _baseClient.RunLevel.IsInGameLike();
var mouseScreenPos = _inputManager.MouseScreenPosition;
var screenSize = _displayManager.ScreenSize;
var screenScale = _displayManager.MainWindow.ContentScale;
EntityCoordinates mouseGridPos;
TileRef tile;
EntityCoordinates mouseGridPos = default;
TileRef tile = default;
MapCoordinates mouseWorldMap = default;
var mouseWorldMap = _eyeManager.PixelToMap(mouseScreenPos);
if (mouseWorldMap == MapCoordinates.Nullspace)
return;
var mapSystem = _entityManager.System<SharedMapSystem>();
var xformSystem = _entityManager.System<SharedTransformSystem>();
if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
if (isInGame)
{
mouseGridPos = mapSystem.MapToGrid(mouseGridUid, mouseWorldMap);
tile = mapSystem.GetTileRef(mouseGridUid, mouseGrid, mouseGridPos);
}
else
{
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
mouseWorldMap.Position);
tile = new TileRef(EntityUid.Invalid, mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
mouseWorldMap = _eyeManager.PixelToMap(mouseScreenPos);
if (mouseWorldMap != MapCoordinates.Nullspace)
{
var mapSystem = _entityManager.System<SharedMapSystem>();
var xformSystem = _entityManager.System<SharedTransformSystem>();
if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
{
mouseGridPos = mapSystem.MapToGrid(mouseGridUid, mouseWorldMap);
tile = mapSystem.GetTileRef(mouseGridUid, mouseGrid, mouseGridPos);
}
else
{
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
mouseWorldMap.Position);
tile = new TileRef(EntityUid.Invalid,
mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
}
}
}
var controlHovered = UserInterfaceManager.CurrentlyHovered;
@@ -95,32 +102,37 @@ Mouse Pos:
{tile}
GUI: {controlHovered}");
_textBuilder.AppendLine("\nAttached NetEntity:");
var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
if (controlledEntity == EntityUid.Invalid)
if (isInGame)
{
_textBuilder.AppendLine("No attached netentity.");
}
else
{
var entityTransform = _entityManager.GetComponent<TransformComponent>(controlledEntity);
var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
var xformSystem = _entityManager.System<SharedTransformSystem>();
var playerCoordinates = entityTransform.Coordinates;
var playerRotation = xformSystem.GetWorldRotation(entityTransform);
var gridRotation = entityTransform.GridUid != null
? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
: Angle.Zero;
_textBuilder.AppendLine("\nAttached NetEntity:");
var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
_textBuilder.Append($@" Screen: {playerScreen}
if (controlledEntity == EntityUid.Invalid)
{
_textBuilder.AppendLine("No attached netentity.");
}
else
{
var entityTransform = _entityManager.GetComponent<TransformComponent>(controlledEntity);
var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
var playerCoordinates = entityTransform.Coordinates;
var playerRotation = xformSystem.GetWorldRotation(entityTransform);
var gridRotation = entityTransform.GridUid != null
? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
: Angle.Zero;
_textBuilder.Append($@" Screen: {playerScreen}
{playerWorldOffset}
{_entityManager.GetNetCoordinates(playerCoordinates)}
Rotation: {playerRotation.Degrees:F2}°
NEntId: {_entityManager.GetNetEntity(controlledEntity)}
Grid NEntId: {_entityManager.GetNetEntity(entityTransform.GridUid)}
Grid Rotation: {gridRotation.Degrees:F2}°");
}
}
_contents.TextMemory = FormatHelpers.BuilderToMemory(_textBuilder, _textBuffer);

View File

@@ -135,6 +135,17 @@ namespace Robust.Client.UserInterface
/// Plays the UI hover sound if relevant.
/// </summary>
void HoverSound();
/// <summary>
/// Sets <see cref="CurrentlyHovered"/> to the given control.
/// </summary>
void SetHovered(Control? control);
/// <summary>
/// Forces <see cref="CurrentlyHovered"/> to get updated. This is done automatically when the mouse is moved,
/// but not necessarily a new or existing control is rearranged.
/// </summary>
void UpdateHovered();
}
public readonly struct PostDrawUIRootEventArgs

View File

@@ -5,7 +5,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
[Prototype("font")]
public sealed class FontPrototype : IPrototype
public sealed partial class FontPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;

View File

@@ -1,12 +1,10 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.ContentPack;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
@@ -18,7 +16,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface.Themes;
[Prototype("uiTheme")]
public sealed class UITheme : IPrototype
public sealed partial class UITheme : IPrototype
{
private IResourceCache? _cache;
private IUserInterfaceManager? _uiMan;

View File

@@ -9,6 +9,7 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface;
@@ -20,9 +21,10 @@ internal partial class UserInterfaceManager
private bool _needUpdateActiveCursor;
[ViewVariables] public Control? KeyboardFocused { get; private set; }
[ViewVariables] public Control? CurrentlyHovered { get; private set; } = default!;
[ViewVariables] public Control? CurrentlyHovered { get; private set; }
private Control? _controlFocused;
[ViewVariables]
public Control? ControlFocused
{
@@ -100,6 +102,7 @@ internal partial class UserInterfaceManager
return;
}
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
args.PointerLocation.Position - control.GlobalPixelPosition);
@@ -111,16 +114,18 @@ internal partial class UserInterfaceManager
args.Handle();
}
// Attempt to ensure that keybind-up events only get raised after a single keybind-down.
DebugTools.Assert(!_focusedControls.ContainsKey(args.Function));
_focusedControls[args.Function] = control;
OnKeyBindDown?.Invoke(control);
}
public void KeyBindUp(BoundKeyEventArgs args)
{
if (!_focusedControls.TryGetValue(args.Function, out var control))
{
// Only raise keybind-up for the control on which we previously raised keybind-down
if (!_focusedControls.Remove(args.Function, out var control) || control.Disposed)
return;
}
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
@@ -131,7 +136,6 @@ internal partial class UserInterfaceManager
// Always mark this as handled.
// The only case it should not be is if we do not have a control to click on,
// in which case we never reach this.
_focusedControls.Remove(args.Function);
args.Handle();
}
@@ -140,23 +144,7 @@ internal partial class UserInterfaceManager
_resetTooltipTimer();
// Update which control is considered hovered.
var newHovered = MouseGetControl(mouseMoveEventArgs.Position);
if (newHovered != CurrentlyHovered)
{
_clearTooltip();
CurrentlyHovered?.MouseExited();
CurrentlyHovered = newHovered;
CurrentlyHovered?.MouseEntered();
if (CurrentlyHovered != null)
{
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
}
else
{
_tooltipDelay = null;
}
_needUpdateActiveCursor = true;
}
SetHovered(newHovered);
var target = ControlFocused ?? newHovered;
if (target != null)
@@ -172,6 +160,33 @@ internal partial class UserInterfaceManager
}
}
public void UpdateHovered()
{
var ctrl = MouseGetControl(_inputManager.MouseScreenPosition);
SetHovered(ctrl);
}
public void SetHovered(Control? control)
{
if (control == CurrentlyHovered)
return;
_clearTooltip();
CurrentlyHovered?.MouseExited();
CurrentlyHovered = control;
CurrentlyHovered?.MouseEntered();
if (CurrentlyHovered != null)
{
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
}
else
{
_tooltipDelay = null;
}
_needUpdateActiveCursor = true;
}
private void UpdateActiveCursor()
{
// Consider mouse input focus first so that dragging windows don't act up etc.

View File

@@ -77,15 +77,12 @@ internal sealed partial class UserInterfaceManager
ReleaseKeyboardFocus(control);
RemoveModal(control);
if (control == CurrentlyHovered)
{
control.MouseExited();
CurrentlyHovered = null;
_clearTooltip();
}
if (control != ControlFocused) return;
ControlFocused = null;
if (control == ControlFocused)
ControlFocused = null;
if (control == CurrentlyHovered)
UpdateHovered();
}
public void PushModal(Control modal)

View File

@@ -42,6 +42,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
component.Source = new DummyAudioSource();
}
public override void SetMapAudio(Entity<AudioComponent>? audio)
{
if (audio == null)
return;
base.SetMapAudio(audio);
// Also need a global override because clients not near 0,0 won't get the audio.
_pvs.AddGlobalOverride(audio.Value);
}
private void AddAudioFilter(EntityUid uid, AudioComponent component, Filter filter)
{
var count = filter.Count;

View File

@@ -49,7 +49,6 @@ public sealed class MapLoaderSystem : EntitySystem
private ISawmill _logLoader = default!;
private ISawmill _logWriter = default!;
private static readonly MapLoadOptions DefaultLoadOptions = new();
private const int MapFormatVersion = 6;
private const int BackwardsVersion = 2;
@@ -132,7 +131,7 @@ public sealed class MapLoaderSystem : EntitySystem
public bool TryLoad(MapId mapId, string path, [NotNullWhen(true)] out IReadOnlyList<EntityUid>? rootUids,
MapLoadOptions? options = null)
{
options ??= DefaultLoadOptions;
options ??= new();
var resPath = new ResPath(path).ToRootedPath();
@@ -663,6 +662,7 @@ public sealed class MapLoaderSystem : EntitySystem
// If map exists swap out
if (_mapSystem.TryGetMap(data.TargetMap, out var existing))
{
data.Options.DoMapInit |= _mapSystem.IsInitialized(data.TargetMap);
data.MapIsPaused = _mapSystem.IsPaused(existing.Value);
// Map exists but we also have a map file with stuff on it soooo swap out the old map.
if (data.Options.LoadMap)
@@ -706,6 +706,7 @@ public sealed class MapLoaderSystem : EntitySystem
}
else
{
data.MapIsPaused = !data.MapIsPostInit;
mapComp.MapId = data.TargetMap;
DebugTools.Assert(mapComp.LifeStage < ComponentLifeStage.Initializing);
EnsureComp<LoadedMapComponent>(rootNode);
@@ -722,6 +723,7 @@ public sealed class MapLoaderSystem : EntitySystem
mapNode = _mapSystem.CreateMap(data.TargetMap, false);
}
data.Options.DoMapInit |= _mapSystem.IsInitialized(data.TargetMap);
data.MapIsPaused = _mapSystem.IsPaused(mapNode.Value);
// If anything has an invalid parent (e.g. it's some form of root node) then parent it to the map.
@@ -887,8 +889,7 @@ public sealed class MapLoaderSystem : EntitySystem
{
EntityManager.SetLifeStage(metadata, EntityLifeStage.MapInitialized);
}
// TODO MAP LOAD cache this
else if (_mapManager.IsMapInitialized(data.TargetMap))
else if (data.Options.DoMapInit)
{
_serverEntityManager.RunMapInit(uid, metadata);
}
@@ -1090,17 +1091,17 @@ public sealed class MapLoaderSystem : EntitySystem
}
}
private bool IsSaveable(EntityUid uid, EntityQuery<MetaDataComponent> metaQuery, EntityQuery<TransformComponent> transformQuery)
private bool IsSaveable(EntityUid uid)
{
// Don't serialize things parented to un savable things.
// For example clothes inside a person.
while (uid.IsValid())
{
var meta = metaQuery.GetComponent(uid);
var meta = MetaData(uid);
if (meta.EntityDeleted || meta.EntityPrototype?.MapSavable == false) break;
uid = transformQuery.GetComponent(uid).ParentUid;
uid = Transform(uid).ParentUid;
}
// If we manage to get up to the map (root node) then it's saveable.
@@ -1115,7 +1116,7 @@ public sealed class MapLoaderSystem : EntitySystem
EntityQuery<TransformComponent> transformQuery,
EntityQuery<MapSaveIdComponent> saveCompQuery)
{
if (!IsSaveable(uid, metaQuery, transformQuery))
if (!IsSaveable(uid))
return;
entities.Add(uid);

View File

@@ -1,416 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using System.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Server.GameObjects
namespace Robust.Server.GameObjects;
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
[Dependency] private readonly IPlayerManager _playerMan = default!;
[Dependency] private readonly TransformSystem _xformSys = default!;
private EntityQuery<IgnoreUIRangeComponent> _ignoreUIRangeQuery;
private readonly List<ICommonSession> _sessionCache = new();
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<BoundUIWrapMessage>(OnMessageReceived);
_playerMan.PlayerStatusChanged += OnPlayerStatusChanged;
_ignoreUIRangeQuery = GetEntityQuery<IgnoreUIRangeComponent>();
}
public override void Shutdown()
{
base.Shutdown();
_playerMan.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
{
if (args.NewStatus != SessionStatus.Disconnected)
return;
if (!OpenInterfaces.TryGetValue(args.Session, out var buis))
return;
foreach (var bui in buis.ToArray())
{
CloseShared(bui, args.Session);
}
}
/// <inheritdoc />
public override void Update(float frameTime)
{
var xformQuery = GetEntityQuery<TransformComponent>();
var query = AllEntityQuery<ActiveUserInterfaceComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var activeUis, out var xform))
{
foreach (var ui in activeUis.Interfaces)
{
CheckRange(uid, activeUis, ui, xform, xformQuery);
if (!ui.StateDirty)
continue;
ui.StateDirty = false;
foreach (var (player, state) in ui.PlayerStateOverrides)
{
RaiseNetworkEvent(state, player.Channel);
}
if (ui.LastStateMsg == null)
continue;
foreach (var session in ui.SubscribedSessions)
{
if (!ui.PlayerStateOverrides.ContainsKey(session))
RaiseNetworkEvent(ui.LastStateMsg, session.Channel);
}
}
}
}
/// <summary>
/// Verify that the subscribed clients are still in range of the interface.
/// </summary>
private void CheckRange(EntityUid uid, ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery<TransformComponent> query)
{
if (ui.InteractionRange <= 0)
return;
// We have to cache the set of sessions because Unsubscribe modifies the original.
_sessionCache.Clear();
_sessionCache.AddRange(ui.SubscribedSessions);
var uiPos = _xformSys.GetWorldPosition(transform, query);
var uiMap = transform.MapID;
foreach (var session in _sessionCache)
{
// The component manages the set of sessions, so this invalid session should be removed soon.
if (!query.TryGetComponent(session.AttachedEntity, out var xform))
continue;
if (_ignoreUIRangeQuery.HasComponent(session.AttachedEntity))
continue;
// Handle pluggable BoundUserInterfaceCheckRangeEvent
var checkRangeEvent = new BoundUserInterfaceCheckRangeEvent(uid, ui, session);
RaiseLocalEvent(uid, ref checkRangeEvent, broadcast: true);
if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Pass)
continue;
if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Fail)
{
CloseUi(ui, session, activeUis);
continue;
}
DebugTools.Assert(checkRangeEvent.Result == BoundUserInterfaceRangeResult.Default);
if (uiMap != xform.MapID)
{
CloseUi(ui, session, activeUis);
continue;
}
var distanceSquared = (uiPos - _xformSys.GetWorldPosition(xform, query)).LengthSquared();
if (distanceSquared > ui.InteractionRangeSqrd)
CloseUi(ui, session, activeUis);
}
}
#region Get BUI
public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
return false;
return ui.Interfaces.ContainsKey(uiKey);
}
public PlayerBoundUserInterface GetUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref ui))
throw new InvalidOperationException($"Cannot get {typeof(PlayerBoundUserInterface)} from an entity without {typeof(UserInterfaceComponent)}!");
return ui.Interfaces[uiKey];
}
public PlayerBoundUserInterface? GetUiOrNull(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
return TryGetUi(uid, uiKey, out var bui, ui)
? bui
: null;
}
/// <summary>
/// Return UIs a session has open.
/// Null if empty.
/// </summary>
public List<PlayerBoundUserInterface>? GetAllUIsForSession(ICommonSession session)
{
OpenInterfaces.TryGetValue(session, out var value);
return value;
}
#endregion
public bool IsUiOpen(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return bui.SubscribedSessions.Count > 0;
}
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return bui.SubscribedSessions.Contains(session);
}
/// <summary>
/// Sets a state. This can be used for stateful UI updating.
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
/// Pretty much how NanoUI did it back in ye olde BYOND.
/// </summary>
/// <param name="state">
/// The state object that will be sent to all current and future client.
/// This can be null.
/// </param>
/// <param name="session">
/// The player session to send this new state to.
/// Set to null for sending it to every subscribed player session.
/// </param>
public bool TrySetUiState(EntityUid uid,
Enum uiKey,
BoundUserInterfaceState state,
ICommonSession? session = null,
UserInterfaceComponent? ui = null,
bool clearOverrides = true)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
SetUiState(bui, state, session, clearOverrides);
return true;
}
/// <summary>
/// Sets a state. This can be used for stateful UI updating.
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
/// Pretty much how NanoUI did it back in ye olde BYOND.
/// </summary>
/// <param name="state">
/// The state object that will be sent to all current and future client.
/// This can be null.
/// </param>
/// <param name="session">
/// The player session to send this new state to.
/// Set to null for sending it to every subscribed player session.
/// </param>
public void SetUiState(PlayerBoundUserInterface bui, BoundUserInterfaceState state, ICommonSession? session = null, bool clearOverrides = true)
{
var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), new UpdateBoundStateMessage(state), bui.UiKey);
if (session == null)
{
bui.LastStateMsg = msg;
if (clearOverrides)
bui.PlayerStateOverrides.Clear();
}
else
{
bui.PlayerStateOverrides[session] = msg;
}
bui.StateDirty = true;
}
#region Close
protected override void CloseShared(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
{
var owner = bui.Owner;
bui._subscribedSessions.Remove(session);
bui.PlayerStateOverrides.Remove(session);
if (OpenInterfaces.TryGetValue(session, out var buis))
buis.Remove(bui);
RaiseLocalEvent(owner, new BoundUIClosedEvent(bui.UiKey, owner, session));
if (bui._subscribedSessions.Count == 0)
DeactivateInterface(bui.Owner, bui, activeUis);
}
/// <summary>
/// Closes this all interface for any clients that have any open.
/// </summary>
public bool TryCloseAll(EntityUid uid, Shared.GameObjects.ActiveUserInterfaceComponent? aui = null)
{
if (!Resolve(uid, ref aui, false))
return false;
foreach (var ui in aui.Interfaces)
{
CloseAll(ui);
}
return true;
}
/// <summary>
/// Closes this specific interface for any clients that have it open.
/// </summary>
public bool TryCloseAll(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
CloseAll(bui);
return true;
}
/// <summary>
/// Closes this interface for any clients that have it open.
/// </summary>
public void CloseAll(PlayerBoundUserInterface bui)
{
foreach (var session in bui.SubscribedSessions.ToArray())
{
CloseUi(bui, session);
}
}
#endregion
#region SendMessage
/// <summary>
/// Send a BUI message to all connected player sessions.
/// </summary>
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
SendUiMessage(bui, message);
return true;
}
/// <summary>
/// Send a BUI message to all connected player sessions.
/// </summary>
public void SendUiMessage(PlayerBoundUserInterface bui, BoundUserInterfaceMessage message)
{
var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey);
foreach (var session in bui.SubscribedSessions)
{
RaiseNetworkEvent(msg, session.Channel);
}
}
/// <summary>
/// Send a BUI message to a specific player session.
/// </summary>
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, ICommonSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return TrySendUiMessage(bui, message, session);
}
/// <summary>
/// Send a BUI message to a specific player session.
/// </summary>
public bool TrySendUiMessage(PlayerBoundUserInterface bui, BoundUserInterfaceMessage message, ICommonSession session)
{
if (!bui.SubscribedSessions.Contains(session))
return false;
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey), session.Channel);
return true;
}
#endregion
}
/// <summary>
/// Raised by <see cref="UserInterfaceSystem"/> to check whether an interface is still accessible by its user.
/// </summary>
[ByRefEvent]
[PublicAPI]
public struct BoundUserInterfaceCheckRangeEvent
{
/// <summary>
/// The entity owning the UI being checked for.
/// </summary>
public readonly EntityUid Target;
/// <summary>
/// The UI itself.
/// </summary>
/// <returns></returns>
public readonly PlayerBoundUserInterface UserInterface;
/// <summary>
/// The player for which the UI is being checked.
/// </summary>
public readonly ICommonSession Player;
/// <summary>
/// The result of the range check.
/// </summary>
public BoundUserInterfaceRangeResult Result;
public BoundUserInterfaceCheckRangeEvent(
EntityUid target,
PlayerBoundUserInterface userInterface,
ICommonSession player)
{
Target = target;
UserInterface = userInterface;
Player = player;
}
}
/// <summary>
/// Possible results for a <see cref="BoundUserInterfaceCheckRangeEvent"/>.
/// </summary>
public enum BoundUserInterfaceRangeResult : byte
{
/// <summary>
/// Run built-in range check.
/// </summary>
Default,
/// <summary>
/// Range check passed, UI is accessible.
/// </summary>
Pass,
/// <summary>
/// Range check failed, UI is inaccessible.
/// </summary>
Fail
}
}

View File

@@ -86,7 +86,7 @@ namespace Robust.Server.GameObjects
StartEntity(entity);
}
private protected override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
internal override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
{
if (prototypeName == null)
return base.CreateEntity(prototypeName, out metadata, context);

View File

@@ -55,13 +55,10 @@ internal sealed partial class PvsSystem
return;
}
var root = (xform.GridUid ?? xform.MapUid);
DebugTools.AssertNotNull(root);
if (xform.ParentUid != root)
if (xform.ParentUid != xform.GridUid && xform.ParentUid != xform.MapUid)
return;
var location = new PvsChunkLocation(root.Value, GetChunkIndices(xform._localPosition));
var location = new PvsChunkLocation(xform.ParentUid, GetChunkIndices(xform._localPosition));
if (meta.LastPvsLocation == location)
return;

View File

@@ -134,7 +134,7 @@ internal sealed partial class PvsSystem : EntitySystem
SubscribeLocalEvent<TransformComponent, TransformStartupEvent>(OnTransformStartup);
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_transform.OnGlobalMoveEvent += OnEntityMove;
_transform.OnBeforeMoveEvent += OnEntityMove;
EntityManager.EntityAdded += OnEntityAdded;
EntityManager.EntityDeleted += OnEntityDeleted;
EntityManager.AfterEntityFlush += AfterEntityFlush;
@@ -159,7 +159,7 @@ internal sealed partial class PvsSystem : EntitySystem
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_transform.OnGlobalMoveEvent -= OnEntityMove;
_transform.OnBeforeMoveEvent -= OnEntityMove;
EntityManager.EntityAdded -= OnEntityAdded;
EntityManager.EntityDeleted -= OnEntityDeleted;
EntityManager.AfterEntityFlush -= AfterEntityFlush;

View File

@@ -53,5 +53,7 @@ namespace Robust.Server.Maps
/// This should be set to false if you want to load a map file onto an existing map and do not wish to overwrite the existing entity.
/// </remarks>
public bool LoadMap { get; set; } = true;
public bool DoMapInit = false;
}
}

View File

@@ -10,7 +10,7 @@ namespace Robust.Shared.Audio;
/// to allow the server to know audio lengths without shipping the large audio files themselves.
/// </summary>
[Prototype(ProtoName)]
public sealed class AudioMetadataPrototype : IPrototype
public sealed partial class AudioMetadataPrototype : IPrototype
{
public const string ProtoName = "audioMetadata";

View File

@@ -9,7 +9,7 @@ namespace Robust.Shared.Audio;
/// This can be used by <see cref="Content.Shared.Audio.SharedContentAudioSystem"/> to apply an audio preset.
/// </summary>
[Prototype("audioPreset")]
public sealed class AudioPresetPrototype : IPrototype
public sealed partial class AudioPresetPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = default!;

View File

@@ -1,13 +1,13 @@
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using System.Collections.Generic;
namespace Robust.Shared.Audio;
[Prototype("soundCollection")]
public sealed class SoundCollectionPrototype : IPrototype
public sealed partial class SoundCollectionPrototype : IPrototype
{
[ViewVariables]
[IdDataField]

View File

@@ -8,6 +8,7 @@ using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@@ -32,11 +33,12 @@ public abstract partial class SharedAudioSystem : EntitySystem
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
[Dependency] protected readonly IRobustRandom RandMan = default!;
[Dependency] protected readonly MetaDataSystem MetadataSys = default!;
/// <summary>
/// Default max range at which the sound can be heard.
/// </summary>
public const float DefaultSoundRange = 20;
public const float DefaultSoundRange = 15;
/// <summary>
/// Used in the PAS to designate the physics collision mask of occluders.
@@ -131,6 +133,18 @@ public abstract partial class SharedAudioSystem : EntitySystem
return (float) (Timing.CurTime - (component.PauseTime ?? TimeSpan.Zero) - component.AudioStart).TotalSeconds;
}
/// <summary>
/// Marks this audio as being map-based.
/// </summary>
public virtual void SetMapAudio(Entity<AudioComponent>? audio)
{
if (audio == null)
return;
audio.Value.Comp.Global = true;
MetadataSys.AddFlag(audio.Value.Owner, MetaDataFlags.Undetachable);
}
/// <summary>
/// Sets the shared state for an audio entity.
/// </summary>

View File

@@ -851,7 +851,7 @@ namespace Robust.Shared
/// See the documentation of the <see cref="Network.AuthMode"/> enum for values.
/// </summary>
public static readonly CVarDef<int> AuthMode =
CVarDef.Create("auth.mode", (int) Network.AuthMode.Optional, CVar.SERVERONLY);
CVarDef.Create("auth.mode", (int) Network.AuthMode.Required, CVar.SERVERONLY);
/// <summary>
/// Allow unauthenticated localhost connections, even if the auth mode is set to required.

View File

@@ -71,6 +71,8 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
}
else
{
// TODO EXCEPTION TOLERANCE
// Ensure lookup trees update before content code handles move events.
SubscribeLocalEvent<TComp, MoveEvent>(HandleMove);
}

View File

@@ -33,7 +33,7 @@ internal sealed class RecursiveMoveSystem : EntitySystem
public override void Shutdown()
{
if (_subscribed)
_transform.OnGlobalMoveEvent -= AnythingMoved;
_transform.OnBeforeMoveEvent -= AnythingMoved;
_subscribed = false;
}
@@ -44,7 +44,7 @@ internal sealed class RecursiveMoveSystem : EntitySystem
return;
_subscribed = true;
_transform.OnGlobalMoveEvent += AnythingMoved;
_transform.OnBeforeMoveEvent += AnythingMoved;
}
private void AnythingMoved(ref MoveEvent args)

View File

@@ -770,7 +770,7 @@ Types:
Array:
Methods:
- "!!0 Find<>(!!0[], System.Predicate`1<!!0>)"
- "!!0 Resize<>(!!0[], int)"
- "void Resize<>(ref !!0[], int)"
- "!!1 ConvertAll<,>(!!0[], System.Converter`2<!!0, !!1>)"
- "!!0[] Empty<>()"
- "!!0[] FindAll<>(!!0[], System.Predicate`1<!!0>)"

View File

@@ -29,9 +29,50 @@ public sealed partial class PrototypeLayerData
[DataField("map")] public HashSet<string>? MapKeys;
[DataField("renderingStrategy")] public LayerRenderingStrategy? RenderingStrategy;
/// <summary>
/// If set, indicates that this sprite layer should instead be used to copy into shader parameters on another layer.
/// </summary>
/// <remarks>
/// <para>
/// If set, this sprite layer is not rendered. Instead, the "result" of rendering it (exact sprite layer and such)
/// are copied into the shader parameters of another object,
/// specified by the <see cref="PrototypeCopyToShaderParameters"/>.
/// </para>
/// <para>
/// The specified layer must have a shader set. When it does, the shader's
/// </para>
/// <para>
/// Note that sprite layers are processed in-order, so to avoid 1-frame delays,
/// the layer doing the copying should occur BEFORE the layer being copied into.
/// </para>
/// </remarks>
[DataField] public PrototypeCopyToShaderParameters? CopyToShaderParameters;
[DataField] public bool Cycle;
}
/// <summary>
/// Stores parameters for <see cref="PrototypeLayerData.CopyToShaderParameters"/>.
/// </summary>
[Serializable, NetSerializable, DataDefinition]
public sealed partial class PrototypeCopyToShaderParameters
{
/// <summary>
/// The map key of the layer that will have its shader modified.
/// </summary>
[DataField(required: true)] public string LayerKey;
/// <summary>
/// The name of the shader parameter that will receive the actual selected texture.
/// </summary>
[DataField] public string? ParameterTexture;
/// <summary>
/// The name of the shader parameter that will receive UVs to select the sprite in <see cref="ParameterTexture"/>.
/// </summary>
[DataField] public string? ParameterUV;
}
[Serializable, NetSerializable]
public enum LayerRenderingStrategy
{

View File

@@ -157,9 +157,7 @@ namespace Robust.Shared.GameObjects
if (!Initialized)
return;
var moveEvent = new MoveEvent((Owner, this, meta), Coordinates, Coordinates, oldRotation, _localRotation, _gameTiming.ApplyingState);
_entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent);
_entMan.System<SharedTransformSystem>().InvokeGlobalMoveEvent(ref moveEvent);
_entMan.System<SharedTransformSystem>().RaiseMoveEvent((Owner, this, meta), _parent, _localPosition, oldRotation, MapUid);
}
}
@@ -334,7 +332,9 @@ namespace Robust.Shared.GameObjects
if (_localPosition.EqualsApprox(value))
return;
var oldGridPos = Coordinates;
var oldParent = _parent;
var oldPos = _localPosition;
_localPosition = value;
var meta = _entMan.GetComponent<MetaDataComponent>(Owner);
_entMan.Dirty(Owner, this, meta);
@@ -343,9 +343,7 @@ namespace Robust.Shared.GameObjects
if (!Initialized)
return;
var moveEvent = new MoveEvent((Owner, this, meta), oldGridPos, Coordinates, _localRotation, _localRotation, _gameTiming.ApplyingState);
_entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent);
_entMan.System<SharedTransformSystem>().InvokeGlobalMoveEvent(ref moveEvent);
_entMan.System<SharedTransformSystem>().RaiseMoveEvent((Owner, this, meta), oldParent, oldPos, _localRotation, MapUid);
}
}
@@ -602,8 +600,12 @@ namespace Robust.Shared.GameObjects
/// move events, subscribe to the <see cref="SharedTransformSystem.OnGlobalMoveEvent"/>.
/// </summary>
[ByRefEvent]
public readonly struct MoveEvent(Entity<TransformComponent, MetaDataComponent> entity, EntityCoordinates oldPos,
EntityCoordinates newPos, Angle oldRotation, Angle newRotation, bool stateHandling = false)
public readonly struct MoveEvent(
Entity<TransformComponent, MetaDataComponent> entity,
EntityCoordinates oldPos,
EntityCoordinates newPos,
Angle oldRotation,
Angle newRotation)
{
public readonly Entity<TransformComponent, MetaDataComponent> Entity = entity;
public readonly EntityCoordinates OldPosition = oldPos;
@@ -615,15 +617,6 @@ namespace Robust.Shared.GameObjects
public TransformComponent Component => Entity.Comp1;
public bool ParentChanged => NewPosition.EntityId != OldPosition.EntityId;
[Obsolete("Check IGameTiming.ApplyingState")]
public readonly bool FromStateHandling = stateHandling;
[Obsolete]
public MoveEvent(EntityUid uid, EntityCoordinates oldPos, EntityCoordinates newPos, Angle oldRot, Angle newRot, TransformComponent xform, bool state)
: this((uid, xform, default!), oldPos, newPos, oldRot, newRot)
{
}
}
public struct TransformChildrenEnumerator : IDisposable

View File

@@ -19,7 +19,7 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// The last received state object sent from the server.
/// </summary>
protected BoundUserInterfaceState? State { get; private set; }
protected internal BoundUserInterfaceState? State { get; internal set; }
protected BoundUserInterface(EntityUid owner, Enum uiKey)
{
@@ -41,14 +41,14 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Invoked when the server uses <c>SetState</c>.
/// </summary>
protected virtual void UpdateState(BoundUserInterfaceState state)
protected internal virtual void UpdateState(BoundUserInterfaceState state)
{
}
/// <summary>
/// Invoked when the server sends an arbitrary message.
/// </summary>
protected virtual void ReceiveMessage(BoundUserInterfaceMessage message)
protected internal virtual void ReceiveMessage(BoundUserInterfaceMessage message)
{
}
@@ -57,7 +57,7 @@ namespace Robust.Shared.GameObjects
/// </summary>
public void Close()
{
UiSystem.TryCloseUi(_playerManager.LocalSession, Owner, UiKey);
UiSystem.CloseUi(Owner, UiKey, _playerManager.LocalEntity, predicted: true);
}
/// <summary>
@@ -65,7 +65,7 @@ namespace Robust.Shared.GameObjects
/// </summary>
public void SendMessage(BoundUserInterfaceMessage message)
{
UiSystem.SendUiMessage(this, message);
UiSystem.ClientSendUiMessage(Owner, UiKey, message);
}
public void SendPredictedMessage(BoundUserInterfaceMessage message)
@@ -73,20 +73,6 @@ namespace Robust.Shared.GameObjects
UiSystem.SendPredictedUiMessage(this, message);
}
internal void InternalReceiveMessage(BoundUserInterfaceMessage message)
{
switch (message)
{
case UpdateBoundStateMessage updateBoundStateMessage:
State = updateBoundStateMessage.State;
UpdateState(State);
break;
default:
ReceiveMessage(message);
break;
}
}
~BoundUserInterface()
{
Dispose(false);

View File

@@ -1,12 +1,12 @@
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
namespace Robust.Server.GameObjects;
namespace Robust.Shared.GameObjects;
/// <summary>
/// Lets any entities with this component ignore user interface range checks that would normally
/// close the UI automatically.
/// </summary>
[RegisterComponent]
[RegisterComponent, NetworkedComponent]
public sealed partial class IgnoreUIRangeComponent : Component
{
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects;
/// <summary>
/// Represents an entity-bound interface that can be opened by multiple players at once.
/// </summary>
[PublicAPI]
public sealed class PlayerBoundUserInterface
{
[ViewVariables]
public float InteractionRange;
[ViewVariables]
public float InteractionRangeSqrd => InteractionRange * InteractionRange;
[ViewVariables]
public Enum UiKey { get; }
[ViewVariables]
public EntityUid Owner { get; }
internal readonly HashSet<ICommonSession> _subscribedSessions = new();
[ViewVariables]
internal BoundUIWrapMessage? LastStateMsg;
[ViewVariables(VVAccess.ReadWrite)]
public bool RequireInputValidation;
[ViewVariables]
internal bool StateDirty;
[ViewVariables]
internal readonly Dictionary<ICommonSession, BoundUIWrapMessage> PlayerStateOverrides =
new();
/// <summary>
/// All of the sessions currently subscribed to this UserInterface.
/// </summary>
[ViewVariables]
public IReadOnlySet<ICommonSession> SubscribedSessions => _subscribedSessions;
public PlayerBoundUserInterface(PrototypeData data, EntityUid owner)
{
RequireInputValidation = data.RequireInputValidation;
UiKey = data.UiKey;
Owner = owner;
InteractionRange = data.InteractionRange;
}
}

View File

@@ -1,29 +1,26 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects
namespace Robust.Shared.GameObjects;
[RegisterComponent, NetworkedComponent]
public sealed partial class ActiveUserInterfaceComponent : Component
{
[RegisterComponent]
public sealed partial class ActiveUserInterfaceComponent : Component
{
[ViewVariables]
public HashSet<PlayerBoundUserInterface> Interfaces = new();
}
}
[PublicAPI]
public sealed class ServerBoundUserInterfaceMessage
{
[ViewVariables]
public BoundUserInterfaceMessage Message { get; }
[ViewVariables]
public ICommonSession Session { get; }
[PublicAPI]
public sealed class ServerBoundUserInterfaceMessage
{
[ViewVariables]
public BoundUserInterfaceMessage Message { get; }
[ViewVariables]
public ICommonSession Session { get; }
public ServerBoundUserInterfaceMessage(BoundUserInterfaceMessage message, ICommonSession session)
{
Message = message;
Session = session;
}
public ServerBoundUserInterfaceMessage(BoundUserInterfaceMessage message, ICommonSession session)
{
Message = message;
Session = session;
}
}

View File

@@ -8,36 +8,51 @@ using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects
{
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, Access(typeof(SharedUserInterfaceSystem))]
public sealed partial class UserInterfaceComponent : Component
{
// TODO: Obviously clean this shit up, I just moved it into shared.
/// <summary>
/// The currently open interfaces. Used clientside to store the UI.
/// </summary>
[ViewVariables, Access(Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.ReadWriteExecute)]
public readonly Dictionary<Enum, BoundUserInterface> ClientOpenInterfaces = new();
[ViewVariables] public readonly Dictionary<Enum, BoundUserInterface> OpenInterfaces = new();
[ViewVariables] public readonly Dictionary<Enum, PlayerBoundUserInterface> Interfaces = new();
public Dictionary<Enum, PrototypeData> MappedInterfaceData = new();
[DataField]
internal Dictionary<Enum, InterfaceData> Interfaces = new();
/// <summary>
/// Loaded on Init from serialized data.
/// Actors that currently have interfaces open.
/// </summary>
[DataField("interfaces")] internal List<PrototypeData> InterfaceData = new();
[DataField]
public Dictionary<Enum, List<EntityUid>> Actors = new();
/// <summary>
/// Legacy data, new BUIs should be using comp states.
/// </summary>
public Dictionary<Enum, BoundUserInterfaceState> States = new();
[Serializable, NetSerializable]
internal sealed class UserInterfaceComponentState(
Dictionary<Enum, List<NetEntity>> actors,
Dictionary<Enum, BoundUserInterfaceState> states)
: IComponentState
{
public Dictionary<Enum, List<NetEntity>> Actors = actors;
public Dictionary<Enum, BoundUserInterfaceState> States = states;
}
}
[DataDefinition]
public sealed partial class PrototypeData
public sealed partial class InterfaceData
{
[DataField("key", required: true)]
public Enum UiKey { get; private set; } = default!;
[DataField("type", required: true)]
public string ClientType { get; private set; } = default!;
/// <summary>
/// Maximum range before a BUI auto-closes. A non-positive number means there is no limit.
/// </summary>
[DataField("range")]
[DataField]
public float InteractionRange = 2f;
// TODO BUI move to content?
@@ -48,7 +63,7 @@ namespace Robust.Shared.GameObjects
/// <remarks>
/// Avoids requiring each system to individually validate client inputs. However, perhaps some BUIs are supposed to be bypass accessibility checks
/// </remarks>
[DataField("requireInputValidation")]
[DataField]
public bool RequireInputValidation = true;
}
@@ -56,18 +71,12 @@ namespace Robust.Shared.GameObjects
/// Raised whenever the server receives a BUI message from a client relating to a UI that requires input
/// validation.
/// </summary>
public sealed class BoundUserInterfaceMessageAttempt : CancellableEntityEventArgs
public sealed class BoundUserInterfaceMessageAttempt(EntityUid actor, EntityUid target, Enum uiKey)
: CancellableEntityEventArgs
{
public readonly ICommonSession Sender;
public readonly EntityUid Target;
public readonly Enum UiKey;
public BoundUserInterfaceMessageAttempt(ICommonSession sender, EntityUid target, Enum uiKey)
{
Sender = sender;
Target = target;
UiKey = uiKey;
}
public readonly EntityUid Actor = actor;
public readonly EntityUid Target = target;
public readonly Enum UiKey = uiKey;
}
[NetSerializable, Serializable]
@@ -104,7 +113,7 @@ namespace Robust.Shared.GameObjects
/// Only set when the message is raised as a directed event.
/// </summary>
[NonSerialized]
public ICommonSession Session = default!;
public EntityUid Actor = default!;
}
/// <summary>
@@ -120,17 +129,6 @@ namespace Robust.Shared.GameObjects
public NetEntity Entity { get; set; } = NetEntity.Invalid;
}
[NetSerializable, Serializable]
internal sealed class UpdateBoundStateMessage : BoundUserInterfaceMessage
{
public readonly BoundUserInterfaceState State;
public UpdateBoundStateMessage(BoundUserInterfaceState state)
{
State = state;
}
}
[NetSerializable, Serializable]
internal sealed class OpenBoundInterfaceMessage : BoundUserInterfaceMessage
{
@@ -142,59 +140,38 @@ namespace Robust.Shared.GameObjects
}
[Serializable, NetSerializable]
internal abstract class BaseBoundUIWrapMessage : EntityEventArgs
internal abstract class BaseBoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey)
: EntityEventArgs
{
public readonly NetEntity Entity;
public readonly BoundUserInterfaceMessage Message;
public readonly Enum UiKey;
public BaseBoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey)
{
Message = message;
UiKey = uiKey;
Entity = entity;
}
public readonly NetEntity Entity = entity;
public readonly BoundUserInterfaceMessage Message = message;
public readonly Enum UiKey = uiKey;
}
/// <summary>
/// Helper message raised from client to server.
/// </summary>
[Serializable, NetSerializable]
internal sealed class BoundUIWrapMessage : BaseBoundUIWrapMessage
{
public BoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey) : base(entity, message, uiKey)
{
}
}
/// <summary>
/// Helper message raised from client to server.
/// </summary>
[Serializable, NetSerializable]
internal sealed class PredictedBoundUIWrapMessage : BaseBoundUIWrapMessage
{
public PredictedBoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey) : base(entity, message, uiKey)
{
}
}
internal sealed class BoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey)
: BaseBoundUIWrapMessage(entity, message, uiKey);
public sealed class BoundUIOpenedEvent : BaseLocalBoundUserInterfaceEvent
{
public BoundUIOpenedEvent(Enum uiKey, EntityUid uid, ICommonSession session)
public BoundUIOpenedEvent(Enum uiKey, EntityUid uid, EntityUid actor)
{
UiKey = uiKey;
Entity = uid;
Session = session;
Actor = actor;
}
}
public sealed class BoundUIClosedEvent : BaseLocalBoundUserInterfaceEvent
{
public BoundUIClosedEvent(Enum uiKey, EntityUid uid, ICommonSession session)
public BoundUIClosedEvent(Enum uiKey, EntityUid uid, EntityUid actor)
{
UiKey = uiKey;
Entity = uid;
Session = session;
Actor = actor;
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.GameObjects;
/// <summary>
/// Stores data about this entity and what BUIs they have open.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class UserInterfaceUserComponent : Component
{
public override bool SessionSpecific => true;
[DataField]
public Dictionary<EntityUid, List<Enum>> OpenInterfaces = new();
}
[Serializable, NetSerializable]
internal sealed class UserInterfaceUserComponentState : IComponentState
{
public Dictionary<NetEntity, List<Enum>> OpenInterfaces = new();
}

View File

@@ -114,7 +114,7 @@ namespace Robust.Shared.GameObjects
public void InitializeComponents(EntityUid uid, MetaDataComponent? metadata = null)
{
DebugTools.AssertOwner(uid, metadata);
metadata ??= GetComponent<MetaDataComponent>(uid);
metadata ??= MetaQuery.GetComponent(uid);
DebugTools.Assert(metadata.EntityLifeStage == EntityLifeStage.PreInit);
SetLifeStage(metadata, EntityLifeStage.Initializing);
@@ -158,13 +158,12 @@ namespace Robust.Shared.GameObjects
// TODO: please for the love of god remove these initialization order hacks.
// Init transform first, we always have it.
var transform = GetComponent<TransformComponent>(uid);
var transform = TransformQuery.GetComponent(uid);
if (transform.LifeStage == ComponentLifeStage.Initialized)
LifeStartup(transform);
// Init physics second if it exists.
if (TryGetComponent<PhysicsComponent>(uid, out var phys)
&& phys.LifeStage == ComponentLifeStage.Initialized)
if (_physicsQuery.TryComp(uid, out var phys) && phys.LifeStage == ComponentLifeStage.Initialized)
{
LifeStartup(phys);
}
@@ -294,7 +293,7 @@ namespace Robust.Shared.GameObjects
if (!uid.IsValid() || !EntityExists(uid))
throw new ArgumentException($"Entity {uid} is not valid.", nameof(uid));
AddComponentInternal(uid, newComponent, false, true);
AddComponentInternal(uid, newComponent, false, true, null);
return new CompInitializeHandle<T>(this, uid, newComponent, reg.Idx);
}
@@ -302,10 +301,11 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
public void AddComponent<T>(EntityUid uid, T component, bool overwrite = false, MetaDataComponent? metadata = null) where T : IComponent
{
if (!uid.IsValid() || !EntityExists(uid))
if (!MetaQuery.Resolve(uid, ref metadata, false))
throw new ArgumentException($"Entity {uid} is not valid.", nameof(uid));
if (component == null) throw new ArgumentNullException(nameof(component));
if (component == null)
throw new ArgumentNullException(nameof(component));
#pragma warning disable CS0618 // Type or member is obsolete
if (component.Owner == default)
@@ -321,14 +321,17 @@ namespace Robust.Shared.GameObjects
AddComponentInternal(uid, component, overwrite, false, metadata);
}
private void AddComponentInternal<T>(EntityUid uid, T component, bool overwrite, bool skipInit, MetaDataComponent? metadata = null) where T : IComponent
private void AddComponentInternal<T>(EntityUid uid, T component, bool overwrite, bool skipInit, MetaDataComponent? metadata) where T : IComponent
{
if (!MetaQuery.ResolveInternal(uid, ref metadata, false))
throw new ArgumentException($"Entity {uid} is not valid.", nameof(uid));
// get interface aliases for mapping
var reg = _componentFactory.GetRegistration(component);
AddComponentInternal(uid, component, reg, overwrite, skipInit, metadata);
}
private void AddComponentInternal<T>(EntityUid uid, T component, ComponentRegistration reg, bool overwrite, bool skipInit, MetaDataComponent? metadata = null) where T : IComponent
private void AddComponentInternal<T>(EntityUid uid, T component, ComponentRegistration reg, bool overwrite, bool skipInit, MetaDataComponent metadata) where T : IComponent
{
// We can't use typeof(T) here in case T is just Component
DebugTools.Assert(component is MetaDataComponent ||
@@ -642,13 +645,14 @@ namespace Robust.Shared.GameObjects
_runtimeLog.LogException(e, nameof(CullRemovedComponents));
}
#endif
DeleteComponent(uid, component, false);
var meta = MetaQuery.GetComponent(uid);
DeleteComponent(uid, component, false, meta);
}
_deleteSet.Clear();
}
private void DeleteComponent(EntityUid entityUid, IComponent component, bool terminating, MetaDataComponent? metadata = null)
private void DeleteComponent(EntityUid entityUid, IComponent component, bool terminating, MetaDataComponent? metadata)
{
if (!MetaQuery.ResolveInternal(entityUid, ref metadata))
return;
@@ -1519,7 +1523,7 @@ namespace Robust.Shared.GameObjects
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool TryComp(EntityUid? uid, [NotNullWhen(true)] out TComp1? component)
public bool TryComp([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component)
=> TryGetComponent(uid, out component);
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -5,16 +5,17 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.Containers;
using Robust.Shared.Maths;
namespace Robust.Shared.GameObjects;
public partial class EntityManager
{
// This method will soon be marked as obsolete.
// This method will soon(TM) be marked as obsolete.
public EntityUid SpawnEntity(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
=> SpawnAttachedTo(protoName, coordinates, overrides);
// This method will soon be marked as obsolete.
// This method will soon(TM) be marked as obsolete.
public EntityUid SpawnEntity(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null)
=> Spawn(protoName, coordinates, overrides);
@@ -90,9 +91,9 @@ public partial class EntityManager
return entity;
}
public virtual EntityUid Spawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null)
public virtual EntityUid Spawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
{
var entity = CreateEntityUninitialized(protoName, coordinates, overrides);
var entity = CreateEntityUninitialized(protoName, coordinates, overrides, rotation);
InitializeAndStartEntity(entity, coordinates.MapId);
return entity;
}

View File

@@ -9,7 +9,9 @@ using Robust.Shared.GameStates;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
@@ -46,6 +48,7 @@ namespace Robust.Shared.GameObjects
public EntityQuery<MetaDataComponent> MetaQuery;
public EntityQuery<TransformComponent> TransformQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<ActorComponent> _actorQuery;
#endregion Dependencies
@@ -209,6 +212,7 @@ namespace Robust.Shared.GameObjects
_containers = System<SharedContainerSystem>();
MetaQuery = GetEntityQuery<MetaDataComponent>();
TransformQuery = GetEntityQuery<TransformComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_actorQuery = GetEntityQuery<ActorComponent>();
}
@@ -297,7 +301,7 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates, ComponentRegistry? overrides = null)
public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
{
var newEntity = CreateEntity(prototypeName, out _, overrides);
var transform = TransformQuery.GetComponent(newEntity);
@@ -322,7 +326,7 @@ namespace Robust.Shared.GameObjects
else
{
coords = new EntityCoordinates(mapEnt, coordinates.Position);
_xforms.SetCoordinates(newEntity, transform, coords, null, newParent: mapXform);
_xforms.SetCoordinates(newEntity, transform, coords, rotation, newParent: mapXform);
}
return newEntity;
@@ -550,17 +554,7 @@ namespace Robust.Shared.GameObjects
// Detach the base entity to null before iterating over children
// This also ensures that the entity-lookup updates don't have to be re-run for every child (which recurses up the transform hierarchy).
if (transform.ParentUid != EntityUid.Invalid)
{
try
{
_xforms.DetachParentToNull((uid, transform, metadata), parentXform, true);
}
catch (Exception e)
{
_sawmill.Error($"Caught exception while trying to detach parent of entity '{ToPrettyString(uid, metadata)}' to null.\n{e}");
}
}
_xforms.DetachEntity(uid, transform, metadata, parentXform, true);
foreach (var child in transform._children)
{
@@ -775,7 +769,7 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Allocates an entity and loads components but does not do initialization.
/// </summary>
private protected virtual EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
internal virtual EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
{
if (prototypeName == null)
return AllocEntity(out metadata);

View File

@@ -5,8 +5,10 @@ using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using TerraFX.Interop.Windows;
namespace Robust.Shared.GameObjects;
@@ -699,32 +701,32 @@ public partial class EntitySystem
#region Entity Spawning
// This method will be obsoleted soon.
// This method will be obsoleted soon(TM).
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid Spawn(string? prototype, EntityCoordinates coordinates)
{
return ((IEntityManager)EntityManager).SpawnEntity(prototype, coordinates);
}
/// <inheritdoc cref="IEntityManager.Spawn(string?, MapCoordinates, ComponentRegistry?)" />
/// <inheritdoc cref="IEntityManager.Spawn(string?, MapCoordinates, ComponentRegistry?, Angle)" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid Spawn(string? prototype, MapCoordinates coordinates)
=> EntityManager.Spawn(prototype, coordinates);
protected EntityUid Spawn(string? prototype, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
=> EntityManager.Spawn(prototype, coordinates, overrides, rotation);
/// <inheritdoc cref="IEntityManager.Spawn(string?, ComponentRegistry?)" />
/// <inheritdoc cref="IEntityManager.Spawn(string?, ComponentRegistry?, bool)" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid Spawn(string? prototype = null, ComponentRegistry? overrides = null, bool doMapInit = true)
=> EntityManager.Spawn(prototype, overrides, doMapInit);
/// <inheritdoc cref="IEntityManager.SpawnAttachedTo" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid SpawnAttachedTo(string? prototype, EntityCoordinates coordinates)
=> EntityManager.SpawnAttachedTo(prototype, coordinates);
protected EntityUid SpawnAttachedTo(string? prototype, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
=> EntityManager.SpawnAttachedTo(prototype, coordinates, overrides);
/// <inheritdoc cref="IEntityManager.SpawnAtPosition" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid SpawnAtPosition(string? prototype, EntityCoordinates coordinates)
=> EntityManager.SpawnAtPosition(prototype, coordinates);
protected EntityUid SpawnAtPosition(string? prototype, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
=> EntityManager.SpawnAtPosition(prototype, coordinates, overrides);
/// <inheritdoc cref="IEntityManager.TrySpawnInContainer" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -19,26 +19,26 @@ namespace Robust.Shared.GameObjects
public EntityUid? OldParent { get; }
/// <summary>
/// The map Id that the entity was on before its parent changed.
/// The map that the entity was on before its parent changed.
/// </summary>
/// <remarks>
/// If the old parent was detached to null without manually updating the map ID of its children, then this
/// is required as we cannot simply use the old parent's map ID. Also avoids having to fetch the old
/// parent's transform component.
/// </remarks>
public MapId OldMapId { get; }
public readonly EntityUid? OldMapId;
public TransformComponent Transform { get; }
/// <summary>
/// Creates a new instance of <see cref="EntParentChangedMessage"/>.
/// </summary>
public EntParentChangedMessage(EntityUid entity, EntityUid? oldParent, MapId oldMapId, TransformComponent xform)
public EntParentChangedMessage(EntityUid entity, EntityUid? oldParent, EntityUid? oldMapId, TransformComponent xform)
{
Entity = entity;
OldParent = oldParent;
OldMapId = oldMapId;
Transform = xform;
OldMapId = oldMapId;
}
}
}

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Robust.Shared.GameObjects;
@@ -32,7 +33,7 @@ public partial interface IEntityManager
/// <summary>
/// Spawns an entity at a specific world position. The entity will either be parented to the map or a grid.
/// </summary>
EntityUid Spawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null);
EntityUid Spawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!);
/// <summary>
/// Spawns an entity and then parents it to the entity that the given entity coordinates are relative to.

View File

@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Prometheus;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -76,7 +77,7 @@ namespace Robust.Shared.GameObjects
EntityUid CreateEntityUninitialized(string? prototypeName, EntityCoordinates coordinates, ComponentRegistry? overrides = null);
EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates, ComponentRegistry? overrides = null);
EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!);
void InitializeAndStartEntity(EntityUid entity, MapId? mapId = null);

View File

@@ -125,7 +125,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
SubscribeLocalEvent<GridAddEvent>(OnGridAdd);
SubscribeLocalEvent<MapChangedEvent>(OnMapChange);
_transform.OnGlobalMoveEvent += OnMove;
_transform.OnBeforeMoveEvent += OnMove;
EntityManager.EntityInitialized += OnEntityInit;
SubscribeLocalEvent<TransformComponent, PhysicsBodyTypeChangedEvent>(OnBodyTypeChange);
@@ -142,7 +142,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
{
base.Shutdown();
EntityManager.EntityInitialized -= OnEntityInit;
_transform.OnGlobalMoveEvent -= OnMove;
_transform.OnBeforeMoveEvent -= OnMove;
}
#region DynamicTree

View File

@@ -26,7 +26,6 @@ internal sealed class SharedGridTraversalSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<TransformStartupEvent>(OnStartup);
_transform.OnGlobalMoveEvent += OnMove;
}
private void OnStartup(ref TransformStartupEvent ev)
@@ -34,17 +33,6 @@ internal sealed class SharedGridTraversalSystem : EntitySystem
CheckTraverse(ev.Entity.Owner, ev.Entity.Comp);
}
public override void Shutdown()
{
_transform.OnGlobalMoveEvent -= OnMove;
}
private void OnMove(ref MoveEvent moveEv)
{
CheckTraverse(moveEv.Sender, moveEv.Component);
}
internal void CheckTraverse(EntityUid uid, TransformComponent xform)
{
if (!Enabled || _timing.ApplyingState)

View File

@@ -8,8 +8,6 @@ namespace Robust.Shared.GameObjects;
public abstract partial class SharedMapSystem
{
private List<EntityUid> _toInitialize = new();
public bool IsInitialized(MapId mapId)
{
if (mapId == MapId.Nullspace)
@@ -67,23 +65,19 @@ public abstract partial class SharedMapSystem
private void RecursiveMapInit(EntityUid entity)
{
_toInitialize.Clear();
_toInitialize.Add(entity);
for (var i = 0; i < _toInitialize.Count; i++)
var toInitialize = new List<EntityUid> {entity};
for (var i = 0; i < toInitialize.Count; i++)
{
var uid = _toInitialize[i];
// _toInitialize might contain deleted entities.
var uid = toInitialize[i];
// toInitialize might contain deleted entities.
if(!_metaQuery.TryComp(uid, out var meta))
continue;
if (meta.EntityLifeStage == EntityLifeStage.MapInitialized)
continue;
_toInitialize.AddRange(Transform(uid)._children);
toInitialize.AddRange(Transform(uid)._children);
EntityManager.RunMapInit(uid, meta);
}
_toInitialize.Clear();
}
}

View File

@@ -145,6 +145,11 @@ namespace Robust.Shared.GameObjects
ChunkIndex = chunkIndex;
}
/// <summary>
/// Was the tile previously empty or is it now empty.
/// </summary>
public bool EmptyChanged => OldTile.IsEmpty != NewTile.Tile.IsEmpty;
/// <summary>
/// EntityUid of the grid with the tile-change. TileRef stores the GridId.
/// </summary>

View File

@@ -5,6 +5,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Robust.Shared.Map.Components;
@@ -43,21 +44,13 @@ public abstract partial class SharedTransformSystem
xform._anchored = true;
var oldPos = xform._localPosition;
var oldRot = xform._localRotation;
var oldMap = xform.MapUid;
xform._localPosition = tilePos + newGrid.TileSizeHalfVector;
xform._localRotation += rotation;
SetGridId(uid, xform, newGridUid, XformQuery);
var reParent = new EntParentChangedMessage(uid, oldGridUid, xform.MapID, xform);
RaiseLocalEvent(uid, ref reParent, true);
var meta = MetaData(uid);
var movEevee = new MoveEvent((uid, xform, meta),
new EntityCoordinates(oldGridUid, oldPos),
new EntityCoordinates(newGridUid, xform._localPosition),
oldRot,
xform.LocalRotation,
_gameTiming.ApplyingState);
RaiseLocalEvent(uid, ref movEevee);
InvokeGlobalMoveEvent(ref movEevee);
RaiseMoveEvent((uid, xform, meta), oldGridUid, oldPos, oldRot, oldMap);
DebugTools.Assert(XformQuery.GetComponent(oldGridUid).MapID == XformQuery.GetComponent(newGridUid).MapID);
DebugTools.Assert(xform._anchored);
@@ -321,7 +314,7 @@ public abstract partial class SharedTransformSystem
// I hate this too. Once again, required for shit like containers because they CBF doing their own init logic
// and rely on parent changed messages instead. Might also be used by broadphase stuff?
var parentEv = new EntParentChangedMessage(uid, null, MapId.Nullspace, xform);
var parentEv = new EntParentChangedMessage(uid, null, null, xform);
RaiseLocalEvent(uid, ref parentEv, true);
var ev = new TransformStartupEvent((uid, xform));
@@ -449,9 +442,6 @@ public abstract partial class SharedTransformSystem
return;
}
var oldPosition = xform._parent.IsValid() ? new EntityCoordinates(xform._parent, xform._localPosition) : default;
var oldRotation = xform._localRotation;
if (xform.Anchored && unanchor)
Unanchor(uid, xform);
@@ -470,6 +460,11 @@ public abstract partial class SharedTransformSystem
}
}
var oldParentUid = xform._parent;
var oldPosition = xform._localPosition;
var oldRotation = xform._localRotation;
var oldMap = xform.MapUid;
// Set new values
Dirty(uid, xform, meta);
xform.MatricesDirty = true;
@@ -485,7 +480,7 @@ public abstract partial class SharedTransformSystem
{
if (value.EntityId == uid)
{
DetachParentToNull(uid, xform);
DetachEntity(uid, xform);
if (_netMan.IsServer || IsClientSide(uid))
QueueDel(uid);
throw new InvalidOperationException($"Attempted to parent an entity to itself: {ToPrettyString(uid)}");
@@ -495,7 +490,7 @@ public abstract partial class SharedTransformSystem
{
if (!XformQuery.Resolve(value.EntityId, ref newParent, false))
{
DetachParentToNull(uid, xform);
DetachEntity(uid, xform);
if (_netMan.IsServer || IsClientSide(uid))
QueueDel(uid);
throw new InvalidOperationException($"Attempted to parent entity {ToPrettyString(uid)} to non-existent entity {value.EntityId}");
@@ -503,7 +498,7 @@ public abstract partial class SharedTransformSystem
if (newParent.LifeStage >= ComponentLifeStage.Stopping || LifeStage(value.EntityId) >= EntityLifeStage.Terminating)
{
DetachParentToNull(uid, xform);
DetachEntity(uid, xform);
if (_netMan.IsServer || IsClientSide(uid))
QueueDel(uid);
throw new InvalidOperationException($"Attempted to re-parent to a terminating object. Entity: {ToPrettyString(uid)}, new parent: {ToPrettyString(value.EntityId)}");
@@ -528,7 +523,7 @@ public abstract partial class SharedTransformSystem
// Even though its temporary, this can still cause the client to get stuck in infinite loops while applying the game state.
// So we will just break the loop by detaching to null and just trusting that the loop wasn't actually a real feature of the server state.
Log.Warning($"Encountered circular transform hierarchy while applying state for entity: {ToPrettyString(uid)}. Detaching child to null: {ToPrettyString(recursiveUid)}");
DetachParentToNull(recursiveUid, recursiveXform);
DetachEntity(recursiveUid, recursiveXform);
break;
}
@@ -545,7 +540,6 @@ public abstract partial class SharedTransformSystem
newParent?._children.Add(uid);
xform._parent = value.EntityId;
var oldMapId = xform.MapID;
if (newParent != null)
{
@@ -576,24 +570,18 @@ public abstract partial class SharedTransformSystem
xform._localRotation += GetWorldRotation(oldParent) - GetWorldRotation(newParent);
DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0);
var entParentChangedMessage = new EntParentChangedMessage(uid, oldParent?.Owner, oldMapId, xform);
RaiseLocalEvent(uid, ref entParentChangedMessage, true);
}
}
if (!xform.Initialized)
return;
var newPosition = xform._parent.IsValid() ? new EntityCoordinates(xform._parent, xform._localPosition) : default;
#if DEBUG
// If an entity is parented to the map, its grid uid should be null (unless it is itself a grid or we have a map-grid)
if (xform.ParentUid == xform.MapUid)
DebugTools.Assert(xform.GridUid == null || xform.GridUid == uid || xform.GridUid == xform.MapUid);
#endif
var moveEvent = new MoveEvent((uid, xform, meta), oldPosition, newPosition, oldRotation, xform._localRotation, _gameTiming.ApplyingState);
RaiseLocalEvent(uid, ref moveEvent);
InvokeGlobalMoveEvent(ref moveEvent);
RaiseMoveEvent(entity, oldParentUid, oldPosition, oldRotation, oldMap);
}
public void SetCoordinates(
@@ -668,13 +656,13 @@ public abstract partial class SharedTransformSystem
public void SetParent(EntityUid uid, TransformComponent xform, EntityUid parent, EntityQuery<TransformComponent> xformQuery, TransformComponent? parentXform = null)
{
DebugTools.Assert(uid == xform.Owner);
DebugTools.AssertOwner(uid, xform);
if (xform.ParentUid == parent)
return;
if (!parent.IsValid())
{
DetachParentToNull(uid, xform);
DetachEntity(uid, xform);
return;
}
@@ -870,6 +858,28 @@ public abstract partial class SharedTransformSystem
return GetMapCoordinates(entity.Comp);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetMapCoordinates(EntityUid entity, MapCoordinates coordinates)
{
var xform = XformQuery.GetComponent(entity);
SetMapCoordinates((entity, xform), coordinates);
}
public void SetMapCoordinates(Entity<TransformComponent> entity, MapCoordinates coordinates)
{
var mapUid = _map.GetMap(coordinates.MapId);
if (!_gridQuery.HasComponent(entity) &&
_mapManager.TryFindGridAt(mapUid, coordinates.Position, out var targetGrid, out _))
{
var invWorldMatrix = GetInvWorldMatrix(targetGrid);
SetCoordinates(entity, new EntityCoordinates(targetGrid, invWorldMatrix.Transform(coordinates.Position)));
}
else
{
SetCoordinates(entity, new EntityCoordinates(mapUid, coordinates.Position));
}
}
[Pure]
public (Vector2 WorldPosition, Angle WorldRotation) GetWorldPositionRotation(EntityUid uid)
{
@@ -954,7 +964,7 @@ public abstract partial class SharedTransformSystem
// Entity was not actually in the transform hierarchy. This is probably a sign that something is wrong, or that the function is being misused.
Log.Warning($"Target entity ({ToPrettyString(relative)}) not in transform hierarchy while calling {nameof(GetRelativePositionRotation)}.");
var relXform = query.GetComponent(relative);
pos = relXform.InvWorldMatrix.Transform(pos);
pos = GetInvWorldMatrix(relXform).Transform(pos);
break;
}
@@ -968,7 +978,6 @@ public abstract partial class SharedTransformSystem
SetWorldPosition(xform, worldPos);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPosition(TransformComponent component, Vector2 worldPos)
{
@@ -1026,6 +1035,16 @@ public abstract partial class SharedTransformSystem
return rotation;
}
public void SetWorldRotationNoLerp(Entity<TransformComponent?> entity, Angle angle)
{
if (!XformQuery.Resolve(entity.Owner, ref entity.Comp))
return;
var current = GetWorldRotation(entity.Comp);
var diff = angle - current;
SetLocalRotationNoLerp(entity, entity.Comp.LocalRotation + diff);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldRotation(EntityUid uid, Angle angle)
{
@@ -1121,7 +1140,8 @@ public abstract partial class SharedTransformSystem
if (xform._localPosition.EqualsApprox(pos) && xform.LocalRotation.EqualsApprox(rot))
return;
var oldPosition = xform.Coordinates;
var oldParent = xform._parent;
var oldPosition = xform._localPosition;
var oldRotation = xform.LocalRotation;
if (!xform.Anchored)
@@ -1139,9 +1159,7 @@ public abstract partial class SharedTransformSystem
if (!xform.Initialized)
return;
var moveEvent = new MoveEvent((uid, xform, meta), oldPosition, xform.Coordinates, oldRotation, rot, _gameTiming.ApplyingState);
RaiseLocalEvent(uid, ref moveEvent);
InvokeGlobalMoveEvent(ref moveEvent);
RaiseMoveEvent((uid, xform, meta), oldParent, oldPosition, oldRotation, xform.MapUid);
}
#endregion
@@ -1304,7 +1322,7 @@ public abstract partial class SharedTransformSystem
if (!_mapManager.IsMap(uid))
Log.Warning($"Failed to attach entity to map or grid. Entity: ({ToPrettyString(uid)}). Trace: {Environment.StackTrace}");
DetachParentToNull(uid, xform);
DetachEntity(uid, xform);
return;
}
@@ -1340,21 +1358,50 @@ public abstract partial class SharedTransformSystem
#region State Handling
[Obsolete("Use DetachEntity")]
public void DetachParentToNull(EntityUid uid, TransformComponent xform)
=> DetachEntity(uid, xform);
/// <inheritdoc cref="DetachEntityInternal"/>
public void DetachEntity(EntityUid uid, TransformComponent xform)
{
XformQuery.TryGetComponent(xform.ParentUid, out var oldXform);
DetachParentToNull(uid, xform, oldXform);
DetachEntity(uid, xform, MetaData(uid), oldXform);
}
public void DetachParentToNull(EntityUid uid, TransformComponent xform, TransformComponent? oldXform)
/// <inheritdoc cref="DetachEntityInternal"/>
public void DetachEntity(
EntityUid uid,
TransformComponent xform,
MetaDataComponent meta,
TransformComponent? oldXform,
bool terminating = false)
{
DetachParentToNull((uid, xform, MetaData(uid)), oldXform);
#if !EXCEPTION_TOLERANCE
DetachEntityInternal(uid, xform, meta, oldXform, terminating);
#else
try
{
DetachEntityInternal(uid, xform, meta, oldXform, terminating);
}
catch (Exception e)
{
Log.Error($"Caught exception while attempting to detach an entity to nullspace. Entity: {ToPrettyString(uid, meta)}. Exception: {e}");
// TODO detach without content event handling.
}
#endif
}
public void DetachParentToNull(Entity<TransformComponent,MetaDataComponent> entity, TransformComponent? oldXform, bool terminating = false)
/// <summary>
/// Remove an entity from the transform hierarchy and send it to null space
/// </summary>
internal void DetachEntityInternal(
EntityUid uid,
TransformComponent xform,
MetaDataComponent meta,
TransformComponent? oldXform,
bool terminating = false)
{
var (uid, xform, meta) = entity;
if (!terminating && meta.EntityLifeStage >= EntityLifeStage.Terminating)
{
// Something is attempting to remove the entity from this entity's parent while it is in the process of being deleted.
@@ -1371,15 +1418,14 @@ public abstract partial class SharedTransformSystem
DebugTools.Assert((MetaData(uid).Flags & MetaDataFlags.InContainer) == 0x0,
$"Entity is in a container but has no parent? Entity: {ToPrettyString(uid)}");
if (xform.Broadphase != null)
{
DebugTools.Assert(
xform.Broadphase == BroadphaseData.Invalid
|| xform.Broadphase.Value.Uid == uid
|| Deleted(xform.Broadphase.Value.Uid)
|| Terminating(xform.Broadphase.Value.Uid),
$"Entity has no parent but is on some broadphase? Entity: {ToPrettyString(uid)}. Broadphase: {ToPrettyString(xform.Broadphase.Value.Uid)}");
}
DebugTools.Assert(
xform.Broadphase == null
|| xform.Broadphase == BroadphaseData.Invalid
|| xform.Broadphase.Value.Uid == uid
|| Deleted(xform.Broadphase.Value.Uid)
|| Terminating(xform.Broadphase.Value.Uid),
$"Entity has no parent but is on some broadphase? Entity: {ToPrettyString(uid)}. Broadphase: {ToPrettyString(xform.Broadphase!.Value.Uid)}");
return;
}
@@ -1403,7 +1449,7 @@ public abstract partial class SharedTransformSystem
RaiseLocalEvent(uid, ref anchorStateChangedEvent, true);
}
SetCoordinates(entity, default, Angle.Zero, oldParent: oldXform);
SetCoordinates((uid, xform, meta), default, Angle.Zero, oldParent: oldXform);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0x0,
$"Entity is in a container after having been detached to null-space? Entity: {ToPrettyString(uid)}");
@@ -1438,7 +1484,7 @@ public abstract partial class SharedTransformSystem
var targetXform = target.Comp;
if (!XformQuery.Resolve(target, ref targetXform) || !targetXform.ParentUid.IsValid())
{
DetachParentToNull(entity, xform);
DetachEntity(entity, xform);
return;
}
@@ -1476,7 +1522,7 @@ public abstract partial class SharedTransformSystem
var targetXform = target.Comp;
if (!XformQuery.Resolve(target, ref targetXform) || !targetXform.ParentUid.IsValid())
{
DetachParentToNull(entity, xform);
DetachEntity(entity, xform);
return;
}
@@ -1496,4 +1542,90 @@ public abstract partial class SharedTransformSystem
PlaceNextTo((entity, xform), targetXform.ParentUid);
}
}
/// <summary>
/// Swaps the position of two entities, placing them inside of containers when applicable.
/// </summary>
/// <returns>Returns if the entities can have their positions swapped. Fails if the entities are parented to one another</returns>
/// <exception cref="InvalidOperationException"></exception>
public bool SwapPositions(Entity<TransformComponent?> entity1, Entity<TransformComponent?> entity2)
{
if (!XformQuery.Resolve(entity1, ref entity1.Comp) || !XformQuery.Resolve(entity2, ref entity2.Comp))
return false;
// save ourselves the hassle and just don't move anything.
if (entity1 == entity2)
return true;
// don't parent things to each other by accident
if (IsParentOf(entity1.Comp, entity2) || IsParentOf(entity2.Comp, entity1))
return false;
MapCoordinates? pos1 = null;
MapCoordinates? pos2 = null;
if (_container.TryGetContainingContainer(entity1, out var container1))
_container.Remove(entity1, container1, force: true);
else
pos1 = GetMapCoordinates(entity1.Comp);
if (_container.TryGetContainingContainer(entity2, out var container2))
_container.Remove(entity2, container2, force: true);
else
pos2 = GetMapCoordinates(entity2.Comp);
// making sure we don't accidentally place something inside of itself
if (container1 != null && container1.Owner == entity2.Owner)
return false;
if (container2 != null && container2.Owner == entity1.Owner)
return false;
if (container2 != null)
{
_container.Insert(entity1, container2);
}
else if (pos2 != null)
{
var mapUid = _mapManager.GetMapEntityId(pos2.Value.MapId);
if (!_gridQuery.HasComponent(entity1) && _mapManager.TryFindGridAt(mapUid, pos2.Value.Position, out var targetGrid, out _))
{
var invWorldMatrix = GetInvWorldMatrix(targetGrid);
SetCoordinates(entity1, new EntityCoordinates(targetGrid, invWorldMatrix.Transform(pos2.Value.Position)));
}
else
{
SetCoordinates(entity1, new EntityCoordinates(mapUid, pos2.Value.Position));
}
}
else
{
throw new InvalidOperationException();
}
if (container1 != null)
{
_container.Insert(entity2, container1);
}
else if (pos1 != null)
{
var mapUid = _mapManager.GetMapEntityId(pos1.Value.MapId);
if (!_gridQuery.HasComponent(entity1) && _mapManager.TryFindGridAt(mapUid, pos1.Value.Position, out var targetGrid, out _))
{
var invWorldMatrix = GetInvWorldMatrix(targetGrid);
SetCoordinates(entity2, new EntityCoordinates(targetGrid, invWorldMatrix.Transform(pos1.Value.Position)));
}
else
{
SetCoordinates(entity2, new EntityCoordinates(mapUid, pos1.Value.Position));
}
}
else
{
throw new InvalidOperationException();
}
return true;
}
}

View File

@@ -25,6 +25,7 @@ namespace Robust.Shared.GameObjects
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedGridTraversalSystem _traversal = default!;
private EntityQuery<MapComponent> _mapQuery;
private EntityQuery<MapGridComponent> _gridQuery;
@@ -40,10 +41,12 @@ namespace Robust.Shared.GameObjects
/// </summary>
public event MoveEventHandler? OnGlobalMoveEvent;
public void InvokeGlobalMoveEvent(ref MoveEvent ev)
{
OnGlobalMoveEvent?.Invoke(ref ev);
}
/// <summary>
/// Internal move event handlers. This gets invoked before the global & directed move events. This is mainly
/// for exception tolerance, we want to ensure that PVS, physics & entity lookups get updated before some
/// content code throws an exception.
/// </summary>
internal event MoveEventHandler? OnBeforeMoveEvent;
public override void Initialize()
{
@@ -104,7 +107,7 @@ namespace Robust.Shared.GameObjects
// If a tile is being removed due to an explosion or somesuch, some entities are likely being deleted.
// Avoid unnecessary entity updates.
if (EntityManager.IsQueuedForDeletion(entity))
DetachParentToNull(entity, xform, gridXform);
DetachEntity(entity, xform, MetaData(entity), gridXform);
else
SetParent(entity, xform, gridXform.MapUid.Value, mapTransform);
}
@@ -255,6 +258,44 @@ namespace Robust.Shared.GameObjects
indices = _map.CoordinatesToTile(xform.GridUid.Value, grid, xform.Coordinates);
return true;
}
public void RaiseMoveEvent(
Entity<TransformComponent, MetaDataComponent> ent,
EntityUid oldParent,
Vector2 oldPosition,
Angle oldRotation,
EntityUid? oldMap)
{
var pos = ent.Comp1._parent == EntityUid.Invalid
? default
: new EntityCoordinates(ent.Comp1._parent, ent.Comp1._localPosition);
var oldPos = oldParent == EntityUid.Invalid
? default
: new EntityCoordinates(oldParent, oldPosition);
var ev = new MoveEvent(ent, oldPos, pos, oldRotation, ent.Comp1._localRotation);
if (oldParent != ent.Comp1._parent)
{
_physics.OnParentChange(ent, oldParent, oldMap);
OnBeforeMoveEvent?.Invoke(ref ev);
var entParentChangedMessage = new EntParentChangedMessage(ev.Sender, oldParent, oldMap, ev.Component);
RaiseLocalEvent(ev.Sender, ref entParentChangedMessage, true);
}
else
{
OnBeforeMoveEvent?.Invoke(ref ev);
}
RaiseLocalEvent(ev.Sender, ref ev);
OnGlobalMoveEvent?.Invoke(ref ev);
// Finally, handle grid traversal. This is handled separately to avoid out-of-order move events.
// I.e., if the traversal raises its own move event, this ensures that all the old move event handlers
// have finished running first. Ideally this shouldn't be required, but this is here just in case
_traversal.CheckTraverse(ent.Owner, ent.Comp1);
}
}
[ByRefEvent]

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
@@ -8,7 +9,7 @@ namespace Robust.Shared.Graphics;
/// Flags for loading of textures.
/// </summary>
[PublicAPI]
public struct TextureLoadParameters
public struct TextureLoadParameters : IEquatable<TextureLoadParameters>
{
/// <summary>
/// The default sampling parameters for the texture.
@@ -41,4 +42,29 @@ public struct TextureLoadParameters
SampleParameters = TextureSampleParameters.Default,
Srgb = true
};
}
public bool Equals(TextureLoadParameters other)
{
return SampleParameters.Equals(other.SampleParameters) && Srgb == other.Srgb;
}
public override bool Equals(object? obj)
{
return obj is TextureLoadParameters other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(SampleParameters, Srgb);
}
public static bool operator ==(TextureLoadParameters left, TextureLoadParameters right)
{
return left.Equals(right);
}
public static bool operator !=(TextureLoadParameters left, TextureLoadParameters right)
{
return !left.Equals(right);
}
}

View File

@@ -12,7 +12,7 @@ namespace Robust.Shared.Graphics;
/// with different sampling parameters than the base texture.
/// </summary>
[PublicAPI]
public struct TextureSampleParameters
public struct TextureSampleParameters : IEquatable<TextureSampleParameters>
{
// NOTE: If somebody is gonna add support for 3D/1D textures, change this doc comment.
// See the note on this page for why: https://www.khronos.org/opengl/wiki/Sampler_Object#Filtering
@@ -62,4 +62,29 @@ public struct TextureSampleParameters
Filter = false,
WrapMode = TextureWrapMode.None
};
}
public bool Equals(TextureSampleParameters other)
{
return Filter == other.Filter && WrapMode == other.WrapMode;
}
public override bool Equals(object? obj)
{
return obj is TextureSampleParameters other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Filter, (int) WrapMode);
}
public static bool operator ==(TextureSampleParameters left, TextureSampleParameters right)
{
return left.Equals(right);
}
public static bool operator !=(TextureSampleParameters left, TextureSampleParameters right)
{
return !left.Equals(right);
}
}

View File

@@ -344,6 +344,9 @@ namespace Robust.Shared.Map
var mapCoordinates = ToMap(entityManager, transformSystem);
var otherMapCoordinates = otherCoordinates.ToMap(entityManager, transformSystem);
if (mapCoordinates.MapId != otherMapCoordinates.MapId)
return false;
return mapCoordinates.InRange(otherMapCoordinates, range);
}

View File

@@ -19,7 +19,7 @@ internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber
[Dependency] private readonly IConsoleHost _conhost = default!;
private ISawmill _sawmill = default!;
private ISawmill _sawmill => _mapSystem.Log;
private SharedMapSystem _mapSystem = default!;
private SharedPhysicsSystem _physics = default!;
@@ -33,9 +33,6 @@ internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber
{
_gridTreeQuery = EntityManager.GetEntityQuery<GridTreeComponent>();
_gridQuery = EntityManager.GetEntityQuery<MapGridComponent>();
_sawmill = Logger.GetSawmill("map");
InitializeMapPausing();
}

View File

@@ -35,7 +35,6 @@ namespace Robust.Shared.Network
public bool IsConnected => _connection.Status == NetConnectionStatus.Connected;
/// <inheritdoc />
[ViewVariables]
public IPEndPoint RemoteEndPoint => _connection.RemoteEndPoint;
/// <summary>
@@ -100,7 +99,7 @@ namespace Robust.Shared.Network
public override string ToString()
{
return $"{RemoteEndPoint}/{UserId}";
return $"{ConnectionId}/{UserId}";
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Text;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Network
@@ -10,11 +11,13 @@ namespace Robust.Shared.Network
{
[ViewVariables]
public NetUserId UserId { get; }
[ViewVariables]
public string UserName { get; }
[ViewVariables]
public string? PatronTier { get; init; }
[ViewVariables]
public ImmutableArray<byte> HWId { get; init; }
public NetUserData(NetUserId userId, string userName)
@@ -22,5 +25,18 @@ namespace Robust.Shared.Network
UserId = userId;
UserName = userName;
}
public sealed override string ToString()
{
var stringBuilder = new StringBuilder();
stringBuilder.Append("NetUserData"); // type name
stringBuilder.Append(" { ");
if ((this with { HWId = default }).PrintMembers(stringBuilder))
{
stringBuilder.Append(' ');
}
stringBuilder.Append('}');
return stringBuilder.ToString();
}
}
}

View File

@@ -181,9 +181,6 @@ namespace Robust.Shared.Physics.Dynamics
{
if (other == null) return false;
// Owner field shouldn't be required, fixtures on other entities shouldn't be getting compared to each other.
// This is mainly here because it might've intruded some physics bugs, so this is here just in case.
DebugTools.Assert(Owner == other.Owner);
return Equivalent(other) && Owner == other.Owner;
}
}

View File

@@ -21,6 +21,7 @@ public abstract partial class SharedJointSystem : EntitySystem
[Dependency] private readonly IGameTiming _gameTiming = default!;
private EntityQuery<JointComponent> _jointsQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<JointRelayTargetComponent> _relayQuery;
private EntityQuery<TransformComponent> _xformQuery;
@@ -37,6 +38,7 @@ public abstract partial class SharedJointSystem : EntitySystem
_jointsQuery = GetEntityQuery<JointComponent>();
_relayQuery = GetEntityQuery<JointRelayTargetComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
UpdatesOutsidePrediction = true;
UpdatesBefore.Add(typeof(SharedPhysicsSystem));
@@ -136,7 +138,7 @@ public abstract partial class SharedJointSystem : EntitySystem
var aUid = joint.BodyAUid;
var bUid = joint.BodyBUid;
if (!Resolve(aUid, ref bodyA, false) || !Resolve(bUid, ref bodyB, false))
if (!_physicsQuery.Resolve(aUid, ref bodyA, false) || !_physicsQuery.Resolve(bUid, ref bodyB, false))
return;
DebugTools.Assert(Transform(aUid).MapID == Transform(bUid).MapID, "Attempted to initialize cross-map joint");
@@ -311,7 +313,7 @@ public abstract partial class SharedJointSystem : EntitySystem
public WeldJoint GetOrCreateWeldJoint(EntityUid bodyA, EntityUid bodyB, string? id = null)
{
if (id != null &&
EntityManager.TryGetComponent(bodyA, out JointComponent? jointComponent) &&
_jointsQuery.TryComp(bodyA, out JointComponent? jointComponent) &&
jointComponent.Joints.TryGetValue(id, out var weldJoint))
{
return (WeldJoint) weldJoint;
@@ -404,17 +406,17 @@ public abstract partial class SharedJointSystem : EntitySystem
protected void AddJoint(Joint joint, PhysicsComponent? bodyA = null, PhysicsComponent? bodyB = null)
{
if (!Resolve(joint.BodyAUid, ref bodyA) || !Resolve(joint.BodyBUid, ref bodyB))
if (!_physicsQuery.Resolve(joint.BodyAUid, ref bodyA) || !_physicsQuery.Resolve(joint.BodyBUid, ref bodyB))
return;
if (!joint.CollideConnected)
FilterContactsForJoint(joint, bodyA, bodyB);
// Maybe make this method AddOrUpdate so we can have an Add one that explicitly throws if present?
var mapidA = EntityManager.GetComponent<TransformComponent>(joint.BodyAUid).MapID;
var mapidA = Transform(joint.BodyAUid).MapID;
if (mapidA == MapId.Nullspace ||
mapidA != EntityManager.GetComponent<TransformComponent>(joint.BodyBUid).MapID)
mapidA != Transform(joint.BodyBUid).MapID)
{
Log.Error($"Tried to add joint to ineligible bodies");
return;
@@ -447,7 +449,8 @@ public abstract partial class SharedJointSystem : EntitySystem
if (!Resolve(uid, ref xform))
return;
Resolve(uid, ref component, ref relay, false);
_jointsQuery.Resolve(uid, ref component, false);
_relayQuery.Resolve(uid, ref relay, false);
if (relay != null)
{
@@ -471,7 +474,7 @@ public abstract partial class SharedJointSystem : EntitySystem
/// </summary>
public void ClearJoints(EntityUid uid, JointComponent? component = null)
{
if (!Resolve(uid, ref component, false))
if (!_jointsQuery.Resolve(uid, ref component, false))
return;
// TODO PERFORMANCE
@@ -497,15 +500,9 @@ public abstract partial class SharedJointSystem : EntitySystem
}
}
[Obsolete("Use the other ClearJoints overload")]
public void ClearJoints(JointComponent joint)
{
ClearJoints(joint.Owner, joint);
}
public void RemoveJoint(EntityUid uid, string id)
{
if (!TryComp<JointComponent>(uid, out var jointComp))
if (!_jointsQuery.TryComp(uid, out var jointComp))
return;
if (!jointComp.Joints.TryGetValue(id, out var joint))
@@ -522,12 +519,12 @@ public abstract partial class SharedJointSystem : EntitySystem
// Originally I logged these but because of prediction the client can just nuke them multiple times in a row
// because each body has its own JointComponent, bleh.
if (!TryComp<JointComponent>(bodyAUid, out var jointComponentA))
if (!_jointsQuery.TryComp(bodyAUid, out var jointComponentA))
{
return;
}
if (!TryComp<JointComponent>(bodyBUid, out var jointComponentB))
if (!_jointsQuery.TryComp(bodyBUid, out var jointComponentB))
{
return;
}
@@ -543,7 +540,7 @@ public abstract partial class SharedJointSystem : EntitySystem
}
// Wake up connected bodies.
if (EntityManager.TryGetComponent<PhysicsComponent>(bodyAUid, out var bodyA) &&
if (_physicsQuery.TryComp(bodyAUid, out var bodyA) &&
MetaData(bodyAUid).EntityLifeStage < EntityLifeStage.Terminating)
{
var uidA = jointComponentA.Relay ?? bodyAUid;
@@ -607,7 +604,7 @@ public abstract partial class SharedJointSystem : EntitySystem
internal void FilterContactsForJoint(Joint joint, PhysicsComponent? bodyA = null, PhysicsComponent? bodyB = null)
{
if (!Resolve(joint.BodyBUid, ref bodyB))
if (!_physicsQuery.Resolve(joint.BodyBUid, ref bodyB))
return;
var node = bodyB.Contacts.First;

View File

@@ -115,7 +115,7 @@ public partial class SharedPhysicsSystem
public void ApplyAngularImpulse(EntityUid uid, float impulse, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
if (!PhysicsQuery.Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
{
return;
}
@@ -125,7 +125,7 @@ public partial class SharedPhysicsSystem
public void ApplyForce(EntityUid uid, Vector2 force, Vector2 point, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
if (!PhysicsQuery.Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
{
return;
}
@@ -137,7 +137,7 @@ public partial class SharedPhysicsSystem
public void ApplyForce(EntityUid uid, Vector2 force, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
if (!PhysicsQuery.Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
{
return;
}
@@ -148,7 +148,7 @@ public partial class SharedPhysicsSystem
public void ApplyTorque(EntityUid uid, float torque, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
if (!PhysicsQuery.Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
{
return;
}
@@ -159,7 +159,7 @@ public partial class SharedPhysicsSystem
public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
if (!PhysicsQuery.Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
{
return;
}
@@ -169,7 +169,7 @@ public partial class SharedPhysicsSystem
public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, Vector2 point, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
if (!PhysicsQuery.Resolve(uid, ref body) || !IsMoveable(body) || !WakeBody(uid, manager: manager, body: body))
{
return;
}
@@ -250,7 +250,10 @@ public partial class SharedPhysicsSystem
public void ResetMassData(EntityUid uid, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref manager, ref body))
if (!PhysicsQuery.Resolve(uid, ref body))
return;
if (!_fixturesQuery.Resolve(uid, ref manager))
return;
body._mass = 0.0f;
@@ -313,55 +316,60 @@ public partial class SharedPhysicsSystem
Dirty(uid, body);
}
public void SetAngularVelocity(EntityUid uid, float value, bool dirty = true, FixturesComponent? manager = null, PhysicsComponent? body = null)
public bool SetAngularVelocity(EntityUid uid, float value, bool dirty = true, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body))
return;
if (!PhysicsQuery.Resolve(uid, ref body))
return false;
if (body.BodyType == BodyType.Static)
return;
return false;
if (value * value > 0.0f)
{
if (!WakeBody(uid, manager: manager, body: body))
return;
return false;
}
// CloseToPercent tolerance needs to be small enough such that an angular velocity just above
// sleep-tolerance can damp down to sleeping.
if (MathHelper.CloseToPercent(body.AngularVelocity, value, 0.00001f))
return;
return false;
body.AngularVelocity = value;
if (dirty)
Dirty(uid, body);
return true;
}
/// <summary>
/// Attempts to set the body to collidable, wake it, then move it.
/// </summary>
public void SetLinearVelocity(EntityUid uid, Vector2 velocity, bool dirty = true, bool wakeBody = true, FixturesComponent? manager = null, PhysicsComponent? body = null)
public bool SetLinearVelocity(EntityUid uid, Vector2 velocity, bool dirty = true, bool wakeBody = true, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body))
return;
if (!PhysicsQuery.Resolve(uid, ref body))
return false;
if (body.BodyType == BodyType.Static) return;
if (body.BodyType == BodyType.Static)
return false;
if (wakeBody && Vector2.Dot(velocity, velocity) > 0.0f)
{
if (!WakeBody(uid, manager: manager, body: body))
return;
return false;
}
if (body.LinearVelocity.EqualsApprox(velocity, 0.0000001f))
return;
return false;
body.LinearVelocity = velocity;
if (dirty)
Dirty(uid, body);
return true;
}
public void SetAngularDamping(EntityUid uid, PhysicsComponent body, float value, bool dirty = true)
@@ -462,7 +470,7 @@ public partial class SharedPhysicsSystem
public void SetBodyType(EntityUid uid, BodyType value, FixturesComponent? manager = null, PhysicsComponent? body = null, TransformComponent? xform = null)
{
if (!Resolve(uid, ref body))
if (!PhysicsQuery.Resolve(uid, ref body))
return;
if (body.BodyType == value)
@@ -526,7 +534,7 @@ public partial class SharedPhysicsSystem
FixturesComponent? manager = null,
PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body))
if (!PhysicsQuery.Resolve(uid, ref body))
return false;
if (body.CanCollide == value)
@@ -540,7 +548,7 @@ public partial class SharedPhysicsSystem
if (_containerSystem.IsEntityOrParentInContainer(uid))
return false;
if (!Resolve(uid, ref manager) || manager.FixtureCount == 0 && !_mapManager.IsGrid(uid))
if (!_fixturesQuery.Resolve(uid, ref manager) || manager.FixtureCount == 0 && !_mapManager.IsGrid(uid))
return false;
}
else
@@ -570,7 +578,7 @@ public partial class SharedPhysicsSystem
public void SetFixedRotation(EntityUid uid, bool value, bool dirty = true, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body) || body.FixedRotation == value)
if (!PhysicsQuery.Resolve(uid, ref body) || body.FixedRotation == value)
return;
body.FixedRotation = value;
@@ -663,10 +671,13 @@ public partial class SharedPhysicsSystem
/// <returns>true if the body is collidable and awake</returns>
public bool WakeBody(EntityUid uid, bool force = false, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!SetCanCollide(uid, true, manager: manager, body: body, force: force) || !Resolve(uid, ref body))
if (!PhysicsQuery.Resolve(uid, ref body))
return false;
SetAwake(uid, body, true);
if (!SetCanCollide(uid, true, manager: manager, body: body, force: force))
return false;
SetAwake((uid, body), true);
return body.Awake;
}
@@ -710,7 +721,9 @@ public partial class SharedPhysicsSystem
public Box2 GetHardAABB(EntityUid uid, FixturesComponent? manager = null, PhysicsComponent? body = null, TransformComponent? xform = null)
{
if (!Resolve(uid, ref body, ref xform, ref manager))
if (!PhysicsQuery.Resolve(uid, ref body)
|| !_fixturesQuery.Resolve(uid, ref manager)
|| !Resolve(uid, ref xform))
{
return Box2.Empty;
}

View File

@@ -660,13 +660,11 @@ public abstract partial class SharedPhysicsSystem
});
// Update data sequentially
var metaQuery = GetEntityQuery<MetaDataComponent>();
for (var i = 0; i < actualIslands.Length; i++)
{
var island = actualIslands[i];
UpdateBodies(in island, solvedPositions, solvedAngles, linearVelocities, angularVelocities, xformQuery, metaQuery);
UpdateBodies(in island, solvedPositions, solvedAngles, linearVelocities, angularVelocities, xformQuery);
SleepBodies(in island, sleepStatus);
}
@@ -1001,8 +999,7 @@ public abstract partial class SharedPhysicsSystem
float[] angles,
Vector2[] linearVelocities,
float[] angularVelocities,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<MetaDataComponent> metaQuery)
EntityQuery<TransformComponent> xformQuery)
{
foreach (var (joint, error) in island.BrokenJoints)
{
@@ -1035,21 +1032,22 @@ public abstract partial class SharedPhysicsSystem
}
var linVelocity = linearVelocities[offset + i];
var physicsDirtied = false;
if (!float.IsNaN(linVelocity.X) && !float.IsNaN(linVelocity.Y))
{
SetLinearVelocity(uid, linVelocity, false, body: body);
physicsDirtied |= SetLinearVelocity(uid, linVelocity, false, body: body);
}
var angVelocity = angularVelocities[offset + i];
if (!float.IsNaN(angVelocity))
{
SetAngularVelocity(uid, angVelocity, false, body: body);
physicsDirtied |= SetAngularVelocity(uid, angVelocity, false, body: body);
}
// TODO: Should check if the values update.
Dirty(uid, body, metaQuery.GetComponent(uid));
if (physicsDirtied)
Dirty(uid, body);
}
}

View File

@@ -198,7 +198,7 @@ public abstract partial class SharedPhysicsSystem
return (linearVelocity + linearVelocityAngularContribution, angularVelocity);
}
private void HandleParentChangeVelocity(EntityUid uid, PhysicsComponent physics, ref EntParentChangedMessage args, TransformComponent xform)
private void HandleParentChangeVelocity(EntityUid uid, PhysicsComponent physics, EntityUid oldParent, TransformComponent xform)
{
// If parent changed due to state handling, don't modify velocities. The physics comp state will take care of itself..
if (_gameTiming.ApplyingState)
@@ -217,15 +217,13 @@ public abstract partial class SharedPhysicsSystem
// I guess the question becomes, what do you do with conservation of momentum in that case. I guess its the job
// of the teleporter to select a velocity at the after the parent has changed.
var xformQuery = GetEntityQuery<TransformComponent>();
var physicsQuery = GetEntityQuery<PhysicsComponent>();
FixturesComponent? manager = null;
// for the new velocities (that need to be updated), we can just use the existing function:
var (newLinear, newAngular) = GetMapVelocities(uid, physics, xform);
// for the old velocities, we need to re-implement this function while using the old parent and old local position:
if (args.OldParent is not { Valid: true } parent)
if (oldParent == EntityUid.Invalid)
{
// no previous parent --> simple
// Old velocity + (old velocity - new velocity)
@@ -234,7 +232,8 @@ public abstract partial class SharedPhysicsSystem
return;
}
TransformComponent? parentXform = xformQuery.GetComponent(parent);
var parent = oldParent;
TransformComponent? parentXform = _xformQuery.GetComponent(parent);
var localPos = _transform.GetInvWorldMatrix(parentXform).Transform(_transform.GetWorldPosition(xform));
var oldLinear = physics.LinearVelocity;
@@ -243,7 +242,7 @@ public abstract partial class SharedPhysicsSystem
do
{
if (physicsQuery.TryGetComponent(parent, out var body))
if (PhysicsQuery.TryGetComponent(parent, out var body))
{
oldAngular += body.AngularVelocity;
@@ -259,7 +258,7 @@ public abstract partial class SharedPhysicsSystem
localPos = parentXform.LocalPosition + parentXform.LocalRotation.RotateVec(localPos);
parent = parentXform.ParentUid;
} while (parent.IsValid() && xformQuery.TryGetComponent(parent, out parentXform));
} while (parent.IsValid() && _xformQuery.TryGetComponent(parent, out parentXform));
oldLinear += linearAngularContribution;

View File

@@ -90,7 +90,6 @@ namespace Robust.Shared.Physics.Systems
SubscribeLocalEvent<GridAddEvent>(OnGridAdd);
SubscribeLocalEvent<CollisionChangeEvent>(OnCollisionChange);
SubscribeLocalEvent<PhysicsComponent, EntGotRemovedFromContainerMessage>(HandleContainerRemoved);
SubscribeLocalEvent<EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<PhysicsMapComponent, ComponentInit>(HandlePhysicsMapInit);
SubscribeLocalEvent<PhysicsComponent, ComponentInit>(OnPhysicsInit);
SubscribeLocalEvent<PhysicsComponent, ComponentShutdown>(OnPhysicsShutdown);
@@ -150,51 +149,45 @@ namespace Robust.Shared.Physics.Systems
_substeps = (int)Math.Ceiling(targetMinTickrate / serverTickrate);
}
private void OnParentChange(ref EntParentChangedMessage args)
internal void OnParentChange(Entity<TransformComponent, MetaDataComponent> ent, EntityUid oldParent, EntityUid? oldMap)
{
// We do not have a directed/body subscription, because the entity changing parents may not have a physics component, but one of its children might.
var uid = args.Entity;
var xform = args.Transform;
var (uid, xform, meta) = ent;
// If this entity has yet to be initialized, then we can skip this as equivalent code will get run during
// init anyways. HOWEVER: it is possible that one of the children of this entity are already post-init, in
// which case they still need to handle map changes. This frequently happens when clients receives a server
// state where a known/old entity gets attached to a new, previously unknown, entity. The new entity will be
// uninitialized but have an initialized child.
if (xform.ChildCount == 0 && LifeStage(uid) < EntityLifeStage.Initialized)
if (xform.ChildCount == 0 && meta.EntityLifeStage < EntityLifeStage.Initialized)
return;
// Is this entity getting recursively detached after it's parent was already detached to null?
if (args.OldMapId == MapId.Nullspace && xform.MapID == MapId.Nullspace)
if (oldMap == null && xform.MapUid == null)
return;
var body = CompOrNull<PhysicsComponent>(uid);
var body = PhysicsQuery.CompOrNull(uid);
// Handle map changes
if (args.OldMapId != xform.MapID)
if (oldMap != xform.MapUid)
{
// This will also handle broadphase updating & joint clearing.
HandleMapChange(uid, xform, body, args.OldMapId, xform.MapID);
HandleMapChange(uid, xform, body, oldMap, xform.MapUid);
return;
}
if (args.OldMapId != xform.MapID)
return;
if (body != null)
HandleParentChangeVelocity(uid, body, ref args, xform);
HandleParentChangeVelocity(uid, body, oldParent, xform);
}
/// <summary>
/// Recursively add/remove from awake bodies, clear joints, remove from move buffer, and update broadphase.
/// </summary>
private void HandleMapChange(EntityUid uid, TransformComponent xform, PhysicsComponent? body, MapId oldMapId, MapId newMapId)
private void HandleMapChange(EntityUid uid, TransformComponent xform, PhysicsComponent? body, EntityUid? oldMapId, EntityUid? newMapId)
{
var jointQuery = GetEntityQuery<JointComponent>();
PhysMapQuery.TryGetComponent(_mapManager.GetMapEntityId(oldMapId), out var oldMap);
PhysMapQuery.TryGetComponent(_mapManager.GetMapEntityId(newMapId), out var newMap);
RecursiveMapUpdate(uid, xform, body, newMap, oldMap, jointQuery);
PhysMapQuery.TryGetComponent(oldMapId, out var oldMap);
PhysMapQuery.TryGetComponent(newMapId, out var newMap);
RecursiveMapUpdate(uid, xform, body, newMap, oldMap);
}
/// <summary>
@@ -205,8 +198,7 @@ namespace Robust.Shared.Physics.Systems
TransformComponent xform,
PhysicsComponent? body,
PhysicsMapComponent? newMap,
PhysicsMapComponent? oldMap,
EntityQuery<JointComponent> jointQuery)
PhysicsMapComponent? oldMap)
{
DebugTools.Assert(!Deleted(uid));
@@ -223,16 +215,14 @@ namespace Robust.Shared.Physics.Systems
DebugTools.Assert(oldMap?.AwakeBodies.Contains(body) != true);
}
if (jointQuery.TryGetComponent(uid, out var joint))
_joints.ClearJoints(uid, joint);
_joints.ClearJoints(uid);
foreach (var child in xform._children)
{
if (_xformQuery.TryGetComponent(child, out var childXform))
{
PhysicsQuery.TryGetComponent(child, out var childBody);
RecursiveMapUpdate(child, childXform, childBody, newMap, oldMap, jointQuery);
RecursiveMapUpdate(child, childXform, childBody, newMap, oldMap);
}
}
}

View File

@@ -200,12 +200,13 @@ internal abstract partial class SharedPlayerManager
if (EntManager.EnsureComponent<ActorComponent>(uid, out var actor))
{
// component already existed.
DebugTools.AssertNotNull(actor.PlayerSession);
if (!force)
return false;
kicked = actor.PlayerSession;
Detach(kicked);
if (kicked != null)
Detach(kicked);
}
if (_netMan.IsServer)

View File

@@ -6,7 +6,7 @@ namespace Robust.Shared.Prototypes;
/// Prototype that represents game entities.
/// </summary>
[Prototype("entityCategory")]
public sealed class EntityCategoryPrototype : IPrototype
public sealed partial class EntityCategoryPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
@@ -22,4 +22,4 @@ public sealed class EntityCategoryPrototype : IPrototype
/// </summary>
[DataField("description")]
public string? Description { get; private set; }
}
}

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Robust.Shared.Random;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown;
@@ -272,7 +273,8 @@ public interface IPrototypeManager
out Dictionary<Type, HashSet<string>> prototypes);
/// <summary>
/// This method uses reflection to validate that prototype id fields correspond to valid prototypes.
/// This method uses reflection to validate that all static prototype id fields correspond to valid prototypes.
/// This will validate all known to <see cref="IReflectionManager"/>
/// </summary>
/// <remarks>
/// This will validate any field that has either a <see cref="ValidatePrototypeIdAttribute{T}"/> attribute, or a
@@ -280,7 +282,12 @@ public interface IPrototypeManager
/// </remarks>
/// <param name="prototypes">A collection prototypes to use for validation. Any prototype not in this collection
/// will be considered invalid.</param>
List<string> ValidateFields(Dictionary<Type, HashSet<string>> prototypes);
List<string> ValidateStaticFields(Dictionary<Type, HashSet<string>> prototypes);
/// <summary>
/// This is a variant of <see cref="ValidateStaticFields(System.Collections.Generic.Dictionary{System.Type,System.Collections.Generic.HashSet{string}})"/> that only validates a single type.
/// </summary>
List<string> ValidateStaticFields(Type type, Dictionary<Type, HashSet<string>> prototypes);
/// <summary>
/// This method will serialize all loaded prototypes into yaml and then validate them. This can be used to ensure

View File

@@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
using BindingFlags = System.Reflection.BindingFlags;
@@ -13,35 +12,41 @@ namespace Robust.Shared.Prototypes;
public partial class PrototypeManager
{
/// <inheritdoc/>
public List<string> ValidateFields(Dictionary<Type, HashSet<string>> prototypes)
public List<string> ValidateStaticFields(Dictionary<Type, HashSet<string>> prototypes)
{
var errors = new List<string>();
foreach (var type in _reflectionManager.FindAllTypes())
{
// TODO validate public static fields on abstract classes that have no implementations?
if (!type.IsAbstract)
ValidateType(type, errors, prototypes);
ValidateStaticFieldsInternal(type, errors, prototypes);
}
return errors;
}
/// <summary>
/// Validate all fields defined on this type and all base types.
/// </summary>
private void ValidateType(Type type, List<string> errors, Dictionary<Type, HashSet<string>> prototypes)
/// <inheritdoc/>
public List<string> ValidateStaticFields(Type type, Dictionary<Type, HashSet<string>> prototypes)
{
object? instance = null;
Type? baseType = type;
var errors = new List<string>();
ValidateStaticFieldsInternal(type, errors, prototypes);
return errors;
}
var flags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public |
BindingFlags.DeclaredOnly;
/// <summary>
/// Validate all static fields defined on this type and all base types.
/// </summary>
private void ValidateStaticFieldsInternal(Type type, List<string> errors, Dictionary<Type, HashSet<string>> prototypes)
{
var baseType = type;
var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly;
while (baseType != null)
{
foreach (var field in baseType.GetFields(flags))
{
ValidateField(field, type, ref instance, errors, prototypes);
DebugTools.Assert(field.IsStatic);
ValidateStaticField(field, type, errors, prototypes);
}
// We need to get the fields on the base type separately in order to get the private fields
@@ -49,92 +54,110 @@ public partial class PrototypeManager
}
}
private void ValidateField(
private void ValidateStaticField(
FieldInfo field,
Type type,
ref object? instance,
List<string> errors,
Dictionary<Type, HashSet<string>> prototypes)
{
DebugTools.Assert(field.IsStatic);
DebugTools.Assert(!field.HasCustomAttribute<DataFieldAttribute>(), "Datafields should not be static");
// Is this even a prototype id related field?
if (!TryGetFieldPrototype(field, out var proto, out var canBeNull, out var canBeEmpty))
if (!TryGetFieldPrototype(field, out var proto))
return;
if (!TryGetFieldValue(field, type, ref instance, errors, out var value))
return;
var id = value?.ToString();
if (id == null)
if (!prototypes.TryGetValue(proto, out var validIds))
{
if (!canBeNull)
errors.Add($"Prototype id field failed validation. Fields should not be null. Field: {field.Name} in {type.FullName}");
errors.Add($"Prototype id field failed validation. Unknown prototype kind {proto.Name}. Field: {field.Name} in {type.FullName}");
return;
}
if (string.IsNullOrWhiteSpace(id))
if (!TryGetIds(field, proto, out var ids))
{
if (!canBeEmpty)
errors.Add($"Prototype id field failed validation. Non-optional non-nullable data-fields must have a default value. Field: {field.Name} in {type.FullName}");
TryGetIds(field, proto, out _);
DebugTools.Assert($"Failed to get ids, despite resolving the field into a prototype kind?");
return;
}
if (!prototypes.TryGetValue(proto, out var ids))
foreach (var id in ids)
{
errors.Add($"Prototype id field failed validation. Unknown prototype kind. Field: {field.Name} in {type.FullName}");
return;
}
if (!ids.Contains(id))
{
errors.Add($"Prototype id field failed validation. Unknown prototype: {id}. Field: {field.Name} in {type.FullName}");
if (!validIds.Contains(id))
errors.Add($"Prototype id field failed validation. Unknown prototype: {id} of type {proto.Name}. Field: {field.Name} in {type.FullName}");
}
}
/// <summary>
/// Get the value of some field. If this is not a static field, this will create instance of the object in order to
/// validate default field values.
/// Extract prototype ids from a string, IEnumerable{string}, EntProtoId, IEnumerable{EntProtoId}, ProtoId{T}, or IEnumerable{ProtoId{T}} field.
/// </summary>
private bool TryGetFieldValue(FieldInfo field, Type type, ref object? instance, List<string> errors, out object? value)
private bool TryGetIds(FieldInfo field, Type proto, [NotNullWhen(true)] out string[]? ids)
{
value = null;
ids = null;
var value = field.GetValue(null);
if (value == null)
return false;
if (field.IsStatic || instance != null)
if (value is string str)
{
value = field.GetValue(instance);
ids = [str];
return true;
}
var constructor = type.GetConstructor(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
Type.EmptyTypes);
// TODO handle parameterless record constructors.
// Figure out how ISerializationManager does it, or just re-use that code somehow.
// In the meantime, record data fields need an explicit parameterless ctor.
if (constructor == null)
if (value is IEnumerable<string> strEnum)
{
errors.Add($"Prototype id field failed validation. Could not create instance to validate default value. Field: {field.Name} in {type.FullName}");
return false;
ids = strEnum.ToArray();
return true;
}
instance = constructor.Invoke(Array.Empty<object>());
value = field.GetValue(instance);
if (value is EntProtoId protoId)
{
ids = [protoId];
return true;
}
return true;
if (value is IEnumerable<EntProtoId> protoIdEnum)
{
ids = protoIdEnum.Select(x=> x.Id).ToArray();
return true;
}
if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(ProtoId<>))
{
ids = [value.ToString()!];
return true;
}
foreach (var iface in field.FieldType.GetInterfaces())
{
if (!iface.IsGenericType)
continue;
if (iface.GetGenericTypeDefinition() != typeof(IEnumerable<>))
continue;
var enumType = iface.GetGenericArguments().Single();
if (!enumType.IsGenericType)
continue;
if (enumType.GetGenericTypeDefinition() != typeof(ProtoId<>))
continue;
ids = GetIdsMethod.MakeGenericMethod(proto).Invoke(null, [value]) as string[];
return ids != null;
}
return false;
}
private bool TryGetFieldPrototype(
FieldInfo field,
[NotNullWhen(true)] out Type? proto,
out bool canBeNull,
out bool canBeEmpty)
private static MethodInfo GetIdsMethod = typeof(PrototypeManager).GetMethod(nameof(GetIds), BindingFlags.NonPublic | BindingFlags.Static)!;
private static string[] GetIds<T>(IEnumerable<ProtoId<T>> enumerable) where T : class, IPrototype
{
proto = null;
canBeNull = false;
canBeEmpty = false;
return enumerable.Select(x => x.Id).ToArray();
}
private bool TryGetFieldPrototype(FieldInfo field, [NotNullWhen(true)] out Type? proto)
{
// Validate anything with the attribute
var attrib = field.GetCustomAttribute(typeof(ValidatePrototypeIdAttribute<>), false);
if (attrib != null)
{
@@ -142,46 +165,40 @@ public partial class PrototypeManager
return true;
}
if (!field.TryGetCustomAttribute(out DataFieldAttribute? dataField))
return false;
if (TryGetPrototypeFromType(field.FieldType, out proto))
return true;
var fieldType = field.FieldType;
canBeEmpty = dataField.Required;
DebugTools.Assert(!field.IsStatic);
// Resolve nullable structs
if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>))
// Allow validating arrays or lists.
foreach (var iface in field.FieldType.GetInterfaces().Where(x => x.IsGenericType))
{
fieldType = fieldType.GetGenericArguments().Single();
canBeNull = true;
if (iface.GetGenericTypeDefinition() != typeof(IEnumerable<>))
continue;
var enumType = iface.GetGenericArguments().Single();
if (TryGetPrototypeFromType(enumType, out proto))
return true;
}
if (fieldType == typeof(EntProtoId))
proto = null;
return false;
}
private bool TryGetPrototypeFromType(Type type, [NotNullWhen(true)] out Type? proto)
{
if (type == typeof(EntProtoId))
{
proto = typeof(EntityPrototype);
return true;
}
if (fieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(ProtoId<>))
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ProtoId<>))
{
proto = field.FieldType.GetGenericArguments().Single();
proto = type.GetGenericArguments().Single();
DebugTools.Assert(proto != typeof(EntityPrototype), "Use EntProtoId instead of ProtoId<EntityPrototype>");
return true;
}
// As far as I know there is no way to check for the nullability of a string field, so we will assume that null
// values imply that the field itself is properly marked as nullable.
canBeNull = true;
if (dataField.CustomTypeSerializer == null)
return false;
if (!dataField.CustomTypeSerializer.IsGenericType)
return false;
if (dataField.CustomTypeSerializer.GetGenericTypeDefinition() != typeof(PrototypeIdSerializer<>))
return false;
proto = dataField.CustomTypeSerializer.GetGenericArguments().First();
return true;
proto = null;
return false;
}
}

View File

@@ -53,7 +53,7 @@ public partial class PrototypeManager
var mapping = node.ToDataNodeCast<MappingDataNode>();
var id = mapping.Get<ValueDataNode>("id").Value;
var data = new PrototypeValidationData(mapping, resourcePath.ToString());
var data = new PrototypeValidationData(id, mapping, resourcePath.ToString());
mapping.Remove("type");
if (prototypes.GetOrNew(type).TryAdd(id, data))
@@ -65,10 +65,14 @@ public partial class PrototypeManager
}
}
var ctx = new YamlValidationContext();
var errors = new List<ErrorNode>();
foreach (var (type, instances) in prototypes)
{
foreach (var data in instances.Values)
var defaultErrorOccurred = false;
foreach (var (id, data) in instances)
{
errors.Clear();
EnsurePushed(data, instances, type);
if (data.Mapping.TryGet("abstract", out ValueDataNode? abstractNode)
&& bool.Parse(abstractNode.Value))
@@ -76,9 +80,25 @@ public partial class PrototypeManager
continue;
}
var result = _serializationManager.ValidateNode(type, data.Mapping).GetErrors().ToHashSet();
if (result.Count > 0)
dict.GetOrNew(data.File).UnionWith(result);
// Validate yaml directly
errors.AddRange(_serializationManager.ValidateNode(type, data.Mapping).GetErrors());
if (errors.Count > 0)
dict.GetOrNew(data.File).UnionWith(errors);
// Create instance & re-serialize it, to validate the default values of data-fields. We still validate
// the yaml directly just in case reading & writing the fields somehow modifies their values.
try
{
var instance = _serializationManager.Read(type, data.Mapping, ctx);
var mapping = _serializationManager.WriteValue(type, instance, alwaysWrite: true, ctx);
errors.AddRange(_serializationManager.ValidateNode(type, mapping, ctx).GetErrors());
if (errors.Count > 0)
dict.GetOrNew(data.File).UnionWith(errors);
}
catch (Exception ex)
{
errors.Add(new ErrorNode(new ValueDataNode(), $"Caught Exception while validating {type} prototype {id}. Exception: {ex}"));
}
}
}
@@ -152,12 +172,17 @@ public partial class PrototypeManager
private sealed class PrototypeValidationData
{
public readonly string Id;
public MappingDataNode Mapping;
public readonly string File;
public bool Pushed;
public PrototypeValidationData(MappingDataNode mapping, string file)
public string[]? Parents;
public MappingDataNode[]? ParentMappings;
public PrototypeValidationData(string id, MappingDataNode mapping, string file)
{
Id = id;
File = file;
Mapping = mapping;
}
@@ -176,23 +201,22 @@ public partial class PrototypeManager
if (!data.Mapping.TryGet(ParentDataFieldAttribute.Name, out var parentNode))
return;
var parents = _serializationManager.Read<string[]>(parentNode, notNullableOverride: true);
var parentNodes = new MappingDataNode[parents.Length];
DebugTools.AssertNull(data.Parents);
DebugTools.AssertNull(data.ParentMappings);
data.Parents = _serializationManager.Read<string[]>(parentNode, notNullableOverride: true);
data.ParentMappings = new MappingDataNode[data.Parents.Length];
foreach (var parentId in parents)
var i = 0;
foreach (var parentId in data.Parents)
{
var parent = prototypes[parentId];
EnsurePushed(parent, prototypes, type);
for (var i = 0; i < parents.Length; i++)
{
parentNodes[i] = parent.Mapping;
}
data.ParentMappings[i++] = parent.Mapping;
}
data.Mapping = _serializationManager.PushCompositionWithGenericNode(
type,
parentNodes,
data.ParentMappings,
data.Mapping);
}
}

View File

@@ -8,7 +8,7 @@ namespace Robust.Shared.Prototypes;
/// Tile alias prototypes, unlike tile prototypes, are implemented here, as they're really just fed to TileDefinitionManager.
/// </summary>
[Prototype("tileAlias")]
public sealed class TileAliasPrototype : IPrototype
public sealed partial class TileAliasPrototype : IPrototype
{
/// <summary>
/// The target tile ID to alias to.

View File

@@ -0,0 +1,62 @@
using System.Globalization;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Prototypes;
internal sealed class YamlValidationContext : ISerializationContext, ITypeSerializer<EntityUid, ValueDataNode>
{
public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
public bool WritingReadingPrototypes => true;
public YamlValidationContext()
{
SerializerProvider.RegisterSerializer(this);
}
ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(ISerializationManager serializationManager,
ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context)
{
if (node.Value == "null" || node.Value == "invalid")
return new ValidatedValueNode(node);
return new ErrorNode(node, "Prototypes should not contain EntityUids", true);
}
public DataNode Write(ISerializationManager serializationManager, EntityUid value,
IDependencyCollection dependencies, bool alwaysWrite = false,
ISerializationContext? context = null)
{
if (!value.Valid)
return new ValueDataNode("invalid");
return new ValueDataNode(value.Id.ToString(CultureInfo.InvariantCulture));
}
EntityUid ITypeReader<EntityUid, ValueDataNode>.Read(ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context, ISerializationManager.InstantiationDelegate<EntityUid>? _)
{
if (node.Value == "invalid")
return EntityUid.Invalid;
return EntityUid.Parse(node.Value);
}
[MustUseReturnValue]
public EntityUid Copy(ISerializationManager serializationManager, EntityUid source, EntityUid target,
bool skipHook,
ISerializationContext? context = null)
{
return new((int)source);
}
}

View File

@@ -2,6 +2,7 @@
using System.IO;
using System.Text.Json;
using JetBrains.Annotations;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -93,7 +94,17 @@ internal static class RsiLoading
states[stateI] = new StateMetadata(stateName, dirValue, delays);
}
return new RsiMetadata(size, states);
var textureParams = TextureLoadParameters.Default;
if (manifestJson.Load is { } load)
{
textureParams = new TextureLoadParameters
{
SampleParameters = TextureSampleParameters.Default,
Srgb = load.Srgb
};
}
return new RsiMetadata(size, states, textureParams);
}
public static void Warmup()
@@ -103,16 +114,11 @@ internal static class RsiLoading
JsonSerializer.Deserialize<RsiJsonMetadata>(warmupJson, SerializerOptions);
}
internal sealed class RsiMetadata
internal sealed class RsiMetadata(Vector2i size, StateMetadata[] states, TextureLoadParameters loadParameters)
{
public readonly Vector2i Size;
public readonly StateMetadata[] States;
public RsiMetadata(Vector2i size, StateMetadata[] states)
{
Size = size;
States = states;
}
public readonly Vector2i Size = size;
public readonly StateMetadata[] States = states;
public readonly TextureLoadParameters LoadParameters = loadParameters;
}
internal sealed class StateMetadata
@@ -134,10 +140,13 @@ internal static class RsiLoading
// To be directly deserialized.
[UsedImplicitly]
private sealed record RsiJsonMetadata(Vector2i Size, StateJsonMetadata[] States);
private sealed record RsiJsonMetadata(Vector2i Size, StateJsonMetadata[] States, RsiJsonLoad? Load);
[UsedImplicitly]
private sealed record StateJsonMetadata(string Name, int? Directions, float[][]? Delays);
[UsedImplicitly]
private sealed record RsiJsonLoad(bool Srgb = true);
}
[Serializable]

View File

@@ -4,8 +4,9 @@ using Robust.Shared.Prototypes;
namespace Robust.Shared.Serialization.Manager.Attributes;
/// <summary>
/// This attribute should be used on string fields to validate that they correspond to a valid YAML prototype id.
/// If the field needs to be have a default value.
/// This attribute should be used on static string or string collection fields to validate that they correspond to
/// valid YAML prototype ids. This attribute is not required for static <see cref="ProtoId{T}"/> and
/// <see cref="EntProtoId"/> fields, as they automatically get validated.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public sealed class ValidatePrototypeIdAttribute<T> : Attribute where T : IPrototype

View File

@@ -143,7 +143,7 @@ namespace Robust.Shared.Serialization.Manager.Definition
nodeVariable),
call,
dfa.Required
? ExpressionUtils.ThrowExpression<RequiredFieldNotMappedException>(fieldDefinition.FieldType, tagConst)
? ExpressionUtils.ThrowExpression<RequiredFieldNotMappedException>(fieldDefinition.FieldType, tagConst, typeof(T))
: AssignIfNotDefaultExpression(i, targetParam, Expression.Constant(DefaultValues[i], fieldDefinition.FieldType))
)));
}

View File

@@ -4,7 +4,7 @@ namespace Robust.Shared.Serialization.Manager.Exceptions;
public sealed class RequiredFieldNotMappedException : Exception
{
public RequiredFieldNotMappedException(Type type, string field) : base($"Required field {field} of type {type} wasn't mapped.")
public RequiredFieldNotMappedException(Type type, string field, Type dataDef) : base($"Required field {field} of type {type} in {dataDef} wasn't mapped.")
{
}
}

View File

@@ -21,7 +21,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Pro
/// <summary>
/// Checks that a string corresponds to a valid prototype id. Note that any data fields using this serializer will
/// also be validated by <see cref="IPrototypeManager.ValidateFields"/>
/// also be validated by <see cref="IPrototypeManager.ValidateStaticFields"/>
/// </summary>
[Virtual]
public class PrototypeIdSerializer<TPrototype> : ITypeValidator<string, ValueDataNode> where TPrototype : class, IPrototype

View File

@@ -97,6 +97,7 @@ namespace Robust.UnitTesting.Client.UserInterface
control4.OnKeyBindDown += _ => Assert.Fail("Control 4 should not get a mouse event.");
_userInterfaceManager.KeyBindDown(mouseEvent);
_userInterfaceManager.KeyBindUp(mouseEvent);
Assert.Multiple(() =>
{
@@ -124,6 +125,7 @@ namespace Robust.UnitTesting.Client.UserInterface
control2.MouseFilter = Control.MouseFilterMode.Pass;
_userInterfaceManager.KeyBindDown(mouseEvent);
_userInterfaceManager.KeyBindUp(mouseEvent);
Assert.Multiple(() =>
{
@@ -247,6 +249,7 @@ namespace Robust.UnitTesting.Client.UserInterface
pos, true, pos.Position / 1 - control.GlobalPosition, pos.Position - control.GlobalPixelPosition);
_userInterfaceManager.KeyBindDown(mouseEvent);
_userInterfaceManager.KeyBindUp(mouseEvent);
Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.Null);

View File

@@ -12,6 +12,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using IgnoreUIRangeComponent = Robust.Shared.GameObjects.IgnoreUIRangeComponent;
namespace Robust.UnitTesting.Server.Maps
{

View File

@@ -92,7 +92,7 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems
Assert.That(parentXform.MapID, Is.EqualTo(mapId));
Assert.That(childXform.MapID, Is.EqualTo(mapId));
xformSystem.DetachParentToNull(parent, parentXform);
xformSystem.DetachEntity(parent, parentXform);
Assert.That(parentXform.MapID, Is.EqualTo(MapId.Nullspace));
Assert.That(childXform.MapID, Is.EqualTo(MapId.Nullspace));
}

View File

@@ -354,7 +354,7 @@ public sealed class Broadphase_Test
Assert.That(lookup.FindBroadphase(child2), Is.EqualTo(mapBroadphase));
// They should get deparented to the map and updated to the map's broadphase instead.
xformSystem.DetachParentToNull(parent, parentXform);
xformSystem.DetachEntity(parent, parentXform);
Assert.That(lookup.FindBroadphase(parent), Is.EqualTo(null));
Assert.That(lookup.FindBroadphase(child1), Is.EqualTo(null));
Assert.That(lookup.FindBroadphase(child2), Is.EqualTo(null));

View File

@@ -142,7 +142,7 @@ namespace Robust.UnitTesting.Shared.Prototypes
}
[Prototype("circle")]
private sealed class CircleTestPrototype : IPrototype, IInheritingPrototype
private sealed partial class CircleTestPrototype : IPrototype, IInheritingPrototype
{
[IdDataField()]
public string ID { get; private set; } = default!;