Compare commits

...

26 Commits

Author SHA1 Message Date
PJB3005
3caffa04da Version: 260.2.2 2025-09-19 09:17:28 +02:00
Skye
06b377d1d5 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:28 +02:00
PJB3005
41fb191dda Version: 260.2.1 2025-09-14 14:55:52 +02:00
PJB3005
d4bcc1dc05 Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

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

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

    Move CEF cache out of data directory

    Don't want content messing with this...

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

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

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

    Update SpaceWizards.NFluidSynth to 0.2.2

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

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

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

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

* Add to GenericEntityPrint

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

* Raise event after regenerating collision

* continue, not return

* No need for GetComponent

* Swap TileRef for Tile + Vector2i

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

* Fix test

* cool
2025-05-15 19:11:20 +10:00
metalgearsloth
2ac7bc3ce4 Version: 258.0.0 2025-05-15 00:51:12 +10:00
Leon Friedrich
05cb4bb1c9 Make SpriteSystem.LayerMapReserve not throw (#5930)
* Make SpriteSystem.LayerMapReserve not throw

* fix SpriteComponent.Visible

* remove region
2025-05-14 23:23:51 +10:00
Leon Friedrich
a393efc87a Modify markup tag interfaces and fix some bugs (#5442)
* Modify markup tag interfaces

* Why are nullable structs like this.

* AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

* Avoid breaking changes

* Replace IMarkupTag with IMarkupTagHandler in engine

* Its a breaking change now I guess

* cleanup
2025-05-12 14:09:18 +10:00
Leon Friedrich
4d47cfa1a6 Minor respath improvements (#5876)
* Minor respath improvements

* Add helpers

* tweak helper

* Throw on more than 1 char

* comments

* No emoji separators
2025-05-12 13:04:40 +10:00
DrSmugleaf
2b1d755d9f Fix Container state handling not forcing inserts (#5916) 2025-05-11 22:45:31 +10:00
55 changed files with 639 additions and 233 deletions

View File

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

View File

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

View File

@@ -54,6 +54,83 @@ END TEMPLATE-->
*None yet*
## 260.2.2
## 260.2.1
## 260.2.0
### New features
* Add `StringBuilder.Insert(int, string)` to sandbox.
* Add the WorldNormal to the StartCollideEvent.
## 260.1.0
### New features
* `ComponentFactory` is now exposed to `EntitySystem` as `Factory`
### Other
* Cleanup warnings in PLacementManager
* Cleanup warnings in Clide.Sprite
## 260.0.0
### Breaking changes
* Fix / change `StartCollideEvent.WorldPoint` to return all points for the collision which may be up to 2 instead of 1.
### New features
* Add SpriteSystem dependency to VisualizerSystem.
* Add Vertical property to progress bars
* Add some `EntProtoId` overloads for group entity spawn methods.
## 259.0.0
### Breaking changes
* TileChangedEvent now has an array of tile changed entries rather than raising an individual event for every single tile changed.
### Other
* `Entity<T>` methods were marked as `readonly` as appropriate.
## 258.0.1
### Bugfixes
* Fix static physics bodies not generating contacts if they spawn onto sleeping bodies.
## 258.0.0
### Breaking changes
* `IMarkupTag` and related methods in `MarkupTagManager` have been obsoleted and should be replaced with the new `IMarkupTagHandler` interface. Various engine tags (e.g., `BoldTag`, `ColorTag`, etc) no longer implement the old interface.
### New features
* Add IsValidPath to ResPath and make some minor performance improvements.
### Bugfixes
* OutputPanel and RichTextLabel now remove controls associated with rich text tags when the text is updated.
* Fix `SpriteComponent.Visible` datafield not being read from yaml.
* Fix container state handling not forcing inserts.
### Other
* `SpriteSystem.LayerMapReserve()` no longer throws an exception if the specified layer already exists. This makes it behave like the obsoleted `SpriteComponent.LayerMapReserveBlank()`.
## 257.0.2
### Bugfixes

View File

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

View File

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

View File

@@ -387,7 +387,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)

View File

@@ -39,7 +39,6 @@ namespace Robust.Client.GameObjects
[RegisterComponent]
public sealed partial class SpriteComponent : Component, IComponentDebug, ISerializationHooks, IComponentTreeEntry<SpriteComponent>, IAnimationProperties
{
#region ECSd
public const string LogCategory = "go.comp.sprite";
[Dependency] private readonly IResourceCache resourceCache = default!;
@@ -59,12 +58,13 @@ namespace Robust.Client.GameObjects
[DataField] // TODO Sprite access restrict.
public bool GranularLayersRendering = false;
[DataField]
[DataField("visible")]
internal bool _visible = true;
// VV convenience variable to examine layer objects using layer keys
// ReSharper disable once UnusedMember.Local
[ViewVariables]
private Dictionary<object, Layer> _mappedLayers => LayerMap.ToDictionary(x => x.Key, x => Layers[x.Value]);
private Dictionary<object, Layer> MappedLayers => LayerMap.ToDictionary(x => x.Key, x => Layers[x.Value]);
[ViewVariables(VVAccess.ReadWrite)]
public bool Visible
@@ -93,7 +93,7 @@ namespace Robust.Client.GameObjects
set => Sys.SetDrawDepth((Owner, this), value);
}
[DataField]
[DataField("scale")] // Explicit name, in case this field ever gets renamed
internal Vector2 scale = Vector2.One;
/// <summary>
@@ -108,7 +108,7 @@ namespace Robust.Client.GameObjects
set => Sys.SetScale((Owner, this), value);
}
[DataField]
[DataField("rotation")] // Explicit name, in case this field ever gets renamed
internal Angle rotation = Angle.Zero;
[Animatable]
@@ -120,7 +120,7 @@ namespace Robust.Client.GameObjects
set => Sys.SetRotation((Owner, this), value);
}
[DataField]
[DataField("offset")] // Explicit name, in case this field ever gets renamed
internal Vector2 offset = Vector2.Zero;
/// <summary>
@@ -135,7 +135,7 @@ namespace Robust.Client.GameObjects
set => Sys.SetOffset((Owner, this), value);
}
[DataField]
[DataField("color")] // Explicit name, in case this field ever gets renamed
internal Color color = Color.White;
[Animatable]
@@ -1052,8 +1052,6 @@ namespace Robust.Client.GameObjects
return Sys.CalculateBounds((Owner, this), worldPosition, worldRotation, eyeRot);
}
#endregion
/// <summary>
/// Enum to "offset" a cardinal direction.
/// </summary>

