Compare commits

...

64 Commits

Author SHA1 Message Date
metalgearsloth
749e547773 Version: 0.8.64 2022-02-28 00:45:53 +11:00
metalgearsloth
af8a010f43 Optimise PVS cangetcompstate (#2560) 2022-02-28 00:45:16 +11:00
metalgearsloth
a8a29e814f Allow content to override occlusion directions (#2528) 2022-02-27 23:15:45 +11:00
metalgearsloth
c3cb5406f6 Fix culling disabled (#2485)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
2022-02-27 22:59:36 +11:00
mirrorcult
73f26d93ca Merge pull request #2565 from ElectroJr/mapping-actions 2022-02-26 20:06:48 -07:00
metalgearsloth
5e86a99060 Version: 0.8.63 2022-02-27 13:00:41 +11:00
Paul Ritter
ce9070b966 changes pvs chunkorder to use a tree-structure, should fix children being sent without parents (#2562)
Co-authored-by: Paul Ritter <ritter.paul1@gmail.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-02-27 13:00:10 +11:00
Vera Aguilera Puerto
aa5bcefaf2 Add System.Threading.Monitor (lock keyword) to sandbox whitelist. 2022-02-26 16:50:41 +01:00
ElectroJr
2df267fc28 Merge remote-tracking branch 'upstream/master' into mapping-actions 2022-02-24 04:13:45 +13:00
ElectroJr
306fcce337 remove todo 2022-02-24 04:05:04 +13:00
ElectroJr
3683b66ef8 Allow SpriteSpecifier to load from entity prototype 2022-02-23 23:37:05 +13:00
Acruid
08a52fb892 MapChunk Cleanup (#2555)
* All IPhysShapes now expose a property to get the local AABB.

* Removed IMapChunk. It's internal, we only have 1 implementation in the engine, no need for abstraction, and removing it helps perf.

* Cleaned up issues in MapChunk file.

* Encapsulate _tiles access inside MapChunk.

* Remove IEnumerable<TileRef> from MapChunk.

* Remove CollidesWithChunk

* Move CalcWorldAABB and RegenerateCollision from MapChunk to MapGrid.
Remove MapChunk.GridId.

* Removed MapChunk.GetAllTiles

* Removed the terrible mocked unit tests.

* Moved the GetTileRef functions from MapChunk to MapGrid.

* Add an event raised on MapChunk when a tile is modified.
Completely remove the IMapGrid dependency from MapChunk.

* Fix bug where you cannot change the tile damage of a tile.
2022-02-21 20:49:30 -08:00
Pieter-Jan Briers
fd3c54b373 Seal some classes. 2022-02-21 14:02:34 +01:00
Pieter-Jan Briers
adee05b009 Make client start with non-seekable streams.
Not marking #2439 as fixed yet due to lack of thorough testing.
2022-02-21 11:23:37 +01:00
Pieter-Jan Briers
a66d40eb19 CVar to force ResourceManager to provide seekability for streams.
To help with #2439
2022-02-21 11:23:37 +01:00
metalgearsloth
d065a96e01 Don't serialize grid fixtures (#2477) 2022-02-21 20:15:30 +11:00
Kara D
ef7709aa78 Version: 0.8.62 2022-02-20 19:34:25 -07:00
Paul Ritter
305b330075 bandaid 2022-02-21 02:30:03 +01:00
Acruid
bec4297ce1 Move Map Pause data from MapManager to MapComponent. (#2543)
* Encapsulated existing _pausedMaps access in MapManager.Pause.
Added more unit tests.

* Moved the MapPaused flag from MapManager.Pause to MapComponent.

* Moved the MapPreInit flag from MapManager.Pause to MapComponent.

* Changed visibility so content can't access the flags directly.

* It's not the code that is wrong, it's the tests that are wrong!

* Removed completely obsolete bookkeeping event.
2022-02-20 13:36:45 -08:00
metalgearsloth
18b21b3d60 Version: 0.8.61 2022-02-20 17:44:49 +11:00
mirrorcult
9b263417b9 Merge pull request #2541 from metalgearsloth/2022-02-14_1 2022-02-19 12:33:46 -07:00
Acruid
13da0a7925 Adds command buffer to client, and input command injection (#2538)
Adds the command buffer to the console.
adds wait <ticks> command to console.
Adds the `incmd` command to inject input commands directly into the simulation, bypassing the frontend.
Removes client side permission check for debug builds.
2022-02-18 19:27:38 -08:00
Kara D
bb28db2412 Version: 0.8.60 2022-02-18 15:50:15 -07:00
mirrorcult
489d150ae0 Merge pull request #2558 from PaulRitter/2022_02_18_real_pvs_hours 2022-02-18 15:49:20 -07:00
Paul Ritter
077dbaf933 yo this slaps 2022-02-18 20:49:03 +01:00
Paul Ritter
26f83ac7a2 bored trainride 2022-02-17 15:08:55 +01:00
metalgearsloth
822009b429 Version: 0.8.59 2022-02-17 18:05:50 +11:00
Leon Friedrich
e07a4e516c Add random vector functions to IRobustRandom (#2551) 2022-02-17 18:05:12 +11:00
metalgearsloth
9ec38b5538 Optimise PVS recursion (#2524) 2022-02-17 18:03:14 +11:00
metalgearsloth
97da770978 More physics ECS (#2529) 2022-02-17 17:52:27 +11:00
metalgearsloth
3acbc8235d Funny struct enumerator for xform kids Part 2 (#2492) 2022-02-17 17:19:08 +11:00
Sam Weaver
a32359d5d4 Only include shadow casting lights in the "max lights per scene" count (#2446)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2022-02-17 14:24:36 +11:00
Paul
78fd39aee2 could fix a racecondition? maybe? 2022-02-16 23:55:04 +01:00
Kara D
fca5c14c67 Version: 0.8.58 2022-02-16 15:53:10 -07:00
Kara D
f60e3a14ef Fix VS2022 17.1 issue 2022-02-16 15:49:19 -07:00
Leon Friedrich
503a7032f9 Hopefully fix appearance component mispredict reconciliation (#2460) 2022-02-16 23:26:13 +01:00
metalgearsloth
918bbd3f01 Version: 0.8.57 2022-02-17 01:06:34 +11:00
Leon Friedrich
a1441d5051 Properly support sprite layer transforms. (#2533) 2022-02-17 00:06:14 +11:00
Leon Friedrich
58d6189c40 Allow sprite layer data to be set using PrototypeLayerData. (#2442) 2022-02-16 19:37:40 +11:00
Leon Friedrich
58defed1d2 Fix InteractionOutline 2: Electric boogaloo (#2511)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-02-16 18:32:40 +11:00
Paul
27a94384d0 v0.8.56 2022-02-15 18:16:07 +01:00
Paul Ritter
4b8f5815db pvs caching (#2547)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-02-15 18:14:44 +01:00
metalgearsloth
d830eef435 Version: 0.8.55 2022-02-15 21:16:35 +11:00
metalgearsloth
e7c4bf7341 Don't use worldbounds for PVS (#2473) 2022-02-15 21:07:41 +11:00
ike709
162e646404 Fix a weird issue with config (#2546)
Co-authored-by: ike709 <ike709@github.com>
2022-02-15 20:57:32 +11:00
metalgearsloth
93049fcacf More transform optimisations (#2519) 2022-02-15 20:43:49 +11:00
metalgearsloth
5b90db4beb inline 2022-02-14 18:30:33 +11:00
metalgearsloth
7692ff736b Some more ECS stuff
- Proxy methods on EntityManager
- Transform system stuff
2022-02-14 18:24:52 +11:00
Kara D
f25ad8ece9 Version: 0.8.54 2022-02-13 20:25:00 -07:00
mirrorcult
1aaf3b9250 Merge pull request #2521 from ElectroJr/public-AppearanceSystem 2022-02-13 20:24:23 -07:00
mirrorcult
11f6cff6df Merge pull request #2539 from Acruid/vv_fix_entName 2022-02-13 20:01:53 -07:00
mirrorcult
c2ea57c95a Merge pull request #2503 from Ygg01/linguini-multiline-error-fix 2022-02-13 20:00:47 -07:00
Ygg01
f61ae5da6b Update Linguini and line info in errors 2022-02-14 01:50:11 +01:00
Acruid
8ae9a5f2da Fix a typo where setting the description of an entity in VV sets the name. 2022-02-13 16:35:51 -08:00
Ygg01
3f7e89e006 Merge remote-tracking branch 'origin/master' into linguini-multiline-error-fix 2022-02-13 23:35:28 +01:00
Ygg01
cee4e4d62e Enable Fluent logs for only .ftl files (#2532) 2022-02-12 21:59:08 +11:00
metalgearsloth
14a3783760 Version: 0.8.53 2022-02-12 15:56:21 +11:00
mirrorcult
b4607f7b1f Draw effects below FOV (#2534) 2022-02-12 15:54:50 +11:00
Acruid
5a28c16cae Map Pausing Fixes (#2520) 2022-02-12 15:54:03 +11:00
ElectroJr
9e8bf861ea Merge remote-tracking branch 'upstream/master' into public-AppearanceSystem 2022-02-11 20:43:19 +13:00
ElectroJr
4cfb9210d0 Make AppearanceSystem public 2022-02-09 04:55:31 +13:00
Ygg01
681f77a796 Change newline append 2022-02-04 02:01:52 +01:00
Ygg01
2a7f1cbf48 Ensure that tests work regardless of platform 2022-02-03 04:06:53 +01:00
Ygg01
c4e63cfdc7 Fix multiline errors, add some tests 2022-02-03 01:43:45 +01:00
106 changed files with 3760 additions and 1998 deletions

View File

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

View File

@@ -109,13 +109,13 @@ namespace Robust.Client.Console
if (AvailableCommands.ContainsKey(commandName))
{
var playerManager = IoCManager.Resolve<IPlayerManager>();
#if !DEBUG
if (!_conGroup.CanCommand(commandName) && playerManager.LocalPlayer?.Session.Status > SessionStatus.Connecting)
{
WriteError(null, $"Insufficient perms for command: {commandName}");
return;
}
#endif
var command1 = AvailableCommands[commandName];
args.RemoveAt(0);
var shell = new ConsoleShell(this, null);

View File

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

View File

@@ -455,6 +455,7 @@ namespace Robust.Client
private void Tick(FrameEventArgs frameEventArgs)
{
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
_console.CommandBufferExecute();
_timerManager.UpdateTimers(frameEventArgs);
_taskManager.ProcessPendingTasks();
@@ -510,7 +511,7 @@ namespace Robust.Client
logManager.GetSawmill("discord").Level = LogLevel.Warning;
logManager.GetSawmill("net.predict").Level = LogLevel.Info;
logManager.GetSawmill("szr").Level = LogLevel.Info;
logManager.GetSawmill("loc").Level = LogLevel.Error;
logManager.GetSawmill("loc").Level = LogLevel.Warning;
#if DEBUG_ONLY_FCE_INFO
#if DEBUG_ONLY_FCE_LOG

View File

@@ -8,7 +8,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
[ComponentReference(typeof(OccluderComponent))]
internal sealed class ClientOccluderComponent : OccluderComponent
public sealed class ClientOccluderComponent : OccluderComponent
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
@@ -71,13 +71,29 @@ namespace Robust.Client.GameObjects
{
Occluding = OccluderDir.None;
if (Deleted || !_entityManager.GetComponent<TransformComponent>(Owner).Anchored)
if (Deleted)
return;
// Content may want to override the default behavior for occlusion.
var xform = _entityManager.GetComponent<TransformComponent>(Owner);
var ev = new OccluderDirectionsEvent
{
Component = xform,
};
_entityManager.EventBus.RaiseLocalEvent(Owner, ref ev);
if (ev.Handled)
{
Occluding = ev.Directions;
return;
}
var grid = _mapManager.GetGrid(_entityManager.GetComponent<TransformComponent>(Owner).GridID);
var position = _entityManager.GetComponent<TransformComponent>(Owner).Coordinates;
if (!xform.Anchored)
return;
var grid = _mapManager.GetGrid(xform.GridID);
var position = xform.Coordinates;
void CheckDir(Direction dir, OccluderDir oclDir)
{
foreach (var neighbor in grid.GetInDir(position, dir))
@@ -90,7 +106,7 @@ namespace Robust.Client.GameObjects
}
}
var angle = _entityManager.GetComponent<TransformComponent>(Owner).LocalRotation;
var angle = xform.LocalRotation;
var dirRolling = angle.GetCardinalDir();
// dirRolling starts at effective south
@@ -105,15 +121,26 @@ namespace Robust.Client.GameObjects
CheckDir(dirRolling, OccluderDir.East);
}
}
[Flags]
internal enum OccluderDir : byte
{
None = 0,
North = 1,
East = 1 << 1,
South = 1 << 2,
West = 1 << 3,
}
[Flags]
public enum OccluderDir : byte
{
None = 0,
North = 1,
East = 1 << 1,
South = 1 << 2,
West = 1 << 3,
}
/// <summary>
/// Raised by occluders when trying to get occlusion directions.
/// </summary>
[ByRefEvent]
public struct OccluderDirectionsEvent
{
public bool Handled = false;
public OccluderDir Directions = OccluderDir.None;
public TransformComponent Component = default!;
}
}

View File

@@ -119,7 +119,8 @@ namespace Robust.Client.GameObjects
/// This is useful to allow layer map configs to be defined in prototypes,
/// while still allowing code to create configs if they're absent.
/// </remarks>
void LayerMapReserveBlank(object key);
/// <returns>Index of the new layer.</returns>
int LayerMapReserveBlank(object key);
/// <summary>
/// Adds a layer without texture (thus falling back to the error texture).
@@ -145,8 +146,8 @@ namespace Robust.Client.GameObjects
void RemoveLayer(int layer);
void RemoveLayer(object layerKey);
void LayerSetShader(int layer, ShaderInstance shader);
void LayerSetShader(object layerKey, ShaderInstance shader);
void LayerSetShader(int layer, ShaderInstance shader, string? prototype = null);
void LayerSetShader(object layerKey, ShaderInstance shader, string? prototype = null);
void LayerSetShader(int layer, string shaderName);
void LayerSetShader(object layerKey, string shaderName);
@@ -217,8 +218,8 @@ namespace Robust.Client.GameObjects
int GetLayerDirectionCount(ISpriteLayer layer);
/// <summary>
/// Calculate sprite bounding box in world-space coordinates.
/// Calculate the rotated sprite bounding box in world-space coordinates.
/// </summary>
Box2 CalculateBoundingBox(Vector2 worldPos);
Box2Rotated CalculateRotatedBoundingBox(Vector2 worldPosition, Angle worldRotation, IEye? eye = null);
}
}

View File

@@ -25,8 +25,6 @@ namespace Robust.Client.GameObjects
RSI.State.Direction EffectiveDirection(Angle worldRotation);
Vector2 LocalToLayer(Vector2 localPos);
/// <summary>
/// Layer size in pixels.
/// Don't account layer scale or sprite world transform.
@@ -37,6 +35,6 @@ namespace Robust.Client.GameObjects
/// Calculate layer bounding box in sprite local-space coordinates.
/// </summary>
/// <returns>Bounding box in sprite local-space coordinates.</returns>
Box2 CalculateBoundingBox();
Box2 CalculateBoundingBox(Angle worldAngle);
}
}

View File

@@ -84,10 +84,16 @@ namespace Robust.Client.GameObjects
foreach (var sprite in comp.SpriteTree.QueryAabb(localAABB))
{
var worldPos = _entityManager.GetComponent<TransformComponent>(sprite.Owner).WorldPosition;
var bounds = sprite.CalculateBoundingBox(worldPos);
var (worldPos, worldRot) = _entityManager.GetComponent<TransformComponent>(sprite.Owner).GetWorldPositionRotation();
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot);
// Get scaled down bounds used to indicate the "south" of a sprite.
var localBound = bounds.Box;
var smallLocal = localBound.Scale(0.2f).Translated(-new Vector2(0f, localBound.Extents.Y));
var southIndicator = new Box2Rotated(smallLocal, bounds.Rotation, bounds.Origin);
handle.DrawRect(bounds, Color.Red.WithAlpha(0.2f));
handle.DrawRect(bounds.Scale(0.2f).Translated(-new Vector2(0f, bounds.Extents.Y)), Color.Blue.WithAlpha(0.5f));
handle.DrawRect(southIndicator, Color.Blue.WithAlpha(0.5f));
}
}
}

View File

@@ -20,6 +20,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using RSIDirection = Robust.Client.Graphics.RSI.State.Direction;
namespace Robust.Client.GameObjects
{
@@ -31,6 +32,8 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IResourceCache resourceCache = default!;
[Dependency] private readonly IPrototypeManager prototypes = default!;
[Dependency] private readonly IEntityManager entities = default!;
[Dependency] private readonly IReflectionManager reflection = default!;
[Dependency] private readonly IEyeManager eyeManager = default!;
[DataField("visible")]
private bool _visible = true;
@@ -72,7 +75,11 @@ namespace Robust.Client.GameObjects
public Vector2 Scale
{
get => scale;
set => scale = value;
set
{
scale = value;
UpdateLocalMatrix();
}
}
[DataField("rotation")]
@@ -83,7 +90,11 @@ namespace Robust.Client.GameObjects
public Angle Rotation
{
get => rotation;
set => rotation = value;
set
{
rotation = value;
UpdateLocalMatrix();
}
}
[DataField("offset")]
@@ -97,12 +108,18 @@ namespace Robust.Client.GameObjects
public Vector2 Offset
{
get => offset;
set => offset = value;
set
{
offset = value;
UpdateLocalMatrix();
}
}
[DataField("color")]
private Color color = Color.White;
public Matrix3 LocalMatrix = Matrix3.Identity;
[Animatable]
[ViewVariables(VVAccess.ReadWrite)]
public Color Color
@@ -134,113 +151,7 @@ namespace Robust.Client.GameObjects
Layers.Clear();
foreach (var layerDatum in value)
{
var anyTextureAttempted = false;
var layer = new Layer(this);
if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath))
{
var path = TextureRoot / layerDatum.RsiPath;
if (IoCManager.Resolve<IResourceCache>().TryGetResource(path, out RSIResource? resource))
{
layer.RSI = resource.RSI;
}
else
{
Logger.ErrorS(LogCategory, "Unable to load layer RSI '{0}'.", path);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.State))
{
anyTextureAttempted = true;
var theRsi = layer.RSI ?? BaseRSI;
if (theRsi == null)
{
Logger.ErrorS(LogCategory,
"Layer has no RSI to load states from. Cannot use 'state' property. ({0})",
layerDatum.State);
}
else
{
var stateid = new RSI.StateId(layerDatum.State);
layer.State = stateid;
if (theRsi.TryGetState(stateid, out var state))
{
// Always use south because this layer will be cached in the serializer.
layer.AnimationTimeLeft = state.GetDelay(0);
}
else
{
Logger.ErrorS(LogCategory,
$"State '{stateid}' not found in RSI: '{theRsi.Path}'.",
stateid);
}
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.TexturePath))
{
anyTextureAttempted = true;
if (layer.State.IsValid)
{
Logger.ErrorS(LogCategory,
"Cannot specify 'texture' on a layer if it has an RSI state specified."
);
}
else
{
layer.Texture =
IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(TextureRoot / layerDatum.TexturePath);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.Shader))
{
if (IoCManager.Resolve<IPrototypeManager>().TryIndex<ShaderPrototype>(layerDatum.Shader, out var prototype))
{
layer.Shader = prototype.Instance();
}
else
{
Logger.ErrorS(LogCategory,
"Shader prototype '{0}' does not exist.",
layerDatum.Shader);
}
}
layer.Color = layerDatum.Color;
layer.Rotation = layerDatum.Rotation;
layer._offset = layerDatum.Offset;
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = anyTextureAttempted && layerDatum.Visible;
layer.Scale = layerDatum.Scale;
Layers.Add(layer);
if (layerDatum.MapKeys != null)
{
var index = Layers.Count - 1;
foreach (var keyString in layerDatum.MapKeys)
{
object key;
if (IoCManager.Resolve<IReflectionManager>().TryParseEnumReference(keyString, out var @enum))
{
key = @enum;
}
else
{
key = keyString;
}
if (LayerMap.ContainsKey(key))
{
Logger.ErrorS(LogCategory, "Duplicate layer map key definition: {0}", key);
continue;
}
LayerMap.Add(key, index);
}
}
AddLayer(layerDatum);
}
_layerMapShared = true;
@@ -336,11 +247,13 @@ namespace Robust.Client.GameObjects
void ISerializationHooks.AfterDeserialization()
{
IoCManager.InjectDependencies(this);
{
if (!string.IsNullOrWhiteSpace(rsi))
{
var rsiPath = TextureRoot / rsi;
if(IoCManager.Resolve<IResourceCache>().TryGetResource(rsiPath, out RSIResource? resource))
if(resourceCache.TryGetResource(rsiPath, out RSIResource? resource))
{
BaseRSI = resource.RSI;
}
@@ -373,6 +286,8 @@ namespace Robust.Client.GameObjects
LayerMap.Clear();
LayerDatums = layerDatums;
}
UpdateLocalMatrix();
}
/// <summary>
@@ -390,6 +305,7 @@ namespace Robust.Client.GameObjects
offset = other.offset;
rotation = other.rotation;
scale = other.scale;
UpdateLocalMatrix();
drawDepth = other.drawDepth;
_screenLock = other._screenLock;
_overrideDirection = other._overrideDirection;
@@ -415,9 +331,14 @@ namespace Robust.Client.GameObjects
RenderOrder = other.RenderOrder;
}
internal void UpdateLocalMatrix()
{
LocalMatrix = Matrix3.CreateTransform(in offset, in rotation, in scale);
}
public Matrix3 GetLocalMatrix()
{
return Matrix3.CreateTransform(in offset, in rotation, in scale);
return LocalMatrix;
}
/// <inheritdoc />
@@ -462,14 +383,17 @@ namespace Robust.Client.GameObjects
_layerMapShared = false;
}
public void LayerMapReserveBlank(object key)
public int LayerMapReserveBlank(object key)
{
if (LayerMapTryGet(key, out var _))
if (LayerMapTryGet(key, out var index))
{
return;
return index;
}
LayerMapSet(key, AddBlankLayer());
index = AddBlankLayer();
LayerMapSet(key, index);
return index;
}
public int AddBlankLayer(int? newIndex = null)
@@ -478,6 +402,19 @@ namespace Robust.Client.GameObjects
return AddLayer(layer, newIndex);
}
/// <summary>
/// Add a new layer based on some <see cref="PrototypeLayerData"/>.
/// </summary>
public int AddLayer(PrototypeLayerData layerDatum, int? newIndex = null)
{
var layer = new Layer(this);
var index = AddLayer(layer, newIndex);
LayerSetData(index, layerDatum);
return index;
}
public int AddLayer(string texturePath, int? newIndex = null)
{
return AddLayer(new ResourcePath(texturePath), newIndex);
@@ -653,7 +590,144 @@ namespace Robust.Client.GameObjects
RemoveLayer(layer);
}
public void LayerSetShader(int layer, ShaderInstance? shader)
/// <summary>
/// Fills in a layer's values using some <see cref="PrototypeLayerData"/>.
/// </summary>
public void LayerSetData(int index, PrototypeLayerData layerDatum)
{
if (Layers.Count <= index)
{
Logger.ErrorS(LogCategory, "Layer with index '{0}' does not exist, cannot set layer data! Trace:\n{1}",
index, Environment.StackTrace);
return;
}
var layer = Layers[index];
var anyTextureAttempted = false;
if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath))
{
var path = TextureRoot / layerDatum.RsiPath;
if (resourceCache.TryGetResource(path, out RSIResource? resource))
{
layer.RSI = resource.RSI;
}
else
{
Logger.ErrorS(LogCategory, "Unable to load layer RSI '{0}'.", path);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.State))
{
anyTextureAttempted = true;
var theRsi = layer.RSI ?? BaseRSI;
if (theRsi == null)
{
Logger.ErrorS(LogCategory,
"Layer has no RSI to load states from. Cannot use 'state' property. ({0})",
layerDatum.State);
}
else
{
var stateid = new RSI.StateId(layerDatum.State);
layer.State = stateid;
if (theRsi.TryGetState(stateid, out var state))
{
// Always use south because this layer will be cached in the serializer.
layer.AnimationTimeLeft = state.GetDelay(0);
}
else
{
Logger.ErrorS(LogCategory,
$"State '{stateid}' not found in RSI: '{theRsi.Path}'.",
stateid);
}
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.TexturePath))
{
anyTextureAttempted = true;
if (layer.State.IsValid)
{
Logger.ErrorS(LogCategory,
"Cannot specify 'texture' on a layer if it has an RSI state specified."
);
}
else
{
layer.Texture =
resourceCache.GetResource<TextureResource>(TextureRoot / layerDatum.TexturePath);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.Shader))
{
if (prototypes.TryIndex<ShaderPrototype>(layerDatum.Shader, out var prototype))
{
layer.ShaderPrototype = layerDatum.Shader;
layer.Shader = prototype.Instance();
}
else
{
Logger.ErrorS(LogCategory,
"Shader prototype '{0}' does not exist.",
layerDatum.Shader);
}
}
if (layerDatum.MapKeys != null)
{
foreach (var keyString in layerDatum.MapKeys)
{
object key;
if (reflection.TryParseEnumReference(keyString, out var @enum))
{
key = @enum;
}
else
{
key = keyString;
}
if (LayerMap.TryGetValue(key, out var mappedIndex))
{
if (mappedIndex != index)
Logger.ErrorS(LogCategory, "Duplicate layer map key definition: {0}", key);
continue;
}
_layerMapEnsurePrivate();
LayerMap[key] = index;
}
}
layer.Color = layerDatum.Color;
layer._rotation = layerDatum.Rotation;
layer._offset = layerDatum.Offset;
layer._scale = layerDatum.Scale;
layer.UpdateLocalMatrix();
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = anyTextureAttempted && layerDatum.Visible;
}
public void LayerSetData(object layerKey, PrototypeLayerData data)
{
if (!LayerMapTryGet(layerKey, out var layer))
{
Logger.ErrorS(LogCategory, "Layer with key '{0}' does not exist, cannot set shader! Trace:\n{1}",
layerKey, Environment.StackTrace);
return;
}
LayerSetData(layer, data);
}
public void LayerSetShader(int layer, ShaderInstance? shader, string? prototype = null)
{
if (Layers.Count <= layer)
{
@@ -664,9 +738,10 @@ namespace Robust.Client.GameObjects
var theLayer = Layers[layer];
theLayer.Shader = shader;
theLayer.ShaderPrototype = prototype;
}
public void LayerSetShader(object layerKey, ShaderInstance shader)
public void LayerSetShader(object layerKey, ShaderInstance shader, string? prototype = null)
{
if (!LayerMapTryGet(layerKey, out var layer))
{
@@ -675,7 +750,7 @@ namespace Robust.Client.GameObjects
return;
}
LayerSetShader(layer, shader);
LayerSetShader(layer, shader, prototype);
}
public void LayerSetShader(int layer, string shaderName)
@@ -684,10 +759,12 @@ namespace Robust.Client.GameObjects
{
Logger.ErrorS(LogCategory, "Shader prototype '{0}' does not exist. Trace:\n{1}", shaderName,
Environment.StackTrace);
LayerSetShader(layer, null, null);
return;
}
// This will set the shader to null if it does not exist.
LayerSetShader(layer, prototype?.Instance());
LayerSetShader(layer, prototype.Instance(), shaderName);
}
public void LayerSetShader(object layerKey, string shaderName)
@@ -1148,7 +1225,7 @@ namespace Robust.Client.GameObjects
return;
}
Layers[layer].SetOffset(layerOffset);
Layers[layer].Offset = layerOffset;
}
public void LayerSetOffset(object layerKey, Vector2 layerOffset)
@@ -1230,25 +1307,6 @@ namespace Robust.Client.GameObjects
RenderInternal(drawingHandle, eyeRotation, worldRotation, worldPosition, overrideDir);
}
private void CalcModelMatrix(int numDirs, Angle eyeRotation, Angle worldRotation, Vector2 worldPosition, out Matrix3 modelMatrix)
{
Angle angle;
if (_screenLock)
{
// Negate the eye rotation in the model matrix, so that later when the view matrix is applied the
// sprite will be locked upright to the screen
angle = new Angle(-eyeRotation.Theta);
}
else
{
angle = CalcRectWorldAngle(worldRotation + eyeRotation, numDirs) - eyeRotation;
}
var sWorldRotation = angle;
modelMatrix = Matrix3.CreateTransform(in worldPosition, in sWorldRotation);
}
private void RenderInternal(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Vector2 worldPosition, Direction? overrideDirection)
{
// Reduce the angles to fix math shenanigans
@@ -1257,56 +1315,16 @@ namespace Robust.Client.GameObjects
if (worldRotation.Theta < 0)
worldRotation = new Angle(worldRotation.Theta + Math.Tau);
// sprite matrix, WITHOUT offset.
// offset is applied after sprite numDirs snapping/rotation correction
// --> apply at same time as layer offset
var spriteMatrix = Matrix3.CreateTransform(Vector2.Zero, rotation, scale);
// worldRotation + eyeRotation should be the angle of the entity on-screen. If no-rot is enabled this is just set to zero.
// However, at some point later the eye-matrix is applied separately, so we subtract -eye rotation for now:
var entityMatrix = Matrix3.CreateTransform(worldPosition, NoRotation ? -eyeRotation : worldRotation);
Matrix3.Multiply(ref LocalMatrix, ref entityMatrix, out var transform);
var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs
foreach (var layer in Layers)
{
if (!layer.Visible)
{
continue;
}
var numDirs = GetLayerDirectionCount(layer);
var layerRotation = worldRotation + layer.Rotation;
var layerPosition = worldPosition + layerRotation.RotateVec(layer._offset + offset);
CalcModelMatrix(numDirs, eyeRotation, layerRotation, layerPosition, out var modelMatrix);
Matrix3.Multiply(ref spriteMatrix, ref modelMatrix, out var transformMatrix);
drawingHandle.SetTransform(in transformMatrix);
RenderLayer(drawingHandle, layer, eyeRotation, layerRotation, overrideDirection);
}
}
private void RenderLayer(DrawingHandleWorld drawingHandle, Layer layer, Angle eyeRotation, Angle worldRotation, Direction? overrideDirection)
{
var texture = GetRenderTexture(layer, worldRotation + eyeRotation, overrideDirection);
if (layer.Shader != null)
{
drawingHandle.UseShader(layer.Shader);
}
var layerColor = color * layer.Color;
var position = -(Vector2)texture.Size / (2f * EyeManager.PixelsPerMeter);
var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(position, textureSize);
// TODO: Implement layer-specific rotation and scale.
// Apply these directly to the box.
// Oh and when you do update Layer.LocalToLayer so content doesn't break.
// handle.Modulate changes the color
// drawingHandle.SetTransform() is set above, turning the quad into local space vertices
drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
if (layer.Shader != null)
{
drawingHandle.UseShader(null);
layer.Render(drawingHandle, ref transform, angle, overrideDirection);
}
}
@@ -1342,27 +1360,6 @@ namespace Robust.Client.GameObjects
};
}
private Texture GetRenderTexture(Layer layer, Angle worldRotation, Direction? overrideDirection)
{
var texture = layer.Texture;
if (layer.State.IsValid)
{
// Pull texture from RSI state instead.
var rsi = layer.RSI ?? BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
{
state = GetFallbackState(resourceCache);
}
var layerSpecificDir = layer.EffectiveDirection(state, worldRotation, overrideDirection);
texture = state.GetFrame(layerSpecificDir, layer.AnimationFrame);
}
texture ??= resourceCache.GetFallback<TextureResource>().Texture;
return texture;
}
public void FrameUpdate(float delta)
{
foreach (var t in Layers)
@@ -1417,12 +1414,14 @@ namespace Robust.Client.GameObjects
Visible = thestate.Visible;
DrawDepth = thestate.DrawDepth;
Scale = thestate.Scale;
Rotation = thestate.Rotation;
Offset = thestate.Offset;
scale = thestate.Scale;
rotation = thestate.Rotation;
offset = thestate.Offset;
UpdateLocalMatrix();
Color = thestate.Color;
RenderOrder = thestate.RenderOrder;
if (thestate.BaseRsiPath != null && BaseRSI != null)
{
if (resourceCache.TryGetResource<RSIResource>(TextureRoot / thestate.BaseRsiPath, out var res))
@@ -1438,43 +1437,8 @@ namespace Robust.Client.GameObjects
}
}
// Maybe optimize this to NOT full clear.
Layers.Clear();
for (var i = 0; i < thestate.Layers.Count; i++)
{
var netlayer = thestate.Layers[i];
var layer = new Layer(this)
{
// These are easy so do them here.
Scale = netlayer.Scale,
Rotation = netlayer.Rotation,
Visible = netlayer.Visible,
Color = netlayer.Color
};
Layers.Add(layer);
// Using the public API to handle errors.
// Probably slow as crap.
// Who am I kidding, DEFINITELY.
if (netlayer.Shader != null)
{
LayerSetShader(i, netlayer.Shader);
}
if (netlayer.RsiPath != null)
{
LayerSetRSI(i, netlayer.RsiPath);
}
if (netlayer.TexturePath != null)
{
LayerSetTexture(i, netlayer.TexturePath);
}
else if (netlayer.State != null)
{
LayerSetState(i, netlayer.State);
}
}
// Maybe optimize this to NOT fully clear the layers. (see LayerDatums setter function)
LayerDatums = thestate.Layers;
}
private void QueueUpdateIsInert()
@@ -1553,35 +1517,61 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc/>
public Box2 CalculateBoundingBox(Vector2 worldPos)
public Box2Rotated CalculateRotatedBoundingBox(Vector2 worldPosition, Angle worldRotation, IEye? eye = null)
{
// fast check for empty sprites
if (Layers.Count == 0)
return new Box2(worldPos, worldPos);
if (!Visible || Layers.Count == 0)
{
return new Box2Rotated(new Box2(worldPosition, worldPosition), Angle.Zero, worldPosition);
}
// We need to modify world rotation so that it lies between 0 and 2pi.
// This matters for 4 or 8 directional sprites deciding which quadrant (octant?) they lie in.
// the 0->2pi convention is set by the sprite-rendering code that selects the layers.
// See RenderInternal().
worldRotation = worldRotation.Reduced();
if (worldRotation.Theta < 0)
worldRotation = new Angle(worldRotation.Theta + Math.Tau);
eye ??= eyeManager.CurrentEye;
// Need relative angle on screen for determining the sprite rsi direction.
Angle relativeRotation = NoRotation
? Angle.Zero
: worldRotation + eye.Rotation;
// we need to calculate bounding box taking into account all nested layers
// because layers can have offsets, scale or rotation we need to calculate a new BB
// based on lowest bottomLeft and hightest topRight points from all layers
var box = Layers[0].CalculateBoundingBox();
// because layers can have offsets, scale or rotation, we need to calculate a new BB
// based on lowest bottomLeft and highest topRight points from all layers
var box = Layers[0].CalculateBoundingBox(relativeRotation);
for (int i = 1; i < Layers.Count; i++)
{
var layer = Layers[i];
var layerBB = layer.CalculateBoundingBox();
if (!layer.Visible) continue;
var layerBB = layer.CalculateBoundingBox(relativeRotation);
box = box.Union(layerBB);
}
// apply sprite transformations and calculate sprite bounding box
// we can optimize it a bit, if sprite doesn't have rotation
var spriteBox = box.Scale(Scale);
var spriteHasRotation = !Rotation.EqualsApprox(Angle.Zero);
var spriteBB = spriteHasRotation ?
new Box2Rotated(spriteBox, Rotation).CalcBoundingBox() : spriteBox;
// Next, what we do is take the box2 and apply the sprite's transform, and then the entity's transform. We
// could do this via Matrix3.TransformBox, but that only yields bounding boxes. So instead we manually
// transform our box by the combination of these matrices:
// move it all to world transform system (with sprite offset)
var worldBB = spriteBB.Translated(Offset + worldPos);
return worldBB;
if (Scale != Vector2.One)
box = box.Scale(Scale);
var adjustedOffset = NoRotation
? (-eye.Rotation).RotateVec(Offset)
: worldRotation.RotateVec(Offset);
Vector2 position = adjustedOffset + worldPosition;
Angle finalRotation = NoRotation
? Rotation - eye.Rotation
: Rotation + worldRotation;
return new Box2Rotated(box.Translated(position), finalRotation, position);
}
internal void UpdateBounds()
@@ -1615,10 +1605,11 @@ namespace Robust.Client.GameObjects
Flip = 3,
}
public sealed class Layer : ISpriteLayer
public sealed class Layer : ISpriteLayer, ISerializationHooks
{
[ViewVariables] private readonly SpriteComponent _parent;
[ViewVariables] public string? ShaderPrototype;
[ViewVariables] public ShaderInstance? Shader;
[ViewVariables] public Texture? Texture;
@@ -1628,11 +1619,37 @@ namespace Robust.Client.GameObjects
[ViewVariables] public float AnimationTime;
[ViewVariables] public int AnimationFrame;
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Scale { get; set; } = Vector2.One;
public Matrix3 LocalMatrix = Matrix3.Identity;
[ViewVariables(VVAccess.ReadWrite)]
public Angle Rotation { get; set; }
public Vector2 Scale
{
get => _scale;
set
{
if (_scale.EqualsApprox(value)) return;
_scale = value;
UpdateLocalMatrix();
_parent.UpdateBounds();
}
}
internal Vector2 _scale = Vector2.One;
[ViewVariables(VVAccess.ReadWrite)]
public Angle Rotation
{
get => _rotation;
set
{
if (_rotation.EqualsApprox(value)) return;
_rotation = value;
UpdateLocalMatrix();
_parent.UpdateBounds();
}
}
internal Angle _rotation = Angle.Zero;
[ViewVariables(VVAccess.ReadWrite)]
public bool Visible = true;
@@ -1652,6 +1669,7 @@ namespace Robust.Client.GameObjects
if (_offset.EqualsApprox(value)) return;
_offset = value;
UpdateLocalMatrix();
_parent.UpdateBounds();
}
}
@@ -1675,6 +1693,7 @@ namespace Robust.Client.GameObjects
if (toClone.Shader != null)
{
Shader = toClone.Shader.Mutable ? toClone.Shader.Duplicate() : toClone.Shader;
ShaderPrototype = toClone.ShaderPrototype;
}
Texture = toClone.Texture;
RSI = toClone.RSI;
@@ -1682,14 +1701,26 @@ namespace Robust.Client.GameObjects
AnimationTimeLeft = toClone.AnimationTimeLeft;
AnimationTime = toClone.AnimationTime;
AnimationFrame = toClone.AnimationFrame;
Scale = toClone.Scale;
Rotation = toClone.Rotation;
_scale = toClone.Scale;
_rotation = toClone.Rotation;
_offset = toClone.Offset;
UpdateLocalMatrix();
Visible = toClone.Visible;
Color = toClone.Color;
DirOffset = toClone.DirOffset;
AutoAnimated = toClone.AutoAnimated;
}
void ISerializationHooks.AfterDeserialization()
{
UpdateLocalMatrix();
}
internal void UpdateLocalMatrix()
{
LocalMatrix = Matrix3.CreateTransform(in _offset, in _rotation, in _scale);
}
RSI? ISpriteLayer.Rsi { get => RSI; set => SetRsi(value); }
RSI.StateId ISpriteLayer.RsiState { get => State; set => SetState(value); }
Texture? ISpriteLayer.Texture { get => Texture; set => SetTexture(value); }
@@ -1701,7 +1732,7 @@ namespace Robust.Client.GameObjects
Color = Color,
Rotation = Rotation,
Scale = Scale,
//todo Shader = Shader,
Shader = ShaderPrototype,
State = State.Name,
Visible = Visible,
RsiPath = RSI?.Path?.ToString(),
@@ -1730,7 +1761,7 @@ namespace Robust.Client.GameObjects
set => SetAutoAnimated(value);
}
public RSI.State.Direction EffectiveDirection(Angle worldRotation)
public RSIDirection EffectiveDirection(Angle worldRotation)
{
if (State == default)
{
@@ -1751,22 +1782,16 @@ namespace Robust.Client.GameObjects
return default;
}
public Vector2 LocalToLayer(Vector2 localPos)
{
// TODO: scale & rotation for layers is currently unimplemented.
return localPos;
}
public RSI.State.Direction EffectiveDirection(RSI.State state, Angle worldRotation,
public RSIDirection EffectiveDirection(RSI.State state, Angle worldRotation,
Direction? overrideDirection)
{
if (state.Directions == RSI.State.DirectionType.Dir1)
{
return RSI.State.Direction.South;
return RSIDirection.South;
}
else
{
RSI.State.Direction dir;
RSIDirection dir;
if (overrideDirection != null)
{
dir = overrideDirection.Value.Convert(state.Directions);
@@ -1898,11 +1923,6 @@ namespace Robust.Client.GameObjects
_parent.QueueUpdateIsInert();
}
public void SetOffset(Vector2 offset)
{
Offset = offset;
}
/// <inheritdoc/>
public Vector2i PixelSize
{
@@ -1923,10 +1943,121 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc/>
public Box2 CalculateBoundingBox()
public Box2 CalculateBoundingBox(Angle angle)
{
// TODO: scale & rotation for layers is currently unimplemented.
return Box2.CenteredAround(Offset, PixelSize / EyeManager.PixelsPerMeter);
// Other than some special cases for simple layers, this will basically just apply the matrix obtained
// via GetLayerDrawMatrix() to this layer's bounding box.
var rsiState = GetActualState();
var dir = (rsiState == null || rsiState.Directions == RSI.State.DirectionType.Dir1)
? RSIDirection.South
: angle.ToRsiDirection(rsiState.Directions);
// special case for simple layers. The vast majority of layers are like this.
if (_rotation == Angle.Zero)
{
var textureSize = PixelSize / EyeManager.PixelsPerMeter;
// this switch block is basically an explicit version of the `rsiDirectionMatrix` in `GetLayerDrawMatrix()`.
var box = dir switch
{
// No rotation:
RSIDirection.South or RSIDirection.North => Box2.CenteredAround(Offset, textureSize),
// rotate 90 degrees:
RSIDirection.East or RSIDirection.West => Box2.CenteredAround(Offset, (textureSize.Y, textureSize.X)),
// rotated 45 degrees (any 45 degree rotated rectangle has a square bounding box with sides of length (x+y)/sqrt(2) )
_ => Box2.CenteredAround(Offset, Vector2.One * (textureSize.X + textureSize.Y) / MathF.Sqrt(2))
};
return _scale == Vector2.One ? box : box.Scale(_scale);
}
// Welp we have some non-zero _rotation, so lets just apply the generalized layer transform and get the bounding box from where;
GetLayerDrawMatrix(dir, out var layerDrawMatrix);
return layerDrawMatrix.TransformBox(Box2.CentredAroundZero(PixelSize / EyeManager.PixelsPerMeter));
}
internal RSI.State? GetActualState()
{
if (!State.IsValid)
return null;
// Pull texture from RSI state
var rsi = RSI ?? _parent.BaseRSI;
if (rsi == null || !rsi.TryGetState(State, out var rsiState))
{
rsiState = GetFallbackState(_parent.resourceCache);
}
return rsiState;
}
/// <summary>
/// Given the apparent rotation of an entity on screen (world + eye rotation), get layer's matrix for drawing &
/// relevant RSI direction.
/// </summary>
public void GetLayerDrawMatrix(RSIDirection dir, out Matrix3 layerDrawMatrix)
{
if (_parent.NoRotation)
layerDrawMatrix = LocalMatrix;
else
{
var rsiDirectionMatrix = Matrix3.CreateTransform(Vector2.Zero, -dir.Convert().ToAngle());
Matrix3.Multiply(ref rsiDirectionMatrix, ref LocalMatrix, out layerDrawMatrix);
}
}
internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3 spriteMatrix, Angle angle, Direction? overrideDirection)
{
if (!Visible)
return;
var rsiState = GetActualState();
var dir = (rsiState == null || rsiState.Directions == RSI.State.DirectionType.Dir1)
? RSIDirection.South
: angle.ToRsiDirection(rsiState.Directions);
// Set the drawing transform for this layer
GetLayerDrawMatrix(dir, out var layerMatrix);
Matrix3.Multiply(ref layerMatrix, ref 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.
if (overrideDirection != null && rsiState != null)
dir = overrideDirection.Value.Convert(rsiState.Directions);
dir = dir.OffsetRsiDir(DirOffset);
// Get the correct directional texture from the state, and draw it!
var texture = GetRenderTexture(rsiState, dir);
RenderTexture(drawingHandle, texture);
}
private void RenderTexture(DrawingHandleWorld drawingHandle, Texture texture)
{
if (Shader != null)
drawingHandle.UseShader(Shader);
var layerColor = _parent.color * Color;
var position = -(Vector2)texture.Size / (2f * EyeManager.PixelsPerMeter);
var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(position, textureSize);
drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
if (Shader != null)
drawingHandle.UseShader(null);
}
private Texture GetRenderTexture(RSI.State? state, RSIDirection dir)
{
if (state == null)
return Texture ?? _parent.resourceCache.GetFallback<TextureResource>().Texture;
return state.GetFrame(dir, AnimationFrame);
}
}
@@ -2034,6 +2165,7 @@ namespace Robust.Client.GameObjects
return results;
}
[Obsolete("Use SpriteSystem")]
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
var icon = IconComponent.GetPrototypeIcon(prototype, resourceCache);

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
@@ -6,7 +7,7 @@ using Robust.Shared.GameStates;
namespace Robust.Client.GameObjects
{
[UsedImplicitly]
internal sealed class AppearanceSystem : SharedAppearanceSystem
public sealed class AppearanceSystem : SharedAppearanceSystem
{
private readonly Queue<ClientAppearanceComponent> _queuedUpdates = new();
@@ -50,10 +51,33 @@ namespace Robust.Client.GameObjects
if (!stateDiff) return;
component.AppearanceData = actualState.Data;
component.AppearanceData = CloneAppearanceData(actualState.Data);
MarkDirty(component);
}
/// <summary>
/// Take in an appearance data dictionary and attempt to clone it.
/// </summary>
/// <remarks>
/// As some appearance data values are not simple value-type objects, this is not just a shallow clone.
/// </remarks>
private Dictionary<object, object> CloneAppearanceData(Dictionary<object, object> data)
{
Dictionary<object, object> newDict = new(data.Count);
foreach (var (key, value) in data)
{
if (value.GetType().IsValueType)
newDict[key] = value;
else if (value is ICloneable cloneable)
newDict[key] = cloneable.Clone();
else
throw new NotSupportedException("Invalid object in appearance data dictionary. Appearance data must be cloneable");
}
return newDict;
}
public override void MarkDirty(AppearanceComponent component)
{
if (component.AppearanceDirty)
@@ -107,7 +131,7 @@ namespace Robust.Client.GameObjects
[ByRefEvent]
public struct AppearanceChangeEvent
{
public AppearanceComponent Component = default!;
public IReadOnlyDictionary<object, object> AppearanceData = default!;
public AppearanceComponent Component;
public IReadOnlyDictionary<object, object> AppearanceData;
}
}

View File

@@ -168,7 +168,7 @@ namespace Robust.Client.GameObjects
// This container is expecting an entity... but it got parented to some other entity???
// Ah well, the sever should send a new container state that updates expected entities so just ignore it for now.
return;
}
}
RemoveExpectedEntity(message.Entity);
@@ -210,68 +210,101 @@ namespace Robust.Client.GameObjects
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
var pointQuery = EntityManager.GetEntityQuery<PointLightComponent>();
var spriteQuery = EntityManager.GetEntityQuery<SpriteComponent>();
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
foreach (var toUpdate in _updateQueue)
{
if (EntityManager.Deleted(toUpdate))
{
if (Deleted(toUpdate))
continue;
}
UpdateEntityRecursively(toUpdate);
UpdateEntityRecursively(toUpdate, xformQuery, pointQuery, spriteQuery);
}
_updateQueue.Clear();
}
private void UpdateEntityRecursively(EntityUid entity)
private void UpdateEntityRecursively(
EntityUid entity,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<PointLightComponent> pointQuery,
EntityQuery<SpriteComponent> spriteQuery)
{
// TODO: Since we are recursing down,
// we could cache ShowContents data here to speed it up for children.
// Am lazy though.
UpdateEntity(entity);
// Recursively go up parents and containers to see whether both sprites and lights need to be occluded
// Could maybe optimise this more by checking nearest parent that has sprite / light and whether it's container
// occluded but this probably isn't a big perf issue.
var xform = xformQuery.GetComponent(entity);
var parent = xform.ParentUid;
var child = entity;
var spriteOccluded = false;
var lightOccluded = false;
foreach (var child in EntityManager.GetComponent<TransformComponent>(entity).Children)
while (parent.IsValid() && !spriteOccluded && !lightOccluded)
{
UpdateEntityRecursively(child.Owner);
var parentXform = xformQuery.GetComponent(parent);
if (TryComp<ContainerManagerComponent>(parent, out var manager) && manager.TryGetContainer(child, out var container))
{
spriteOccluded = spriteOccluded || !container.ShowContents;
lightOccluded = lightOccluded || container.OccludesLight;
}
child = parent;
parent = parentXform.ParentUid;
}
// Alright so
// This is the CBT bit.
// The issue is we need to go through the children and re-check whether they are or are not contained.
// if they are contained then the occlusion values may need updating for all those children
UpdateEntity(entity, xform, xformQuery, pointQuery, spriteQuery, spriteOccluded, lightOccluded);
}
private void UpdateEntity(EntityUid entity)
private void UpdateEntity(
EntityUid entity,
TransformComponent xform,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<PointLightComponent> pointQuery,
EntityQuery<SpriteComponent> spriteQuery,
bool spriteOccluded,
bool lightOccluded)
{
if (EntityManager.TryGetComponent(entity, out SpriteComponent? sprite))
if (spriteQuery.TryGetComponent(entity, out var sprite))
{
sprite.ContainerOccluded = false;
// We have to recursively scan for containers upwards in case of nested containers.
var tempParent = entity;
while (tempParent.TryGetContainer(out var container))
{
if (!container.ShowContents)
{
sprite.ContainerOccluded = true;
break;
}
tempParent = container.Owner;
}
sprite.ContainerOccluded = spriteOccluded;
}
if (EntityManager.TryGetComponent(entity, out PointLightComponent? light))
if (pointQuery.TryGetComponent(entity, out var light))
{
light.ContainerOccluded = false;
light.ContainerOccluded = lightOccluded;
}
// We have to recursively scan for containers upwards in case of nested containers.
var tempParent = entity;
while (tempParent.TryGetContainer(out var container))
var childEnumerator = xform.ChildEnumerator;
// Try to avoid TryComp if we already know stuff is occluded.
if ((!spriteOccluded || !lightOccluded) && TryComp<ContainerManagerComponent>(entity, out var manager))
{
while (childEnumerator.MoveNext(out var child))
{
if (container.OccludesLight)
// Thank god it's by value and not by ref.
var childSpriteOccluded = spriteOccluded;
var childLightOccluded = lightOccluded;
// We already know either sprite or light is not occluding so need to check container.
if (manager.TryGetContainer(child.Value, out var container))
{
light.ContainerOccluded = true;
break;
childSpriteOccluded = childSpriteOccluded || !container.ShowContents;
childLightOccluded = childLightOccluded || container.OccludesLight;
}
tempParent = container.Owner;
UpdateEntity(child.Value, xformQuery.GetComponent(child.Value), xformQuery, pointQuery, spriteQuery, childSpriteOccluded, childLightOccluded);
}
}
else
{
while (childEnumerator.MoveNext(out var child))
{
UpdateEntity(child.Value, xformQuery.GetComponent(child.Value), xformQuery, pointQuery, spriteQuery, spriteOccluded, lightOccluded);
}
}
}

View File

@@ -323,7 +323,7 @@ namespace Robust.Client.GameObjects
{
private readonly IPlayerManager _playerManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
private readonly ShaderInstance _unshadedShader;
private readonly EffectSystem _owner;

View File

@@ -4,11 +4,15 @@ using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
@@ -21,6 +25,8 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientGameStateManager _stateManager = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly IPlayerCommandStates _cmdStates = new PlayerCommandStates();
@@ -108,6 +114,43 @@ namespace Robust.Client.GameObjects
public override void Initialize()
{
SubscribeLocalEvent<PlayerAttachSysMessage>(OnAttachedEntityChanged);
_conHost.RegisterCommand("incmd",
"Inserts an input command into the simulation",
"incmd <KeyFunction> <d|u KeyState> <wxPos> <wyPos>",
GenerateInputCommand);
}
public override void Shutdown()
{
base.Shutdown();
_conHost.UnregisterCommand("incmd");
}
private void GenerateInputCommand(IConsoleShell shell, string argstr, string[] args)
{
var localPlayer = _playerManager.LocalPlayer;
if(localPlayer is null)
return;
var pent = localPlayer.ControlledEntity;
if(pent is null)
return;
BoundKeyFunction keyFunction = new BoundKeyFunction(args[0]);
BoundKeyState state = args[1] == "u" ? BoundKeyState.Up: BoundKeyState.Down;
var pxform = Transform(pent.Value);
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
var coords = EntityCoordinates.FromMap(EntityManager, pent.Value, new MapCoordinates(wPos, pxform.MapID));
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state,
coords, new ScreenCoordinates(0, 0, default), EntityUid.Invalid);
HandleInputCommand(localPlayer.Session, keyFunction, message);
}
private void OnAttachedEntityChanged(PlayerAttachSysMessage message)

View File

@@ -113,12 +113,18 @@ namespace Robust.Client.GameObjects
private void AnythingMoved(ref MoveEvent args)
{
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
var pointQuery = EntityManager.GetEntityQuery<PointLightComponent>();
var spriteQuery = EntityManager.GetEntityQuery<SpriteComponent>();
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
AnythingMovedSubHandler(args.Sender, xforms);
AnythingMovedSubHandler(args.Sender, xformQuery, pointQuery, spriteQuery);
}
private void AnythingMovedSubHandler(EntityUid uid, EntityQuery<TransformComponent> xforms)
private void AnythingMovedSubHandler(
EntityUid uid,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<PointLightComponent> pointQuery,
EntityQuery<SpriteComponent> spriteQuery)
{
// To avoid doing redundant updates (and we don't need to update a grid's children ever)
if (!_checkedChildren.Add(uid) || EntityManager.HasComponent<RenderingTreeComponent>(uid)) return;
@@ -127,17 +133,19 @@ namespace Robust.Client.GameObjects
// WHATEVER YOU DO, DON'T REPLACE THIS WITH SPAMMING EVENTS UNLESS YOU HAVE A GUARANTEE IT WON'T LAG THE GC.
// (Struct-based events ok though)
// Ironically this was lagging the GC lolz
if (EntityManager.TryGetComponent(uid, out SpriteComponent? sprite))
if (spriteQuery.TryGetComponent(uid, out var sprite))
QueueSpriteUpdate(sprite);
if (EntityManager.TryGetComponent(uid, out PointLightComponent? light))
if (pointQuery.TryGetComponent(uid, out var light))
QueueLightUpdate(light);
if (!xforms.TryGetComponent(uid, out var xform)) return;
if (!xformQuery.TryGetComponent(uid, out var xform)) return;
foreach (var child in xform.ChildEntities)
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
AnythingMovedSubHandler(child, xforms);
AnythingMovedSubHandler(child.Value, xformQuery, pointQuery, spriteQuery);
}
}
@@ -369,13 +377,10 @@ namespace Robust.Client.GameObjects
private Box2 SpriteAabbFunc(in SpriteComponent value)
{
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
var xform = xforms.GetComponent(value.Owner);
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
var bounds = new Box2Rotated(value.CalculateBoundingBox(worldPos), worldRot, worldPos);
var tree = GetRenderTree(value.Owner, xforms);
return tree == null ? bounds.CalcBoundingBox() : xforms.GetComponent(tree.Owner).InvWorldMatrix.TransformBox(bounds);
return SpriteAabbFunc(value, worldPos, worldRot, xforms);
}
private Box2 LightAabbFunc(in PointLightComponent value)
@@ -392,7 +397,7 @@ namespace Robust.Client.GameObjects
private Box2 SpriteAabbFunc(SpriteComponent value, Vector2 worldPos, Angle worldRot, EntityQuery<TransformComponent> xforms)
{
var bounds = new Box2Rotated(value.CalculateBoundingBox(worldPos), worldRot, worldPos);
var bounds = value.CalculateRotatedBoundingBox(worldPos, worldRot);
var tree = GetRenderTree(value.Owner, xforms);
return tree == null ? bounds.CalcBoundingBox() : xforms.GetComponent(tree.Owner).InvWorldMatrix.TransformBox(bounds);

View File

@@ -0,0 +1,84 @@
using System;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed partial class SpriteSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Pure]
public Texture Frame0(SpriteSpecifier specifier)
{
return RsiStateLike(specifier).Default;
}
[Pure]
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
{
switch (specifier)
{
case SpriteSpecifier.Texture tex:
return tex.GetTexture(_resourceCache);
case SpriteSpecifier.Rsi rsi:
return GetState(rsi);
case SpriteSpecifier.EntityPrototype prototypeIcon:
if (!_proto.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
{
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
return SpriteComponent.GetFallbackState(_resourceCache);
}
return SpriteComponent.GetPrototypeIcon(prototype, _resourceCache);
default:
throw new NotSupportedException();
}
}
[Pure]
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
var icon = IconComponent.GetPrototypeIcon(prototype, _resourceCache);
if (icon != null) return icon;
if (!prototype.Components.ContainsKey("Sprite"))
{
return SpriteComponent.GetFallbackState(resourceCache);
}
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? SpriteComponent.GetFallbackState(resourceCache);
Del(dummy);
return result;
}
[Pure]
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
{
if (_resourceCache.TryGetResource<RSIResource>(
SharedSpriteComponent.TextureRoot / rsiSpecifier.RsiPath,
out var theRsi) &&
theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
{
return state;
}
Logger.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
return SpriteComponent.GetFallbackState(_resourceCache);
}
}

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.GameObjects
/// Updates the layer animation for every visible sprite.
/// </summary>
[UsedImplicitly]
public sealed class SpriteSystem : EntitySystem
public sealed partial class SpriteSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly RenderingTreeSystem _treeSystem = default!;