View File

@@ -1,15 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using static Robust.Shared.Containers.ContainerManagerComponent;
namespace Robust.Client.GameObjects
@@ -58,7 +57,7 @@ namespace Robust.Client.GameObjects
if (!RemoveExpectedEntity(meta.NetEntity, out var container))
return;
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container);
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container, force: true);
}
public override void ShutdownContainer(BaseContainer container)
@@ -232,7 +231,7 @@ namespace Robust.Client.GameObjects
return;
}
Insert(message.Entity, container);
Insert(message.Entity, container, force: true);
}
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)

View File

@@ -213,32 +213,30 @@ public sealed partial class SpriteSystem
}
/// <summary>
/// Create a new blank layer and map the given key to it.
/// Ensures that a layer with the given key exists and return the layer's index.
/// If the layer does not yet exist, this will create and add a blank layer.
/// </summary>
public int LayerMapReserve(Entity<SpriteComponent?> sprite, Enum key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
if (LayerExists(sprite, key))
throw new Exception("Layer already exists");
if (LayerMapTryGet(sprite, key, out var layerIndex, false))
return layerIndex;
var layer = AddBlankLayer(sprite!);
LayerMapSet(sprite, key, layer.Index);
return layer.Index;
}
/// <summary>
/// A create a new blank layer and map the given key to it. If possible, it is preferred to use an enum key.
/// string keys mainly exist to make it easier to define custom layer keys in yaml.
/// </summary>
/// <inheritdoc cref="LayerMapReserve(Entity{SpriteComponent?},System.Enum)"/>
public int LayerMapReserve(Entity<SpriteComponent?> sprite, string key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
if (LayerExists(sprite, key))
throw new Exception("Layer already exists");
if (LayerMapTryGet(sprite, key, out var layerIndex, false))
return layerIndex;
var layer = AddBlankLayer(sprite!);
LayerMapSet(sprite, key, layer.Index);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -95,6 +95,12 @@ namespace Robust.Client.UserInterface.Controls
public void Clear()
{
_firstLine = true;
foreach (var entry in _entries)
{
entry.RemoveControls();
}
_entries.Clear();
_totalContentHeight = 0;
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
@@ -104,6 +110,7 @@ namespace Robust.Client.UserInterface.Controls
public void RemoveEntry(Index index)
{
var entry = _entries[index];
entry.RemoveControls();
_entries.RemoveAt(index.GetOffset(_entries.Count));
var font = _getFont();
@@ -189,6 +196,9 @@ namespace Robust.Client.UserInterface.Controls
if (entryOffset > contentBox.Height)
{
entry.HideControls();
// We know that every subsequent entry will also fail the test, but we also need to
// hide all the controls, so we cannot simply break out of the loop
continue;
}

View File

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

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.Graphics;
@@ -15,8 +17,7 @@ namespace Robust.Client.UserInterface.Controls
{
[Dependency] private readonly MarkupTagManager _tagManager = default!;
private FormattedMessage? _message;
private RichTextEntry _entry;
private RichTextEntry? _entry;
private float _lineHeightScale = 1;
private bool _lineHeightOverride;
@@ -40,19 +41,26 @@ namespace Robust.Client.UserInterface.Controls
public string? Text
{
get => _message?.ToMarkup();
get => _entry?.Message.ToMarkup();
set
{
if (value == null)
{
_message?.Clear();
return;
}
SetMessage(FormattedMessage.FromMarkupPermissive(value));
Clear();
else
SetMessage(FormattedMessage.FromMarkupPermissive(value));
}
}
public void Clear()
{
_entry?.RemoveControls();
_entry = null;
InvalidateMeasure();
}
public IEnumerable<Control> Controls => _entry?.Controls?.Values ?? Enumerable.Empty<Control>();
public IReadOnlyList<MarkupNode> Nodes => _entry?.Message.Nodes ?? Array.Empty<MarkupNode>();
public RichTextLabel()
{
IoCManager.InjectDependencies(this);
@@ -61,8 +69,8 @@ namespace Robust.Client.UserInterface.Controls
public void SetMessage(FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
_message = message;
_entry = new RichTextEntry(_message, this, _tagManager, tagsAllowed, defaultColor);
_entry?.RemoveControls();
_entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor);
InvalidateMeasure();
}
@@ -73,31 +81,31 @@ namespace Robust.Client.UserInterface.Controls
SetMessage(msg, tagsAllowed, defaultColor);
}
public string? GetMessage() => _message?.ToMarkup();
public string? GetMessage() => _entry?.Message.ToMarkup();
/// <summary>
/// Returns a copy of the currently used formatted message.
/// </summary>
public FormattedMessage? GetFormattedMessage() => _entry == null ? null : new FormattedMessage(_entry.Value.Message);
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
if (_message == null)
{
if (_entry == null)
return Vector2.Zero;
}
var font = _getFont();
_entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
// _entry is nullable struct.
// cannot just call _entry.Value.Update() as that doesn't actually update _entry.
_entry = _entry.Value.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
return new Vector2(_entry.Value.Width / UIScale, _entry.Value.Height / UIScale);
}
protected internal override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (_message == null)
{
return;
}
_entry.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
_entry?.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
}
[Pure]

View File

@@ -5,7 +5,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class BoldItalicTag : IMarkupTag
public sealed class BoldItalicTag : IMarkupTagHandler
{
public const string BoldItalicFont = "DefaultBoldItalic";

View File

@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class BoldTag : IMarkupTag
public sealed class BoldTag : IMarkupTagHandler
{
public const string BoldFont = "DefaultBold";

View File

@@ -2,7 +2,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class BulletTag : IMarkupTag
public sealed class BulletTag : IMarkupTagHandler
{
public string Name => "bullet";

View File

@@ -6,7 +6,7 @@ namespace Robust.Client.UserInterface.RichText;
/// <summary>
/// Colors the text inside its opening and closing nodes
/// </summary>
public sealed class ColorTag : IMarkupTag
public sealed class ColorTag : IMarkupTagHandler
{
public static readonly Color DefaultColor = new(200, 200, 200);

View File

@@ -8,14 +8,14 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class CommandLinkTag : IMarkupTag
public sealed class CommandLinkTag : IMarkupTagHandler
{
[Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!;
public string Name => "cmdlink";
/// <inheritdoc/>
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
if (!node.Value.TryGetString(out var text)
|| !node.Attributes.TryGetValue("command", out var commandParameter)

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.UserInterface.RichText;
/// Applies the font provided as the tags parameter to the markup drawing context.
/// Definitely not save for user supplied markup
/// </summary>
public sealed class FontTag : IMarkupTag
public sealed class FontTag : IMarkupTagHandler
{
public const string DefaultFont = "Default";
public const int DefaultSize = 12;

View File

@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class HeadingTag : IMarkupTag
public sealed class HeadingTag : IMarkupTagHandler
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;

View File

@@ -1,9 +1,16 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public interface IMarkupTag
/// <summary>
/// Classes that implement this interface will be instantiated by <see cref="MarkupTagManager"/> and used to handle
/// the parsing and behaviour of markup tags. Note that each class is only ever instantiated once by the tag manager,
/// and wil be used to handle all tags of that kind, and thus should not contain state information relevant to a
/// specific tag.
/// </summary>
public interface IMarkupTagHandler
{
/// <summary>
/// The string used as the tags name when writing rich text
@@ -54,17 +61,32 @@ public interface IMarkupTag
}
/// <summary>
/// Called inside the constructor of <see cref="RichTextEntry"/> to
/// supply a control that gets rendered inline before this tags children<br/>
/// Text continues to the right of the control until the next line and then continues bellow it
/// Called inside the constructor of <see cref="RichTextEntry"/> to supply a control that gets rendered inline
/// before this tags children. The returned control must be new instance to avoid issues with shallow cloning
/// <see cref="FormattedMessage"/> nodes. Text continues to the right of the control until the next line and
/// then continues bellow it.
/// </summary>
/// <param name="node">The markup node containing the parameter and attributes</param>
/// <param name="control">A UI control for placing in line with this tags children</param>
/// <returns>true if this tag supplies a control</returns>
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
control = null;
return false;
}
}
[Obsolete("Use IMarkupTagHandler")]
public interface IMarkupTag : IMarkupTagHandler
{
bool IMarkupTagHandler.TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
return TryGetControl(node, out control);
}
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
control = null;
return false;
}
}

View File

@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class ItalicTag : IMarkupTag
public sealed class ItalicTag : IMarkupTagHandler
{
public const string ItalicFont = "DefaultItalic";

View File

@@ -16,7 +16,7 @@ public sealed class MarkupTagManager
/// <summary>
/// Tags defined in engine need to be instantiated here because of sandboxing
/// </summary>
private readonly Dictionary<string, IMarkupTag> _markupTagTypes = new IMarkupTag[] {
private readonly Dictionary<string, IMarkupTagHandler> _markupTagTypes = new IMarkupTagHandler[] {
new BoldItalicTag(),
new BoldTag(),
new BulletTag(),
@@ -44,13 +44,13 @@ public sealed class MarkupTagManager
public void Initialize()
{
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTag>())
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTagHandler>())
{
//Prevent tags defined inside engine from being instantiated
if (_engineTypes.Contains(type))
continue;
var instance = (IMarkupTag)_sandboxHelper.CreateInstance(type);
var instance = (IMarkupTagHandler)_sandboxHelper.CreateInstance(type);
_markupTagTypes[instance.Name.ToLower()] = instance;
}
@@ -60,22 +60,48 @@ public sealed class MarkupTagManager
}
}
[Obsolete("Use GetMarkupTagHandler")]
public IMarkupTag? GetMarkupTag(string name)
{
return _markupTagTypes.GetValueOrDefault(name) as IMarkupTag;
}
public IMarkupTagHandler? GetMarkupTagHandler(string name)
{
return _markupTagTypes.GetValueOrDefault(name);
}
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
/// <summary>
/// Attempt to get the tag handler with the corresponding name.
/// </summary>
/// <param name="name">The name of the tag, as specified by <see cref="IMarkupTag.Name"/></param>
/// <param name="tagsAllowed">List of allowed tag types. If null, all types are allowed.</param>
/// <param name="handler">The instance responsible for handling tags of this type.</param>
/// <returns></returns>
public bool TryGetMarkupTagHandler(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTagHandler? handler)
{
if (_markupTagTypes.TryGetValue(name, out var markupTag)
// Using a whitelist prevents new tags from sneaking in.
&& (tagsAllowed == null || Array.IndexOf(tagsAllowed, markupTag.GetType()) != -1))
{
tag = markupTag;
handler = markupTag;
return true;
}
tag = null;
handler = null;
return false;
}
[Obsolete("Use TryGetMarkupTagHandler")]
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
{
if (!TryGetMarkupTagHandler(name, tagsAllowed, out var handler) || handler is not IMarkupTag cast)
{
tag = null;
return false;
}
tag = cast;
return true;
}
}

View File

@@ -13,6 +13,9 @@ namespace Robust.Client.UserInterface
{
/// <summary>
/// Used by <see cref="OutputPanel"/> and <see cref="RichTextLabel"/> to handle rich text layout.
/// Note that if this text is ever removed or modified without removing the owning control,
/// then <see cref="RemoveControls"/> should be called to ensure that any controls that were added by this
/// entry are also removed.
/// </summary>
internal struct RichTextEntry
{
@@ -36,7 +39,7 @@ namespace Robust.Client.UserInterface
/// </summary>
public ValueList<int> LineBreaks;
private readonly Dictionary<int, Control>? _tagControls;
public readonly Dictionary<int, Control>? Controls;
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
@@ -56,15 +59,35 @@ namespace Robust.Client.UserInterface
if (node.Name == null)
continue;
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var handler) || !handler.TryCreateControl(node, out var control))
continue;
// Markup tag handler instances are shared across controls. We need to ensure that the hanlder doesn't
// store state information and return the same control for each rich text entry.
DebugTools.Assert(handler.TryCreateControl(node, out var other) && other != control);
parent.Children.Add(control);
tagControls ??= new Dictionary<int, Control>();
tagControls.Add(nodeIndex, control);
}
_tagControls = tagControls;
Controls = tagControls;
}
// TODO RICH TEXT
// Somehow ensure that this **has** to be called when removing rich text from some control.
/// <summary>
/// Remove all owned controls from their parents.
/// </summary>
public readonly void RemoveControls()
{
if (Controls == null)
return;
foreach (var ctrl in Controls.Values)
{
ctrl.Orphan();
}
}
/// <summary>
@@ -74,7 +97,7 @@ namespace Robust.Client.UserInterface
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
/// <param name="uiScale"></param>
/// <param name="lineHeightScale"></param>
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
public RichTextEntry Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
{
// This method is gonna suck due to complexity.
// Bear with me here.
@@ -112,10 +135,10 @@ namespace Robust.Client.UserInterface
continue;
if (ProcessMetric(ref this, metrics, out breakLine))
return;
return this;
}
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
continue;
control.Measure(new Vector2(Width, Height));
@@ -128,12 +151,14 @@ namespace Robust.Client.UserInterface
desiredSize.Y);
if (ProcessMetric(ref this, controlMetrics, out breakLine))
return;
return this;
}
Width = wordWrap.FinalizeText(out breakLine);
CheckLineBreak(ref this, breakLine);
return this;
bool ProcessRune(ref RichTextEntry src, Rune rune, out int? outBreakLine)
{
wordWrap.NextRune(rune, out breakLine, out var breakNewLine, out var skip);
@@ -166,9 +191,10 @@ namespace Robust.Client.UserInterface
internal readonly void HideControls()
{
if (_tagControls == null)
if (Controls == null)
return;
foreach (var control in _tagControls.Values)
foreach (var control in Controls.Values)
{
control.Visible = false;
}
@@ -220,7 +246,7 @@ namespace Robust.Client.UserInterface
globalBreakCounter += 1;
}
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
continue;
// Controls may have been previously hidden via HideControls due to being "out-of frame".
@@ -243,7 +269,7 @@ namespace Robust.Client.UserInterface
return node.Value.StringValue ?? "";
//Skip the node if there is no markup tag for it.
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var tag))
return "";
if (!node.Closing)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -150,6 +150,7 @@ namespace Robust.Shared.GameObjects
[ViewVariables, Access(typeof(EntityManager), Other = AccessPermissions.ReadExecute)]
public EntityLifeStage EntityLifeStage { get; internal set; }
[ViewVariables(VVAccess.ReadOnly)]
public MetaDataFlags Flags
{
get => _flags;

View File

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

View File

@@ -52,6 +52,16 @@ public partial class EntityManager
return ents;
}
public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params EntProtoId[] protoNames)
{
var ents = new EntityUid[protoNames.Length];
for (var i = 0; i < protoNames.Length; i++)
{
ents[i] = SpawnAttachedTo(protoNames[i], coordinates);
}
return ents;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, List<string?> protoNames)
{
@@ -74,6 +84,14 @@ public partial class EntityManager
return ents;
}
public void SpawnEntitiesAttachedTo(EntityCoordinates coordinates, IEnumerable<EntProtoId> protoNames)
{
foreach (var protoName in protoNames)
{
SpawnAttachedTo(protoName, coordinates);
}
}
public virtual EntityUid SpawnAttachedTo(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
{
if (!coordinates.IsValid(this))

View File

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

View File

@@ -22,6 +22,8 @@ public partial interface IEntityManager
EntityUid[] SpawnEntities(MapCoordinates coordinates, params string?[] protoNames);
EntityUid[] SpawnEntities(MapCoordinates coordinates, string? prototype, int count);
EntityUid[] SpawnEntities(MapCoordinates coordinates, List<string?> protoNames);
void SpawnEntitiesAttachedTo(EntityCoordinates coordinates, IEnumerable<EntProtoId> protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params EntProtoId[] protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, List<string?> protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params string?[] protoNames);

View File

@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -833,7 +834,7 @@ public abstract partial class SharedMapSystem
}
var offset = chunk.GridTileToChunkTile(gridIndices);
SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile);
SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile, out _);
}
public void SetTiles(EntityUid uid, MapGridComponent grid, List<(Vector2i GridIndices, Tile Tile)> tiles)
@@ -842,6 +843,11 @@ public abstract partial class SharedMapSystem
return;
var modified = new HashSet<MapChunk>(Math.Max(1, tiles.Count / grid.ChunkSize));
var tileChanges = new ValueList<TileChangedEntry>(tiles.Count);
// Suppress sending out events for each tile changed
// We're going to send them all out together at the end
MapManager.SuppressOnTileChanged = true;
foreach (var (gridIndices, tile) in tiles)
{
@@ -859,8 +865,11 @@ public abstract partial class SharedMapSystem
var offset = chunk.GridTileToChunkTile(gridIndices);
chunk.SuppressCollisionRegeneration = true;
if (SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile))
if (SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile, out var oldTile))
{
modified.Add(chunk);
tileChanges.Add(new TileChangedEntry(tile, oldTile, offset, gridIndices));
}
}
foreach (var chunk in modified)
@@ -869,6 +878,13 @@ public abstract partial class SharedMapSystem
}
RegenerateCollision(uid, grid, modified);
// Notify of all tile changes in one event
var ev = new TileChangedEvent((uid, grid), tileChanges.ToArray());
RaiseLocalEvent(uid, ref ev, true);
// Back to normal
MapManager.SuppressOnTileChanged = false;
}
public TilesEnumerator GetLocalTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2 aabb,