View File

@@ -18,8 +18,8 @@ namespace Robust.Client.Graphics.Clyde
private readonly Dictionary<GridId, Dictionary<Vector2i, MapChunkData>> _mapChunkData =
new();
private int _verticesPerChunk(IMapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * 4;
private int _indicesPerChunk(IMapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
private int _verticesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * 4;
private int _indicesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
private void _drawGrids(Viewport viewport, Box2Rotated worldBounds, IEye eye)
{
@@ -78,7 +78,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
private void _updateChunkMesh(IMapGrid grid, IMapChunk chunk)
private void _updateChunkMesh(IMapGrid grid, MapChunk chunk)
{
var data = _mapChunkData[grid.Index];
@@ -91,25 +91,36 @@ namespace Robust.Client.Graphics.Clyde
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
var i = 0;
foreach (var tile in chunk)
var cSz = grid.ChunkSize;
var cScaled = chunk.Indices * cSz;
for (ushort x = 0; x < cSz; x++)
{
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile.Tile);
if (regionMaybe == null)
for (ushort y = 0; y < cSz; y++)
{
continue;
var tile = chunk.GetTile(x, y);
if (tile.IsEmpty)
continue;
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile.TypeId);
if (regionMaybe == null)
{
continue;
}
var region = regionMaybe.Value;
var gx = x + cScaled.X;
var gy = y + cScaled.Y;
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(gx, gy, region.Left, region.Bottom);
vertexBuffer[vIdx + 1] = new Vertex2D(gx + 1, gy, region.Right, region.Bottom);
vertexBuffer[vIdx + 2] = new Vertex2D(gx + 1, gy + 1, region.Right, region.Top);
vertexBuffer[vIdx + 3] = new Vertex2D(gx, gy + 1, region.Left, region.Top);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort)(i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
i += 1;
}
var region = regionMaybe.Value;
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(tile.X, tile.Y, region.Left, region.Bottom);
vertexBuffer[vIdx + 1] = new Vertex2D(tile.X + 1, tile.Y, region.Right, region.Bottom);
vertexBuffer[vIdx + 2] = new Vertex2D(tile.X + 1, tile.Y + 1, region.Right, region.Top);
vertexBuffer[vIdx + 3] = new Vertex2D(tile.X, tile.Y + 1, region.Left, region.Top);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort) (i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
i += 1;
}
GL.BindVertexArray(datum.VAO);
@@ -122,7 +133,7 @@ namespace Robust.Client.Graphics.Clyde
datum.TileCount = i;
}
private MapChunkData _initChunkBuffers(IMapGrid grid, IMapChunk chunk)
private MapChunkData _initChunkBuffers(IMapGrid grid, MapChunk chunk)
{
var vao = GenVertexArray();
BindVertexArray(vao);
@@ -159,7 +170,7 @@ namespace Robust.Client.Graphics.Clyde
return datum;
}
private bool _isChunkDirty(IMapGrid grid, IMapChunk chunk)
private bool _isChunkDirty(IMapGrid grid, MapChunk chunk)
{
var data = _mapChunkData[grid.Index];
return !data.TryGetValue(chunk.Indices, out var datum) || datum.Dirty;

View File

@@ -24,11 +24,17 @@ namespace Robust.Client.Graphics.Clyde
{
public ClydeDebugLayers DebugLayers { get; set; }
private readonly RefList<(SpriteComponent sprite, Matrix3 worldMatrix, Angle worldRotation, float yWorldPos)>
private readonly RefList<(SpriteComponent sprite, Vector2 worldPos, Angle worldRotation, Box2 spriteScreenBB)>
_drawingSpriteList
=
new();
// TODO allow this scale to be passed with PostShader as variable
/// <summary>
/// Some shaders that enlarge the final sprite, like emission or highlight effects, need to use a slightly larger render target.
/// </summary>
public static float PostShadeScale = 1.25f;
public void Render()
{
CheckTransferringScreenshots();
@@ -217,9 +223,8 @@ namespace Robust.Client.Graphics.Clyde
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldAABB, worldBounds);
var screenSize = viewport.Size;
eye.GetViewMatrix(out var eyeMatrix, eye.Scale);
ProcessSpriteEntities(mapId, eyeMatrix, worldBounds, _drawingSpriteList);
ProcessSpriteEntities(mapId, viewport, eye, worldBounds, _drawingSpriteList);
var worldOverlays = new List<Overlay>();
@@ -276,30 +281,12 @@ namespace Robust.Client.Graphics.Clyde
break;
}
var matrix = entry.worldMatrix;
var worldPosition = new Vector2(matrix.R0C2, matrix.R1C2);
RenderTexture? entityPostRenderTarget = null;
Vector2i roundedPos = default;
if (entry.sprite.PostShader != null)
{
// calculate world bounding box
var spriteBB = entry.sprite.CalculateBoundingBox(worldPosition);
var spriteLB = spriteBB.BottomLeft;
var spriteRT = spriteBB.TopRight;
// finally we can calculate screen bounding in pixels
var screenLB = viewport.WorldToLocal(spriteLB);
var screenRT = viewport.WorldToLocal(spriteRT);
// we need to scale RT a for effects like emission or highlight
// scale can be passed with PostShader as variable in future
var postShadeScale = 1.25f;
var screenSpriteSize = (Vector2i) ((screenRT - screenLB) * postShadeScale).Rounded();
// Rotate the vector by the eye angle, otherwise the bounding box will be incorrect
screenSpriteSize = (Vector2i) eye.Rotation.RotateVec(screenSpriteSize).Rounded();
screenSpriteSize.Y = -screenSpriteSize.Y;
// get the size of the sprite on screen, scaled slightly to allow for shaders that increase the final sprite size.
var screenSpriteSize = (Vector2i) (entry.spriteScreenBB.Size * PostShadeScale).Rounded();
// I'm not 100% sure why it works, but without it post-shader
// can be lower or upper by 1px than original sprite depending on sprite rotation or scale
@@ -322,16 +309,14 @@ namespace Robust.Client.Graphics.Clyde
// Calculate viewport so that the entity thinks it's drawing to the same position,
// which is necessary for light application,
// but it's ACTUALLY drawing into the center of the render target.
var spritePos = spriteBB.Center;
var screenPos = viewport.WorldToLocal(spritePos);
var (roundedX, roundedY) = roundedPos = (Vector2i) screenPos;
var flippedPos = new Vector2i(roundedX, screenSize.Y - roundedY);
roundedPos = (Vector2i) entry.spriteScreenBB.Center;
var flippedPos = new Vector2i(roundedPos.X, screenSize.Y - roundedPos.Y);
flippedPos -= entityPostRenderTarget.Size / 2;
_renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
}
}
entry.sprite.Render(_renderHandle.DrawingHandleWorld, eye.Rotation, in entry.worldRotation, in worldPosition);
entry.sprite.Render(_renderHandle.DrawingHandleWorld, eye.Rotation, in entry.worldRotation, in entry.worldPos);
if (entry.sprite.PostShader != null && entityPostRenderTarget != null)
{
@@ -369,17 +354,26 @@ namespace Robust.Client.Graphics.Clyde
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ProcessSpriteEntities(MapId map, Matrix3 eyeMatrix, Box2Rotated worldBounds,
RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> list)
private void ProcessSpriteEntities(MapId map, Viewport view, IEye eye, Box2Rotated worldBounds,
RefList<(SpriteComponent sprite, Vector2 worldPos, Angle worldRot, Box2 spriteScreenBB)> list)
{
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
// Construct a matrix equivalent for Viewport.WorldToLocal()
eye.GetViewMatrix(out var viewMatrix, view.RenderScale);
var uiProjmatrix = Matrix3.Identity;
uiProjmatrix.R0C0 = EyeManager.PixelsPerMeter;
uiProjmatrix.R1C1 = -EyeManager.PixelsPerMeter;
uiProjmatrix.R0C2 = view.Size.X / 2f;
uiProjmatrix.R1C2 = view.Size.Y / 2f;
var worldToLocal = viewMatrix * uiProjmatrix;
foreach (var comp in _entitySystemManager.GetEntitySystem<RenderingTreeSystem>().GetRenderTrees(map, worldBounds))
{
var bounds = xforms.GetComponent(comp.Owner).InvWorldMatrix.TransformBox(worldBounds);
comp.SpriteTree.QueryAabb(ref list, (
ref RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> state,
ref RefList<(SpriteComponent sprite, Vector2 worldPos, Angle worldRot, Box2 spriteScreenBB)> state,
in SpriteComponent value) =>
{
var entity = value.Owner;
@@ -387,12 +381,10 @@ namespace Robust.Client.Graphics.Clyde
ref var entry = ref state.AllocAdd();
entry.sprite = value;
Vector2 worldPos;
(worldPos, entry.worldRot, entry.matrix) = transform.GetWorldPositionRotationMatrix();
var eyePos = eyeMatrix.Transform(worldPos);
// Didn't use the bounds from the query as that has to be re-calculated (and is probably more expensive than this).
var bounds = value.CalculateBoundingBox(eyePos);
entry.yWorldPos = eyePos.Y - bounds.Extents.Y;
(entry.worldPos, entry.worldRot) = transform.GetWorldPositionRotation();
var spriteWorldBB = value.CalculateRotatedBoundingBox(entry.worldPos, entry.worldRot, eye);
entry.spriteScreenBB = worldToLocal.TransformBox(spriteWorldBB);
return true;
}, bounds, true);

View File

@@ -514,14 +514,14 @@ namespace Robust.Client.Graphics.Clyde
var enlargedBounds = worldAABB.Enlarged(renderingTreeSystem.MaxLightRadius);
// Use worldbounds for this one as we only care if the light intersects our actual bounds
var state = (this, worldAABB, count: 0);
var state = (this, worldAABB, count: 0, shadowCastingCount: 0);
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var comp in renderingTreeSystem.GetRenderTrees(map, enlargedBounds))
{
var bounds = xforms.GetComponent(comp.Owner).InvWorldMatrix.TransformBox(worldBounds);
comp.LightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldAABB, int count) state, in PointLightComponent light) =>
comp.LightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldAABB, int count, int shadowCastingCount) state, in PointLightComponent light) =>
{
if (state.count >= LightsToRenderListSize)
{
@@ -543,6 +543,9 @@ namespace Robust.Client.Graphics.Clyde
return true;
}
// If the light is a shadow casting light, keep a separate track of that
if (light.CastShadows) state.shadowCastingCount++;
float distanceSquared = (state.worldAABB.Center - lightPos).LengthSquared;
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
@@ -550,17 +553,29 @@ namespace Robust.Client.Graphics.Clyde
}, bounds);
}
if (state.count > _maxLightsPerScene)
if (state.shadowCastingCount > _maxLightsPerScene)
{
// There are too many lights to fit in the scene.
// There are too many lights casting shadows to fit in the scene.
// This check must occur before occluder expansion, or else bad things happen.
// Sort lights by distance.
// First, partition the array based on whether the lights are shadow casting or not
// (non shadow casting lights should be the first partition, shadow casting lights the second)
Array.Sort(_lightsToRenderList, 0, state.count, Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
if (x.light.CastShadows && !y.light.CastShadows) return 1;
else if (!x.light.CastShadows && y.light.CastShadows) return -1;
else return 0;
}));
// Next, sort just the shadow casting lights by distance.
Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount, Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
return x.distanceSquared.CompareTo(y.distanceSquared);
}));
// Then effectively delete the furthest lights.
state.count = _maxLightsPerScene;
// Then effectively delete the furthest lights, by setting the end of the array to exclude N
// number of shadow casting lights (where N is the number above the max number per scene.)
state.count -= state.shadowCastingCount - _maxLightsPerScene;
}
// When culling occluders later, we can't just remove any occluders outside the worldBounds.