View File

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

View File

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

View File

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

View File

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

View File

@@ -294,6 +294,14 @@ public abstract partial class SharedPhysicsSystem
DebugTools.Assert(!fixB.Contacts.ContainsKey(fixA));
fixB.Contacts.Add(fixA, contact);
bodB.Contacts.AddLast(contact.BodyBNode);
// If it's a spawned static ent then need to wake any contacting entities.
// The issue is that static ents can never be awake and if it spawns on an asleep entity never gets a contact.
// Checking only bodyA should be okay because if bodyA is the other ent (i.e. dynamic / kinematic) then it should already be awake.
if (bodyA.BodyType == BodyType.Static && !bodyB.Awake)
{
WakeBody(uidB, body: bodyB);
}
}
/// <summary>
@@ -543,7 +551,7 @@ public abstract partial class SharedPhysicsSystem
}
var status = ArrayPool<ContactStatus>.Shared.Rent(index);
var worldPoints = ArrayPool<Vector2>.Shared.Rent(index);
var worldPoints = ArrayPool<FixedArray4<Vector2>>.Shared.Rent(index);
// Update contacts all at once.
BuildManifolds(contacts, index, status, worldPoints);
@@ -578,9 +586,11 @@ public abstract partial class SharedPhysicsSystem
var uidA = contact.EntityA;
var uidB = contact.EntityB;
var worldPoint = worldPoints[i];
var points = new FixedArray2<Vector2>(worldPoint._00, worldPoint._01);
var worldNormal = worldPoint._02;
var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, worldPoint);
var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, worldPoint);
var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, points, contact.Manifold.PointCount, worldNormal);
var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, points, contact.Manifold.PointCount, worldNormal);
RaiseLocalEvent(uidA, ref ev1, true);
RaiseLocalEvent(uidB, ref ev2, true);
@@ -618,10 +628,10 @@ public abstract partial class SharedPhysicsSystem
ArrayPool<Contact>.Shared.Return(contacts);
ArrayPool<ContactStatus>.Shared.Return(status);
ArrayPool<Vector2>.Shared.Return(worldPoints);
ArrayPool<FixedArray4<Vector2>>.Shared.Return(worldPoints);
}
private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, Vector2[] worldPoints)
private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, FixedArray4<Vector2>[] worldPoints)
{
if (count == 0)
return;
@@ -664,7 +674,7 @@ public abstract partial class SharedPhysicsSystem
public Contact[] Contacts;
public ContactStatus[] Status;
public Vector2[] WorldPoints;
public FixedArray4<Vector2>[] WorldPoints;
public bool[] Wake;
public void Execute(int index)
@@ -673,7 +683,7 @@ public abstract partial class SharedPhysicsSystem
}
}
private void UpdateContact(Contact[] contacts, int index, ContactStatus[] status, bool[] wake, Vector2[] worldPoints)
private void UpdateContact(Contact[] contacts, int index, ContactStatus[] status, bool[] wake, FixedArray4<Vector2>[] worldPoints)
{
var contact = contacts[index];
@@ -698,7 +708,11 @@ public abstract partial class SharedPhysicsSystem
if (contactStatus == ContactStatus.StartTouching)
{
worldPoints[index] = Physics.Transform.Mul(bodyATransform, contacts[index].Manifold.LocalPoint);
var points = new FixedArray4<Vector2>();
contact.GetWorldManifold(bodyATransform, bodyBTransform, out var worldNormal, points.AsSpan);
// Use the 3rd Vector2 as the world normal, 4th is blank.
points._02 = worldNormal;
worldPoints[index] = points;
}
}