View File

@@ -994,9 +994,9 @@ namespace Robust.Client.Graphics.Clyde
private sealed class SpriteDrawingOrderComparer : IComparer<int>
{
private readonly RefList<(SpriteComponent, Matrix3, Angle, float)> _drawList;
private readonly RefList<(SpriteComponent, Vector2, Angle, Box2)> _drawList;
public SpriteDrawingOrderComparer(RefList<(SpriteComponent, Matrix3, Angle, float)> drawList)
public SpriteDrawingOrderComparer(RefList<(SpriteComponent, Vector2, Angle, Box2)> drawList)
{
_drawList = drawList;
}
@@ -1019,7 +1019,8 @@ namespace Robust.Client.Graphics.Clyde
return cmp;
}
cmp = _drawList[y].Item4.CompareTo(_drawList[x].Item4);
// compare the top of the sprite's BB for y-sorting. Because screen coordinates are flipped, the "top" of the BB is actually the "bottom".
cmp = _drawList[x].Item4.Top.CompareTo(_drawList[y].Item4.Top);
if (cmp != 0)
{

View File

@@ -24,9 +24,16 @@ namespace Robust.Client.Map
private readonly Dictionary<ushort, Box2> _tileRegions = new();
/// <inheritdoc />
public Box2? TileAtlasRegion(Tile tile)
{
if (_tileRegions.TryGetValue(tile.TypeId, out var region))
return TileAtlasRegion(tile.TypeId);
}
/// <inheritdoc />
public Box2? TileAtlasRegion(ushort tileType)
{
if (_tileRegions.TryGetValue(tileType, out var region))
{
return region;
}

View File

@@ -19,5 +19,11 @@ namespace Robust.Client.Map
/// </summary>
/// <returns>If null, do not draw the tile at all.</returns>
Box2? TileAtlasRegion(Tile tile);
/// <summary>
/// Gets the region inside the texture atlas to use to draw a tile type.
/// </summary>
/// <returns>If null, do not draw the tile at all.</returns>
Box2? TileAtlasRegion(ushort tileType);
}
}

View File

@@ -230,7 +230,7 @@ namespace Robust.Client.ResourceManagement
using (var manifestFile = cache.ContentFileRead(manifestPath))
{
if (manifestFile.Length <= 4096)
if (manifestFile.CanSeek && manifestFile.Length <= 4096)
{
// Most RSIs are actually tiny so if that's the case just load them into a stackalloc buffer.
// Avoids a ton of allocations with stream reader etc

View File

@@ -22,6 +22,7 @@ namespace Robust.Client.Utility
.Texture;
}
[Obsolete("Use SpriteSystem")]
public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourceCache cache)
{
if (cache.TryGetResource<RSIResource>(
@@ -36,6 +37,7 @@ namespace Robust.Client.Utility
return SpriteComponent.GetFallbackState(cache);
}
[Obsolete("Use SpriteSystem")]
public static Texture Frame0(this SpriteSpecifier specifier)
{
return specifier.RsiStateLike().Default;
@@ -80,6 +82,7 @@ namespace Robust.Client.Utility
return specifier.RsiStateLike();
}
[Obsolete("Use SpriteSystem")]
public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier)
{
var resC = IoCManager.Resolve<IResourceCache>();

View File

@@ -659,6 +659,7 @@ namespace Robust.Server
using (TickUsage.WithLabels("Timers").NewTimer())
{
_consoleHost.CommandBufferExecute();
timerManager.UpdateTimers(frameEventArgs);
}

View File

@@ -29,14 +29,13 @@ namespace Robust.Server.Console.Commands
var mapId = new MapId(int.Parse(args[0]));
var mapMgr = IoCManager.Resolve<IMapManager>();
var pauseMgr = IoCManager.Resolve<IPauseManager>();
if (!mapMgr.MapExists(mapId))
{
mapMgr.CreateMap(mapId);
if (args.Length >= 2 && args[1] == "false")
{
pauseMgr.AddUninitializedMap(mapId);
mapMgr.AddUninitializedMap(mapId);
}
shell.WriteLine($"Map with ID {mapId} created.");
@@ -318,7 +317,6 @@ namespace Robust.Server.Console.Commands
}
var mapManager = IoCManager.Resolve<IMapManager>();
var pauseManager = IoCManager.Resolve<IPauseManager>();
var arg = args[0];
var mapId = new MapId(int.Parse(arg, CultureInfo.InvariantCulture));
@@ -329,13 +327,13 @@ namespace Robust.Server.Console.Commands
return;
}
if (pauseManager.IsMapInitialized(mapId))
if (mapManager.IsMapInitialized(mapId))
{
shell.WriteError("Map is already initialized!");
return;
}
pauseManager.DoMapInitialize(mapId);
mapManager.DoMapInitialize(mapId);
}
}
@@ -348,15 +346,14 @@ namespace Robust.Server.Console.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mapManager = IoCManager.Resolve<IMapManager>();
var pauseManager = IoCManager.Resolve<IPauseManager>();
var msg = new StringBuilder();
foreach (var mapId in mapManager.GetAllMapIds().OrderBy(id => id.Value))
{
msg.AppendFormat("{0}: init: {1}, paused: {2}, ent: {3}, grids: {4}\n",
mapId, pauseManager.IsMapInitialized(mapId),
pauseManager.IsMapPaused(mapId),
mapId, mapManager.IsMapInitialized(mapId),
mapManager.IsMapPaused(mapId),
string.Join(",", mapManager.GetAllMapGrids(mapId).Select(grid => grid.Index)),
mapManager.GetMapEntityId(mapId));
}

View File