View File

@@ -66,7 +66,7 @@ public readonly struct ResPath : IEquatable<ResPath>
public ResPath(string canonPath)
{
// Paths should never have non-standardised directory separators passed in, the caller should have already sanitised it.
DebugTools.Assert(!canonPath.Contains('\\'));
DebugTools.Assert(IsValidPath(canonPath));
CanonPath = canonPath;
}
@@ -77,6 +77,21 @@ public readonly struct ResPath : IEquatable<ResPath>
{
}
/// <summary>
/// Check whether the given string paths contains any non-standard directory separators.
/// </summary>
public static bool IsValidPath(string path) => !path.Contains('\\');
/// <summary>
/// Check whether a string is a valid path (<see cref="IsValidPath"/>) and corresponds to a simple file name.
/// </summary>
public static bool IsValidFilename([NotNullWhen(true)] string? filename)
=> !string.IsNullOrEmpty(filename)
&& IsValidPath(filename)
&& !filename.Contains('/')
&& filename != "."
&& filename != "..";
/// <summary>
/// Returns true if the path is equal to "."
/// </summary>
@@ -106,7 +121,7 @@ public readonly struct ResPath : IEquatable<ResPath>
}
var ind = CanonPath.Length > 1 && CanonPath[^1] == '/'
? CanonPath[..^1].LastIndexOf('/')
? CanonPath.LastIndexOf('/', CanonPath.Length - 2)
: CanonPath.LastIndexOf('/');
return ind switch
{
@@ -206,7 +221,7 @@ public readonly struct ResPath : IEquatable<ResPath>
// it's a filename
// Uses +1 to skip `/` found in or starts from beginning of string
// if we found nothing (ind == -1)
var ind = CanonPath[..^1].LastIndexOf('/') + 1;
var ind = CanonPath.LastIndexOf('/', CanonPath.Length - 2) + 1;
return CanonPath[^1] == '/'
? CanonPath[ind .. ^1] // Omit last `/`
: CanonPath[ind..];
@@ -242,7 +257,6 @@ public readonly struct ResPath : IEquatable<ResPath>
return CanonPath.GetHashCode();
}
public static bool operator ==(ResPath left, ResPath right)
{
return left.Equals(right);
@@ -283,7 +297,7 @@ public readonly struct ResPath : IEquatable<ResPath>
}
// Avoid double separators
if (left.CanonPath.EndsWith("/"))
if (left.CanonPath.EndsWith('/'))
{
return new ResPath(left.CanonPath + right.CanonPath);
}
@@ -475,7 +489,7 @@ public readonly struct ResPath : IEquatable<ResPath>
/// </summary>
public string ToRelativeSystemPath()
{
return ToRelativePath().ChangeSeparator(SystemSeparatorStr);
return ToRelativePath().ChangeSeparator(SystemSeparator);
}
/// <summary>
@@ -495,14 +509,19 @@ public readonly struct ResPath : IEquatable<ResPath>
/// </summary>
public string ChangeSeparator(string newSeparator)
{
if (newSeparator is "." or "\0")
{
throw new ArgumentException("New separator can't be `.` or `NULL`");
}
if (newSeparator.Length != 1)
throw new InvalidOperationException("new separator must be a single character.");
return ChangeSeparator(newSeparator[0]);
}
return newSeparator == "/"
? CanonPath
: CanonPath.Replace("/", newSeparator);
/// <inheritdoc cref="ChangeSeparator(string)"/>
public string ChangeSeparator(char newSeparator)
{
if (newSeparator is '.' or '\0')
throw new ArgumentException("New separator can't be `.` or `NULL`");
// String.Replace() already checks if newSeparator == '/'
return CanonPath.Replace('/', newSeparator);
}
}

View File

@@ -179,7 +179,7 @@ public sealed class GenericEntityPrint
{
public EntityUid Owner;
{{fields.ToString().TrimEnd()}}
EntityUid IFluentEntityUid.FluentOwner => Owner;
readonly EntityUid IFluentEntityUid.FluentOwner => Owner;
public Entity(EntityUid owner{{parameters}})
{
@@ -212,9 +212,9 @@ public sealed class GenericEntityPrint
}
{{castRegion}}
public override int GetHashCode() => Owner.GetHashCode();
public Entity<{{nullableGenerics}}> AsNullable() => new(Owner{{selfAccess}});
public EntityUid AsType() => Owner;
public override readonly int GetHashCode() => Owner.GetHashCode();
public readonly Entity<{{nullableGenerics}}> AsNullable() => new(Owner{{selfAccess}});
public readonly EntityUid AsType() => Owner;
}

View File

@@ -4,6 +4,7 @@ using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
@@ -17,6 +18,61 @@ namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
public sealed class Broadphase_Test
{
/// <summary>
/// Tests that spawned static ents properly collide with entities in range.
/// </summary>
[Test]
public void TestStaticSpawn()
{
var sim = RobustServerSimulation
.NewSimulation()
.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var fixtureSystem = entManager.System<FixtureSystem>();
var physicsSystem = entManager.System<SharedPhysicsSystem>();
var (mapEnt, mapId) = sim.CreateMap();
var dynamicEnt = entManager.SpawnAtPosition(null, new EntityCoordinates(mapEnt, Vector2.Zero));
var dynamicBody = entManager.AddComponent<PhysicsComponent>(dynamicEnt);
physicsSystem.SetBodyType(dynamicEnt, BodyType.Dynamic, body: dynamicBody);
fixtureSystem.TryCreateFixture(dynamicEnt, new PhysShapeCircle(1f), "fix1", collisionMask: 10);
physicsSystem.WakeBody(dynamicEnt, body: dynamicBody);
Assert.That(dynamicBody.Awake);
physicsSystem.SetAwake((dynamicEnt, dynamicBody), false);
Assert.That(!dynamicBody.Awake);
// Clear move buffer
entManager.System<SharedBroadphaseSystem>().FindNewContacts(
entManager.GetComponent<PhysicsMapComponent>(mapEnt),
entManager.GetComponent<MapComponent>(mapEnt).MapId);
var staticEnt = entManager.SpawnAtPosition(null, new EntityCoordinates(mapEnt, Vector2.Zero));
var staticBody = entManager.AddComponent<PhysicsComponent>(staticEnt);
physicsSystem.SetBodyType(staticEnt, BodyType.Static, body: staticBody);
fixtureSystem.TryCreateFixture(staticEnt, new PhysShapeCircle(1f), "fix1", collisionLayer: 10);
physicsSystem.SetCanCollide(staticEnt, true);
Assert.That(!staticBody.Awake);
Assert.That(staticBody.ContactCount, Is.EqualTo(0));
entManager.System<SharedBroadphaseSystem>().FindNewContacts(
entManager.GetComponent<PhysicsMapComponent>(mapEnt),
entManager.GetComponent<MapComponent>(mapEnt).MapId);
Assert.That(staticBody.ContactCount, Is.EqualTo(1));
physicsSystem.CollideContacts();
// Make sure it's actually marked as touching and not just "well it's in range right".
Assert.That(staticBody.Contacts.First!.Value.IsTouching, Is.EqualTo(true));
}
/// <summary>
/// If we reparent a sundries entity to another broadphase does it correctly update.
/// </summary>

View File

@@ -24,7 +24,6 @@ namespace Robust.UnitTesting.Shared.Physics
var server = StartServer();
await server.WaitIdleAsync();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var fixtureSystem = server.ResolveDependency<IEntitySystemManager>()
.GetEntitySystem<FixtureSystem>();
var physicsSystem = server.ResolveDependency<IEntitySystemManager>()

View File

@@ -24,7 +24,7 @@ namespace Robust.UnitTesting.Shared.Resources
_testDir = Directory.CreateDirectory(_testDirPath);
var subDir = Path.Combine(_testDirPath, "writable");
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir));
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir), hideRootDir: false);
}
[OneTimeTearDown]