@@ -109,9 +109,8 @@ namespace Robust.Server.Console.Commands
private void SetupPlayer(MapId mapId, IConsoleShell shell, IPlayerSession? player, IMapManager mapManager)
{
if (mapId == MapId.Nullspace) return;
var pauseManager = IoCManager.Resolve<IPauseManager>();
pauseManager.SetMapPaused(mapId, false);
var mapUid = IoCManager.Resolve<IMapManager>().GetMapEntityIdOrThrow(mapId);
mapManager.SetMapPaused(mapId, false);
var mapUid = mapManager.GetMapEntityIdOrThrow(mapId);
IoCManager.Resolve<IEntityManager>().GetComponent<SharedPhysicsMapComponent>(mapUid).Gravity = new Vector2(0, -9.8f);
return;

View File

@@ -3,7 +3,7 @@ using Robust.Shared.GameStates;
namespace Robust.Server.GameObjects;
internal sealed class AppearanceSystem : SharedAppearanceSystem
public sealed class AppearanceSystem : SharedAppearanceSystem
{
public override void Initialize()
{

View File

@@ -5,30 +5,31 @@ namespace Robust.Server.GameStates;
public struct ChunkIndicesEnumerator
{
private Vector2i _topLeft;
private Vector2i _bottomRight;
private Vector2i _bottomLeft;
private Vector2i _topRight;
private int _x;
private int _y;
public ChunkIndicesEnumerator(Box2 viewBox, float chunkSize)
public ChunkIndicesEnumerator(Vector2 viewPos, float range, float chunkSize)
{
_topLeft = (viewBox.TopLeft / chunkSize).Floored();
_bottomRight = (viewBox.BottomRight / chunkSize).Floored();
_bottomLeft = ((viewPos - range) / chunkSize).Floored();
// Also floor this as we get the whole chunk anyway.
_topRight = ((viewPos + range) / chunkSize).Floored();
_x = _topLeft.X;
_y = _bottomRight.Y;
_x = _bottomLeft.X;
_y = _bottomLeft.Y;
}
public bool MoveNext([NotNullWhen(true)] out Vector2i? chunkIndices)
{
if (_y > _topLeft.Y)
if (_y > _topRight.Y)
{
_x++;
_y = _bottomRight.Y;
_y = _bottomLeft.Y;
}
if (_x > _bottomRight.X)
if (_x > _topRight.X)
{
chunkIndices = null;
return false;

View File

@@ -37,7 +37,6 @@ public interface IPVSCollection
public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : IComparable<TIndex>, IEquatable<TIndex>
{
[Shared.IoC.Dependency] private readonly IEntityManager _entityManager = default!;
[Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector2i GetChunkIndices(Vector2 coordinates)
@@ -81,14 +80,14 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
private readonly List<(GameTick tick, TIndex index)> _deletionHistory = new();
/// <summary>
/// An index containing the <see cref="IndexLocation"/>s of all <see cref="TIndex"/>.
/// An index containing the <see cref="IIndexLocation"/>s of all <see cref="TIndex"/>.
/// </summary>
private readonly Dictionary<TIndex, IndexLocation> _indexLocations = new();
private readonly Dictionary<TIndex, IIndexLocation> _indexLocations = new();
/// <summary>
/// Buffer of all locationchanges since the last process call
/// </summary>
private readonly Dictionary<TIndex, IndexLocation> _locationChangeBuffer = new();
private readonly Dictionary<TIndex, IIndexLocation> _locationChangeBuffer = new();
/// <summary>
/// Buffer of all indexremovals since the last process call
/// </summary>
@@ -103,7 +102,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
{
var changedIndices = new HashSet<TIndex>(_locationChangeBuffer.Keys);
var changedChunkLocations = new HashSet<IndexLocation>();
var changedChunkLocations = new HashSet<IIndexLocation>();
foreach (var (index, tick) in _removalBuffer)
{
//changes dont need to be computed if we are removing the index anyways
@@ -154,7 +153,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
public HashSet<TIndex>.Enumerator GetElementsForSession(ICommonSession session) => _localOverrides[session].GetEnumerator();
private void AddIndexInternal(TIndex index, IndexLocation location)
private void AddIndexInternal(TIndex index, IIndexLocation location)
{
switch (location)
{
@@ -186,7 +185,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
_indexLocations.Add(index, location);
}
private IndexLocation? RemoveIndexInternal(TIndex index)
private IIndexLocation? RemoveIndexInternal(TIndex index)
{
// the index might be gone due to disconnects/grid-/map-deletions
if (!_indexLocations.TryGetValue(index, out var location))
@@ -352,14 +351,14 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
var gridId = coordinates.GetGridId(_entityManager);
if (gridId != GridId.Invalid)
{
var gridIndices = GetChunkIndices(_mapManager.GetGrid(gridId).LocalToGrid(coordinates));
var gridIndices = GetChunkIndices(coordinates.Position);
UpdateIndex(index, gridId, gridIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
return;
}
var mapId = coordinates.GetMapId(_entityManager);
var mapIndices = GetChunkIndices(coordinates.ToMapPos(_entityManager));
UpdateIndex(index, mapId, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
var mapCoordinates = coordinates.ToMap(_entityManager);
var mapIndices = GetChunkIndices(coordinates.Position);
UpdateIndex(index, mapCoordinates.MapId, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
}
/// <summary>
@@ -392,7 +391,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
RegisterUpdate(index, new MapChunkLocation(mapId, chunkIndices));
}
private void RegisterUpdate(TIndex index, IndexLocation location)
private void RegisterUpdate(TIndex index, IIndexLocation location)
{
if(_indexLocations.TryGetValue(index, out var oldLocation) && oldLocation == location) return;
@@ -400,14 +399,78 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
}
#endregion
#region IndexLocations
private abstract record IndexLocation;
private record MapChunkLocation(MapId MapId, Vector2i ChunkIndices) : IndexLocation;
private record GridChunkLocation(GridId GridId, Vector2i ChunkIndices) : IndexLocation;
private record GlobalOverride : IndexLocation;
private record LocalOverride(ICommonSession Session) : IndexLocation;
#endregion
}
#region IndexLocations
public interface IIndexLocation {};
public interface IChunkIndexLocation{ };
public struct MapChunkLocation : IIndexLocation, IChunkIndexLocation, IEquatable<MapChunkLocation>
{
public MapChunkLocation(MapId mapId, Vector2i chunkIndices)
{
MapId = mapId;
ChunkIndices = chunkIndices;
}
public MapId MapId { get; init; }
public Vector2i ChunkIndices { get; init; }
public bool Equals(MapChunkLocation other)
{
return MapId.Equals(other.MapId) && ChunkIndices.Equals(other.ChunkIndices);
}
public override bool Equals(object? obj)
{
return obj is MapChunkLocation other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(MapId, ChunkIndices);
}
}
public struct GridChunkLocation : IIndexLocation, IChunkIndexLocation, IEquatable<GridChunkLocation>
{
public GridChunkLocation(GridId gridId, Vector2i chunkIndices)
{
GridId = gridId;
ChunkIndices = chunkIndices;
}
public GridId GridId { get; init; }
public Vector2i ChunkIndices { get; init; }
public bool Equals(GridChunkLocation other)
{
return GridId.Equals(other.GridId) && ChunkIndices.Equals(other.ChunkIndices);
}
public override bool Equals(object? obj)
{
return obj is GridChunkLocation other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(GridId, ChunkIndices);
}
}
public struct GlobalOverride : IIndexLocation { }
public struct LocalOverride : IIndexLocation
{
public LocalOverride(ICommonSession session)
{
Session = session;
}
public ICommonSession Session { get; init; }
}
#endregion

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Composition;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.ObjectPool;
using NetSerializer;
@@ -23,7 +25,9 @@ internal sealed partial class PVSSystem : EntitySystem
[Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
[Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
[Shared.IoC.Dependency] private readonly IConfigurationManager _configManager = default!;
[Shared.IoC.Dependency] private readonly IServerEntityManager _serverEntManager = default!;
[Shared.IoC.Dependency] private readonly IServerGameStateManager _stateManager = default!;
[Shared.IoC.Dependency] private readonly SharedTransformSystem _transform = default!;
public const float ChunkSize = 8;
@@ -35,7 +39,7 @@ internal sealed partial class PVSSystem : EntitySystem
/// <summary>
/// Is view culling enabled, or will we send the whole map?
/// </summary>
private bool _cullingEnabled;
public bool CullingEnabled { get; private set; }
/// <summary>
/// How many new entities we can send per tick (dont wanna nuke the clients mailbox).
@@ -52,21 +56,41 @@ internal sealed partial class PVSSystem : EntitySystem
/// </summary>
private float _viewSize;
/// <summary>
/// If PVS disabled then we'll track if we've dumped all entities on the player.
/// This way any future ticks can be orders of magnitude faster as we only send what changes.
/// </summary>
public HashSet<ICommonSession> SeenAllEnts = new();
/// <summary>
/// All <see cref="Robust.Shared.GameObjects.EntityUid"/>s a <see cref="ICommonSession"/> saw last iteration.
/// </summary>
private readonly Dictionary<ICommonSession, Dictionary<EntityUid, PVSEntityVisiblity>> _playerVisibleSets = new();
/// <summary>
/// All <see cref="Robust.Shared.GameObjects.EntityUid"/>s a <see cref="ICommonSession"/> saw along its entire connection.
/// </summary>
private readonly Dictionary<ICommonSession, HashSet<EntityUid>> _playerSeenSets = new();
private PVSCollection<EntityUid> _entityPvsCollection = default!;
public PVSCollection<EntityUid> EntityPVSCollection => _entityPvsCollection;
private readonly List<IPVSCollection> _pvsCollections = new();
private readonly ObjectPool<Dictionary<EntityUid, PVSEntityVisiblity>> _visSetPool =
new DefaultObjectPool<Dictionary<EntityUid, PVSEntityVisiblity>>(
new DefaultPooledObjectPolicy<Dictionary<EntityUid, PVSEntityVisiblity>>(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<EntityUid>> _viewerEntsPool
= new DefaultObjectPool<HashSet<EntityUid>>(new DefaultPooledObjectPolicy<HashSet<EntityUid>>(), MaxVisPoolSize);
private readonly ObjectPool<Dictionary<EntityUid, PVSEntityVisiblity>> _visSetPool
= new DefaultObjectPool<Dictionary<EntityUid, PVSEntityVisiblity>>(
new DictPolicy<EntityUid, PVSEntityVisiblity>(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<EntityUid>> _uidSetPool
= new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), MaxVisPoolSize);
private readonly ObjectPool<Dictionary<EntityUid, MetaDataComponent>> _chunkCachePool =
new DefaultObjectPool<Dictionary<EntityUid, MetaDataComponent>>(
new DictPolicy<EntityUid, MetaDataComponent>(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<int>> _playerChunkPool =
new DefaultObjectPool<HashSet<int>>(new SetPolicy<int>(), MaxVisPoolSize);
private readonly ObjectPool<RobustTree<EntityUid>> _treePool =
new DefaultObjectPool<RobustTree<EntityUid>>(new TreePolicy<EntityUid>());
public override void Initialize()
{
@@ -79,7 +103,7 @@ internal sealed partial class PVSSystem : EntitySystem
_mapManager.OnGridRemoved += OnGridRemoved;
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<MoveEvent>(OnEntityMove);
SubscribeLocalEvent<TransformComponent, ComponentInit>(OnTransformInit);
SubscribeLocalEvent<TransformComponent, ComponentStartup>(OnTransformStartup);
EntityManager.EntityDeleted += OnEntityDeleted;
_configManager.OnValueChanged(CVars.NetPVS, SetPvs, true);
@@ -121,7 +145,7 @@ internal sealed partial class PVSSystem : EntitySystem
private void SetPvs(bool value)
{
_cullingEnabled = value;
CullingEnabled = value;
}
private void OnNewEntityBudgetChanged(int obj)
@@ -139,7 +163,21 @@ internal sealed partial class PVSSystem : EntitySystem
public void Cleanup(IEnumerable<IPlayerSession> sessions)
{
CleanupDirty(sessions);
var playerSessions = sessions.ToArray();
if (!CullingEnabled)
{
foreach (var player in playerSessions)
{
SeenAllEnts.Add(player);
}
}
else
{
SeenAllEnts.Clear();
}
CleanupDirty(playerSessions);
}
public void CullDeletionHistory(GameTick oldestAck)
@@ -170,29 +208,36 @@ internal sealed partial class PVSSystem : EntitySystem
private void OnEntityMove(ref MoveEvent ev)
{
UpdateEntityRecursive(ev.Sender, ev.Component);
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var coordinates = _transform.GetMoverCoordinates(ev.Component);
UpdateEntityRecursive(ev.Sender, ev.Component, coordinates, xformQuery, false);
}
private void OnTransformInit(EntityUid uid, TransformComponent component, ComponentInit args)
private void OnTransformStartup(EntityUid uid, TransformComponent component, ComponentStartup args)
{
UpdateEntityRecursive(uid, component);
// use Startup because GridId is not set during the eventbus init yet!
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var coordinates = _transform.GetMoverCoordinates(component);
UpdateEntityRecursive(uid, component, coordinates, xformQuery, false);
}
private void UpdateEntityRecursive(EntityUid uid, TransformComponent? transformComponent = null)
private void UpdateEntityRecursive(EntityUid uid, TransformComponent xform, EntityCoordinates coordinates, EntityQuery<TransformComponent> xformQuery, bool mover)
{
if(!Resolve(uid, ref transformComponent))
return;
if (mover && !xform.LocalPosition.Equals(Vector2.Zero))
{
coordinates = _transform.GetMoverCoordinates(xform);
}
_entityPvsCollection.UpdateIndex(uid, transformComponent.Coordinates);
_entityPvsCollection.UpdateIndex(uid, coordinates);
// since elements are cached grid-/map-relative, we dont need to update a given grids/maps children
if(_mapManager.IsGrid(uid) || _mapManager.IsMap(uid)) return;
var children = transformComponent.ChildEnumerator;
var children = xform.ChildEnumerator;
while (children.MoveNext(out var child))
{
UpdateEntityRecursive(child.Value);
UpdateEntityRecursive(child.Value, xformQuery.GetComponent(child.Value), coordinates, xformQuery, true);
}
}
@@ -209,8 +254,9 @@ internal sealed partial class PVSSystem : EntitySystem
}
else if (e.NewStatus == SessionStatus.Disconnected)
{
_visSetPool.Return(_playerVisibleSets[e.Session]);
var playerVisSet = _playerVisibleSets[e.Session];
_playerVisibleSets.Remove(e.Session);
_visSetPool.Return(playerVisSet);
_playerSeenSets.Remove(e.Session);
foreach (var pvsCollection in _pvsCollections)
{
@@ -260,114 +306,219 @@ internal sealed partial class PVSSystem : EntitySystem
#endregion
public (List<EntityState>? updates, List<EntityUid>? deletions) CalculateEntityStates(ICommonSession session,
GameTick fromTick, GameTick toTick)
public (List<(uint, IChunkIndexLocation)> , HashSet<int>[], EntityUid[][] viewers) GetChunks(IPlayerSession[] sessions)
{
DebugTools.Assert(session.Status == SessionStatus.InGame);
var newEntitiesSent = 0;
var entitiesSent = 0;
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
if (!_cullingEnabled)
{
var allStates = GetAllEntityStates(session, fromTick, toTick);
return (allStates, deletions);
}
var playerVisibleSet = _playerVisibleSets[session];
var visibleEnts = _visSetPool.Get();
var seenSet = _playerSeenSets[session];
visibleEnts.Clear();
var chunkList = new List<(uint, IChunkIndexLocation)>();
var playerChunks = new HashSet<int>[sessions.Length];
var eyeQuery = EntityManager.GetEntityQuery<EyeComponent>();
var transformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var metadataQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
var viewerEntities = new EntityUid[sessions.Length][];
var globalOverridesEnumerator = _entityPvsCollection.GlobalOverridesEnumerator;
while(globalOverridesEnumerator.MoveNext())
// Keep track of the index of each chunk we use for a faster index lookup.
var mapIndices = new Dictionary<uint, Dictionary<MapChunkLocation, int>>(4);
var gridIndices = new Dictionary<uint, Dictionary<GridChunkLocation, int>>(4);
for (int i = 0; i < sessions.Length; i++)
{
var uid = globalOverridesEnumerator.Current;
//todo paul reenable budgetcheck here once you fix mapmanager
TryAddToVisibleEnts(
in uid,
seenSet,
playerVisibleSet,
visibleEnts,
fromTick,
ref newEntitiesSent,
ref entitiesSent,
metadataQuery,
transformQuery,
dontSkip: true);
}
globalOverridesEnumerator.Dispose();
var session = sessions[i];
playerChunks[i] = _playerChunkPool.Get();
var localOverridesEnumerator = _entityPvsCollection.GetElementsForSession(session);
while (localOverridesEnumerator.MoveNext())
{
var uid = localOverridesEnumerator.Current;
//todo paul reenable budgetcheck here once you fix mapmanager
TryAddToVisibleEnts(in uid, seenSet, playerVisibleSet, visibleEnts, fromTick, ref newEntitiesSent, ref entitiesSent, metadataQuery, transformQuery, dontSkip: true);
}
localOverridesEnumerator.Dispose();
var viewers = GetSessionViewers(session);
viewerEntities[i] = new EntityUid[viewers.Count];
viewers.CopyTo(viewerEntities[i]);
var expandEvent = new ExpandPvsEvent((IPlayerSession) session, new List<EntityUid>());
RaiseLocalEvent(ref expandEvent);
foreach (var entityUid in expandEvent.Entities)
{
TryAddToVisibleEnts(in entityUid, seenSet, playerVisibleSet, visibleEnts, fromTick, ref newEntitiesSent, ref entitiesSent, metadataQuery, transformQuery);
}
var viewers = GetSessionViewers(session);
foreach (var eyeEuid in viewers)
{
var (viewBox, mapId) = CalcViewBounds(in eyeEuid, transformQuery);
uint visMask = EyeComponent.DefaultVisibilityMask;
if (eyeQuery.TryGetComponent(eyeEuid, out var eyeComp))
visMask = eyeComp.VisibilityMask;
//todo at some point just register the viewerentities as localoverrides
TryAddToVisibleEnts(in eyeEuid, seenSet, playerVisibleSet, visibleEnts, fromTick, ref newEntitiesSent, ref entitiesSent, metadataQuery, transformQuery, visMask, dontSkip: true);
var mapChunkEnumerator = new ChunkIndicesEnumerator(viewBox, ChunkSize);
while (mapChunkEnumerator.MoveNext(out var chunkIndices))
foreach (var eyeEuid in viewers)
{
if(_entityPvsCollection.TryGetChunk(mapId, chunkIndices.Value, out var chunk))
var (viewPos, range, mapId) = CalcViewBounds(in eyeEuid, transformQuery);
uint visMask = EyeComponent.DefaultVisibilityMask;
if (eyeQuery.TryGetComponent(eyeEuid, out var eyeComp))
visMask = eyeComp.VisibilityMask;
// Get the nyoom dictionary for index lookups.
if (!mapIndices.TryGetValue(visMask, out var mapDict))
{
foreach (var index in chunk)
mapDict = new Dictionary<MapChunkLocation, int>(32);
mapIndices[visMask] = mapDict;
}
var mapChunkEnumerator = new ChunkIndicesEnumerator(viewPos, range, ChunkSize);
while (mapChunkEnumerator.MoveNext(out var chunkIndices))
{
var chunkLocation = new MapChunkLocation(mapId, chunkIndices.Value);
var entry = (visMask, chunkLocation);
if (mapDict.TryGetValue(chunkLocation, out var indexOf))
{
TryAddToVisibleEnts(in index, seenSet, playerVisibleSet, visibleEnts, fromTick, ref newEntitiesSent, ref entitiesSent, metadataQuery, transformQuery, visMask);
playerChunks[i].Add(indexOf);
}
else
{
playerChunks[i].Add(chunkList.Count);
mapDict.Add(chunkLocation, chunkList.Count);
chunkList.Add(entry);
}
}
}
_mapManager.FindGridsIntersectingEnumerator(mapId, viewBox, out var gridEnumerator, true);
while (gridEnumerator.MoveNext(out var mapGrid))
{
var gridXform = transformQuery.GetComponent(mapGrid.GridEntityId);
var gridChunkEnumerator =
new ChunkIndicesEnumerator(gridXform.InvWorldMatrix.TransformBox(viewBox), ChunkSize);
while (gridChunkEnumerator.MoveNext(out var gridChunkIndices))
// Get the nyoom dictionary for index lookups.
if (!gridIndices.TryGetValue(visMask, out var gridDict))
{
if (_entityPvsCollection.TryGetChunk(mapGrid.Index, gridChunkIndices.Value, out var chunk))
gridDict = new Dictionary<GridChunkLocation, int>(32);
gridIndices[visMask] = gridDict;
}
_mapManager.FindGridsIntersectingEnumerator(mapId, new Box2(viewPos - range, viewPos + range), out var gridEnumerator, true);
while (gridEnumerator.MoveNext(out var mapGrid))
{
var localPos = transformQuery.GetComponent(mapGrid.GridEntityId).InvWorldMatrix.Transform(viewPos);
var gridChunkEnumerator =
new ChunkIndicesEnumerator(localPos, range, ChunkSize);
while (gridChunkEnumerator.MoveNext(out var gridChunkIndices))
{
foreach (var index in chunk)
var chunkLocation = new GridChunkLocation(mapGrid.Index, gridChunkIndices.Value);
var entry = (visMask, chunkLocation);
if (gridDict.TryGetValue(chunkLocation, out var indexOf))
{
TryAddToVisibleEnts(in index, seenSet, playerVisibleSet, visibleEnts, fromTick, ref newEntitiesSent, ref entitiesSent, metadataQuery, transformQuery, visMask);
playerChunks[i].Add(indexOf);
}
else
{
playerChunks[i].Add(chunkList.Count);
gridDict.Add(chunkLocation, chunkList.Count);
chunkList.Add(entry);
}
}
}
}
_uidSetPool.Return(viewers);
}
viewers.Clear();
_viewerEntsPool.Return(viewers);
return (chunkList, playerChunks, viewerEntities);
}
public (Dictionary<EntityUid, MetaDataComponent> mData, RobustTree<EntityUid> tree)? CalculateChunk(IChunkIndexLocation chunkLocation, uint visMask, EntityQuery<TransformComponent> transform, EntityQuery<MetaDataComponent> metadata)
{
var chunk = chunkLocation switch
{
GridChunkLocation gridChunkLocation => _entityPvsCollection.TryGetChunk(gridChunkLocation.GridId,
gridChunkLocation.ChunkIndices, out var gridChunk)
? gridChunk
: null,
MapChunkLocation mapChunkLocation => _entityPvsCollection.TryGetChunk(mapChunkLocation.MapId,
mapChunkLocation.ChunkIndices, out var mapChunk)
? mapChunk
: null
};
if (chunk == null) return null;
var chunkSet = _chunkCachePool.Get();
var tree = _treePool.Get();
foreach (var uid in chunk)
{
AddToChunkSetRecursively(in uid, visMask, tree, chunkSet, transform, metadata);
}
return (chunkSet, tree);
}
public void ReturnToPool((Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[] chunkCache, HashSet<int>[] playerChunks)
{
foreach (var chunk in chunkCache)
{
if(!chunk.HasValue) continue;
_chunkCachePool.Return(chunk.Value.metadata);
_treePool.Return(chunk.Value.tree);
}
foreach (var playerChunk in playerChunks)
{
_playerChunkPool.Return(playerChunk);
}
}
private bool AddToChunkSetRecursively(in EntityUid uid, uint visMask, RobustTree<EntityUid> tree, Dictionary<EntityUid, MetaDataComponent> set, EntityQuery<TransformComponent> transform,
EntityQuery<MetaDataComponent> metadata)
{
//are we valid?
//sometimes uids gets added without being valid YET (looking at you mapmanager) (mapcreate & gridcreated fire before the uids becomes valid)
if (!uid.IsValid()) return false;
if (set.ContainsKey(uid)) return true;
var mComp = metadata.GetComponent(uid);
// TODO: Don't need to know about parents so no longer need to use bool for this method.
// If the eye is missing ANY layer this entity or any of its parents belongs to, it is considered invisible.
if ((visMask & mComp.VisibilityMask) != mComp.VisibilityMask)
return false;
var parent = transform.GetComponent(uid).ParentUid;
if (parent.IsValid() && //is it not a worldentity?
!set.ContainsKey(parent) && //was the parent not yet added to toSend?
!AddToChunkSetRecursively(in parent, visMask, tree, set, transform, metadata)) //did we just fail to add the parent?
return false; //we failed? suppose we dont get added either
//todo paul i want it to crash here if it gets added double bc that shouldnt happen and will add alot of unneeded cycles, make this a simpl assignment at some point maybe idk
tree.Set(uid, parent);
set.Add(uid, mComp);
return true;
}
public (List<EntityState>? updates, List<EntityUid>? deletions) CalculateEntityStates(IPlayerSession session,
GameTick fromTick, GameTick toTick,
(Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[] chunkCache,
HashSet<int> chunkIndices, EntityQuery<MetaDataComponent> mQuery, EntityQuery<TransformComponent> tQuery,
EntityUid[] viewerEntities)
{
DebugTools.Assert(session.Status == SessionStatus.InGame);
var newEntitiesSent = 0;
var entitiesSent = 0;
var playerVisibleSet = _playerVisibleSets[session];
var visibleEnts = _visSetPool.Get();
var seenSet = _playerSeenSets[session];
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
foreach (var i in chunkIndices)
{
var cache = chunkCache[i];
if(!cache.HasValue) continue;
var rootNodes = cache.Value.tree.GetRootNodes();
foreach (var rootNode in rootNodes)
{
RecursivelyAddTreeNode(in rootNode, seenSet, playerVisibleSet, visibleEnts, fromTick, ref newEntitiesSent,
ref entitiesSent, cache.Value.metadata);
}
cache.Value.tree.ReturnRootNodes(rootNodes);
}
var globalEnumerator = _entityPvsCollection.GlobalOverridesEnumerator;
while (globalEnumerator.MoveNext())
{
var uid = globalEnumerator.Current;
RecursivelyAddOverride(in uid, seenSet, playerVisibleSet, visibleEnts, fromTick, ref newEntitiesSent,
ref entitiesSent, mQuery, tQuery);
}
globalEnumerator.Dispose();
var localEnumerator = _entityPvsCollection.GetElementsForSession(session);
while (localEnumerator.MoveNext())
{
var uid = localEnumerator.Current;
RecursivelyAddOverride(in uid, seenSet, playerVisibleSet, visibleEnts, fromTick, ref newEntitiesSent,
ref entitiesSent, mQuery, tQuery);
}
localEnumerator.Dispose();
foreach (var viewerEntity in viewerEntities)
{
RecursivelyAddOverride(in viewerEntity, seenSet, playerVisibleSet, visibleEnts, fromTick, ref newEntitiesSent,
ref entitiesSent, mQuery, tQuery);
}
var entityStates = new List<EntityState>();
@@ -377,8 +528,7 @@ internal sealed partial class PVSSystem : EntitySystem
continue;
var @new = visiblity == PVSEntityVisiblity.Entered;
var state = GetEntityState(session, entityUid, @new ? GameTick.Zero : fromTick);
var state = GetEntityState(session, entityUid, @new ? GameTick.Zero : fromTick, mQuery.GetComponent(entityUid).Flags);
//this entity is not new & nothing changed
if(!@new && state.Empty) continue;
@@ -411,7 +561,42 @@ internal sealed partial class PVSSystem : EntitySystem
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private bool TryAddToVisibleEnts(
private void RecursivelyAddTreeNode(
in RobustTree<EntityUid>.TreeNode node,
HashSet<EntityUid> seenSet,
Dictionary<EntityUid, PVSEntityVisiblity> previousVisibleEnts,
Dictionary<EntityUid, PVSEntityVisiblity> toSend,
GameTick fromTick,
ref int newEntitiesSent,
ref int totalEnteredEntities,
Dictionary<EntityUid, MetaDataComponent> metaDataCache)
{
//are we valid?
//sometimes uids gets added without being valid YET (looking at you mapmanager) (mapcreate & gridcreated fire before the uids becomes valid)
// As every map is parented to uid 0 in the tree we still need to get their children, plus because we go top-down
// we may find duplicate parents with children we haven't encountered before
// on different chunks (this is especially common with direct grid children)
if (node.Value.IsValid() && !toSend.ContainsKey(node.Value))
{
//are we new?
var (entered, budgetFail) = ProcessEntry(in node.Value, seenSet, previousVisibleEnts, ref newEntitiesSent,
ref totalEnteredEntities);
if (budgetFail) return;
AddToSendSet(in node.Value, metaDataCache[node.Value], toSend, fromTick, entered);
}
//our children are important regardless! iterate them!
foreach (var child in node.Children)
{
RecursivelyAddTreeNode(in child, seenSet, previousVisibleEnts, toSend, fromTick, ref newEntitiesSent,
ref totalEnteredEntities, metaDataCache);
}
}
public bool RecursivelyAddOverride(
in EntityUid uid,
HashSet<EntityUid> seenSet,
Dictionary<EntityUid, PVSEntityVisiblity> previousVisibleEnts,
@@ -419,50 +604,39 @@ internal sealed partial class PVSSystem : EntitySystem
GameTick fromTick,
ref int newEntitiesSent,
ref int totalEnteredEntities,
EntityQuery<MetaDataComponent> metadataQuery,
EntityQuery<TransformComponent> transformQuery,
uint? visMask = null,
bool dontSkip = false,
bool trustParent = false)
EntityQuery<MetaDataComponent> metaQuery,
EntityQuery<TransformComponent> transQuery)
{
//are we valid yet?
//are we valid?
//sometimes uids gets added without being valid YET (looking at you mapmanager) (mapcreate & gridcreated fire before the uids becomes valid)
if (!uid.IsValid()) return false;
//did we already get added?
if (toSend.ContainsKey(uid)) return true;
var metadata = metadataQuery.GetComponent(uid);
var parent = transQuery.GetComponent(uid).ParentUid;
if (parent.IsValid() && !RecursivelyAddOverride(in parent, seenSet, previousVisibleEnts, toSend, fromTick,
ref newEntitiesSent, ref totalEnteredEntities, metaQuery, transQuery))
return false;
// if we are invisible, we are not going into the visSet, so don't worry about parents, and children are not going in
if (visMask != null)
{
// TODO: Don't need to know about parents so no longer need to use bool for this method.
var (entered, _) = ProcessEntry(in uid, seenSet, previousVisibleEnts, ref newEntitiesSent, ref totalEnteredEntities);
// If the eye is missing ANY layer this entity or any of its parents belongs to, it is considered invisible.
if ((visMask & metadata.VisibilityMask) != metadata.VisibilityMask)
return false;
}
AddToSendSet(in uid, metaQuery.GetComponent(uid), toSend, fromTick, entered);
return true;
}
var parent = transformQuery.GetComponent(uid).ParentUid;
if (!trustParent && //do we have it on good authority the parent exists?
parent.IsValid() && //is it not a worldentity?
!toSend.ContainsKey(parent) && //was the parent not yet added to toSend?
!TryAddToVisibleEnts(in parent, seenSet, previousVisibleEnts, toSend, fromTick, ref newEntitiesSent, ref totalEnteredEntities, metadataQuery, transformQuery, visMask)) //did we just fail to add the parent?
return false; //we failed? suppose we dont get added either
//did we already get added through the parent call?
if (toSend.ContainsKey(uid)) return true;
//are we new?
private (bool entered, bool budgetFail) ProcessEntry(in EntityUid uid, HashSet<EntityUid> seenSet,
Dictionary<EntityUid, PVSEntityVisiblity> previousVisibleEnts,
ref int newEntitiesSent,
ref int totalEnteredEntities)
{
var @new = !seenSet.Contains(uid);
var entered = @new | !previousVisibleEnts.Remove(uid);
if (entered)
{
if (!dontSkip && totalEnteredEntities >= _entityBudget)
return false;
if (totalEnteredEntities >= _entityBudget)
return (entered, true);
totalEnteredEntities++;
}
@@ -470,81 +644,110 @@ internal sealed partial class PVSSystem : EntitySystem
if (@new)
{
//we just entered pvs, do we still have enough budget to send us?
if(!dontSkip && newEntitiesSent >= _newEntityBudget)
return false;
if(newEntitiesSent >= _newEntityBudget)
return (entered, true);
newEntitiesSent++;
seenSet.Add(uid);
}
return (entered, false);
}
private void AddToSendSet(in EntityUid uid, MetaDataComponent metaDataComponent, Dictionary<EntityUid, PVSEntityVisiblity> toSend, GameTick fromTick, bool entered)
{
if (entered)
{
toSend.Add(uid, PVSEntityVisiblity.Entered);
return true;
return;
}
if (metadata.EntityLastModifiedTick < fromTick)
if (metaDataComponent.EntityLastModifiedTick < fromTick)
{
//entity has been sent before and hasnt been updated since
toSend.Add(uid, PVSEntityVisiblity.StayedUnchanged);
return true;
return;
}
//add us
toSend.Add(uid, PVSEntityVisiblity.StayedChanged);
return true;
}
/// <summary>
/// Gets all entity states that have been modified after and including the provided tick.
/// </summary>
private List<EntityState>? GetAllEntityStates(ICommonSession player, GameTick fromTick, GameTick toTick)
public (List<EntityState>? updates, List<EntityUid>? deletions) GetAllEntityStates(ICommonSession player, GameTick fromTick, GameTick toTick)
{
List<EntityState> stateEntities;
var deletions = _entityPvsCollection.GetDeletedIndices(fromTick);
// no point sending an empty collection
if (deletions.Count == 0) deletions = default;
stateEntities = new List<EntityState>();
var stateEntities = new List<EntityState>();
var seenEnts = new HashSet<EntityUid>();
var slowPath = false;
var metadataQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
for (var i = fromTick.Value; i <= toTick.Value; i++)
if (!SeenAllEnts.Contains(player))
{
var tick = new GameTick(i);
if (!TryGetTick(tick, out var add, out var dirty))
// Give them E V E R Y T H I N G
stateEntities = new List<EntityState>(EntityManager.EntityCount);
// This is the same as iterating every existing entity.
foreach (var md in EntityManager.EntityQuery<MetaDataComponent>(true))
{
slowPath = true;
break;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
stateEntities.Add(GetEntityState(player, md.Owner, GameTick.Zero, md.Flags));
}
foreach (var uid in add)
return (stateEntities.Count == 0 ? default : stateEntities, deletions);
}
// Just get the relevant entities that have been dirtied
// This should be extremely fast.
if (!slowPath)
{
for (var i = fromTick.Value; i <= toTick.Value; i++)
{
if (!seenEnts.Add(uid)) continue;
// This is essentially the same as IEntityManager.EntityExists, but returning MetaDataComponent.
if (!metadataQuery.TryGetComponent(uid, out MetaDataComponent? md)) continue;
// Fallback to dumping every entity on them.
var tick = new GameTick(i);
if (!TryGetTick(tick, out var add, out var dirty))
{
slowPath = true;
break;
}
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
foreach (var uid in add)
{
if (!seenEnts.Add(uid)) continue;
// This is essentially the same as IEntityManager.EntityExists, but returning MetaDataComponent.
if (!metadataQuery.TryGetComponent(uid, out var md)) continue;
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, uid, GameTick.Zero));
}
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
foreach (var uid in dirty)
{
DebugTools.Assert(!add.Contains(uid));
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, uid, GameTick.Zero, md.Flags));
}
if (!seenEnts.Add(uid)) continue;
if (!metadataQuery.TryGetComponent(uid, out MetaDataComponent? md)) continue;
foreach (var uid in dirty)
{
DebugTools.Assert(!add.Contains(uid));
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (!seenEnts.Add(uid)) continue;
if (!metadataQuery.TryGetComponent(uid, out var md)) continue;
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, uid, fromTick));
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, uid, fromTick, md.Flags));
}
}
}
if (!slowPath)
{
return stateEntities.Count == 0 ? default : stateEntities;
if (stateEntities.Count == 0) stateEntities = default;
return (stateEntities, deletions);
}
stateEntities = new List<EntityState>(EntityManager.EntityCount);
@@ -555,11 +758,13 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
if (md.EntityLastModifiedTick >= fromTick)
stateEntities.Add(GetEntityState(player, md.Owner, fromTick));
stateEntities.Add(GetEntityState(player, md.Owner, fromTick, md.Flags));
}
// no point sending an empty collection
return stateEntities.Count == 0 ? default : stateEntities;
if (stateEntities.Count == 0) stateEntities = default;
return (stateEntities, deletions);
}
/// <summary>
@@ -568,11 +773,16 @@ internal sealed partial class PVSSystem : EntitySystem
/// <param name="player">The player to generate this state for.</param>
/// <param name="entityUid">Uid of the entity to generate the state from.</param>
/// <param name="fromTick">Only provide delta changes from this tick.</param>
/// <param name="flags">Any applicable metadata flags</param>
/// <returns>New entity State for the given entity.</returns>
private EntityState GetEntityState(ICommonSession player, EntityUid entityUid, GameTick fromTick)
private EntityState GetEntityState(ICommonSession player, EntityUid entityUid, GameTick fromTick, MetaDataFlags flags)
{
var bus = EntityManager.EventBus;
var changed = new List<ComponentChange>();
// Whether this entity has any component states that are only for a specific session.
// TODO: This GetComp is probably expensive, less expensive than before, but ideally we'd cache it somewhere or something from a previous getcomp
// Probably still needs tweaking but checking for add / changed states up front should do most of the work.
var specificStates = (flags & MetaDataFlags.EntitySpecific) == MetaDataFlags.EntitySpecific;
foreach (var (netId, component) in EntityManager.GetNetComponents(entityUid))
{
@@ -587,10 +797,22 @@ internal sealed partial class PVSSystem : EntitySystem
DebugTools.Assert(component.LastModifiedTick >= component.CreationTick);
if (!EntityManager.CanGetComponentState(bus, component, player))
var addState = false;
var changeState = false;
// We'll check the properties first; if we ever have specific states then doing the struct event is expensive.
if (component.CreationTick != GameTick.Zero && component.CreationTick >= fromTick && !component.Deleted)
addState = true;
else if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero && component.LastModifiedTick >= fromTick)
changeState = true;
if (!addState && !changeState)
continue;
if (component.CreationTick != GameTick.Zero && component.CreationTick >= fromTick && !component.Deleted)
if (specificStates && !EntityManager.CanGetComponentState(bus, component, player))
continue;
if (addState)
{
ComponentState? state = null;
if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero &&
@@ -601,14 +823,14 @@ internal sealed partial class PVSSystem : EntitySystem
// ReSharper disable once PossibleInvalidOperationException
changed.Add(ComponentChange.Added(netId, state));
}
else if (component.NetSyncEnabled && component.LastModifiedTick != GameTick.Zero &&
component.LastModifiedTick >= fromTick)
else
{
DebugTools.Assert(changeState);
changed.Add(ComponentChange.Changed(netId, EntityManager.GetComponentState(bus, component)));
}
}
foreach (var netId in ((IServerEntityManager)EntityManager).GetDeletedComponents(entityUid, fromTick))
foreach (var netId in _serverEntManager.GetDeletedComponents(entityUid, fromTick))
{
changed.Add(ComponentChange.Removed(netId));
}
@@ -618,7 +840,7 @@ internal sealed partial class PVSSystem : EntitySystem
private HashSet<EntityUid> GetSessionViewers(ICommonSession session)
{
var viewers = _viewerEntsPool.Get();
var viewers = _uidSetPool.Get();
if (session.Status != SessionStatus.InGame)
return viewers;
@@ -638,14 +860,69 @@ internal sealed partial class PVSSystem : EntitySystem
}
// Read Safe
private (Box2 view, MapId mapId) CalcViewBounds(in EntityUid euid, EntityQuery<TransformComponent> transformQuery)
private (Vector2 worldPos, float range, MapId mapId) CalcViewBounds(in EntityUid euid, EntityQuery<TransformComponent> transformQuery)
{
var xform = transformQuery.GetComponent(euid);
return (xform.WorldPosition, _viewSize / 2f, xform.MapID);
}
var view = Box2.UnitCentered.Scale(_viewSize).Translated(xform.WorldPosition);
var map = xform.MapID;
public sealed class SetPolicy<T> : PooledObjectPolicy<HashSet<T>>
{
public override HashSet<T> Create()
{
return new HashSet<T>();
}
return (view, map);
public override bool Return(HashSet<T> obj)
{
obj.Clear();
return true;
}
}
public sealed class ListPolicy<T> : PooledObjectPolicy<List<T>>
{
public override List<T> Create()
{
return new();
}
public override bool Return(List<T> obj)
{
obj.Clear();
return true;
}
}
public sealed class DictPolicy<T1, T2> : PooledObjectPolicy<Dictionary<T1, T2>> where T1 : notnull
{
public override Dictionary<T1, T2> Create()
{
return new Dictionary<T1, T2>();
}
public override bool Return(Dictionary<T1, T2> obj)
{
obj.Clear();
return true;
}
}
public sealed class TreePolicy<T> : PooledObjectPolicy<RobustTree<T>> where T : notnull
{
private readonly ObjectPool<HashSet<RobustTree<T>.TreeNode>> _treeNodeSetPool =
new DefaultObjectPool<HashSet<RobustTree<T>.TreeNode>>(new SetPolicy<RobustTree<T>.TreeNode>());
public override RobustTree<T> Create()
{
return new RobustTree<T>(_treeNodeSetPool.Get, _treeNodeSetPool.Return);
}
public override bool Return(RobustTree<T> obj)
{
obj.Clear();
return true;
}
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Robust.Server.GameStates;
public sealed class RobustTree<T> where T : notnull
{
private Dictionary<T, TreeNode> _nodeIndex = new();
private Dictionary<T, T> _parents = new();
private HashSet<T> _rootNodes = new();
private Func<HashSet<TreeNode>> _setProvider;
private Action<HashSet<TreeNode>> _setConsumer;
public RobustTree(Func<HashSet<TreeNode>>? setProvider = null, Action<HashSet<TreeNode>>? setConsumer = null)
{
_setProvider = setProvider ?? (static () => new());
_setConsumer = setConsumer ?? (static (_) => {});
}
public void Clear()
{
// TODO: This is hella expensive
foreach (var value in _nodeIndex.Values)
{
_setConsumer(value.Children);
}
_nodeIndex.Clear();
_parents.Clear();
_rootNodes.Clear();
}
public void Remove(T value, bool mend = false)
{
if (!_nodeIndex.TryGetValue(value, out var node))
throw new InvalidOperationException("Node doesnt exist.");
if (_rootNodes.Contains(value))
{
foreach (var child in node.Children)
{
_parents.Remove(child.Value);
_rootNodes.Add(child.Value);
}
_setConsumer(node.Children);
_rootNodes.Remove(value);
_nodeIndex.Remove(value);
return;
}
if (_parents.TryGetValue(value, out var parent))
{
foreach (var child in node.Children)
{
if (mend)
{
_parents[child.Value] = parent;
_nodeIndex[parent].Children.Add(child);
}
else
{
_parents.Remove(child.Value);
_rootNodes.Add(child.Value);
}
}
_setConsumer(node.Children);
_parents.Remove(value);
_nodeIndex.Remove(value);
}
throw new InvalidOperationException("Node neither had a parent nor was a RootNode.");
}
public TreeNode Set(T rootNode)
{
//root node, for now
if (_nodeIndex.TryGetValue(rootNode, out var node))
{
if(!_rootNodes.Contains(rootNode))
throw new InvalidOperationException("Node already exists as non-root node.");
return node;
}
node = new TreeNode(rootNode, _setProvider());
_nodeIndex.Add(rootNode, node);
_rootNodes.Add(rootNode);
return node;
}
public TreeNode Set(T child, T parent)
{
if (!_nodeIndex.TryGetValue(parent, out var parentNode))
parentNode = Set(parent);
if (_nodeIndex.TryGetValue(child, out var existingNode))
{
if (_rootNodes.Contains(child))
{
parentNode.Children.Add(existingNode);
_rootNodes.Remove(child);
_parents.Add(child, parent);
return existingNode;
}
if (!_parents.TryGetValue(child, out var previousParent) || _nodeIndex.TryGetValue(previousParent, out var previousParentNode))
throw new InvalidOperationException("Could not find old parent for non-root node.");
previousParentNode.Children.Remove(existingNode);
parentNode.Children.Add(existingNode);
_parents[child] = parent;
return existingNode;
}
existingNode = new TreeNode(child, _setProvider());
_nodeIndex.Add(child, existingNode);
parentNode.Children.Add(existingNode);
_parents.Add(child, parent);
return existingNode;
}
// todo paul optimize this maybe as its basically all this is used for.
public HashSet<TreeNode> GetRootNodes()
{
var nodes = _setProvider();
foreach (var node in _rootNodes)
{
nodes.Add(_nodeIndex[node]);
}
return nodes;
}
public void ReturnRootNodes(HashSet<TreeNode> rootNodes)
{
_setConsumer(rootNodes);
}
public readonly struct TreeNode : IEquatable<TreeNode>
{
public readonly T Value;
public readonly HashSet<TreeNode> Children;
public TreeNode(T value, HashSet<TreeNode> children)
{
Value = value;
Children = children;
}
public bool Equals(TreeNode other)
{
return Value.Equals(other.Value) && Children.Equals(other.Children);
}
public override bool Equals(object? obj)
{
return obj is TreeNode other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Value, Children);
}
}
}

View File

@@ -1,9 +1,12 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.ObjectPool;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Enums;
@@ -125,6 +128,37 @@ namespace Robust.Server.GameStates
// people not in the game don't get states
var players = _playerManager.ServerSessions.Where(o => o.Status == SessionStatus.InGame).ToArray();
//todo paul oh my god make this less shit
EntityQuery<MetaDataComponent> metadataQuery = default!;
EntityQuery<TransformComponent> transformQuery = default!;
HashSet<int>[] playerChunks = default!;
EntityUid[][] viewerEntities = default!;
(Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[] chunkCache = default!;
if (_pvs.CullingEnabled)
{
List<(uint, IChunkIndexLocation)> chunks;
(chunks, playerChunks, viewerEntities) = _pvs.GetChunks(players);
const int ChunkBatchSize = 2;
var chunksCount = chunks.Count;
var chunkBatches = (int) MathF.Ceiling((float) chunksCount / ChunkBatchSize);
chunkCache =
new (Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[chunksCount];
transformQuery = _entityManager.GetEntityQuery<TransformComponent>();
metadataQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
Parallel.For(0, chunkBatches, i =>
{
var start = i * ChunkBatchSize;
var end = Math.Min(start + ChunkBatchSize, chunksCount);
for (var j = start; j < end; ++j)
{
var (visMask, chunkIndexLocation) = chunks[j];
chunkCache[j] = _pvs.CalculateChunk(chunkIndexLocation, visMask, transformQuery, metadataQuery);
}
});
}
const int BatchSize = 2;
var batches = (int) MathF.Ceiling((float) players.Length / BatchSize);
@@ -135,11 +169,9 @@ namespace Robust.Server.GameStates
for (var j = start; j < end; ++j)
{
var session = players[j];
try
{
SendStateUpdate(session);
SendStateUpdate(j);
}
catch (Exception e) // Catch EVERY exception
{
@@ -148,8 +180,10 @@ namespace Robust.Server.GameStates
}
});
void SendStateUpdate(IPlayerSession session)
void SendStateUpdate(int sessionIndex)
{
var session = players[sessionIndex];
// KILL IT WITH FIRE
if(mainThread != Thread.CurrentThread)
IoCManager.InitThread(new DependencyCollection(parentDeps), true);
@@ -161,7 +195,10 @@ namespace Robust.Server.GameStates
DebugTools.Assert("Why does this channel not have an entry?");
}
var (entStates, deletions) = _pvs.CalculateEntityStates(session, lastAck, _gameTiming.CurTick);
var (entStates, deletions) = _pvs.CullingEnabled
? _pvs.CalculateEntityStates(session, lastAck, _gameTiming.CurTick, chunkCache,
playerChunks[sessionIndex], metadataQuery, transformQuery, viewerEntities[sessionIndex])
: _pvs.GetAllEntityStates(session, lastAck, _gameTiming.CurTick);
var playerStates = _playerManager.GetPlayerStates(lastAck);
var mapData = _mapManager.GetStateData(lastAck);
@@ -194,6 +231,8 @@ namespace Robust.Server.GameStates
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
}
if(_pvs.CullingEnabled)
_pvs.ReturnToPool(chunkCache, playerChunks);
_pvs.Cleanup(_playerManager.ServerSessions);
var oldestAck = new GameTick(oldestAckValue);

View File

@@ -24,7 +24,6 @@ using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
@@ -44,7 +43,6 @@ namespace Robust.Server.Maps
[Dependency] private readonly IMapManagerInternal _mapManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IServerEntityManagerInternal _serverEntityManager = default!;
[Dependency] private readonly IPauseManager _pauseManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public event Action<YamlStream, string>? LoadedMapData;
@@ -54,7 +52,7 @@ namespace Robust.Server.Maps
{
var grid = _mapManager.GetGrid(gridId);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _prototypeManager);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _prototypeManager);
context.RegisterGrid(grid);
var root = context.Serialize();
var document = new YamlDocument(root);
@@ -100,7 +98,7 @@ namespace Robust.Server.Maps
throw new InvalidDataException("Cannot instance map with multiple grids as blueprint.");
}
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager,
_prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
context.Deserialize();
grid = context.Grids[0];
@@ -120,7 +118,7 @@ namespace Robust.Server.Maps
_serverEntityManager.GetComponent<MetaDataComponent>(entity).EntityLifeStage = EntityLifeStage.MapInitialized;
}
}
else if (_pauseManager.IsMapInitialized(mapId))
else if (_mapManager.IsMapInitialized(mapId))
{
foreach (var entity in context.Entities)
{
@@ -128,7 +126,7 @@ namespace Robust.Server.Maps
}
}
if (_pauseManager.IsMapPaused(mapId))
if (_mapManager.IsMapPaused(mapId))
{
foreach (var entity in context.Entities)
{
@@ -141,7 +139,7 @@ namespace Robust.Server.Maps
public void SaveMap(MapId mapId, string yamlPath)
{
Logger.InfoS("map", $"Saving map {mapId} to {yamlPath}");
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _prototypeManager);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _prototypeManager);
foreach (var grid in _mapManager.GetAllMapGrids(mapId))
{
context.RegisterGrid(grid);
@@ -207,7 +205,7 @@ namespace Robust.Server.Maps
LoadedMapData?.Invoke(data.Stream, resPath.ToString());
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager,
_prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
context.Deserialize();
@@ -226,7 +224,6 @@ namespace Robust.Server.Maps
private readonly IMapManagerInternal _mapManager;
private readonly ITileDefinitionManager _tileDefinitionManager;
private readonly IServerEntityManagerInternal _serverEntityManager;
private readonly IPauseManager _pauseManager;
private readonly IPrototypeManager _prototypeManager;
private readonly MapLoadOptions? _loadOptions;
@@ -260,12 +257,11 @@ namespace Robust.Server.Maps
public bool MapIsPostInit { get; private set; }
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
IServerEntityManagerInternal entities, IPauseManager pauseManager, IPrototypeManager prototypeManager)
IServerEntityManagerInternal entities, IPrototypeManager prototypeManager)
{
_mapManager = maps;
_tileDefinitionManager = tileDefs;
_serverEntityManager = entities;
_pauseManager = pauseManager;
_prototypeManager = prototypeManager;
RootNode = new YamlMappingNode();
@@ -283,13 +279,12 @@ namespace Robust.Server.Maps
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
IServerEntityManagerInternal entities,
IPauseManager pauseManager, IPrototypeManager prototypeManager,
IPrototypeManager prototypeManager,
YamlMappingNode node, MapId targetMapId, MapLoadOptions options)
{
_mapManager = maps;
_tileDefinitionManager = tileDefs;
_serverEntityManager = entities;
_pauseManager = pauseManager;
_loadOptions = options;
RootNode = node;
@@ -612,7 +607,7 @@ namespace Robust.Server.Maps
if (!MapIsPostInit)
{
_pauseManager.AddUninitializedMap(TargetMap);
_mapManager.AddUninitializedMap(TargetMap);
}
}
}
@@ -719,7 +714,7 @@ namespace Robust.Server.Maps
var isPostInit = false;
foreach (var grid in Grids)
{
if (_pauseManager.IsMapInitialized(grid.ParentMapId))
if (_mapManager.IsMapInitialized(grid.ParentMapId))
{
isPostInit = true;
break;

View File

@@ -36,7 +36,7 @@ namespace Robust.Server.Maps
return gridn;
}
private static YamlNode SerializeChunk(IMapChunk chunk)
private static YamlNode SerializeChunk(MapChunk chunk)
{
var root = new YamlMappingNode();
var value = new YamlScalarNode($"{chunk.X},{chunk.Y}");
@@ -51,7 +51,7 @@ namespace Robust.Server.Maps
return root;
}
private static string SerializeTiles(IMapChunk chunk)
private static string SerializeTiles(MapChunk chunk)
{
// number of bytes written per tile, because sizeof(Tile) is useless.
const int structSize = 4;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -135,5 +135,17 @@ namespace Robust.Shared.Maths
{
return $"({Left}, {Bottom}, {Right}, {Top})";
}
/// <summary>
/// Multiplies each side of the box by the scalar.
/// </summary>
public Box2i Scale(int scalar)
{
return new Box2i(
Left * scalar,
Bottom * scalar,
Right * scalar,
Top * scalar);
}
}
}

View File

@@ -130,6 +130,12 @@ namespace Robust.Shared.Maths
return new((int) MathF.Floor(X), (int) MathF.Floor(Y));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector2i Ceiled()
{
return new((int) MathF.Ceiling(X), (int) MathF.Ceiling(Y));
}
/// <summary>
/// Subtracts a vector from another, returning a new vector.
/// </summary>

View File

@@ -1006,6 +1006,17 @@ namespace Robust.Shared
public static readonly CVarDef<bool> ResTexturePreloadCache =
CVarDef.Create("res.texture_preload_cache", true, CVar.CLIENTONLY);
/// <summary>
/// Override seekability of resource streams returned by ResourceManager.
/// See <see cref="ContentPack.StreamSeekMode"/> for int values.
/// </summary>
/// <remarks>
/// This is intended to be a debugging tool primarily.
/// Non-default seek modes WILL result in worse performance.
/// </remarks>
public static readonly CVarDef<int> ResStreamSeekMode =
CVarDef.Create("res.stream_seek_mode", (int)ContentPack.StreamSeekMode.None);
/*
* DEBUG
*/

View File

@@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Robust.Shared.Console;
public sealed class CommandBuffer
{
private const string DelayMarker = "-DELAY-";
private int _tickrate = 0;
private int _delay = 0;
private readonly LinkedList<string> _commandBuffer = new();
public void Append(string command)
{
_commandBuffer.AddLast(command);
}
public void Insert(string command)
{
_commandBuffer.AddFirst(command);
}
public void Tick(byte tickRate)
{
_tickrate = tickRate;
if (_delay > 0)
{
_delay -= 1;
}
}
public bool TryGetCommand([MaybeNullWhen(false)]out string command)
{
var next = _commandBuffer.First;
if (next is null) // nothing to do here
{
command = null;
return false;
}
if (next.Value.Equals(DelayMarker))
{
if (_delay == 0) // just finished
{
_commandBuffer.RemoveFirst();
return TryGetCommand(out command);
}
else // currently counting down delay
{
command = null;
return false;
}
}
if (next.Value.StartsWith("wait "))
{
var sTicks = next.Value.Substring(5);
_commandBuffer.RemoveFirst();
if (string.IsNullOrWhiteSpace(sTicks) || !int.TryParse(sTicks, out var ticks)) // messed up command
{
return TryGetCommand(out command);
}
// Setup Timing
_commandBuffer.AddFirst(DelayMarker);
_delay = ticks;
command = null;
return false;
}
// normal command
_commandBuffer.RemoveFirst();
command = next.Value;
return true;
}
}

View File

@@ -48,7 +48,7 @@ namespace Robust.Shared.Console.Commands
continue;
}
shell.ExecuteCommand(line);
shell.ConsoleHost.AppendCommand(line);
}
}
}

View File

@@ -7,6 +7,7 @@ using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Console
@@ -17,13 +18,16 @@ namespace Robust.Shared.Console
protected const string SawmillName = "con";
[Dependency] protected readonly ILogManager LogManager = default!;
[Dependency] protected readonly IReflectionManager ReflectionManager = default!;
[Dependency] private readonly IReflectionManager ReflectionManager = default!;
[Dependency] protected readonly INetManager NetManager = default!;
[Dependency] private readonly IDynamicTypeFactoryInternal _typeFactory = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[ViewVariables]
protected readonly Dictionary<string, IConsoleCommand> AvailableCommands = new();
private readonly CommandBuffer _commandBuffer = new CommandBuffer();
/// <inheritdoc />
public bool IsServer => NetManager.IsServer;
@@ -116,6 +120,36 @@ namespace Robust.Shared.Console
ExecuteCommand(null, command);
}
/// <inheritdoc />
public void AppendCommand(string command)
{
_commandBuffer.Append(command);
}
/// <inheritdoc />
public void InsertCommand(string command)
{
_commandBuffer.Insert(command);
}
/// <inheritdoc />
public void CommandBufferExecute()
{
_commandBuffer.Tick(_timing.TickRate);
while (_commandBuffer.TryGetCommand(out var cmd))
{
try
{
ExecuteCommand(cmd);
}
catch (Exception e)
{
LocalShell.WriteError(e.Message);
}
}
}
/// <summary>
/// A console command that was registered inline through <see cref="IConsoleHost"/>.
/// </summary>

View File

@@ -81,11 +81,35 @@ namespace Robust.Shared.Console
IConsoleShell GetSessionShell(ICommonSession session);
/// <summary>
/// Execute a command string on the local shell.
/// Execute a command string immediately on the local shell, bypassing the command buffer completely.
/// </summary>
/// <param name="command">Command string to execute.</param>
void ExecuteCommand(string command);
/// <summary>
/// Appends a command into the end of the command buffer on the local shell.
/// </summary>
/// <remarks>
/// This command will be ran *sometime* in the future, depending on how many waits are in the buffer.
/// </remarks>
/// <param name="command">Command string to execute.</param>
void AppendCommand(string command);
/// <summary>
/// Inserts a command into the front of the command buffer on the local shell.
/// </summary>
/// <remarks>
/// This command will preempt the next command executed in the command buffer.
/// </remarks>
/// <param name="command">Command string to execute.</param>
void InsertCommand(string command);
/// <summary>
/// Processes any contents of the command buffer on the local shell. This needs to be called regularly (once a tick),
/// inside the simulation. Pausing the server should prevent the buffer from being processed.
/// </summary>
void CommandBufferExecute();
/// <summary>
/// Executes a command string on this specific session shell. If the command does not exist, the command will be forwarded
/// to the

View File

@@ -111,7 +111,7 @@ namespace Robust.Shared.ContentPack
var fullStopwatch = Stopwatch.StartNew();
var resolver = CreateResolver();
using var peReader = new PEReader(assembly, PEStreamOptions.LeaveOpen);
using var peReader = ModLoader.MakePEReader(assembly, leaveOpen: true);
var reader = peReader.GetMetadataReader();
var asmName = reader.GetString(reader.GetAssemblyDefinition().Name);
@@ -853,13 +853,13 @@ namespace Robust.Shared.ContentPack
continue;
}
return ReaderFromStream(File.OpenRead(path));
return ModLoader.MakePEReader(File.OpenRead(path));
}
var extraStream = _parent.ExtraRobustLoader?.Invoke(dllName);
if (extraStream != null)
{
return ReaderFromStream(extraStream);
return ModLoader.MakePEReader(extraStream);
}
foreach (var resLoadPath in _resLoadPaths)
@@ -867,7 +867,7 @@ namespace Robust.Shared.ContentPack
try
{
var path = resLoadPath / dllName;
return ReaderFromStream(_parent._res.ContentFileRead(path));
return ModLoader.MakePEReader(_parent._res.ContentFileRead(path));
}
catch (FileNotFoundException)
{
@@ -877,23 +877,6 @@ namespace Robust.Shared.ContentPack
return null;
}
private static PEReader ReaderFromStream(Stream stream)
{
if (OperatingSystem.IsLinux() && stream is FileStream)
{
// PEReader is bugged on Linux and not properly thread safe when doing memory mapping.
// As such, we never pass it a file stream so it uses a different, non-bugged code path.
// See https://github.com/dotnet/runtime/issues/60545
var ms = new MemoryStream();
stream.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
stream.Dispose();
stream = ms;
}
return new PEReader(stream);
}
public PEReader? Resolve(string simpleName)
{
return _dictionary.GetOrAdd(simpleName, ResolveCore);

View File

@@ -160,7 +160,7 @@ namespace Robust.Shared.ContentPack
private static (string[] refs, string name) GetAssemblyReferenceData(Stream stream)
{
using var reader = new PEReader(stream);
using var reader = ModLoader.MakePEReader(stream);
var metaReader = reader.GetMetadataReader();
var name = metaReader.GetString(metaReader.GetAssemblyDefinition().Name);
@@ -383,5 +383,13 @@ namespace Robust.Shared.ContentPack
EngineModuleDirectories = _engineModuleDirectories.ToArray()
};
}
internal static PEReader MakePEReader(Stream stream, bool leaveOpen=false)
{
if (!stream.CanSeek)
stream = leaveOpen ? stream.CopyToMemoryStream() : stream.ConsumeToMemoryStream();
return new PEReader(stream, leaveOpen ? PEStreamOptions.LeaveOpen : default);
}
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Robust.Shared.ContentPack;
internal sealed class NonSeekableStream : Stream
{
private readonly Stream _baseStream;
public NonSeekableStream(Stream baseStream)
{
_baseStream = baseStream;
}
protected override void Dispose(bool disposing)
{
if (disposing)
_baseStream.Dispose();
}
public override ValueTask DisposeAsync()
{
return _baseStream.DisposeAsync();
}
public override void Flush()
{
_baseStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return _baseStream.Read(buffer, offset, count);
}
public override int Read(Span<byte> buffer)
{
return _baseStream.Read(buffer);
}
public override int ReadByte()
{
return _baseStream.ReadByte();
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = new CancellationToken())
{
return _baseStream.ReadAsync(buffer, cancellationToken);
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
_baseStream.Write(buffer, offset, count);
}
public override void Write(ReadOnlySpan<byte> buffer)
{
_baseStream.Write(buffer);
}
public override void WriteByte(byte value)
{
_baseStream.WriteByte(value);
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = new CancellationToken())
{
return _baseStream.WriteAsync(buffer, cancellationToken);
}
public override bool CanRead => _baseStream.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => _baseStream.CanWrite;
// .NET mingles seekability and exposing length.
// This makes absolutely no sense but ok.
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
}

View File

@@ -23,6 +23,8 @@ namespace Robust.Shared.ContentPack
private readonly List<(ResourcePath prefix, IContentRoot root)> _contentRoots =
new();
private StreamSeekMode _streamSeekMode;
// Special file names on Windows like serial ports.
private static readonly Regex BadPathSegmentRegex =
new("^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$", RegexOptions.IgnoreCase);
@@ -45,6 +47,8 @@ namespace Robust.Shared.ContentPack
{
UserData = new VirtualWritableDirProvider();
}
_config.OnValueChanged(CVars.ResStreamSeekMode, i => _streamSeekMode = (StreamSeekMode)i, true);
}
/// <inheritdoc />
@@ -191,8 +195,9 @@ namespace Robust.Shared.ContentPack
continue;
}
if (root.TryGetFile(relative, out fileStream))
if (root.TryGetFile(relative, out var stream))
{
fileStream = WrapStream(stream);
return true;
}
}
@@ -206,6 +211,35 @@ namespace Robust.Shared.ContentPack
}
}
/// <summary>
/// Apply <see cref="_streamSeekMode"/> to the provided stream.
/// </summary>
private Stream WrapStream(Stream stream)
{
switch (_streamSeekMode)
{
case StreamSeekMode.None:
return stream;
case StreamSeekMode.ForceSeekable:
if (stream.CanSeek)
return stream;
var ms = new MemoryStream(stream.CopyToArray(), writable: false);
stream.Dispose();
return ms;
case StreamSeekMode.ForceNonSeekable:
if (!stream.CanSeek)
return stream;
return new NonSeekableStream(stream);
default:
throw new InvalidOperationException();
}
}
/// <inheritdoc />
public bool ContentFileExists(string path)
{

View File

@@ -689,6 +689,7 @@ Types:
CancellationToken: { All: True }
CancellationTokenSource: { All: True }
Interlocked: { All: True }
Monitor: { All: True }
System.Web:
HttpUtility:
Methods:

View File

@@ -0,0 +1,22 @@
namespace Robust.Shared.ContentPack;
/// <summary>
/// Seekability force mode for ResourceManager.
/// </summary>
public enum StreamSeekMode
{
/// <summary>
/// Do not do anything special. Streams will be seekable if the VFS can provide it (e.g. not compressed).
/// </summary>
None = 0,
/// <summary>
/// All streams will be forced as seekable by buffering them in memory if necessary.
/// </summary>
ForceSeekable = 1,
/// <summary>
/// Force streams to be non-seekable by wrapping them in another stream instances.
/// </summary>
ForceNonSeekable = 2
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects;
@@ -27,6 +28,8 @@ public abstract class AppearanceComponent : Component
if (AppearanceData.TryGetValue(key, out var existing) && existing.Equals(value))
return;
DebugTools.Assert(value.GetType().IsValueType || value is ICloneable, "Appearance data values must be cloneable.");
AppearanceData[key] = value;
Dirty();
EntitySystem.Get<SharedAppearanceSystem>().MarkDirty(this);
@@ -37,6 +40,8 @@ public abstract class AppearanceComponent : Component
if (AppearanceData.TryGetValue(key, out var existing) && existing.Equals(value))
return;
DebugTools.Assert(value.GetType().IsValueType || value is ICloneable, "Appearance data values must be cloneable.");
AppearanceData[key] = value;
Dirty();
EntitySystem.Get<SharedAppearanceSystem>().MarkDirty(this);

View File

@@ -1,25 +1,24 @@
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using Robust.Shared.Map;
namespace Robust.Shared.GameObjects
{
[RegisterComponent]
public sealed class IgnorePauseComponent : Component
{
[Dependency] private readonly IEntityManager _entMan = default!;
protected override void OnAdd()
{
base.OnAdd();
_entMan.GetComponent<MetaDataComponent>(Owner).EntityPaused = false;
IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(Owner).EntityPaused = false;
}
protected override void OnRemove()
{
base.OnRemove();
if (IoCManager.Resolve<IPauseManager>().IsMapPaused(_entMan.GetComponent<TransformComponent>(Owner).MapID))
var entMan = IoCManager.Resolve<IEntityManager>();
if (IoCManager.Resolve<IMapManager>().IsMapPaused(entMan.GetComponent<TransformComponent>(Owner).MapID))
{
_entMan.GetComponent<MetaDataComponent>(Owner).EntityPaused = true;
entMan.GetComponent<MetaDataComponent>(Owner).EntityPaused = true;
}
}
}

View File

@@ -15,12 +15,13 @@ namespace Robust.Shared.GameObjects
{
bool LightingEnabled { get; set; }
MapId WorldMap { get; }
void ClearMapId();
bool MapPaused { get; internal set; }
bool MapPreInit { get; internal set; }
}
/// <inheritdoc cref="IMapComponent"/>
[ComponentReference(typeof(IMapComponent))]
[NetworkedComponent()]
[NetworkedComponent]
public sealed class MapComponent : Component, IMapComponent
{
[Dependency] private readonly IEntityManager _entMan = default!;
@@ -40,10 +41,22 @@ namespace Robust.Shared.GameObjects
internal set => _mapIndex = value;
}
internal bool MapPaused { get; set; } = false;
/// <inheritdoc />
public void ClearMapId()
bool IMapComponent.MapPaused
{
_mapIndex = MapId.Nullspace;
get => this.MapPaused;
set => this.MapPaused = value;
}
internal bool MapPreInit { get; set; } = false;
/// <inheritdoc />
bool IMapComponent.MapPreInit
{
get => this.MapPreInit;
set => this.MapPreInit = value;
}
/// <inheritdoc />
@@ -62,8 +75,9 @@ namespace Robust.Shared.GameObjects
_mapIndex = state.MapId;
LightingEnabled = state.LightingEnabled;
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
_entMan.GetComponent<TransformComponent>(Owner).ChangeMapId(_mapIndex);
xformQuery.GetComponent(Owner).ChangeMapId(_mapIndex, xformQuery);
}
}

View File

@@ -50,11 +50,9 @@ namespace Robust.Shared.GameObjects
[NetworkedComponent]
public sealed class MetaDataComponent : Component
{
[DataField("name")]
private string? _entityName;
[DataField("desc")]
private string? _entityDescription;
private EntityPrototype? _entityPrototype;
[DataField("name")] internal string? _entityName;
[DataField("desc")] internal string? _entityDescription;
internal EntityPrototype? _entityPrototype;
private bool _entityPaused;
// Every entity starts at tick 1, because they are conceptually created in the time between 0->1
@@ -134,6 +132,9 @@ namespace Robust.Shared.GameObjects
[ViewVariables]
public EntityLifeStage EntityLifeStage { get; internal set; }
[ViewVariables]
public MetaDataFlags Flags { get; internal set; }
/// <summary>
/// The sum of our visibility layer and our parent's visibility layers.
/// Server-only.
@@ -147,9 +148,11 @@ namespace Robust.Shared.GameObjects
get => _entityPaused;
set
{
var entMan = IoCManager.Resolve<IEntityManager>();
if (_entityPaused == value)
return;
if (_entityPaused == value || value && entMan.HasComponent<IgnorePauseComponent>(Owner))
var entMan = IoCManager.Resolve<IEntityManager>();
if (value && entMan.HasComponent<IgnorePauseComponent>(Owner))
return;
_entityPaused = value;
@@ -161,26 +164,6 @@ namespace Robust.Shared.GameObjects
public bool EntityInitializing => EntityLifeStage == EntityLifeStage.Initializing;
public bool EntityDeleted => EntityLifeStage >= EntityLifeStage.Deleted;
public override ComponentState GetComponentState()
{
return new MetaDataComponentState(_entityName, _entityDescription, EntityPrototype?.ID);
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (!(curState is MetaDataComponentState state))
return;
_entityName = state.Name;
_entityDescription = state.Description;
if(state.PrototypeId != null)
_entityPrototype = IoCManager.Resolve<IPrototypeManager>().Index<EntityPrototype>(state.PrototypeId);
}
internal override void ClearTicks()
{
// Do not clear modified ticks.
@@ -190,4 +173,14 @@ namespace Robust.Shared.GameObjects
ClearCreationTick();
}
}
[Flags]
public enum MetaDataFlags : byte
{
None = 0,
/// <summary>
/// Whether the entity has states specific to a particular player.
/// </summary>
EntitySpecific = 1 << 0,
}
}

View File

@@ -77,7 +77,7 @@ namespace Robust.Shared.GameObjects
[DataField("color")]
public Color Color = Color.White;
[DataField("map")]
public List<string>? MapKeys;
public HashSet<string>? MapKeys;
public static PrototypeLayerData New()
{

View File

@@ -8,6 +8,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Log; //Needed for release build, do not remove
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
@@ -84,10 +85,12 @@ namespace Robust.Shared.GameObjects
if (_gridId.Equals(value)) return;
_gridId = value;
foreach (var transformComponent in Children)
var childEnumerator = ChildEnumerator;
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
while (childEnumerator.MoveNext(out var child))
{
var child = transformComponent;
child.GridID = value;
xformQuery.GetComponent(child.Value).GridID = value;
}
}
}
@@ -155,12 +158,18 @@ namespace Robust.Shared.GameObjects
{
get
{
if (_parent.IsValid())
var parent = _parent;
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
var rotation = _localRotation;
while (parent.IsValid())
{
return Parent!.WorldRotation + _localRotation;
var parentXform = xformQuery.GetComponent(parent);
rotation += parentXform._localRotation;
parent = parentXform.ParentUid;
}
return _localRotation;
return rotation;
}
set
{
@@ -211,15 +220,21 @@ namespace Robust.Shared.GameObjects
{
get
{
if (_parent.IsValid())
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
var parent = _parent;
var myMatrix = _localMatrix;
while (parent.IsValid())
{
var parentMatrix = Parent!.WorldMatrix;
var myMatrix = GetLocalMatrix();
var parentXform = xformQuery.GetComponent(parent);
var parentMatrix = parentXform._localMatrix;
parent = parentXform.ParentUid;
Matrix3.Multiply(ref myMatrix, ref parentMatrix, out var result);
return result;
myMatrix = result;
}
return GetLocalMatrix();
return myMatrix;
}
}
@@ -230,15 +245,21 @@ namespace Robust.Shared.GameObjects
{
get
{
if (_parent.IsValid())
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
var parent = _parent;
var myMatrix = _invLocalMatrix;
while (parent.IsValid())
{
var matP = Parent!.InvWorldMatrix;
var myMatrix = GetLocalMatrixInv();
Matrix3.Multiply(ref matP, ref myMatrix, out var result);
return result;
var parentXform = xformQuery.GetComponent(parent);
var parentMatrix = parentXform._invLocalMatrix;
parent = parentXform.ParentUid;
Matrix3.Multiply(ref parentMatrix, ref myMatrix, out var result);
myMatrix = result;
}
return GetLocalMatrixInv();
return myMatrix;
}
}
@@ -314,25 +335,26 @@ namespace Robust.Shared.GameObjects
if (!sameParent)
{
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
changedParent = true;
var newParent = _entMan.GetComponent<TransformComponent>(value.EntityId);
var newParent = xformQuery.GetComponent(value.EntityId);
DebugTools.Assert(newParent != this,
$"Can't parent a {nameof(TransformComponent)} to itself.");
// That's already our parent, don't bother attaching again.
var oldParent = Parent;
var oldParent = _parent.IsValid() ? xformQuery.GetComponent(_parent) : null;
var uid = Owner;
oldParent?._children.Remove(uid);
newParent._children.Add(uid);
// offset position from world to parent
_parent = value.EntityId;
ChangeMapId(newParent.MapID);
ChangeMapId(newParent.MapID, xformQuery);
// Cache new GridID before raising the event.
GridID = GetGridIndex();
GridID = GetGridIndex(xformQuery);
var entParentChangedMessage = new EntParentChangedMessage(Owner, oldParent?.Owner);
_entMan.EventBus.RaiseLocalEvent(Owner, ref entParentChangedMessage);
@@ -343,7 +365,8 @@ namespace Robust.Shared.GameObjects
// This may not in fact be the right thing.
if (changedParent || !DeferUpdates)
RebuildMatrices();
Dirty();
Dirty(_entMan);
if (!DeferUpdates)
{
@@ -459,10 +482,11 @@ namespace Robust.Shared.GameObjects
if (_children.Count == 0) yield break;
var xforms = _entMan.GetEntityQuery<TransformComponent>();
var children = ChildEnumerator;
foreach (var child in _children)
while (children.MoveNext(out var child))
{
yield return xforms.GetComponent(child);
yield return xforms.GetComponent(child.Value);
}
}
}
@@ -507,22 +531,23 @@ namespace Robust.Shared.GameObjects
// Children MAY be initialized here before their parents are.
// We do this whole dance to handle this recursively,
// setting _mapIdInitialized along the way to avoid going to the IMapComponent every iteration.
static MapId FindMapIdAndSet(TransformComponent p, IEntityManager entMan)
static MapId FindMapIdAndSet(TransformComponent xform, IEntityManager entMan, EntityQuery<TransformComponent> xformQuery)
{
if (p._mapIdInitialized)
if (xform._mapIdInitialized)
{
return p.MapID;
return xform.MapID;
}
MapId value;
if (p._parent.IsValid())
if (xform._parent.IsValid())
{
value = FindMapIdAndSet((TransformComponent) p.Parent!, entMan);
value = FindMapIdAndSet(xformQuery.GetComponent(xform._parent), entMan, xformQuery);
}
else
{
// second level node, terminates recursion up the branch of the tree
if (entMan.TryGetComponent(p.Owner, out IMapComponent? mapComp))
if (entMan.TryGetComponent(xform.Owner, out IMapComponent? mapComp))
{
value = mapComp.WorldMap;
}
@@ -532,14 +557,16 @@ namespace Robust.Shared.GameObjects
}
}
p.MapID = value;
p._mapIdInitialized = true;
xform.MapID = value;
xform._mapIdInitialized = true;
return value;
}
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
if (!_mapIdInitialized)
{
FindMapIdAndSet(this, _entMan);
FindMapIdAndSet(this, _entMan, xformQuery);
_mapIdInitialized = true;
}
@@ -549,14 +576,14 @@ namespace Robust.Shared.GameObjects
{
// Note that _children is a SortedSet<EntityUid>,
// so duplicate additions (which will happen) don't matter.
((TransformComponent) Parent!)._children.Add(Owner);
xformQuery.GetComponent(_parent)._children.Add(Owner);
}
GridID = GetGridIndex();
GridID = GetGridIndex(xformQuery);
RebuildMatrices();
}
private GridId GetGridIndex()
private GridId GetGridIndex(EntityQuery<TransformComponent> xformQuery)
{
if (_entMan.HasComponent<IMapComponent>(Owner))
{
@@ -570,7 +597,7 @@ namespace Robust.Shared.GameObjects
if (_parent.IsValid())
{
return Parent!.GridID;
return xformQuery.GetComponent(_parent).GridID;
}
return _mapManager.TryFindGridAt(MapID, WorldPosition, out var mapgrid) ? mapgrid.Index : GridId.Invalid;
@@ -585,7 +612,7 @@ namespace Robust.Shared.GameObjects
base.Startup();
// Keep the cached matrices in sync with the fields.
Dirty();
Dirty(_entMan);
}
/// <summary>
@@ -721,25 +748,36 @@ namespace Robust.Shared.GameObjects
Coordinates = new EntityCoordinates(newParent.Owner, newParent.InvWorldMatrix.Transform(WorldPosition));
}
internal void ChangeMapId(MapId newMapId)
internal void ChangeMapId(MapId newMapId, EntityQuery<TransformComponent> xformQuery)
{
if (newMapId == MapID)
return;
var oldMapId = MapID;
//Set Paused state
var mapPaused = _mapManager.IsMapPaused(newMapId);
var metaData = _entMan.GetComponent<MetaDataComponent>(Owner);
metaData.EntityPaused = mapPaused;
MapID = newMapId;
MapIdChanged(oldMapId);
UpdateChildMapIdsRecursive(MapID, _entMan);
var xforms = _entMan.GetEntityQuery<TransformComponent>();
var metaEnts = _entMan.GetEntityQuery<MetaDataComponent>();
UpdateChildMapIdsRecursive(MapID, mapPaused, xforms, metaEnts);
}
private void UpdateChildMapIdsRecursive(MapId newMapId, IEntityManager entMan)
private void UpdateChildMapIdsRecursive(MapId newMapId, bool mapPaused, EntityQuery<TransformComponent> xformQuery, EntityQuery<MetaDataComponent> metaQuery)
{
var xforms = _entMan.GetEntityQuery<TransformComponent>();
var childEnumerator = ChildEnumerator;
foreach (var child in _children)
while (childEnumerator.MoveNext(out var child))
{
var concrete = xforms.GetComponent(child);
//Set Paused state
var metaData = metaQuery.GetComponent(child.Value);
metaData.EntityPaused = mapPaused;
var concrete = xformQuery.GetComponent(child.Value);
var old = concrete.MapID;
concrete.MapID = newMapId;
@@ -747,7 +785,7 @@ namespace Robust.Shared.GameObjects
if (concrete.ChildCount != 0)
{
concrete.UpdateChildMapIdsRecursive(newMapId, entMan);
concrete.UpdateChildMapIdsRecursive(newMapId, mapPaused, xformQuery, metaQuery);
}
}
}
@@ -800,14 +838,14 @@ namespace Robust.Shared.GameObjects
{
var parent = _parent;
var worldRot = _localRotation;
var worldMatrix = GetLocalMatrix();
var worldMatrix = _localMatrix;
// By doing these all at once we can elide multiple IsValid + GetComponent calls
while (parent.IsValid())
{
var xform = xforms.GetComponent(parent);
worldRot += xform.LocalRotation;
var parentMatrix = xform.GetLocalMatrix();
var parentMatrix = xform._localMatrix;
Matrix3.Multiply(ref worldMatrix, ref parentMatrix, out var result);
worldMatrix = result;
parent = xform.ParentUid;
@@ -861,8 +899,8 @@ namespace Robust.Shared.GameObjects
{
var parent = _parent;
var worldRot = _localRotation;
var invMatrix = GetLocalMatrixInv();
var worldMatrix = GetLocalMatrix();
var invMatrix = _invLocalMatrix;
var worldMatrix = _localMatrix;
// By doing these all at once we can elide multiple IsValid + GetComponent calls
while (parent.IsValid())
@@ -870,11 +908,11 @@ namespace Robust.Shared.GameObjects
var xform = xformQuery.GetComponent(parent);
worldRot += xform.LocalRotation;
var parentMatrix = xform.GetLocalMatrix();
var parentMatrix = xform._localMatrix;
Matrix3.Multiply(ref worldMatrix, ref parentMatrix, out var result);
worldMatrix = result;
var parentInvMatrix = xform.GetLocalMatrixInv();
var parentInvMatrix = xform._invLocalMatrix;
Matrix3.Multiply(ref parentInvMatrix, ref invMatrix, out var invResult);
invMatrix = invResult;
@@ -1005,16 +1043,6 @@ namespace Robust.Shared.GameObjects
}
}
public Matrix3 GetLocalMatrix()
{
return _localMatrix;
}
public Matrix3 GetLocalMatrixInv()
{
return _invLocalMatrix;
}
private void RebuildMatrices()
{
var pos = _localPosition;

View File

@@ -20,7 +20,6 @@ namespace Robust.Shared.GameObjects
[IoC.Dependency] protected readonly IEntitySystemManager EntitySystemManager = default!;
[IoC.Dependency] private readonly IMapManager _mapManager = default!;
[IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
[IoC.Dependency] private readonly IPauseManager _pauseManager = default!;
#endregion Dependencies
@@ -438,7 +437,7 @@ namespace Robust.Shared.GameObjects
StartEntity(entity);
// If the map we're initializing the entity on is initialized, run map init on it.
if (_pauseManager.IsMapInitialized(mapId))
if (_mapManager.IsMapInitialized(mapId))
entity.RunMapInit();
}
catch (Exception e)

View File

@@ -571,4 +571,47 @@ public partial class EntitySystem
=> new($"Entity {uid} does not have a component of type {typeof(T)}");
#endregion
#region Entity Query
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityQuery<T> GetEntityQuery<T>() where T : Component
{
return EntityManager.GetEntityQuery<T>();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected IEnumerable<TComp1> EntityQuery<TComp1>(bool includePaused = false) where TComp1 : Component
{
return EntityManager.EntityQuery<TComp1>(includePaused);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected IEnumerable<(TComp1, TComp2)> EntityQuery<TComp1, TComp2>(bool includePaused = false)
where TComp1 : Component
where TComp2 : Component
{
return EntityManager.EntityQuery<TComp1, TComp2>(includePaused);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected IEnumerable<(TComp1, TComp2, TComp3)> EntityQuery<TComp1, TComp2, TComp3>(bool includePaused = false)
where TComp1 : Component
where TComp2 : Component
where TComp3 : Component
{
return EntityManager.EntityQuery<TComp1, TComp2, TComp3>(includePaused);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected IEnumerable<(TComp1, TComp2, TComp3, TComp4)> EntityQuery<TComp1, TComp2, TComp3, TComp4>(bool includePaused = false)
where TComp1 : Component
where TComp2 : Component
where TComp3 : Component
where TComp4 : Component
{
return EntityManager.EntityQuery<TComp1, TComp2, TComp3, TComp4>(includePaused);
}
#endregion
}

View File

@@ -3,7 +3,6 @@ using JetBrains.Annotations;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
@@ -172,7 +171,7 @@ namespace Robust.Shared.GameObjects
set
{
if (MetaData is {} metaData)
metaData.EntityName = value;
metaData.EntityDescription = value;
}
}

View File

@@ -0,0 +1,66 @@
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
namespace Robust.Shared.GameObjects;
public sealed class MetaDataSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
public override void Initialize()
{
SubscribeLocalEvent<MetaDataComponent, ComponentHandleState>(OnMetaDataHandle);
SubscribeLocalEvent<MetaDataComponent, ComponentGetState>(OnMetaDataGetState);
}
private void OnMetaDataGetState(EntityUid uid, MetaDataComponent component, ref ComponentGetState args)
{
args.State = new MetaDataComponentState(component._entityName, component._entityDescription, component._entityPrototype?.ID);
}
private void OnMetaDataHandle(EntityUid uid, MetaDataComponent component, ref ComponentHandleState args)
{
if (args.Current is not MetaDataComponentState state)
return;
component._entityName = state.Name;
component._entityDescription = state.Description;
if(state.PrototypeId != null)
component._entityPrototype = _proto.Index<EntityPrototype>(state.PrototypeId);
}
public void AddFlag(EntityUid uid, MetaDataFlags flags, MetaDataComponent? component = null)
{
if (!Resolve(uid, ref component)) return;
component.Flags |= flags;
}
/// <summary>
/// Attempts to remove the specific flag from metadata.
/// Other systems can choose not to allow the removal if it's still relevant.
/// </summary>
public void RemoveFlag(EntityUid uid, MetaDataFlags flags, MetaDataComponent? component = null)
{
if (!Resolve(uid, ref component) ||
(component.Flags & flags) == 0x0) return;
var ev = new MetaFlagRemoveAttemptEvent();
EntityManager.EventBus.RaiseLocalEvent(component.Owner, ref ev);
if (ev.Cancelled) return;
component.Flags &= ~flags;
}
}
/// <summary>
/// Raised if <see cref="MetaDataSystem"/> is trying to remove a particular flag.
/// </summary>
[ByRefEvent]
public struct MetaFlagRemoveAttemptEvent
{
public bool Cancelled = false;
}

View File

@@ -4,7 +4,7 @@ using Robust.Shared.Serialization;
namespace Robust.Shared.GameObjects;
internal abstract class SharedAppearanceSystem : EntitySystem
public abstract class SharedAppearanceSystem : EntitySystem
{
public virtual void MarkDirty(AppearanceComponent component) {}
}

View File

@@ -52,30 +52,25 @@ namespace Robust.Shared.GameObjects
// Just in case there's any deleted we'll ToArray
foreach (var (_, chunk) in gridInternal.GetMapChunks().ToArray())
{
chunk.RegenerateCollision();
gridInternal.RegenerateCollision(chunk);
}
}
internal void RegenerateCollision(MapChunk chunk, List<Box2i> rectangles)
internal void RegenerateCollision(EntityUid gridEuid, MapChunk chunk, List<Box2i> rectangles)
{
if (!_enabled) return;
if (!_mapManager.TryGetGrid(chunk.GridId, out var grid) ||
!EntityManager.EntityExists(grid.GridEntityId)) return;
DebugTools.Assert(chunk.FilledTiles > 0);
var gridEnt = grid.GridEntityId;
DebugTools.Assert(chunk.ValidTiles > 0);
if (!EntityManager.TryGetComponent(gridEnt, out PhysicsComponent? physicsComponent))
if (!EntityManager.TryGetComponent(gridEuid, out PhysicsComponent? physicsComponent))
{
Logger.ErrorS("physics", $"Trying to regenerate collision for {gridEnt} that doesn't have {nameof(physicsComponent)}");
Logger.ErrorS("physics", $"Trying to regenerate collision for {gridEuid} that doesn't have {nameof(physicsComponent)}");
return;
}
if (!EntityManager.TryGetComponent(gridEnt, out FixturesComponent? fixturesComponent))
if (!EntityManager.TryGetComponent(gridEuid, out FixturesComponent? fixturesComponent))
{
Logger.ErrorS("physics", $"Trying to regenerate collision for {gridEnt} that doesn't have {nameof(fixturesComponent)}");
Logger.ErrorS("physics", $"Trying to regenerate collision for {gridEuid} that doesn't have {nameof(fixturesComponent)}");
return;
}
@@ -170,7 +165,7 @@ namespace Robust.Shared.GameObjects
if (updated)
{
_fixtures.FixtureUpdate(fixturesComponent, physicsComponent);
EntityManager.EventBus.RaiseLocalEvent(gridEnt,new GridFixtureChangeEvent {NewFixtures = chunk.Fixtures});
EntityManager.EventBus.RaiseLocalEvent(gridEuid,new GridFixtureChangeEvent {NewFixtures = chunk.Fixtures});
}
}
}

View File

@@ -0,0 +1,105 @@
using Robust.Shared.IoC;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects;
public abstract partial class SharedPhysicsSystem
{
[Dependency] private readonly FixtureSystem _fixtures = default!;
#region Collision Masks & Layers
public void AddCollisionMask(FixturesComponent component, Fixture fixture, int mask)
{
if ((fixture.CollisionMask & mask) == mask) return;
DebugTools.Assert(component.Fixtures.ContainsKey(fixture.ID));
fixture._collisionMask |= mask;
if (TryComp<PhysicsComponent>(component.Owner, out var body))
{
_fixtures.FixtureUpdate(component, body);
}
_broadphaseSystem.Refilter(fixture);
}
public void SetCollisionMask(FixturesComponent component, Fixture fixture, int mask)
{
if (fixture.CollisionMask == mask) return;
DebugTools.Assert(component.Fixtures.ContainsKey(fixture.ID));
fixture._collisionMask = mask;
if (TryComp<PhysicsComponent>(component.Owner, out var body))
{
_fixtures.FixtureUpdate(component, body);
}
_broadphaseSystem.Refilter(fixture);
}
public void RemoveCollisionMask(FixturesComponent component, Fixture fixture, int mask)
{
if ((fixture.CollisionMask & mask) == 0x0) return;
DebugTools.Assert(component.Fixtures.ContainsKey(fixture.ID));
fixture._collisionMask &= ~mask;
if (TryComp<PhysicsComponent>(component.Owner, out var body))
{
_fixtures.FixtureUpdate(component, body);
}
_broadphaseSystem.Refilter(fixture);
}
public void AddCollisionLayer(FixturesComponent component, Fixture fixture, int layer)
{
if ((fixture.CollisionLayer & layer) == layer) return;
DebugTools.Assert(component.Fixtures.ContainsKey(fixture.ID));
fixture._collisionLayer |= layer;
if (TryComp<PhysicsComponent>(component.Owner, out var body))
{
_fixtures.FixtureUpdate(component, body);
}
_broadphaseSystem.Refilter(fixture);
}
public void SetCollisionLayer(FixturesComponent component, Fixture fixture, int layer)
{
if (fixture.CollisionLayer == layer) return;
DebugTools.Assert(component.Fixtures.ContainsKey(fixture.ID));
fixture._collisionLayer = layer;
if (TryComp<PhysicsComponent>(component.Owner, out var body))
{
_fixtures.FixtureUpdate(component, body);
}
_broadphaseSystem.Refilter(fixture);
}
public void RemoveCollisionLayer(FixturesComponent component, Fixture fixture, int layer)
{
if ((fixture.CollisionLayer & layer) == 0x0) return;
DebugTools.Assert(component.Fixtures.ContainsKey(fixture.ID));
fixture._collisionLayer &= ~layer;
if (TryComp<PhysicsComponent>(component.Owner, out var body))
{
_fixtures.FixtureUpdate(component, body);
}
_broadphaseSystem.Refilter(fixture);
}
#endregion
}

View File

@@ -70,7 +70,7 @@ namespace Robust.Shared.GameObjects
IoCManager.Resolve<IIslandManager>().Initialize();
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CVars.AutoClearForces, OnAutoClearChange, true);
configManager.OnValueChanged(CVars.AutoClearForces, OnAutoClearChange);
}
private void HandlePhysicsMapInit(EntityUid uid, SharedPhysicsMapComponent component, ComponentInit args)
@@ -81,6 +81,7 @@ namespace Robust.Shared.GameObjects
component.ContactManager = new();
component.ContactManager.Initialize();
component.ContactManager.MapId = component.MapId;
component.AutoClearForces = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.AutoClearForces);
component.ContactManager.KinematicControllerCollision += KinematicControllerCollision;
}

View File

@@ -0,0 +1,58 @@
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Robust.Shared.Maths;
namespace Robust.Shared.GameObjects;
public abstract partial class SharedTransformSystem
{
#region World Matrix
[Pure]
public Matrix3 GetWorldMatrix(EntityUid uid)
{
return Comp<TransformComponent>(uid).WorldMatrix;
}
// Temporary until it's moved here
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Matrix3 GetWorldMatrix(TransformComponent component)
{
return component.WorldMatrix;
}
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Matrix3 GetWorldMatrix(EntityUid uid, EntityQuery<TransformComponent> xformQuery)
{
return GetWorldMatrix(xformQuery.GetComponent(uid));
}
#endregion
#region Inverse World Matrix
[Pure]
public Matrix3 GetInvWorldMatrix(EntityUid uid)
{
return Comp<TransformComponent>(uid).InvWorldMatrix;
}
// Temporary until it's moved here
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Matrix3 GetInvWorldMatrix(TransformComponent component)
{
return component.InvWorldMatrix;
}
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Matrix3 GetInvWorldMatrix(EntityUid uid, EntityQuery<TransformComponent> xformQuery)
{
return GetInvWorldMatrix(xformQuery.GetComponent(uid));
}
#endregion
}

View File

@@ -7,7 +7,7 @@ using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects
{
public abstract class SharedTransformSystem : EntitySystem
public abstract partial class SharedTransformSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityLookup _entityLookup = default!;
@@ -100,5 +100,29 @@ namespace Robust.Shared.GameObjects
}
}
}
public EntityCoordinates GetMoverCoordinates(TransformComponent xform)
{
// If they're parented directly to the map or grid then just return the coordinates.
if (!_mapManager.TryGetGrid(xform.GridID, out var grid))
{
var mapUid = _mapManager.GetMapEntityId(xform.MapID);
var coordinates = xform.Coordinates;
// Parented directly to the map.
if (xform.ParentUid == mapUid)
return coordinates;
return new EntityCoordinates(mapUid, coordinates.ToMapPos(EntityManager));
}
// Parented directly to the grid
if (grid.GridEntityId == xform.ParentUid)
return xform.Coordinates;
// Parented to grid so convert their pos back to the grid.
var gridPos = Transform(grid.GridEntityId).InvWorldMatrix.Transform(xform.WorldPosition);
return new EntityCoordinates(grid.GridEntityId, gridPos);
}
}
}