View File

@@ -36,13 +36,17 @@ public sealed class ResPathTest
[TestCase("x/y/z", ExpectedResult = "z")]
[TestCase("/bar", ExpectedResult = "bar")]
[TestCase("bar/", ExpectedResult = "bar")] // Trailing / gets trimmed.
[TestCase("/foo/bar/", ExpectedResult = "bar")]
// These next two tests are the current behaviour. I don't know if this is how it should behave, these tests just
// ensure that it doesn't change unintentionally
[TestCase("/foo/bar//", ExpectedResult = "")]
[TestCase("/foo/bar///", ExpectedResult = "")]
public string FilenameTest(string input)
{
var resPathFilename = new ResPath(input).Filename;
return resPathFilename;
}
[Test]
[TestCase(@"", ExpectedResult = @".")]
[TestCase(@".", ExpectedResult = @".")]
@@ -66,6 +70,11 @@ public sealed class ResPathTest
[TestCase(@"/foo/bar/x", ExpectedResult = @"/foo/bar")]
[TestCase(@"/foo/bar.txt", ExpectedResult = @"/foo")]
[TestCase(@"/bar.txt", ExpectedResult = @"/")]
// These next three tests are the current behaviour. I don't know if this is how it should behave, these tests just
// ensure that it doesn't change unintentionally
[TestCase(@"/foo/bar//", ExpectedResult = "/foo/bar")]
[TestCase(@"/foo/bar///", ExpectedResult = "/foo/bar/")]
[TestCase(@"/foo/bar////", ExpectedResult = "/foo/bar//")]
public string DirectoryTest(string path)
{
var resPathDirectory = new ResPath(path).Directory.ToString();
@@ -73,8 +82,7 @@ public sealed class ResPathTest
}
[Test]
[TestCase(@"a/b/c", "👏", ExpectedResult = "a👏b👏c")]
[TestCase(@"/a/b/c", "👏", ExpectedResult = "👏a👏b👏c")]
[TestCase(@"a/b/c", "\\", ExpectedResult = @"a\b\c")]
[TestCase(@"/a/b/c", "\\", ExpectedResult = @"\a\b\c")]
public string ChangeSeparatorTest(string input, string separator)
{