View File

@@ -1,30 +1,69 @@
using System;
using System.Collections.Generic;
using System.Text;
using Linguini.Bundle.Errors;
using Linguini.Syntax.Parser.Error;
namespace Robust.Shared.Localization
namespace Robust.Shared.Localization;
internal static class LocHelper
{
internal static class LocHelper
public static string FormatCompileErrors(this ParseError self, ReadOnlyMemory<char> resource,
string? newLine = null)
{
public static string FormatCompileErrors(this ParseError self, ReadOnlyMemory<char> resource)
ErrorSpan span = new(self.Row, self.Slice!.Value.Start.Value, self.Slice.Value.End.Value,
self.Position.Start.Value, self.Position.End.Value);
return FormatErrors(self.Message, span, resource, newLine);
}
private static string FormatErrors(string message, ErrorSpan span, ReadOnlyMemory<char> resource, string? newLine)
{
var sb = new StringBuilder();
var errContext = resource.Slice(span.StartSpan, span.EndSpan - span.StartSpan).ToString();
var lines = new List<ReadOnlyMemory<char>>(5);
var currLineOffset = 0;
var lastStart = 0;
for (var i = 0; i < span.StartMark - span.StartSpan; i++)
{
ErrorSpan span = new(self.Row, self.Slice!.Value.Start.Value, self.Slice.Value.End.Value,
self.Position.Start.Value, self.Position.End.Value);
return FormatErrors(self.Message, span, resource);
switch (errContext[i])
{
// Reset current line so that mark aligns with the reported error
// We cheat here a bit, since we both `\r\n` and `\n` end with '\n'
case '\n':
if (i > 0 && errContext[i - 1] == '\r')
{
lines.Add(resource.Slice(lastStart, currLineOffset - 1));
}
else
{
lines.Add(resource.Slice(lastStart, currLineOffset));
}
lastStart = currLineOffset + 1;
currLineOffset = 0;
break;
default:
currLineOffset++;
break;
}
}
private static string FormatErrors(string message, ErrorSpan span, ReadOnlyMemory<char> resource)
lines.Add(resource.Slice(lastStart, resource.Length - lastStart));
var lastLine = $"{span.Row + lines.Count - 1}".Length;
for (var index = 0; index < lines.Count; index++)
{
var sb = new StringBuilder();
var row = $" {span.Row} ";
var errContext = resource.Slice(span.StartSpan, span.EndSpan - span.StartSpan).ToString();
sb.Append(row).Append('|')
.AppendLine(errContext);
sb.Append(' ', row.Length).Append('|')
.Append(' ', span.StartMark - span.StartSpan - 1).Append('^', span.EndMark - span.StartMark)
.AppendLine($" {message}");
return sb.ToString();
var line = lines[index];
sb.Append(newLine ?? Environment.NewLine).Append(' ').Append($"{span.Row + index}".PadLeft(lastLine))
.Append(" |").Append(line);
}
sb.Append(newLine ?? Environment.NewLine)
.Append(' ', currLineOffset + lastLine + 3)
.Append('^', span.EndMark - span.StartMark)
.Append($" {message}");
return sb.ToString();
}
}

View File

@@ -47,7 +47,7 @@ namespace Robust.Shared.Localization
if (!TryGetString(messageId, out var msg))
{
_logSawmill.Warning("Unknown messageId ({culture}): {messageId}", _defaultCulture.Name, messageId);
_logSawmill.Debug("Unknown messageId ({culture}): {messageId}", _defaultCulture.Name, messageId);
msg = messageId;
}
@@ -62,7 +62,7 @@ namespace Robust.Shared.Localization
if (!TryGetString(messageId, out var msg, args0))
{
_logSawmill.Warning("Unknown messageId ({culture}): {messageId}", _defaultCulture.Name, messageId);
_logSawmill.Debug("Unknown messageId ({culture}): {messageId}", _defaultCulture.Name, messageId);
msg = messageId;
}

View File

@@ -15,7 +15,7 @@ namespace Robust.Shared.Map
/// <param name="chunk"></param>
/// <param name="bounds">The overall bounds that covers every rectangle.</param>
/// <param name="rectangles">Each individual rectangle comprising the chunk's bounds</param>
public static void PartitionChunk(IMapChunk chunk, out Box2i bounds, out List<Box2i> rectangles)
public static void PartitionChunk(MapChunk chunk, out Box2i bounds, out List<Box2i> rectangles)
{
rectangles = new List<Box2i>();

View File

@@ -1,4 +1,6 @@
using System;
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
namespace Robust.Shared.Map
@@ -13,6 +15,14 @@ namespace Robust.Shared.Map
internal readonly int Value;
/// <summary>
/// Constructs a new instance of <see cref="GridId"/>.
/// </summary>
/// <remarks>
/// This should NOT be used in regular code, and is only public for special/legacy
/// cases. Generally you should only use this for parsing a GridId in console commands
/// and immediately check if the grid actually exists in the <see cref="IMapManager"/>.
/// </remarks>
public GridId(int value)
{
Value = value;
@@ -57,6 +67,30 @@ namespace Robust.Shared.Map
return self.Value;
}
/// <summary>
/// <see cref="GridId"/> is an alias of the <see cref="EntityUid"/> that
/// holds the <see cref="IMapGridComponent"/>, so it can be implicitly converted.
/// </summary>
public static implicit operator EntityUid(GridId self)
{
// If this throws, you are either using an unallocated gridId,
// or using it after the grid was freed. Both of these are bugs.
return IoCManager.Resolve<IMapManager>().GetGridEuid(self);
}
/// <summary>
/// <see cref="GridId"/> is an alias of the <see cref="EntityUid"/> that
/// holds the <see cref="IMapGridComponent"/>.
/// </summary>
public static implicit operator GridId(EntityUid euid)
{
// If this throws, you are using an EntityUid that isn't a grid.
// This would raise the question, "Why does your code think this entity is a grid?".
// Grid-ness is defined by the entity having an IMapGridComponent,
// was the component removed without you knowing?
return IoCManager.Resolve<IMapManager>().GetGridComp(euid).GridIndex;
}
public override string ToString()
{
return Value.ToString();

View File

@@ -1,110 +0,0 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Robust.Shared.Map
{
/// <summary>
/// A square section of a <see cref="IMapGrid"/>.
/// </summary>
internal interface IMapChunk : IEnumerable<TileRef>
{
/// <summary>
/// The number of tiles per side of the square chunk.
/// </summary>
ushort ChunkSize { get; }
/// <summary>
/// The X index of this chunk inside the <see cref="IMapGrid"/>.
/// </summary>
int X { get; }
/// <summary>
/// The Y index of this chunk inside the <see cref="IMapGrid"/>.
/// </summary>
int Y { get; }
/// <summary>
/// The positional indices of this chunk in the <see cref="IMapGrid"/>.
/// </summary>
Vector2i Indices { get; }
/// <summary>
/// Returns the tile at the given indices.
/// </summary>
/// <param name="xIndex">The X tile index relative to the chunk origin.</param>
/// <param name="yIndex">The Y tile index relative to the chunk origin.</param>
/// <returns>A reference to a tile.</returns>
TileRef GetTileRef(ushort xIndex, ushort yIndex);
/// <summary>
/// Returns the tile reference at the given indices.
/// </summary>
/// <param name="indices">The tile indices relative to the chunk origin.</param>
/// <returns>A reference to a tile.</returns>
TileRef GetTileRef(Vector2i indices);
Tile GetTile(ushort xIndex, ushort yIndex);
/// <summary>
/// Returns all of the tiles in the chunk, while optionally filtering empty files.
/// Returned order is guaranteed to be row-major.
/// </summary>
/// <param name="ignoreEmpty">Will empty (space) tiles be added to the collection?</param>
/// <returns></returns>
IEnumerable<TileRef> GetAllTiles(bool ignoreEmpty = true);
/// <summary>
/// Replaces a single tile inside of the chunk.
/// </summary>
/// <param name="xIndex">The X tile index relative to the chunk.</param>
/// <param name="yIndex">The Y tile index relative to the chunk.</param>
/// <param name="tile">The new tile to insert.</param>
void SetTile(ushort xIndex, ushort yIndex, Tile tile);
/// <summary>
/// Transforms Tile indices relative to the grid into tile indices relative to this chunk.
/// </summary>
/// <param name="gridTile">Tile indices relative to the grid.</param>
/// <returns>Tile indices relative to this chunk.</returns>
Vector2i GridTileToChunkTile(Vector2i gridTile);
/// <summary>
/// Translates chunk tile indices to grid tile indices.
/// </summary>
/// <param name="chunkTile">The indices relative to the chunk origin.</param>
/// <returns>The indices relative to the grid origin.</returns>
Vector2i ChunkTileToGridTile(Vector2i chunkTile);
IEnumerable<EntityUid> GetSnapGridCell(ushort xCell, ushort yCell);
void AddToSnapGridCell(ushort xCell, ushort yCell, EntityUid euid);
void RemoveFromSnapGridCell(ushort xCell, ushort yCell, EntityUid euid);
IEnumerable<EntityUid> GetAllAnchoredEnts();
/// <summary>
/// Like <see cref="GetAllAnchoredEnts"/>... but fast.
/// </summary>
void FastGetAllAnchoredEnts(EntityUidQueryCallback callback);
Box2i CalcLocalBounds();
// TODO: We can rely on the fixture instead for the bounds in the future but we also need to
// update the rendering to account for it. Better working on accurate grid bounds after rotation IMO.
/// <summary>
/// Calculate the bounds of this chunk.
/// </summary>
Box2Rotated CalcWorldBounds(Vector2? worldPos = null, Angle? worldRot = null);
/// <summary>
/// Calculate the AABB for this chunk.
/// </summary>
Box2 CalcWorldAABB(Vector2? worldPos = null, Angle? worldRot = null);
/// <summary>
/// Tests if a point is on top of a non-empty tile.
/// </summary>
/// <param name="localIndices">Local tile indices</param>
bool CollidesWithChunk(Vector2i localIndices);
}
}

View File

@@ -1,22 +0,0 @@
using System.Collections.Generic;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Timing;
namespace Robust.Shared.Map
{
/// <inheritdoc />
internal interface IMapChunkInternal : IMapChunk
{
List<Fixture> Fixtures { get; set; }
bool SuppressCollisionRegeneration { get; set; }
void RegenerateCollision();
/// <summary>
/// The last game simulation tick that a tile on this chunk was modified.
/// </summary>
GameTick LastTileModifiedTick { get; }
}
}

View File

@@ -1,25 +1,15 @@
using System.Collections.Generic;
using System.Text;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Robust.Shared.Map
{
internal interface IMapGridInternal : IMapGrid
{
GameTick LastTileModifiedTick { get; }
GameTick CurTick { get; }
/// <summary>
/// The total number of chunks contained on this grid.
/// </summary>
int ChunkCount { get; }
void NotifyTileChanged(in TileRef tileRef, in Tile oldTile);
void UpdateAABB();
/// <summary>
/// Returns the chunk at the given indices. If the chunk does not exist,
/// then a new one is generated that is filled with empty space.
@@ -27,7 +17,7 @@ namespace Robust.Shared.Map
/// <param name="xIndex">The X index of the chunk in this grid.</param>
/// <param name="yIndex">The Y index of the chunk in this grid.</param>
/// <returns>The existing or new chunk.</returns>
IMapChunkInternal GetChunk(int xIndex, int yIndex);
MapChunk GetChunk(int xIndex, int yIndex);
/// <summary>
/// Removes the chunk with the specified origin.
@@ -40,7 +30,7 @@ namespace Robust.Shared.Map
/// </summary>
/// <param name="chunkIndices">The indices of the chunk in this grid.</param>
/// <returns>The existing or new chunk.</returns>
IMapChunkInternal GetChunk(Vector2i chunkIndices);
MapChunk GetChunk(Vector2i chunkIndices);
/// <summary>
/// Returns whether a chunk exists with the specified indices.
@@ -51,13 +41,35 @@ namespace Robust.Shared.Map
/// Returns all chunks in this grid. This will not generate new chunks.
/// </summary>
/// <returns>All chunks in the grid.</returns>
IReadOnlyDictionary<Vector2i, IMapChunkInternal> GetMapChunks();
IReadOnlyDictionary<Vector2i, MapChunk> GetMapChunks();
/// <summary>
/// Returns all the <see cref="IMapChunkInternal"/> intersecting the worldAABB.
/// Returns all the <see cref="MapChunk"/> intersecting the worldAABB.
/// </summary>
void GetMapChunks(Box2 worldAABB, out MapGrid.ChunkEnumerator enumerator);
/// <summary>
/// Returns all the <see cref="MapChunk"/> intersecting the rotated world box.
/// </summary>
void GetMapChunks(Box2Rotated worldArea, out MapGrid.ChunkEnumerator enumerator);
/// <summary>
/// Regenerates the chunk local bounds of this chunk.
/// </summary>
void RegenerateCollision(MapChunk mapChunk);
/// <summary>
/// Calculate the world space AABB for this chunk.
/// </summary>
Box2 CalcWorldAABB(MapChunk mapChunk);
/// <summary>
/// Returns the tile at the given chunk indices.
/// </summary>
/// <param name="mapChunk"></param>
/// <param name="xIndex">The X tile index relative to the chunk origin.</param>
/// <param name="yIndex">The Y tile index relative to the chunk origin.</param>
/// <returns>A reference to a tile.</returns>
TileRef GetTileRef(MapChunk mapChunk, ushort xIndex, ushort yIndex);
}
}

View File

@@ -3,13 +3,14 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Robust.Shared.Map
{
/// <summary>
/// This manages all of the grids in the world.
/// </summary>
public interface IMapManager
public interface IMapManager : IPauseManager
{
/// <summary>
/// The default <see cref="MapId" /> that is always available. Equivalent to SS13 Null space.

View File

@@ -14,7 +14,7 @@ namespace Robust.Shared.Map
void OnComponentRemoved(MapGridComponent comp);
void ChunkRemoved(MapChunk chunk);
void ChunkRemoved(GridId gridId, MapChunk chunk);
/// <summary>
/// Raises the OnTileChanged event.

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Timing;
@@ -10,48 +8,63 @@ using Robust.Shared.Utility;
namespace Robust.Shared.Map
{
/// <inheritdoc />
internal sealed class MapChunk : IMapChunkInternal
/// <summary>
/// A square section of a <see cref="IMapGrid"/>.
/// </summary>
internal sealed class MapChunk
{
/// <summary>
/// New SnapGrid cells are allocated with this capacity.
/// </summary>
private const int SnapCellStartingCapacity = 1;
public GridId GridId => _grid.Index;
private readonly IMapGridInternal _grid;
private readonly Vector2i _gridIndices;
private readonly Tile[,] _tiles;
private readonly SnapGridCell[,] _snapGrid;
// We'll keep a running count of how many tiles are non-empty.
// If this ever hits 0 then we know the chunk can be deleted.
// The alternative is that every time we SetTile we iterate every tile in the chunk.
internal int ValidTiles { get; private set; }
/// <summary>
/// Invoked when a tile is modified on this chunk.
/// </summary>
public event TileModifiedDelegate? TileModified;
private Box2i _cachedBounds;
/// <summary>
/// Keeps a running count of the number of filled tiles in this chunk.
/// </summary>
/// <remarks>
/// This will always be between 1 and <see cref="ChunkSize"/>^2.
/// </remarks>
internal int FilledTiles { get; private set; }
public List<Fixture> Fixtures { get; set; } = new();
/// <summary>
/// Chunk-local AABB of this chunk.
/// </summary>
public Box2i CachedBounds { get; set; }
/// <inheritdoc />
public GameTick LastTileModifiedTick { get; private set; }
/// <summary>
/// Physics fixtures that make up this grid chunk.
/// </summary>
public List<Fixture> Fixtures { get; } = new();
/// <inheritdoc />
public GameTick LastAnchoredModifiedTick { get; set; }
/// <summary>
/// The last game simulation tick that a tile on this chunk was modified.
/// </summary>
public GameTick LastTileModifiedTick { get; set; }
/// <summary>
/// Setting this property to <see langword="true"/> suppresses collision regeneration on the chunk until the
/// property is set to <see langword="false"/>.
/// </summary>
public bool SuppressCollisionRegeneration { get; set; }
/// <summary>
/// Constructs an instance of a MapGrid chunk.
/// </summary>
/// <param name="grid"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="chunkSize"></param>
public MapChunk(IMapGridInternal grid, int x, int y, ushort chunkSize)
public MapChunk(int x, int y, ushort chunkSize)
{
_grid = grid;
LastTileModifiedTick = grid.CurTick;
_gridIndices = new Vector2i(x, y);
ChunkSize = chunkSize;
@@ -59,42 +72,33 @@ namespace Robust.Shared.Map
_snapGrid = new SnapGridCell[ChunkSize, ChunkSize];
}
/// <inheritdoc />
/// <summary>
/// The number of tiles per side of the square chunk.
/// </summary>
public ushort ChunkSize { get; }
/// <inheritdoc />
/// <summary>
/// The X index of this chunk inside the <see cref="IMapGrid"/>.
/// </summary>
public int X => _gridIndices.X;
/// <inheritdoc />
/// <summary>
/// The Y index of this chunk inside the <see cref="IMapGrid"/>.
/// </summary>
public int Y => _gridIndices.Y;
/// <inheritdoc />
/// <summary>
/// The positional indices of this chunk in the <see cref="IMapGrid"/>.
/// </summary>
public Vector2i Indices => _gridIndices;
/// <inheritdoc />
public TileRef GetTileRef(ushort xIndex, ushort yIndex)
{
if (xIndex >= ChunkSize)
throw new ArgumentOutOfRangeException(nameof(xIndex), "Tile indices out of bounds.");
if (yIndex >= ChunkSize)
throw new ArgumentOutOfRangeException(nameof(yIndex), "Tile indices out of bounds.");
var indices = ChunkTileToGridTile(new Vector2i(xIndex, yIndex));
return new TileRef(_grid.ParentMapId, _grid.Index, indices, _tiles[xIndex, yIndex]);
}
/// <inheritdoc />
public TileRef GetTileRef(Vector2i indices)
{
if (indices.X >= ChunkSize || indices.X < 0 || indices.Y >= ChunkSize || indices.Y < 0)
throw new ArgumentOutOfRangeException(nameof(indices), "Tile indices out of bounds.");
var chunkIndices = ChunkTileToGridTile(indices);
return new TileRef(_grid.ParentMapId, _grid.Index, chunkIndices, _tiles[indices.X, indices.Y]);
}
/// <inheritdoc />
/// <summary>
/// Returns the tile at the given chunk indices.
/// </summary>
/// <param name="xIndex">The X tile index relative to the chunk origin.</param>
/// <param name="yIndex">The Y tile index relative to the chunk origin.</param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException">The index is less than or greater than the size of the chunk.</exception>
public Tile GetTile(ushort xIndex, ushort yIndex)
{
if (xIndex >= ChunkSize)
@@ -106,23 +110,12 @@ namespace Robust.Shared.Map
return _tiles[xIndex, yIndex];
}
/// <inheritdoc />
public IEnumerable<TileRef> GetAllTiles(bool ignoreEmpty = true)
{
for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
if (ignoreEmpty && _tiles[x, y].IsEmpty)
continue;
var indices = ChunkTileToGridTile(new Vector2i(x, y));
yield return new TileRef(_grid.ParentMapId, _grid.Index, indices.X, indices.Y, _tiles[x, y]);
}
}
}
/// <inheritdoc />
/// <summary>
/// Replaces a single tile inside of the chunk.
/// </summary>
/// <param name="xIndex">The X tile index relative to the chunk.</param>
/// <param name="yIndex">The Y tile index relative to the chunk.</param>
/// <param name="tile">The new tile to insert.</param>
public void SetTile(ushort xIndex, ushort yIndex, Tile tile)
{
if (xIndex >= ChunkSize)
@@ -132,81 +125,58 @@ namespace Robust.Shared.Map
throw new ArgumentOutOfRangeException(nameof(yIndex), "Tile indices out of bounds.");
// same tile, no point to continue
if (_tiles[xIndex, yIndex].TypeId == tile.TypeId)
if (_tiles[xIndex, yIndex] == tile)
return;
var oldIsEmpty = _tiles[xIndex, yIndex].IsEmpty;
var oldValidTiles = ValidTiles;
var oldTile = _tiles[xIndex, yIndex];
var oldFilledTiles = FilledTiles;
if (oldIsEmpty != tile.IsEmpty)
if (oldTile.IsEmpty != tile.IsEmpty)
{
if (oldIsEmpty)
if (oldTile.IsEmpty)
{
ValidTiles += 1;
FilledTiles += 1;
}
else
{
ValidTiles -= 1;
FilledTiles -= 1;
}
}
DebugTools.Assert(ValidTiles >= 0);
var gridTile = ChunkTileToGridTile(new Vector2i(xIndex, yIndex));
var newTileRef = new TileRef(_grid.ParentMapId, _grid.Index, gridTile, tile);
var oldTile = _tiles[xIndex, yIndex];
LastTileModifiedTick = _grid.CurTick;
var shapeChanged = oldFilledTiles != FilledTiles;
DebugTools.Assert(FilledTiles >= 0);
_tiles[xIndex, yIndex] = tile;
// As the collision regeneration can potentially delete the chunk we'll notify of the tile changed first.
_grid.NotifyTileChanged(newTileRef, oldTile);
if (!SuppressCollisionRegeneration && oldValidTiles != ValidTiles)
{
RegenerateCollision();
}
var tileIndices = new Vector2i(xIndex, yIndex);
TileModified?.Invoke(this, tileIndices, tile, oldTile, shapeChanged);
}
/// <summary>
/// Returns an enumerator that iterates through all grid tiles.
/// Transforms Tile indices relative to the grid into tile indices relative to this chunk.
/// </summary>
/// <returns></returns>
public IEnumerator<TileRef> GetEnumerator()
{
for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
if (_tiles[x, y].IsEmpty)
continue;
var gridTile = ChunkTileToGridTile(new Vector2i(x, y));
yield return new TileRef(_grid.ParentMapId, _grid.Index, gridTile.X, gridTile.Y, _tiles[x, y]);
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc />
/// <param name="gridTile">Tile indices relative to the grid.</param>
/// <returns>Tile indices relative to this chunk.</returns>
public Vector2i GridTileToChunkTile(Vector2i gridTile)
{
var size = ChunkSize;
var x = MathHelper.Mod(gridTile.X, size);
var y = MathHelper.Mod(gridTile.Y, size);
var x = MathHelper.Mod(gridTile.X, ChunkSize);
var y = MathHelper.Mod(gridTile.Y, ChunkSize);
return new Vector2i(x, y);
}
/// <inheritdoc />
/// <summary>
/// Translates chunk tile indices to grid tile indices.
/// </summary>
/// <param name="chunkTile">The indices relative to the chunk origin.</param>
/// <returns>The indices relative to the grid origin.</returns>
public Vector2i ChunkTileToGridTile(Vector2i chunkTile)
{
return chunkTile + _gridIndices * ChunkSize;
}
/// <inheritdoc />
/// <summary>
/// Returns the anchored cell at the given tile indices.
/// </summary>
public IEnumerable<EntityUid> GetSnapGridCell(ushort xCell, ushort yCell)
{
if (xCell >= ChunkSize)
@@ -226,7 +196,9 @@ namespace Robust.Shared.Map
return list;
}
/// <inheritdoc />
/// <summary>
/// Adds an entity to the anchor cell at the given tile indices.
/// </summary>
public void AddToSnapGridCell(ushort xCell, ushort yCell, EntityUid euid)
{
if (xCell >= ChunkSize)
@@ -240,10 +212,11 @@ namespace Robust.Shared.Map
DebugTools.Assert(!cell.Center.Contains(euid));
cell.Center.Add(euid);
LastAnchoredModifiedTick = _grid.CurTick;
}
/// <inheritdoc />
/// <summary>
/// Removes an entity from the anchor cell at the given tile indices.
/// </summary>
public void RemoveFromSnapGridCell(ushort xCell, ushort yCell, EntityUid euid)
{
if (xCell >= ChunkSize)
@@ -254,93 +227,6 @@ namespace Robust.Shared.Map
ref var cell = ref _snapGrid[xCell, yCell];
cell.Center?.Remove(euid);
LastAnchoredModifiedTick = _grid.CurTick;
}
public IEnumerable<EntityUid> GetAllAnchoredEnts()
{
foreach (var cell in _snapGrid)
{
if (cell.Center is null)
continue;
foreach (var euid in cell.Center)
{
yield return euid;
}
}
}
public void FastGetAllAnchoredEnts(EntityUidQueryCallback callback)
{
foreach (var cell in _snapGrid)
{
if (cell.Center is null)
continue;
foreach (var euid in cell.Center)
{
callback(euid);
}
}
}
public bool SuppressCollisionRegeneration { get; set; }
public void RegenerateCollision()
{
// Even if the chunk is still removed still need to make sure bounds are updated (for now...)
if (ValidTiles == 0)
{
var grid = (IMapGridInternal) IoCManager.Resolve<IMapManager>().GetGrid(GridId);
grid.RemoveChunk(_gridIndices);
}
// generate collision rects
GridChunkPartition.PartitionChunk(this, out _cachedBounds, out var rectangles);
_grid.UpdateAABB();
// TryGet because unit tests YAY
if (ValidTiles > 0 && EntitySystem.TryGet(out SharedGridFixtureSystem? system))
system.RegenerateCollision(this, rectangles);
}
/// <inheritdoc />
public Box2i CalcLocalBounds()
{
return _cachedBounds;
}
public Box2Rotated CalcWorldBounds(Vector2? gridPos = null, Angle? gridRot = null)
{
gridRot ??= _grid.WorldRotation;
gridPos ??= _grid.WorldPosition;
var worldPos = gridPos.Value + gridRot.Value.RotateVec(Indices * _grid.TileSize * ChunkSize);
var localBounds = CalcLocalBounds();
var ts = _grid.TileSize;
var scaledLocalBounds = new Box2Rotated(new Box2(
localBounds.Left * ts,
localBounds.Bottom * ts,
localBounds.Right * ts,
localBounds.Top * ts).Translated(worldPos), gridRot.Value, worldPos);
return scaledLocalBounds;
}
public Box2 CalcWorldAABB(Vector2? gridPos = null, Angle? gridRot = null)
{
var bounds = CalcWorldBounds(gridPos, gridRot);
return bounds.CalcBoundingBox();
}
/// <inheritdoc />
public bool CollidesWithChunk(Vector2i localIndices)
{
return _tiles[localIndices.X, localIndices.Y].TypeId != Tile.Empty.TypeId;
}
/// <inheritdoc />
@@ -355,18 +241,13 @@ namespace Robust.Shared.Map
}
}
internal sealed class RegenerateChunkCollisionEvent : EntityEventArgs
{
public MapChunk Chunk { get; }
public RegenerateChunkCollisionEvent(MapChunk chunk)
{
Chunk = chunk;
}
}
internal sealed class ChunkRemovedEvent : EntityEventArgs
{
public MapChunk Chunk = default!;
}
/// <summary>
/// Event delegate for <see cref="MapChunk.TileModified"/>.
/// </summary>
/// <param name="mapChunk">Chunk that the tile was on.</param>
/// <param name="tileIndices">hunk Indices of the tile that was modified.</param>
/// <param name="newTile">New version of the tile.</param>
/// <param name="oldTile">Old version of the tile.</param>
/// <param name="chunkShapeChanged">If changing this tile changed the shape of the chunk.</param>
internal delegate void TileModifiedDelegate(MapChunk mapChunk, Vector2i tileIndices, Tile newTile, Tile oldTile, bool chunkShapeChanged);
}

View File

@@ -27,9 +27,6 @@ namespace Robust.Shared.Map
[ViewVariables]
public GameTick LastTileModifiedTick { get; private set; }
/// <inheritdoc />
public GameTick CurTick => _mapManager.GameTiming.CurTick;
/// <inheritdoc />
[ViewVariables]
public MapId ParentMapId { get; set; }
@@ -45,7 +42,7 @@ namespace Robust.Shared.Map
/// <summary>
/// Grid chunks than make up this grid.
/// </summary>
private readonly Dictionary<Vector2i, IMapChunkInternal> _chunks = new();
private readonly Dictionary<Vector2i, MapChunk> _chunks = new();
private readonly IMapManagerInternal _mapManager;
private readonly IEntityManager _entityManager;
@@ -59,7 +56,6 @@ namespace Robust.Shared.Map
/// <param name="entityManager"></param>
/// <param name="gridIndex">Index identifier of this grid.</param>
/// <param name="chunkSize">The dimension of this square chunk.</param>
/// <param name="snapSize">Distance in world units between the lines on the conceptual snap grid.</param>
/// <param name="parentMapId">Parent map identifier.</param>
internal MapGrid(IMapManagerInternal mapManager, IEntityManager entityManager, GridId gridIndex, ushort chunkSize, MapId parentMapId)
{
@@ -82,8 +78,6 @@ namespace Robust.Shared.Map
[ViewVariables]
public Box2 LocalBounds { get; private set; }
public bool SuppressCollisionRegeneration { get; set; }
/// <inheritdoc />
[ViewVariables]
public ushort ChunkSize { get; }
@@ -168,40 +162,6 @@ namespace Robust.Shared.Map
}
}
/// <summary>
/// Expands the AABB for this grid when a new tile is added. If the tile is already inside the existing AABB,
/// nothing happens. If it is outside, the AABB is expanded to fit the new tile.
/// </summary>
public void UpdateAABB()
{
LocalBounds = new Box2();
foreach (var chunk in _chunks.Values)
{
var chunkBounds = chunk.CalcLocalBounds();
if(chunkBounds.Size.Equals(Vector2i.Zero))
continue;
if (LocalBounds.Size == Vector2.Zero)
{
var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize);
LocalBounds = gridBounds;
}
else
{
var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize);
LocalBounds = LocalBounds.Union(gridBounds);
}
}
}
/// <inheritdoc />
public void NotifyTileChanged(in TileRef tileRef, in Tile oldTile)
{
LastTileModifiedTick = _mapManager.GameTiming.CurTick;
_mapManager.RaiseOnTileChanged(tileRef, oldTile);
}
#region TileAccess
/// <inheritdoc />
@@ -228,7 +188,26 @@ namespace Robust.Shared.Map
}
var chunkTileIndices = output.GridTileToChunkTile(tileCoordinates);
return output.GetTileRef((ushort)chunkTileIndices.X, (ushort)chunkTileIndices.Y);
return GetTileRef(output, (ushort)chunkTileIndices.X, (ushort)chunkTileIndices.Y);
}
/// <summary>
/// Returns the tile at the given chunk indices.
/// </summary>
/// <param name="mapChunk"></param>
/// <param name="xIndex">The X tile index relative to the chunk origin.</param>
/// <param name="yIndex">The Y tile index relative to the chunk origin.</param>
/// <returns>A reference to a tile.</returns>
public TileRef GetTileRef(MapChunk mapChunk, ushort xIndex, ushort yIndex)
{
if (xIndex >= mapChunk.ChunkSize)
throw new ArgumentOutOfRangeException(nameof(xIndex), "Tile indices out of bounds.");
if (yIndex >= mapChunk.ChunkSize)
throw new ArgumentOutOfRangeException(nameof(yIndex), "Tile indices out of bounds.");
var indices = mapChunk.ChunkTileToGridTile(new Vector2i(xIndex, yIndex));
return new TileRef(ParentMapId, Index, indices, mapChunk.GetTile(xIndex, yIndex));
}
/// <inheritdoc />
@@ -236,10 +215,19 @@ namespace Robust.Shared.Map
{
foreach (var kvChunk in _chunks)
{
foreach (var tileRef in kvChunk.Value)
var chunk = kvChunk.Value;
for (ushort x = 0; x < ChunkSize; x++)
{
if (!ignoreSpace || !tileRef.Tile.IsEmpty)
yield return tileRef;
for (ushort y = 0; y < ChunkSize; y++)
{
var tile = chunk.GetTile(x, y);
if (ignoreSpace && tile.IsEmpty)
continue;
var (gridX, gridY) = new Vector2i(x, y) + chunk.Indices * ChunkSize;
yield return new TileRef(ParentMapId, Index, gridX, gridY, tile);
}
}
}
}
@@ -261,7 +249,7 @@ namespace Robust.Shared.Map
/// <inheritdoc />
public void SetTiles(List<(Vector2i GridIndices, Tile Tile)> tiles)
{
var chunks = new HashSet<IMapChunkInternal>();
var chunks = new HashSet<MapChunk>();
foreach (var (gridIndices, tile) in tiles)
{
@@ -274,10 +262,11 @@ namespace Robust.Shared.Map
foreach (var chunk in chunks)
{
chunk.SuppressCollisionRegeneration = false;
chunk.RegenerateCollision();
RegenerateCollision(chunk);
}
}
/// <inheritdoc />
public IEnumerable<TileRef> GetTilesIntersecting(Box2Rotated worldArea, bool ignoreEmpty = true,
Predicate<TileRef>? predicate = null)
{
@@ -316,7 +305,7 @@ namespace Robust.Shared.Map
if (_chunks.TryGetValue(gridChunk, out var chunk))
{
var chunkTile = chunk.GridTileToChunkTile(new Vector2i(x, y));
var tile = chunk.GetTileRef((ushort)chunkTile.X, (ushort)chunkTile.Y);
var tile = GetTileRef(chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y);
if (ignoreEmpty && tile.Tile.IsEmpty)
continue;
@@ -369,18 +358,19 @@ namespace Robust.Shared.Map
public int ChunkCount => _chunks.Count;
/// <inheritdoc />
public IMapChunkInternal GetChunk(int xIndex, int yIndex)
public MapChunk GetChunk(int xIndex, int yIndex)
{
return GetChunk(new Vector2i(xIndex, yIndex));
}
/// <inheritdoc />
public void RemoveChunk(Vector2i origin)
{
if (!_chunks.TryGetValue(origin, out var chunk)) return;
_chunks.Remove(origin);
_mapManager.ChunkRemoved((MapChunk) chunk);
_mapManager.ChunkRemoved(Index, chunk);
if (_chunks.Count == 0)
{
@@ -389,12 +379,15 @@ namespace Robust.Shared.Map
}
/// <inheritdoc />
public IMapChunkInternal GetChunk(Vector2i chunkIndices)
public MapChunk GetChunk(Vector2i chunkIndices)
{
if (_chunks.TryGetValue(chunkIndices, out var output))
return output;
return _chunks[chunkIndices] = new MapChunk(this, chunkIndices.X, chunkIndices.Y, ChunkSize);
var newChunk = new MapChunk(chunkIndices.X, chunkIndices.Y, ChunkSize);
newChunk.LastTileModifiedTick = _mapManager.GameTiming.CurTick;
newChunk.TileModified += OnTileModified;
return _chunks[chunkIndices] = newChunk;
}
/// <inheritdoc />
@@ -404,21 +397,21 @@ namespace Robust.Shared.Map
}
/// <inheritdoc />
public IReadOnlyDictionary<Vector2i, IMapChunkInternal> GetMapChunks()
public IReadOnlyDictionary<Vector2i, MapChunk> GetMapChunks()
{
return _chunks;
}
internal struct ChunkEnumerator
{
private Dictionary<Vector2i, IMapChunkInternal> _chunks;
private Dictionary<Vector2i, MapChunk> _chunks;
private Vector2i _chunkLB;
private Vector2i _chunkRT;
private int _xIndex;
private int _yIndex;
internal ChunkEnumerator(Dictionary<Vector2i, IMapChunkInternal> chunks, Box2 localAABB, int chunkSize)
internal ChunkEnumerator(Dictionary<Vector2i, MapChunk> chunks, Box2 localAABB, int chunkSize)
{
_chunks = chunks;
@@ -429,7 +422,7 @@ namespace Robust.Shared.Map
_yIndex = _chunkLB.Y;
}
public bool MoveNext([NotNullWhen(true)] out IMapChunkInternal? chunk)
public bool MoveNext([NotNullWhen(true)] out MapChunk? chunk)
{
if (_yIndex > _chunkRT.Y)
{
@@ -456,12 +449,14 @@ namespace Robust.Shared.Map
}
}
/// <inheritdoc />
public void GetMapChunks(Box2 worldAABB, out ChunkEnumerator enumerator)
{
var localArea = InvWorldMatrix.TransformBox(worldAABB);
enumerator = new ChunkEnumerator(_chunks, localArea, ChunkSize);
}
/// <inheritdoc />
public void GetMapChunks(Box2Rotated worldArea, out ChunkEnumerator enumerator)
{
var matrix = InvWorldMatrix;
@@ -583,7 +578,7 @@ namespace Robust.Shared.Map
RemoveFromSnapGridCell(TileIndicesFor(coords), euid);
}
private (IMapChunkInternal, Vector2i) ChunkAndOffsetForTile(Vector2i pos)
private (MapChunk, Vector2i) ChunkAndOffsetForTile(Vector2i pos)
{
var gridChunkIndices = GridTileToChunkIndices(pos);
var chunk = GetChunk(gridChunkIndices);
@@ -756,7 +751,7 @@ namespace Robust.Shared.Map
return false;
var cTileIndices = chunk.GridTileToChunkTile(indices);
return chunk.CollidesWithChunk(cTileIndices);
return chunk.GetTile((ushort) cTileIndices.X, (ushort) cTileIndices.Y).TypeId != Tile.Empty.TypeId;
}
/// <inheritdoc />
@@ -798,7 +793,7 @@ namespace Robust.Shared.Map
}
var cTileIndices = chunk.GridTileToChunkTile(indices);
tile = chunk.GetTileRef(cTileIndices);
tile = GetTileRef(chunk, (ushort)cTileIndices.X, (ushort)cTileIndices.Y);
return true;
}
@@ -815,6 +810,81 @@ namespace Robust.Shared.Map
}
#endregion Transforms
/// <summary>
/// Regenerates the chunk local bounds of this chunk.
/// </summary>
public void RegenerateCollision(MapChunk mapChunk)
{
// Even if the chunk is still removed still need to make sure bounds are updated (for now...)
if (mapChunk.FilledTiles == 0)
{
RemoveChunk(mapChunk.Indices);
}
// generate collision rectangles for this chunk based on filled tiles.
GridChunkPartition.PartitionChunk(mapChunk, out var localBounds, out var rectangles);
mapChunk.CachedBounds = localBounds;
LocalBounds = new Box2();
foreach (var chunk in _chunks.Values)
{
var chunkBounds = chunk.CachedBounds;
if(chunkBounds.Size.Equals(Vector2i.Zero))
continue;
if (LocalBounds.Size == Vector2.Zero)
{
var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize);
LocalBounds = gridBounds;
}
else
{
var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize);
LocalBounds = LocalBounds.Union(gridBounds);
}
}
// TryGet because unit tests YAY
if (mapChunk.FilledTiles > 0 && _entityManager.EntitySysManager.TryGetEntitySystem(out SharedGridFixtureSystem? system))
system.RegenerateCollision(GridEntityId, mapChunk, rectangles);
}
/// <summary>
/// Calculate the world space AABB for this chunk.
/// </summary>
public Box2 CalcWorldAABB(MapChunk mapChunk)
{
var rotation = WorldRotation;
var position = WorldPosition;
var chunkPosition = mapChunk.Indices;
var tileScale = TileSize;
var chunkScale = mapChunk.ChunkSize;
var worldPos = position + rotation.RotateVec(chunkPosition * tileScale * chunkScale);
return new Box2Rotated(
((Box2)mapChunk.CachedBounds
.Scale(tileScale))
.Translated(worldPos),
rotation, worldPos).CalcBoundingBox();
}
private void OnTileModified(MapChunk mapChunk, Vector2i tileIndices, Tile newTile, Tile oldTile, bool shapeChanged)
{
// As the collision regeneration can potentially delete the chunk we'll notify of the tile changed first.
var gridTile = mapChunk.ChunkTileToGridTile(tileIndices);
var newTileRef = new TileRef(ParentMapId, Index, gridTile, newTile);
mapChunk.LastTileModifiedTick = _mapManager.GameTiming.CurTick;
LastTileModifiedTick = _mapManager.GameTiming.CurTick;
_mapManager.RaiseOnTileChanged(newTileRef, oldTile);
if (shapeChanged && !mapChunk.SuppressCollisionRegeneration)
{
RegenerateCollision(mapChunk);
}
}
}
/// <summary>

View File

@@ -63,7 +63,7 @@ internal partial class MapManager
private GridId _highestGridId = GridId.Invalid;
public virtual void ChunkRemoved(MapChunk chunk) { }
public virtual void ChunkRemoved(GridId gridId, MapChunk chunk) { }
public EntityUid GetGridEuid(GridId id)
{

View File

@@ -0,0 +1,266 @@
using System;
using System.Globalization;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
namespace Robust.Shared.Map
{
internal partial class MapManager
{
/// <inheritdoc />
public void SetMapPaused(MapId mapId, bool paused)
{
if(mapId == MapId.Nullspace)
return;
if(!MapExists(mapId))
throw new ArgumentException("That map does not exist.");
if (paused)
{
SetMapPause(mapId);
}
else
{
ClearMapPause(mapId);
}
var mapEnt = GetMapEntityId(mapId);
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var metaQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
RecursiveSetPaused(mapEnt, paused, in xformQuery, in metaQuery);
}
private static void RecursiveSetPaused(EntityUid entity, bool paused,
in EntityQuery<TransformComponent> xformQuery,
in EntityQuery<MetaDataComponent> metaQuery)
{
metaQuery.GetComponent(entity).EntityPaused = paused;
foreach (var child in xformQuery.GetComponent(entity)._children)
{
RecursiveSetPaused(child, paused, in xformQuery, in metaQuery);
}
}
/// <inheritdoc />
public void DoMapInitialize(MapId mapId)
{
if(!MapExists(mapId))
throw new ArgumentException("That map does not exist.");
if (IsMapInitialized(mapId))
throw new ArgumentException("That map is already initialized.");
ClearMapPreInit(mapId);
var mapEnt = GetMapEntityId(mapId);
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var metaQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
RecursiveDoMapInit(mapEnt, in xformQuery, in metaQuery);
}
private static void RecursiveDoMapInit(EntityUid entity,
in EntityQuery<TransformComponent> xformQuery,
in EntityQuery<MetaDataComponent> metaQuery)
{
// RunMapInit can modify the TransformTree
// ToArray caches deleted euids, we check here if they still exist.
if(!metaQuery.TryGetComponent(entity, out var meta))
return;
entity.RunMapInit();
meta.EntityPaused = false;
foreach (var child in xformQuery.GetComponent(entity)._children.ToArray())
{
RecursiveDoMapInit(child, in xformQuery, in metaQuery);
}
}
/// <inheritdoc />
public void DoGridMapInitialize(IMapGrid grid)
{
// NOP
}
/// <inheritdoc />
public void DoGridMapInitialize(GridId gridId)
{
// NOP
}
/// <inheritdoc />
public void AddUninitializedMap(MapId mapId)
{
SetMapPreInit(mapId);
}
private void SetMapPause(MapId mapId)
{
if(mapId == MapId.Nullspace)
return;
var mapEuid = GetMapEntityId(mapId);
var mapComp = EntityManager.GetComponent<IMapComponent>(mapEuid);
mapComp.MapPaused = true;
}
private bool CheckMapPause(MapId mapId)
{
if(mapId == MapId.Nullspace)
return false;
var mapEuid = GetMapEntityId(mapId);
if (mapEuid == EntityUid.Invalid)
return false;
var mapComp = EntityManager.GetComponent<IMapComponent>(mapEuid);
return mapComp.MapPaused;
}
private void ClearMapPause(MapId mapId)
{
if(mapId == MapId.Nullspace)
return;
var mapEuid = GetMapEntityId(mapId);
var mapComp = EntityManager.GetComponent<IMapComponent>(mapEuid);
mapComp.MapPaused = false;
}
private void SetMapPreInit(MapId mapId)
{
if(mapId == MapId.Nullspace)
return;
var mapEuid = GetMapEntityId(mapId);
var mapComp = EntityManager.GetComponent<IMapComponent>(mapEuid);
mapComp.MapPreInit = true;
}
private bool CheckMapPreInit(MapId mapId)
{
if(mapId == MapId.Nullspace)
return false;
var mapEuid = GetMapEntityId(mapId);
if (mapEuid == EntityUid.Invalid)
return false;
var mapComp = EntityManager.GetComponent<IMapComponent>(mapEuid);
return mapComp.MapPreInit;
}
private void ClearMapPreInit(MapId mapId)
{
if(mapId == MapId.Nullspace)
return;
var mapEuid = GetMapEntityId(mapId);
var mapComp = EntityManager.GetComponent<IMapComponent>(mapEuid);
mapComp.MapPreInit = false;
}
/// <inheritdoc />
public bool IsMapPaused(MapId mapId)
{
return CheckMapPause(mapId) || CheckMapPreInit(mapId);
}
/// <inheritdoc />
public bool IsGridPaused(IMapGrid grid)
{
return IsMapPaused(grid.ParentMapId);
}
/// <inheritdoc />
public bool IsGridPaused(GridId gridId)
{
if (TryGetGrid(gridId, out var grid))
{
return IsGridPaused(grid);
}
Logger.ErrorS("map", $"Tried to check if unknown grid {gridId} was paused.");
return true;
}
/// <inheritdoc />
public bool IsMapInitialized(MapId mapId)
{
return !CheckMapPreInit(mapId);
}
/// <summary>
/// Initializes the map pausing system.
/// </summary>
private void InitializeMapPausing()
{
_conhost.RegisterCommand("pausemap",
"Pauses a map, pausing all simulation processing on it.",
"pausemap <map ID>",
(shell, _, args) =>
{
if (args.Length != 1)
{
shell.WriteError("Need to supply a valid MapId");
return;
}
var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture));
if (!MapExists(mapId))
{
shell.WriteError("That map does not exist.");
return;
}
SetMapPaused(mapId, true);
});
_conhost.RegisterCommand("querymappaused",
"Check whether a map is paused or not.",
"querymappaused <map ID>",
(shell, _, args) =>
{
var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture));
if (!MapExists(mapId))
{
shell.WriteError("That map does not exist.");
return;
}
shell.WriteLine(IsMapPaused(mapId).ToString());
});
_conhost.RegisterCommand("unpausemap",
"unpauses a map, resuming all simulation processing on it.",
"Usage: unpausemap <map ID>",
(shell, _, args) =>
{
if (args.Length != 1)
{
shell.WriteLine("Need to supply a valid MapId");
return;
}
var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture));
if (!MapExists(mapId))
{
shell.WriteLine("That map does not exist.");
return;
}
SetMapPaused(mapId, false);
});
}
}
}

View File

@@ -133,9 +133,10 @@ internal partial class MapManager
if (!mapGrid.HasChunk(chunkIndices)) continue;
var chunk = mapGrid.GetChunk(chunkIndices);
var chunkTile = chunk.GetTileRef(chunk.GridTileToChunkTile(tile));
Vector2i indices = chunk.GridTileToChunkTile(tile);
var chunkTile = chunk.GetTile((ushort)indices.X, (ushort)indices.Y);
if (chunkTile.Tile.IsEmpty) continue;
if (chunkTile.IsEmpty) continue;
grid = mapGrid;
return true;
}

View File

@@ -1,3 +1,4 @@
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -7,20 +8,24 @@ using Robust.Shared.Utility;
namespace Robust.Shared.Map;
/// <inheritdoc cref="IMapManager" />
[Virtual]
internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber
{
[field: Dependency] public IGameTiming GameTiming { get; } = default!;
[field: Dependency] public IEntityManager EntityManager { get; } = default!;
[Dependency] private readonly IConsoleHost _conhost = default!;
/// <inheritdoc />
public void Initialize()
{
InitializeGridTrees();
#if DEBUG
DebugTools.Assert(!_dbgGuardInit);
DebugTools.Assert(!_dbgGuardRunning);
_dbgGuardInit = true;
#endif
InitializeGridTrees();
InitializeMapPausing();
}
/// <inheritdoc />

View File

@@ -21,7 +21,7 @@ internal interface INetworkedMapManager : IMapManagerInternal
void ApplyGameStatePost(GameStateMapData? data);
}
internal class NetworkedMapManager : MapManager, INetworkedMapManager
internal sealed class NetworkedMapManager : MapManager, INetworkedMapManager
{
private readonly Dictionary<GridId, List<(GameTick tick, Vector2i indices)>> _chunkDeletionHistory = new();
private readonly List<(GameTick tick, GridId gridId)> _gridDeletionHistory = new();
@@ -41,19 +41,19 @@ internal class NetworkedMapManager : MapManager, INetworkedMapManager
_chunkDeletionHistory.Remove(gridId);
}
public override void ChunkRemoved(MapChunk chunk)
public override void ChunkRemoved(GridId gridId, MapChunk chunk)
{
base.ChunkRemoved(chunk);
if (!_chunkDeletionHistory.TryGetValue(chunk.GridId, out var chunks))
base.ChunkRemoved(gridId, chunk);
if (!_chunkDeletionHistory.TryGetValue(gridId, out var chunks))
{
chunks = new List<(GameTick tick, Vector2i indices)>();
_chunkDeletionHistory[chunk.GridId] = chunks;
_chunkDeletionHistory[gridId] = chunks;
}
chunks.Add((GameTiming.CurTick, chunk.Indices));
// Seemed easier than having this method on GridFixtureSystem
if (!TryGetGrid(chunk.GridId, out var grid) ||
if (!TryGetGrid(gridId, out var grid) ||
!EntityManager.TryGetComponent(grid.GridEntityId, out PhysicsComponent? body) ||
chunk.Fixtures.Count == 0)
return;
@@ -308,7 +308,7 @@ internal class NetworkedMapManager : MapManager, INetworkedMapManager
for (ushort y = 0; y < grid.ChunkSize; y++)
{
var tile = chunkData.TileData[counter++];
if (chunk.GetTileRef(x, y).Tile != tile)
if (chunk.GetTile(x, y) != tile)
{
chunk.SetTile(x, y, tile);
modified.Add((new Vector2i(chunk.X * grid.ChunkSize + x, chunk.Y * grid.ChunkSize + y), tile));
@@ -324,7 +324,7 @@ internal class NetworkedMapManager : MapManager, INetworkedMapManager
{
var chunk = grid.GetChunk(chunkData.Index);
chunk.SuppressCollisionRegeneration = false;
chunk.RegenerateCollision();
grid.RegenerateCollision(chunk);
}
foreach (var chunkData in gridDatum.DeletedChunkData)

View File

@@ -59,6 +59,9 @@ namespace Robust.Shared.Physics.Collision.Shapes
public ShapeType ShapeType => ShapeType.Edge;
/// <inheritdoc />
public Box2 LocalBounds => CalcLocalBounds();
public float Radius
{
get => _radius;
@@ -122,6 +125,15 @@ namespace Robust.Shared.Physics.Collision.Shapes
return new Box2(lower - radius, upper + radius);
}
private Box2 CalcLocalBounds()
{
var lower = Vector2.ComponentMin(Vertex1, Vertex2);
var upper = Vector2.ComponentMax(Vertex1, Vertex2);
var radius = new Vector2(PhysicsConstants.PolygonRadius, PhysicsConstants.PolygonRadius);
return new Box2(lower - radius, upper + radius);
}
public float CalculateArea()
{
// It's a line

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Shared.Maths;
namespace Robust.Shared.Physics.Collision.Shapes
@@ -35,6 +35,11 @@ namespace Robust.Shared.Physics.Collision.Shapes
ShapeType ShapeType { get; }
/// <summary>
/// Local Axis Aligned Bounding Box (AABB) of the shape.
/// </summary>
Box2 LocalBounds { get; }
/// <summary>
/// Calculate the AABB of the shape.
/// </summary>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
@@ -42,23 +42,11 @@ namespace Robust.Shared.Physics.Collision.Shapes
public ShapeType ShapeType => ShapeType.Aabb;
[DataField("bounds")]
[ViewVariables(VVAccess.ReadWrite)]
private Box2 _localBounds = Box2.UnitCentered;
/// <summary>
/// Local AABB bounds of this shape.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Box2 LocalBounds
{
get => _localBounds;
set
{
if (_localBounds == value)
return;
_localBounds = value;
}
}
/// <inheritdoc />
public Box2 LocalBounds => _localBounds;
public PhysShapeAabb(float radius)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -19,6 +19,9 @@ namespace Robust.Shared.Physics.Collision.Shapes
public ShapeType ShapeType => ShapeType.Circle;
/// <inheritdoc />
public Box2 LocalBounds => CalcLocalBounds();
private const float DefaultRadius = 0.5f;
[DataField("radius")]
@@ -72,6 +75,16 @@ namespace Robust.Shared.Physics.Collision.Shapes
return new Box2(p.X - _radius, p.Y - _radius, p.X + _radius, p.Y + _radius);
}
private Box2 CalcLocalBounds()
{
// circle inscribed in box
return new Box2(
_position.X - _radius,
_position.Y - _radius,
_position.X + _radius,
_position.Y + _radius);
}
public bool Equals(IPhysShape? other)
{
if (other is not PhysShapeCircle otherCircle) return false;

View File

@@ -133,6 +133,9 @@ namespace Robust.Shared.Physics.Collision.Shapes
public ShapeType ShapeType => ShapeType.Polygon;
/// <inheritdoc />
public Box2 LocalBounds => CalcLocalBounds();
public PolygonShape()
{
_radius = PhysicsConstants.PolygonRadius;
@@ -245,6 +248,22 @@ namespace Robust.Shared.Physics.Collision.Shapes
return new Box2(lower - r, upper + r);
}
private Box2 CalcLocalBounds()
{
var lower = Vertices[0];
var upper = lower;
for (var i = 1; i < Vertices.Length; ++i)
{
var v = Vertices[i];
lower = Vector2.ComponentMin(lower, v);
upper = Vector2.ComponentMax(upper, v);
}
var r = new Vector2(_radius, _radius);
return new Box2(lower - r, upper + r);
}
public static explicit operator PolygonShape(PhysShapeAabb aabb)
{
// TODO: Need a test for this probably, if there is no AABB manifold generator done at least.

View File

@@ -190,7 +190,7 @@ namespace Robust.Shared.Physics.Dynamics
}
[DataField("layer", customTypeSerializer: typeof(FlagSerializer<CollisionLayer>))]
private int _collisionLayer;
internal int _collisionLayer;
/// <summary>
/// Bitmask of the layers this component collides with.
@@ -211,7 +211,7 @@ namespace Robust.Shared.Physics.Dynamics
}
[DataField("mask", customTypeSerializer: typeof(FlagSerializer<CollisionMask>))]
private int _collisionMask;
internal int _collisionMask;
public float Area
{

View File

@@ -375,9 +375,7 @@ namespace Robust.Shared.Physics
public void FixtureUpdate(FixturesComponent component, PhysicsComponent? body = null)
{
if (!Resolve(component.Owner, ref body))
{
return;
}
var mask = 0;
var layer = 0;
@@ -396,7 +394,7 @@ namespace Robust.Shared.Physics
body.CollisionMask = mask;
body.CollisionLayer = layer;
body.Hard = hard;
component.Dirty();
Dirty(component);
}
[Serializable, NetSerializable]

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -41,6 +42,10 @@ namespace Robust.Shared.Physics
// Can't assert the count is 0 because it's possible it gets re-serialized before init!
if (SerializedFixtures.Count > 0) return;
// At some stage we'll serialize non-default grids (gridfixturesystem does support it)
// but for now just for smaller map file + reading speed we'll just globally cull them.
if (IoCManager.Resolve<IEntityManager>().HasComponent<MapGridComponent>(Owner)) return;
foreach (var (_, fixture) in Fixtures)
{
SerializedFixtures.Add(fixture);

View File

@@ -402,23 +402,18 @@ namespace Robust.Shared.Physics
public void Refilter(Fixture fixture)
{
// TODO: Call this method whenever collisionmask / collisionlayer changes
if (fixture.Body == null) return;
// TODO: This should never becalled when body is null.
DebugTools.Assert(fixture.Body != null);
if (fixture.Body == null)
{
return;
}
var body = fixture.Body;
var node = body.Contacts.First;
while (node != null)
foreach (var (_, contact) in fixture.Contacts)
{
var contact = node.Value;
node = node.Next;
var fixtureA = contact.FixtureA;
var fixtureB = contact.FixtureB;
if (fixtureA == fixture || fixtureB == fixture)
{
contact.FilterFlag = true;
}
contact.FilterFlag = true;
}
var broadphase = body.Broadphase;
@@ -426,7 +421,7 @@ namespace Robust.Shared.Physics
// If nullspace or whatever ignore it.
if (broadphase == null) return;
TouchProxies(EntityManager.GetComponent<TransformComponent>(fixture.Body.Owner).MapID, broadphase, fixture);
TouchProxies(Transform(fixture.Body.Owner).MapID, broadphase, fixture);
}
private void TouchProxies(MapId mapId, BroadphaseComponent broadphase, Fixture fixture)

View File

@@ -20,6 +20,22 @@ namespace Robust.Shared.Random
public Angle NextAngle(Angle minValue, Angle maxValue) => NextFloat() * (maxValue - minValue) + minValue;
public Angle NextAngle(Angle maxValue) => NextFloat() * maxValue;
/// <summary>
/// Random vector, created from a uniform distribution of magnitudes and angles.
/// </summary>
/// <remarks>
/// In general, NextVector2(1) will tend to result in vectors with smaller magnitudes than
/// NextVector2Box(1,1), even if you ignored any vectors with a magnitude larger than one.
/// </remarks>
public Vector2 NextVector2(float minMagnitude, float maxMagnitude) => NextAngle().RotateVec((NextFloat(minMagnitude, maxMagnitude), 0));
public Vector2 NextVector2(float maxMagnitude = 1) => NextVector2(0, maxMagnitude);
/// <summary>
/// Random vector, created from a uniform distribution of x and y coordinates lying inside some box.
/// </summary>
public Vector2 NextVector2Box(float minX, float minY, float maxX, float maxY) => new Vector2(NextFloat(minX, maxX), NextFloat(minY, maxY));
public Vector2 NextVector2Box(float maxAbsX = 1, float maxAbsY = 1) => NextVector2Box(-maxAbsX, -maxAbsY, maxAbsX, maxAbsY);
void Shuffle<T>(IList<T> list)
{
var n = list.Count;

View File

@@ -19,7 +19,7 @@
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="YamlDotNet" Version="9.1.4" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Linguini.Bundle" Version="0.1.2" />
<PackageReference Include="Linguini.Bundle" Version="0.1.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lidgren.Network\Lidgren.Network.csproj" />

View File

@@ -38,13 +38,13 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations
{
try
{
return ((ITypeReader<Texture, ValueDataNode>) this).Read(serializationManager, node, dependencies, skipHook, context);
return ((ITypeReader<EntityPrototype, ValueDataNode>)this).Read(serializationManager, node, dependencies, skipHook, context);
}
catch { /* ignored */ }
try
{
return ((ITypeReader<EntityPrototype, ValueDataNode>) this).Read(serializationManager, node, dependencies, skipHook, context);
return ((ITypeReader<Texture, ValueDataNode>) this).Read(serializationManager, node, dependencies, skipHook, context);
}
catch { /* ignored */ }
@@ -57,6 +57,9 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations
IDependencyCollection dependencies,
bool skipHook, ISerializationContext? context)
{
if (!IoCManager.Resolve<Prototypes.IPrototypeManager>().HasIndex<Prototypes.EntityPrototype>(node.Value))
throw new InvalidMappingException("Invalid Entity Prototype");
return new DeserializedValue<EntityPrototype>(new EntityPrototype(node.Value));
}
@@ -107,8 +110,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations
IDependencyCollection dependencies,
ISerializationContext? context)
{
// TODO Serialization: actually validate the id
return string.IsNullOrWhiteSpace(node.Value)
return !IoCManager.Resolve<Prototypes.IPrototypeManager>().HasIndex<Prototypes.EntityPrototype>(node.Value)
? new ErrorNode(node, $"Invalid {nameof(EntityPrototype)} id")
: new ValidatedValueNode(node);
}

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Asynchronous;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
@@ -32,7 +32,7 @@ namespace Robust.Shared
IoCManager.Register<ILocalizationManager, LocalizationManager>();
IoCManager.Register<ILocalizationManagerInternal, LocalizationManager>();
IoCManager.Register<ILogManager, LogManager>();
IoCManager.Register<IPauseManager, PauseManager>();
IoCManager.Register<IPauseManager, NetworkedMapManager>();
IoCManager.Register<IModLoader, ModLoader>();
IoCManager.Register<IModLoaderInternal, ModLoader>();
IoCManager.Register<INetManager, NetManager>();

View File

@@ -1,15 +1,20 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.Map;
namespace Robust.Shared.Timing
{
[Obsolete("Use the same functions on IMapManager.")]
public interface IPauseManager
{
void SetMapPaused(MapId mapId, bool paused);
void DoMapInitialize(MapId mapId);
[Obsolete("This function does nothing, per-grid pausing isn't a thing anymore.")]
void DoGridMapInitialize(GridId gridId);
[Obsolete("This function does nothing, per-grid pausing isn't a thing anymore.")]
void DoGridMapInitialize(IMapGrid grid);
void AddUninitializedMap(MapId mapId);

View File

@@ -1,186 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Timing
{
internal sealed class PauseManager : IPauseManager, IPostInjectInit
{
[Dependency] private readonly IConsoleHost _conhost = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityLookup _entityLookup = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[ViewVariables] private readonly HashSet<MapId> _pausedMaps = new();
[ViewVariables] private readonly HashSet<MapId> _unInitializedMaps = new();
public void SetMapPaused(MapId mapId, bool paused)
{
if (paused)
{
_pausedMaps.Add(mapId);
foreach (var entity in _entityLookup.GetEntitiesInMap(mapId))
{
_entityManager.GetComponent<MetaDataComponent>(entity).EntityPaused = true;
}
}
else
{
_pausedMaps.Remove(mapId);
foreach (var entity in _entityLookup.GetEntitiesInMap(mapId))
{
_entityManager.GetComponent<MetaDataComponent>(entity).EntityPaused = false;
}
}
}
public void DoMapInitialize(MapId mapId)
{
if (IsMapInitialized(mapId))
throw new ArgumentException("That map is already initialized.");
_unInitializedMaps.Remove(mapId);
foreach (var entity in IoCManager.Resolve<IEntityLookup>().GetEntitiesInMap(mapId).ToArray())
{
entity.RunMapInit();
// MapInit could have deleted this entity.
if(_entityManager.TryGetComponent(entity, out MetaDataComponent? meta))
meta.EntityPaused = false;
}
}
public void DoGridMapInitialize(IMapGrid grid)
{
DoGridMapInitialize(grid.Index);
}
public void DoGridMapInitialize(GridId gridId)
{
var mapId = _mapManager.GetGrid(gridId).ParentMapId;
foreach (var entity in _entityLookup.GetEntitiesInMap(mapId))
{
if (_entityManager.GetComponent<TransformComponent>(entity).GridID != gridId)
continue;
entity.RunMapInit();
_entityManager.GetComponent<MetaDataComponent>(entity).EntityPaused = false;
}
}
public void AddUninitializedMap(MapId mapId)
{
_unInitializedMaps.Add(mapId);
}
public bool IsMapPaused(MapId mapId)
{
return _pausedMaps.Contains(mapId) || _unInitializedMaps.Contains(mapId);
}
public bool IsGridPaused(IMapGrid grid)
{
return IsMapPaused(grid.ParentMapId);
}
public bool IsGridPaused(GridId gridId)
{
if (_mapManager.TryGetGrid(gridId, out var grid))
{
return IsGridPaused(grid);
}
Logger.ErrorS("map", $"Tried to check if unknown grid {gridId} was paused.");
return true;
}
public bool IsMapInitialized(MapId mapId)
{
return !_unInitializedMaps.Contains(mapId);
}
/// <inheritdoc />
public void PostInject()
{
_mapManager.MapDestroyed += (_, args) =>
{
_pausedMaps.Remove(args.Map);
_unInitializedMaps.Add(args.Map);
};
_conhost.RegisterCommand("pausemap",
"Pauses a map, pausing all simulation processing on it.",
"pausemap <map ID>",
(shell, _, args) =>
{
if (args.Length != 1)
{
shell.WriteError("Need to supply a valid MapId");
return;
}
string? arg = args[0];
var mapId = new MapId(int.Parse(arg, CultureInfo.InvariantCulture));
if (!_mapManager.MapExists(mapId))
{
shell.WriteError("That map does not exist.");
return;
}
SetMapPaused(mapId, true);
});
_conhost.RegisterCommand("querymappaused",
"Check whether a map is paused or not.",
"querymappaused <map ID>",
(shell, _, args) =>
{
string? arg = args[0];
var mapId = new MapId(int.Parse(arg, CultureInfo.InvariantCulture));
if (!_mapManager.MapExists(mapId))
{
shell.WriteError("That map does not exist.");
return;
}
shell.WriteLine(IsMapPaused(mapId).ToString());
});
_conhost.RegisterCommand("unpausemap",
"unpauses a map, resuming all simulation processing on it.",
"Usage: unpausemap <map ID>",
(shell, _, args) =>
{
if (args.Length != 1)
{
shell.WriteLine("Need to supply a valid MapId");
return;
}
string? arg = args[0];
var mapId = new MapId(int.Parse(arg, CultureInfo.InvariantCulture));
if (!_mapManager.MapExists(mapId))
{
shell.WriteLine("That map does not exist.");
return;
}
SetMapPaused(mapId, false);
});
}
}
}

View File

@@ -22,6 +22,21 @@ namespace Robust.Shared.Utility
}
}
internal static MemoryStream ConsumeToMemoryStream(this Stream stream)
{
var ms = stream.CopyToMemoryStream();
stream.Dispose();
return ms;
}
internal static MemoryStream CopyToMemoryStream(this Stream stream)
{
var ms = new MemoryStream();
stream.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
return ms;
}
/// <exception cref="EndOfStreamException">
/// Thrown if not exactly <paramref name="amount"/> bytes could be read.
/// </exception>

View File

@@ -0,0 +1,51 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.UnitTesting.Client.GameObjects.Components;
/// <summary>
/// Asserts that content can correctly override an occluder's directions instead of relying on the default anchoring behaviour.
/// The directions are used for connecting occluders together.
/// </summary>
[TestFixture]
public sealed class OccluderDirectionsTest : RobustIntegrationTest
{
/* See https://github.com/space-wizards/RobustToolbox/pull/2528 for why this is commented out as the technology isn't there yet.
[Test]
public async Task TestOccluderOverride()
{
var client = StartClient();
await client.WaitIdleAsync();
var entManager = client.ResolveDependency<IEntityManager>();
var mapManager = client.ResolveDependency<IMapManager>();
var overrider = new OccluderOverrider();
entManager.EventBus.SubscribeEvent<OccluderDirectionsEvent>(EventSource.Local, overrider, EventHandler);
await client.WaitAssertion(() =>
{
var mapId = mapManager.CreateMap();
var occ = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var occluder = entManager.AddComponent<ClientOccluderComponent>(occ);
Assert.That(occluder.Occluding, Is.EqualTo(OccluderDir.None));
occluder.Update();
Assert.That(occluder.Occluding, Is.EqualTo(OccluderDir.East));
});
}
private static void EventHandler(ref OccluderDirectionsEvent ev)
{
ev.Handled = true;
ev.Directions = OccluderDir.East;
}
private sealed class OccluderOverrider : IEntityEventSubscriber {}
*/
}

View File

@@ -207,7 +207,7 @@ namespace Robust.UnitTesting.Server
container.Register<IIslandManager, IslandManager>();
container.Register<IManifoldManager, CollisionManager>();
container.Register<IMapManagerInternal, MapManager>();
container.RegisterInstance<IPauseManager>(new Mock<IPauseManager>().Object); // TODO: get timing working similar to RobustIntegrationTest
container.Register<IPauseManager, MapManager>();
container.Register<IPhysicsManager, PhysicsManager>();
_diFactory?.Invoke(container);

View File

@@ -47,6 +47,42 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems
}
}
/// <summary>
/// Checks that the MoverCoordinates between parent and children is correct.
/// </summary>
[Test]
public void MoverCoordinatesCorrect()
{
var sim = SimulationFactory();
var entManager = sim.Resolve<IEntityManager>();
var xformSystem = sim.Resolve<IEntitySystemManager>().GetEntitySystem<SharedTransformSystem>();
var mapId = new MapId(1);
var parent = entManager.SpawnEntity(null, new MapCoordinates(Vector2.One, mapId));
var xform = entManager.GetComponent<TransformComponent>(parent);
Assert.That(xform.LocalPosition, Is.EqualTo(Vector2.One));
var child1 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.One, mapId));
var child2 = entManager.SpawnEntity(null, new MapCoordinates(new Vector2(10f, 10f), mapId));
var child1Xform = entManager.GetComponent<TransformComponent>(child1);
var child2Xform = entManager.GetComponent<TransformComponent>(child2);
child1Xform.AttachParent(xform);
child2Xform.AttachParent(xform);
var mover1 = xformSystem.GetMoverCoordinates(child1Xform);
var mover2 = xformSystem.GetMoverCoordinates(child2Xform);
Assert.That(mover1.Position, Is.EqualTo(Vector2.One));
Assert.That(mover2.Position, Is.EqualTo(new Vector2(10f, 10f)));
var child3 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.One, mapId));
var child3Xform = entManager.GetComponent<TransformComponent>(child3);
child3Xform.AttachParent(child2Xform);
Assert.That(xformSystem.GetMoverCoordinates(child3Xform).Position, Is.EqualTo(Vector2.One));
}
private sealed class Subscriber : IEntityEventSubscriber { }
}
}

View File

@@ -0,0 +1,98 @@
using System;
using Linguini.Syntax.Parser.Error;
using NUnit.Framework;
using Robust.Shared.Localization;
namespace Robust.UnitTesting.Shared.Localization;
[TestFixture]
[Parallelizable]
public sealed class TestFormatErrors
{
private const string Res1 = "err1 = $user)";
private const string Res2 = "a = b {{\r\n err = x";
private const string Res2Lf = "a = b {{\n err = x";
#region ExpectedCrlf
private const string Expect1Wide1Crlf = "\r\n 99 |err1 = $user)\r\n ^ Unbalanced closing brace";
private const string Expect2Wide1Crlf = "\r\n 99 |err1 = $user)\r\n ^ Unbalanced closing brace";
private const string Expect1Wide4Crlf =
"\r\n 99 |err1 = $user)\r\n ^^^^ Expected a message field for \"x\"";
private const string Expect2Wide4Crlf = "\r\n 99 |err1 = $user)\r\n ^^^^ Expected a message field for \"x\"";
private const string ExpectMulti1Wide1Crlf =
"\r\n 99 |a = b {{\r\n 100 | err = x\r\n ^ Unbalanced closing brace";
private const string ExpectMulti1Wide3Crlf =
"\r\n 99 |a = b {{\r\n 100 | err = x\r\n ^^^ Expected a message field for \"x\"";
#endregion
[Test]
[TestCase(Expect1Wide1Crlf, Res1, 7)]
[TestCase(Expect2Wide1Crlf, Res1, 0)]
[TestCase(Expect1Wide4Crlf, Res1, 8, 12)]
[TestCase(Expect2Wide4Crlf, Res1, 0, 4)]
[TestCase(ExpectMulti1Wide1Crlf, Res2, 12)]
[TestCase(ExpectMulti1Wide3Crlf, Res2, 12, 15)]
[TestCase(ExpectMulti1Wide1Crlf, Res2Lf, 11)]
[TestCase(ExpectMulti1Wide3Crlf, Res2Lf, 11, 14)]
public void TestSingleLineTestCrlf(string expected, string resource, int start, int? end = null)
{
var err = ParseError.UnbalancedClosingBrace(start, 99);
if (end != null)
{
err = ParseError.ExpectedMessageField("x".AsMemory(), start, end.Value, 99);
}
err.Slice = new Range(0, resource.Length);
var actual = err.FormatCompileErrors(resource.AsMemory(), "\r\n");
Assert.That(actual, Is.EqualTo(expected));
}
#region ExpectedLf
private const string Expect1Wide1Lf = "\n 99 |err1 = $user)\n ^ Unbalanced closing brace";
private const string Expect2Wide1Lf = "\n 99 |err1 = $user)\n ^ Unbalanced closing brace";
private const string Expect1Wide4Lf =
"\n 99 |err1 = $user)\n ^^^^ Expected a message field for \"x\"";
private const string Expect2Wide4Lf = "\n 99 |err1 = $user)\n ^^^^ Expected a message field for \"x\"";
private const string ExpectMulti1Wide1Lf = "\n 99 |a = b {{\n 100 | err = x\n ^ Unbalanced closing brace";
private const string ExpectMulti1Wide3Lf =
"\n 99 |a = b {{\n 100 | err = x\n ^^^ Expected a message field for \"x\"";
#endregion
[Test]
[TestCase(Expect1Wide1Lf, Res1, 7)]
[TestCase(Expect2Wide1Lf, Res1, 0)]
[TestCase(Expect1Wide4Lf, Res1, 8, 12)]
[TestCase(Expect2Wide4Lf, Res1, 0, 4)]
[TestCase(ExpectMulti1Wide1Lf, Res2, 12)]
[TestCase(ExpectMulti1Wide3Lf, Res2, 12, 15)]
[TestCase(ExpectMulti1Wide1Lf, Res2Lf, 11)]
[TestCase(ExpectMulti1Wide3Lf, Res2Lf, 11, 14)]
public void TestSingleLineTestLf(string expected, string resource, int start, int? end = null)
{
var err = ParseError.UnbalancedClosingBrace(start, 99);
if (end != null)
{
err = ParseError.ExpectedMessageField("x".AsMemory(), start, end.Value, 99);
}
err.Slice = new Range(0, resource.Length);
var actual = err.FormatCompileErrors(resource.AsMemory(), "\n");
Assert.That(actual, Is.EqualTo(expected));
}
}

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