diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index 7d113e790ac..38eb985ac7a 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -27,7 +27,8 @@ jobs:
run: dotnet restore
- name: Build
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- - name: Test Engine
+ - name: Robust.UnitTesting
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0
-
+ - name: Robust.Analyzers.Tests
+ run: dotnet test --no-build Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj -- NUnit.ConsoleOut=0
diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props
index 4dcb01ee645..d3d045acda2 100644
--- a/MSBuild/Robust.Engine.Version.props
+++ b/MSBuild/Robust.Engine.Version.props
@@ -1,4 +1,4 @@
- 214.2.0
+ 220.2.0
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 9d6f5fb0749..535730bb689 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,4 +1,4 @@
-# Release notes for RobustToolbox.
+# Release notes for RobustToolbox.
*None yet*
+## 220.2.0
+
+### New features
+
+* RSIs can now specify load parameters, mimicking the ones from `.png.yml`. Currently only disabling sRGB is supported.
+* Added a second UV channel to Clyde's vertex format. On regular batched sprite draws, this goes 0 -> 1 across the sprite quad.
+* Added a new `CopyToShaderParameters` system for `SpriteComponent` layers.
+
+
+## 220.1.0
+
+### Bugfixes
+
+* Fix client-side replay exceptions due to dropped states when recording.
+
+### Other
+
+* Remove IP + HWId from ViewVariables.
+* Close BUIs upon disconnect.
+
+
+## 220.0.0
+
+### Breaking changes
+
+* Refactor UserInterfaceSystem.
+ - The API has been significantly cleaned up and standardised, most noticeably callers don't need to worry about TryGetUi and can rely on either HasUi, SetUiState, CloseUi, or OpenUi to handle their code as appropriate.
+ - Interface data is now stored via key rather than as a flat list which is a breaking change for YAML.
+ - BoundUserInterfaces can now be completely handled via Shared code. Existing Server-side callers will behave similarly to before.
+ - BoundUserInterfaces now properly close in many more situations, additionally they are now attached to the entity so reconnecting can re-open them and they can be serialized properly.
+
+
+## 219.2.0
+
+### New features
+
+* Add SetMapCoordinates to TransformSystem.
+* Improve YAML Linter and validation of static fields.
+
+### Bugfixes
+
+* Fix DebugCoordsPanel freezing when hovering a control.
+
+### Other
+
+* Optimise physics networking to not dirty every tick of movement.
+
+
+## 219.1.3
+
+### Bugfixes
+
+* Fix map-loader not pausing pre-init maps when not actively overwriting an existing map.
+
+
+## 219.1.2
+
+### Bugfixes
+
+* Fix map-loader not map-initialising grids when loading into a post-init map.
+
+
+## 219.1.1
+
+### Bugfixes
+
+* Fix map-loader not map-initialising maps when overwriting a post-init map.
+
+
+## 219.1.0
+
+### New features
+
+* Added a new optional arguments to various entity spawning methods, including a new argument to set the entity's rotation.
+
+### Bugfixes
+
+* Fixes map initialisation not always initialising all entities on a map.
+
+### Other
+
+* The default value of the `auth.mode` cvar has changed
+
+
+## 219.0.0
+
+### Breaking changes
+
+* Move most IMapManager functionality to SharedMapSystem.
+
+
+## 218.2.0
+
+### New features
+
+* Control layout properties such as `Margin` can now be set via style sheets.
+* Expose worldposition in SpriteComponent.Render
+* Network audio entity Play/Pause/Stop states and playback position.
+* Add `Disabled` functionality to `Slider` control.
+
+
+## 218.1.0
+
+### New features
+
+* Add IEquatable.Equals to the sandbox.
+* Enable roslyn extensions tests in CI.
+* Add a VerticalTabContainer control to match the horizontal one.
+
+### Bugfixes
+
+* Fix divison remainder issue for Colors, fixing purples.
+
+### Other
+
+* Default hub address (`hub.hub_urls`) has been changed to `https://hub.spacestation14.com/`.
+
+
+## 218.0.0
+
+### Breaking changes
+
+* `Robust.Shared.Configuration.EnvironmentVariables` is now internal and no longer usable by content.
+
+### New features
+
+* Add TryGetRandom to EntityManager to get a random entity with the specified component and TryGetRandom to IPrototypeManager to return a random prototype of the specified type.
+* Add CopyData to AppearanceSystem.
+* Update UI themes on prototype reloads.
+* Allow scaling the line height of a RichTextLabel.
+* You can now specify CVar overrides via environment variable with the `ROBUST_CVAR_*` prefix. For example `ROBUST_CVAR_game__hostname=foobar` would set the appropriate CVar. Double underscores in the environment variable name are replaced with ".".
+* Added non-generic variant of `GetCVar` to `IConfigurationManager`.
+* Add type tracking to FieldNotFoundErrorNode for serialization.
+* Distance between lines of a `RichTextLabel` can now be modified with `LineHeightScale`.
+* UI theme prototypes are now updated when reloaded.
+* New `RA0025` analyzer diagnostic warns for manual assignment to `[Dependency]` fields.
+
+### Bugfixes
+
+* Request headers in `IStatusHandlerContext` are now case-insensitive.
+* SetWorldPosition rotation now more closely aligns with prior behavior.
+* Fix exception when inspecting elements in some cases.
+* Fix HTTP errors on watchdog ping not being reported.
+
+### Other
+
+* Add an analyzer for redundantly assigning to dependency fields.
+
+### Internal
+
+* Remove redundant Exists checks in ContainerSystem.
+* Improve logging on watchdog pings.
+
+
+## 217.2.1
+
+### Bugfixes
+
+* Fix LineEdit tests on engine.
+
+### Internal
+
+* Make various ValueList enumerators access the span directly for performance.
+
+
+## 217.2.0
+
+### New features
+
+* Added `AddComponents` and `RemoveComponents` methods to EntityManager that handle EntityPrototype / ComponentRegistry bulk component changes.
+* Add double-clicking to LineEdit.
+
+### Bugfixes
+
+* Properly ignore non-hard fixtures for IntersectRayWithPredicate.
+* Fix nullable TimeSpan addition on some platforms.
+
+
+## 217.1.0
+
+### New features
+
+* Added `IRobustRandom.GetItems` extension methods for randomly picking multiple items from a collections.
+* Added `SharedPhysicsSystem.EffectiveCurTime`. This is effectively a variation of `IGameTiming.CurTime` that takes into account the current physics sub-step.
+
+### Bugfixes
+
+* Fix `MapComponent.LightingEnabled` not leaving FOV rendering in a broken state.
+
+### Internal
+
+* `Shuffle(Span, System.Random)` has been removed, just use the builtin method.
+
+
+## 217.0.0
+
+### Breaking changes
+
+* TransformSystem.SetWorldPosition and SetWorldPositionRotation will now also perform parent updates as necessary. Previously it would just set the entity's LocalPosition which may break if they were inside of a container. Now they will be removed from their container and TryFindGridAt will run to correctly parent them to the new position. If the old functionality is desired then you can use GetInvWorldMatrix to update the LocalPosition (bearing in mind containers may prevent this).
+
+### New features
+
+* Implement VV for AudioParams on SoundSpecifiers.
+* Add AddUi to the shared UI system.
+
+### Bugfixes
+
+* Fix the first measure of ScrollContainer bars.
+
+
+## 216.0.0
+
+### Breaking changes
+
+* The `net.low_lod_distance` cvar has been replaced with a new `net.pvs_priority_range`. Instead of limiting the range at which all entities are sent to a player, it now extends the range at which high priorities can be sent. The default value of this new cvar is 32.5, which is larger than the default `net.pvs_range` value of 25.
+
+### New features
+
+* You can now specify a component to not be saved to map files with `[UnsavedComponent]`.
+* Added `ITileDefinitionManager.TryGetDefinition`.
+* The map loader now tries to preserve the `tilemap` contents of map files, which should reduce diffs when re-saving a map after the game's internal tile IDs have changed.
+
+### Bugfixes
+
+* Fix buffered audio sources not being disposed.
+
+
+## 215.3.1
+
+### Bugfixes
+
+* Revert zstd update.
+
+
+## 215.3.0
+
+### New features
+
+* `EntityQuery` now has `HasComp` and `TryComp` methods that are shorter than its existing ones.
+* Added `PlacementInformation.UseEditorContext`.
+* Added `Vector2Helpers` functions for comparing ranges between vectors.
+
+### Bugfixes
+
+* `Texture.GetPixel()`: fixed off-by-one with Y coordinate.
+* `Texture.GetPixel()`: fix stack overflow when reading large images.
+* `Texture.GetPixel()`: use more widely compatible OpenGL calls.
+
+### Other
+
+* Disabled `net.mtu_expand` again by default, as it was causing issues.
+* Updated `SharpZstd` dependency.
+
+
+## 215.2.0
+
+### New features
+
+* Implement basic VV for SoundSpecifiers.
+
+### Bugfixes
+
+* Fix QueueDel during EndCollideEvents from throwing while removing contacts.
+
+
+## 215.1.0
+
+### New features
+
+* Add a CompletionHelper for audio filepaths that handles server packaging.
+* Add Random.NextAngle(min, max) method and Pick for `ValueList`.
+* Added an `ICommonSession` parser for toolshed commands.
+
+### Bugfixes
+
+
+## 215.0.0
+
+### Breaking changes
+
+* Update Lidgren to 0.3.0
+
+### New features
+
+* Made a new `IMetricsManager` interface with an `UpdateMetrics` event that can be used to update Prometheus metrics whenever they are scraped.
+ * Also added a `metrics.update_interval` CVar to go along with this, when metrics are scraped without usage of Prometheus directly.
+* IoC now contains an `IMeterFactory` implementation that you can use to instantiate metric meters.
+* `net.mtu_ipv6` CVar allows specifying a different MTU value for IPv6.
+* Allows `player:entity` to take a parameter representing the player name.
+* Add collection parsing to the dev window for UI.
+* Add a debug assert to Dirty(uid, comp) to catch mismatches being passed in.
+
+### Bugfixes
+
+* Support transform states with unknown parents.
+* Fix serialization error logging.
+* Fix naming of ResizableMemoryRegion metrics.
+* Fix uncaught overflow exception when parsing NetEntities.
+
+### Other
+
+* The replay system now allows loading a replay with a mismatching serializer type hash. This means replays should be more robust against future version updates (engine security patches or .NET updates).
+* `CheckBox`'s interior texture is now vertically centered.
+* Lidgren.Network has been updated to [`v0.3.0`](https://github.com/space-wizards/SpaceWizards.Lidgren.Network/blob/v0.3.0/RELEASE-NOTES.md).
+* Lowered default IPv4 MTU to 900 (from 1000).
+* Automatic MTU expansion (`net.mtu_expand`) is now enabled by default.
+
+### Internal
+
+* Cleanup some Dirty component calls internally.
+
+
## 214.2.0
### New features
diff --git a/Resources/Locale/en-US/commands.ftl b/Resources/Locale/en-US/commands.ftl
index c997a65501e..caeab57bfff 100644
--- a/Resources/Locale/en-US/commands.ftl
+++ b/Resources/Locale/en-US/commands.ftl
@@ -11,6 +11,7 @@ cmd-parse-failure-uid = {$arg} is not a valid entity UID.
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
cmd-parse-failure-grid = {$arg} is not a valid grid.
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
+cmd-parse-failure-session = There is no session with username: {$username}
cmd-error-file-not-found = Could not find file: {$file}.
cmd-error-dir-not-found = Could not find directory: {$dir}.
diff --git a/Resources/Locale/en-US/view-variables.ftl b/Resources/Locale/en-US/view-variables.ftl
index 0a27bb4162e..c16b1434fdd 100644
--- a/Resources/Locale/en-US/view-variables.ftl
+++ b/Resources/Locale/en-US/view-variables.ftl
@@ -10,3 +10,18 @@ view-variable-instance-entity-client-components-search-bar-placeholder = Search
view-variable-instance-entity-server-components-search-bar-placeholder = Search
view-variable-instance-entity-add-window-server-components = Add Component [S]
view-variable-instance-entity-add-window-client-components = Add Component [C]
+
+
+## SoundSpecifier
+vv-sound-none = None
+vv-sound-path = Path
+vv-sound-collection = Collection
+
+vv-sound-volume = volume
+vv-sound-pitch = Pitch
+vv-sound-max-distance = Max Distance
+vv-sound-rolloff-factor = Rolloff Factor
+vv-sound-reference-distance = Reference Distance
+vv-sound-loop = Loop
+vv-sound-play-offset = Play Offset (s)
+vv-sound-variation = Pitch variation
diff --git a/Robust.Analyzers.Tests/AccessAnalyzer_Test.cs b/Robust.Analyzers.Tests/AccessAnalyzer_Test.cs
index 49be0c67ffc..ab6e46111b1 100644
--- a/Robust.Analyzers.Tests/AccessAnalyzer_Test.cs
+++ b/Robust.Analyzers.Tests/AccessAnalyzer_Test.cs
@@ -20,11 +20,16 @@ public Task Verifier(string code, params DiagnosticResult[] expected)
{
TestState =
{
- AdditionalReferences = { typeof(AccessAnalyzer).Assembly },
Sources = { code }
},
};
+ TestHelper.AddEmbeddedSources(
+ test.TestState,
+ "Robust.Shared.Analyzers.AccessAttribute.cs",
+ "Robust.Shared.Analyzers.AccessPermissions.cs"
+ );
+
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
diff --git a/Robust.Analyzers.Tests/DependencyAssignAnalyzerTest.cs b/Robust.Analyzers.Tests/DependencyAssignAnalyzerTest.cs
new file mode 100644
index 00000000000..f55a2cf65a0
--- /dev/null
+++ b/Robust.Analyzers.Tests/DependencyAssignAnalyzerTest.cs
@@ -0,0 +1,58 @@
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+using NUnit.Framework;
+using VerifyCS =
+ Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier;
+
+namespace Robust.Analyzers.Tests;
+
+[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
+[TestFixture]
+public sealed class DependencyAssignAnalyzerTest
+{
+ private static Task Verifier(string code, params DiagnosticResult[] expected)
+ {
+ var test = new CSharpAnalyzerTest()
+ {
+ TestState =
+ {
+ Sources = { code }
+ },
+ };
+
+ TestHelper.AddEmbeddedSources(
+ test.TestState,
+ "Robust.Shared.IoC.DependencyAttribute.cs"
+ );
+
+ // ExpectedDiagnostics cannot be set, so we need to AddRange here...
+ test.TestState.ExpectedDiagnostics.AddRange(expected);
+
+ return test.RunAsync();
+ }
+
+ [Test]
+ public async Task Test()
+ {
+ const string code = """
+ using Robust.Shared.IoC;
+
+ public sealed class Foo
+ {
+ [Dependency]
+ private object? Field;
+
+ public Foo()
+ {
+ Field = "A";
+ }
+ }
+ """;
+
+ await Verifier(code,
+ // /0/Test0.cs(10,9): warning RA0025: Tried to assign to [Dependency] field 'Field'. Remove [Dependency] or inject it via field injection instead.
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 20).WithArguments("Field"));
+ }
+}
diff --git a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj
index 954204dcbe4..513bdebfbfa 100644
--- a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj
+++ b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj
@@ -6,6 +6,13 @@
+
+
+
+
+
+
+
false
diff --git a/Robust.Analyzers.Tests/TestHelper.cs b/Robust.Analyzers.Tests/TestHelper.cs
new file mode 100644
index 00000000000..2e5e6374c3e
--- /dev/null
+++ b/Robust.Analyzers.Tests/TestHelper.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Robust.Analyzers.Tests;
+
+public static class TestHelper
+{
+ public static void AddEmbeddedSources(SolutionState state, params string[] embeddedFiles)
+ {
+ AddEmbeddedSources(state, (IEnumerable) embeddedFiles);
+ }
+
+ public static void AddEmbeddedSources(SolutionState state, IEnumerable embeddedFiles)
+ {
+ foreach (var fileName in embeddedFiles)
+ {
+ using var stream = typeof(AccessAnalyzer_Test).Assembly.GetManifestResourceStream(fileName)!;
+ state.Sources.Add((fileName, SourceText.From(stream)));
+ }
+ }
+}
diff --git a/Robust.Analyzers/DependencyAssignAnalyzer.cs b/Robust.Analyzers/DependencyAssignAnalyzer.cs
new file mode 100644
index 00000000000..607a1e5d919
--- /dev/null
+++ b/Robust.Analyzers/DependencyAssignAnalyzer.cs
@@ -0,0 +1,61 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+using Robust.Roslyn.Shared;
+
+namespace Robust.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class DependencyAssignAnalyzer : DiagnosticAnalyzer
+{
+ private const string DependencyAttributeType = "Robust.Shared.IoC.DependencyAttribute";
+
+ private static readonly DiagnosticDescriptor Rule = new (
+ Diagnostics.IdDependencyFieldAssigned,
+ "Assignment to dependency field",
+ "Tried to assign to [Dependency] field '{0}'. Remove [Dependency] or inject it via field injection instead.",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ true);
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.RegisterOperationAction(CheckAssignment, OperationKind.SimpleAssignment);
+ }
+
+ private static void CheckAssignment(OperationAnalysisContext context)
+ {
+ if (context.Operation is not ISimpleAssignmentOperation assignment)
+ return;
+
+ if (assignment.Target is not IFieldReferenceOperation fieldRef)
+ return;
+
+ var field = fieldRef.Field;
+ var attributes = field.GetAttributes();
+ if (attributes.Length == 0)
+ return;
+
+ var depAttribute = context.Compilation.GetTypeByMetadataName(DependencyAttributeType);
+ if (!HasAttribute(attributes, depAttribute))
+ return;
+
+ context.ReportDiagnostic(Diagnostic.Create(Rule, assignment.Syntax.GetLocation(), field.Name));
+ }
+
+ private static bool HasAttribute(ImmutableArray attributes, ISymbol symbol)
+ {
+ foreach (var attribute in attributes)
+ {
+ if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, symbol))
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/Robust.Analyzers/Robust.Analyzers.csproj b/Robust.Analyzers/Robust.Analyzers.csproj
index c115cd001bf..8bbb58f0fcf 100644
--- a/Robust.Analyzers/Robust.Analyzers.csproj
+++ b/Robust.Analyzers/Robust.Analyzers.csproj
@@ -2,24 +2,29 @@
-
+
-
-
+
+
-
+
disable
+
+ $(DefineConstants);ROBUST_ANALYZERS_IMPL
diff --git a/Robust.Benchmarks/Collections/ValueListEnumerationBenchmarks.cs b/Robust.Benchmarks/Collections/ValueListEnumerationBenchmarks.cs
new file mode 100644
index 00000000000..69681f1294d
--- /dev/null
+++ b/Robust.Benchmarks/Collections/ValueListEnumerationBenchmarks.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Collections;
+
+namespace Robust.Benchmarks.Collections;
+
+[Virtual]
+public class ValueListEnumerationBenchmarks
+{
+ [Params(4, 16, 64)]
+ public int N { get; set; }
+
+ private sealed class Data(int i)
+ {
+ public readonly int I = i;
+ }
+
+ private ValueList _valueList;
+ private Data[] _array = default!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ var list = new List(N);
+ for (var i = 0; i < N; i++)
+ {
+ list.Add(new(i));
+ }
+
+ _array = list.ToArray();
+ _valueList = new(list.ToArray());
+ }
+
+ [Benchmark]
+ public int ValueList()
+ {
+ var total = 0;
+ foreach (var ev in _valueList)
+ {
+ total += ev.I;
+ }
+
+ return total;
+ }
+
+ [Benchmark]
+ public int ValueListSpan()
+ {
+ var total = 0;
+ foreach (var ev in _valueList.Span)
+ {
+ total += ev.I;
+ }
+
+ return total;
+ }
+
+ [Benchmark]
+ public int Array()
+ {
+ var total = 0;
+ foreach (var ev in _array)
+ {
+ total += ev.I;
+ }
+
+ return total;
+ }
+
+ [Benchmark]
+ public int Span()
+ {
+ var total = 0;
+ foreach (var ev in _array.AsSpan())
+ {
+ total += ev.I;
+ }
+
+ return total;
+ }
+}
diff --git a/Robust.Benchmarks/EntityManager/AddRemoveComponentBenchmark.cs b/Robust.Benchmarks/EntityManager/AddRemoveComponentBenchmark.cs
index 767ba7db3be..adb9020c204 100644
--- a/Robust.Benchmarks/EntityManager/AddRemoveComponentBenchmark.cs
+++ b/Robust.Benchmarks/EntityManager/AddRemoveComponentBenchmark.cs
@@ -26,9 +26,8 @@ public void GlobalSetup()
.InitializeInstance();
_entityManager = _simulation.Resolve();
-
- var coords = new MapCoordinates(0, 0, new MapId(1));
- _simulation.AddMap(coords.MapId);
+ var map = _simulation.CreateMap().Uid;
+ var coords = new EntityCoordinates(map, default);
for (var i = 0; i < N; i++)
{
diff --git a/Robust.Benchmarks/EntityManager/ComponentIteratorBenchmark.cs b/Robust.Benchmarks/EntityManager/ComponentIteratorBenchmark.cs
index f8bbba7bd78..bd551658b63 100644
--- a/Robust.Benchmarks/EntityManager/ComponentIteratorBenchmark.cs
+++ b/Robust.Benchmarks/EntityManager/ComponentIteratorBenchmark.cs
@@ -29,8 +29,8 @@ public void GlobalSetup()
Comps = new A[N+2];
- var coords = new MapCoordinates(0, 0, new MapId(1));
- _simulation.AddMap(coords.MapId);
+ var map = _simulation.CreateMap().MapId;
+ var coords = new MapCoordinates(default, map);
for (var i = 0; i < N; i++)
{
diff --git a/Robust.Benchmarks/EntityManager/GetComponentBenchmark.cs b/Robust.Benchmarks/EntityManager/GetComponentBenchmark.cs
index d3f16b6d387..72568de6824 100644
--- a/Robust.Benchmarks/EntityManager/GetComponentBenchmark.cs
+++ b/Robust.Benchmarks/EntityManager/GetComponentBenchmark.cs
@@ -31,8 +31,8 @@ public void GlobalSetup()
Comps = new A[N+2];
- var coords = new MapCoordinates(0, 0, new MapId(1));
- _simulation.AddMap(coords.MapId);
+ var map = _simulation.CreateMap().Uid;
+ var coords = new EntityCoordinates(map, default);
for (var i = 0; i < N; i++)
{
diff --git a/Robust.Benchmarks/EntityManager/SpawnDeleteEntityBenchmark.cs b/Robust.Benchmarks/EntityManager/SpawnDeleteEntityBenchmark.cs
index 4acb57101e3..d7a47a10aa9 100644
--- a/Robust.Benchmarks/EntityManager/SpawnDeleteEntityBenchmark.cs
+++ b/Robust.Benchmarks/EntityManager/SpawnDeleteEntityBenchmark.cs
@@ -29,10 +29,9 @@ public void GlobalSetup()
.InitializeInstance();
_entityManager = _simulation.Resolve();
-
- _mapCoords = new MapCoordinates(0, 0, new MapId(1));
- var uid = _simulation.AddMap(_mapCoords.MapId);
- _entCoords = new EntityCoordinates(uid, 0, 0);
+ var (map, mapId) = _simulation.CreateMap();
+ _mapCoords = new MapCoordinates(default, mapId);
+ _entCoords = new EntityCoordinates(map, 0, 0);
}
[Benchmark(Baseline = true)]
diff --git a/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs b/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs
index fba81cd10fc..6deaa7e1290 100644
--- a/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs
+++ b/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs
@@ -91,8 +91,7 @@ public void GlobalSetup()
// Set up map and spawn player
server.WaitPost(() =>
{
- var mapId = mapMan.CreateMap();
- var map = mapMan.GetMapEntityId(mapId);
+ var map = server.ResolveDependency().CreateMap(out var mapId);
var gridComp = mapMan.CreateGridEntity(mapId);
var grid = gridComp.Owner;
mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1));
diff --git a/Robust.Client/Audio/AudioOverlay.cs b/Robust.Client/Audio/AudioOverlay.cs
index 73ef2d24e2c..9cf129d0a21 100644
--- a/Robust.Client/Audio/AudioOverlay.cs
+++ b/Robust.Client/Audio/AudioOverlay.cs
@@ -74,11 +74,13 @@ protected internal override void Draw(in OverlayDrawArgs args)
output.Clear();
output.AppendLine("Audio Source");
output.AppendLine("Runtime:");
+ output.AppendLine($"- Distance: {_audio.GetAudioDistance(distance.Length()):0.00}");
output.AppendLine($"- Occlusion: {posOcclusion:0.0000}");
output.AppendLine("Params:");
+ output.AppendLine($"- RolloffFactor: {comp.RolloffFactor:0.0000}");
output.AppendLine($"- Volume: {comp.Volume:0.0000}");
- output.AppendLine($"- Reference distance: {comp.ReferenceDistance}");
- output.AppendLine($"- Max distance: {comp.MaxDistance}");
+ output.AppendLine($"- Reference distance: {comp.ReferenceDistance:0.00}");
+ output.AppendLine($"- Max distance: {comp.MaxDistance:0.00}");
var outputText = output.ToString().Trim();
var dimensions = screenHandle.GetDimensions(_font, outputText, 1f);
var buffer = new Vector2(3f, 3f);
diff --git a/Robust.Client/Audio/AudioSystem.cs b/Robust.Client/Audio/AudioSystem.cs
index dccd639a9b3..c52d99b134a 100644
--- a/Robust.Client/Audio/AudioSystem.cs
+++ b/Robust.Client/Audio/AudioSystem.cs
@@ -126,6 +126,33 @@ private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAuto
{
component.Source.SetAuxiliary(null);
}
+
+ switch (component.State)
+ {
+ case AudioState.Playing:
+ component.StartPlaying();
+ break;
+ case AudioState.Paused:
+ component.Pause();
+ break;
+ case AudioState.Stopped:
+ component.StopPlaying();
+ component.PlaybackPosition = 0f;
+ break;
+ }
+
+ // If playback position changed then update it.
+ if (!string.IsNullOrEmpty(component.FileName))
+ {
+ var position = (float) ((component.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
+ var currentPosition = component.Source.PlaybackPosition;
+ var diff = Math.Abs(position - currentPosition);
+
+ if (diff > 0.1f)
+ {
+ component.PlaybackPosition = position;
+ }
+ }
}
///
@@ -173,7 +200,7 @@ private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentSt
private void SetupSource(Entity entity, AudioResource audioResource, TimeSpan? length = null)
{
var component = entity.Comp;
-
+
if (TryAudioLimit(component.FileName))
{
var newSource = _audio.CreateAudioSource(audioResource);
@@ -361,7 +388,7 @@ private void ProcessStream(EntityUid entity, AudioComponent component, Transform
var distance = delta.Length();
// Out of range so just clip it for us.
- if (distance > component.MaxDistance)
+ if (GetAudioDistance(distance) > component.MaxDistance)
{
// Still keeps the source playing, just with no volume.
component.Gain = 0f;
@@ -427,13 +454,13 @@ private bool TryGetAudio(AudioStream stream, [NotNullWhen(true)] out AudioResour
return false;
}
- public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
+ public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
}
- public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
}
@@ -460,8 +487,11 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(Soun
///
/// The resource path to the OGG Vorbis file to play.
///
- private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, AudioParams? audioParams = null, bool recordReplay = true)
+ private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, AudioParams? audioParams = null, bool recordReplay = true)
{
+ if (string.IsNullOrEmpty(filename))
+ return null;
+
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
@@ -493,8 +523,11 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(Soun
///
/// The resource path to the OGG Vorbis file to play.
/// The entity "emitting" the audio.
- private (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
+ private (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
{
+ if (string.IsNullOrEmpty(filename))
+ return null;
+
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
@@ -534,8 +567,11 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(Soun
/// The resource path to the OGG Vorbis file to play.
/// The coordinates at which to play the audio.
///
- private (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
+ private (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
{
+ if (string.IsNullOrEmpty(filename))
+ return null;
+
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
@@ -569,25 +605,25 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(Soun
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
{
return PlayEntity(filename, entity, audioParams);
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}
@@ -603,31 +639,31 @@ public override void LoadStream(Entity entity, T stream)
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}
diff --git a/Robust.Client/Audio/Sources/BaseAudioSource.cs b/Robust.Client/Audio/Sources/BaseAudioSource.cs
index 1523b41194e..073e25a9fa7 100644
--- a/Robust.Client/Audio/Sources/BaseAudioSource.cs
+++ b/Robust.Client/Audio/Sources/BaseAudioSource.cs
@@ -314,6 +314,8 @@ public float PlaybackPosition
set
{
_checkDisposed();
+
+ value = MathF.Max(value, 0f);
AL.Source(SourceHandle, ALSourcef.SecOffset, value);
Master._checkAlError($"Tried to set invalid playback position of {value:0.00}");
}
diff --git a/Robust.Client/BaseClient.cs b/Robust.Client/BaseClient.cs
index d96537ba08f..146dd1cc212 100644
--- a/Robust.Client/BaseClient.cs
+++ b/Robust.Client/BaseClient.cs
@@ -285,6 +285,7 @@ void IPostInjectInit.PostInject()
///
/// Enumeration of the run levels of the BaseClient.
///
+ ///
public enum ClientRunLevel : byte
{
Error = 0,
@@ -315,6 +316,21 @@ public enum ClientRunLevel : byte
SinglePlayerGame,
}
+ ///
+ /// Helper functions for working with .
+ ///
+ public static class ClientRunLevelExt
+ {
+ ///
+ /// Check if a is
+ /// or .
+ ///
+ public static bool IsInGameLike(this ClientRunLevel runLevel)
+ {
+ return runLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame;
+ }
+ }
+
///
/// Event arguments for when something changed with the player.
///
diff --git a/Robust.Client/Console/Commands/Debug.cs b/Robust.Client/Console/Commands/Debug.cs
index 893710777f3..3a5e2df0aa9 100644
--- a/Robust.Client/Console/Commands/Debug.cs
+++ b/Robust.Client/Console/Commands/Debug.cs
@@ -294,7 +294,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
internal sealed class SnapGridGetCell : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
- [Dependency] private readonly IMapManager _map = default!;
public override string Command => "sggcell";
@@ -320,7 +319,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
return;
}
- if (_map.TryGetGrid(_entManager.GetEntity(gridNet), out var grid))
+ if (_entManager.TryGetComponent(_entManager.GetEntity(gridNet), out var grid))
{
foreach (var entity in grid.GetAnchoredEntities(new Vector2i(
int.Parse(indices.Split(',')[0], CultureInfo.InvariantCulture),
@@ -429,7 +428,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
internal sealed class GridTileCount : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
- [Dependency] private readonly IMapManager _map = default!;
public override string Command => "gridtc";
@@ -448,7 +446,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
return;
}
- if (_map.TryGetGrid(gridUid, out var grid))
+ if (_entManager.TryGetComponent(gridUid, out var grid))
{
shell.WriteLine(grid.GetAllTiles().Count().ToString());
}
@@ -555,7 +553,7 @@ internal static List GetAllMembers(Control control)
if (type != typeof(Control))
cname = $"Control > {cname}";
- returnVal.GetOrNew(cname).Add((member.Name, member.GetValue(control)?.ToString() ?? "null"));
+ returnVal.GetOrNew(cname).Add((member.Name, GetMemberValue(member, control, ", ")));
}
foreach (var (attachedProperty, value) in control.AllAttachedProperties)
@@ -570,6 +568,28 @@ internal static List GetAllMembers(Control control)
}
return returnVal;
}
+
+ internal static string PropertyValuesString(Control control, string key)
+ {
+ var member = GetAllMembers(control).Find(m => m.Name == key);
+ return GetMemberValue(member, control, "\n", "\"{0}\"");
+ }
+
+ private static string GetMemberValue(MemberInfo? member, Control control, string separator, string
+ wrap = "{0}")
+ {
+ var value = member?.GetValue(control);
+ var o = value switch
+ {
+ ICollection controls => string.Join(separator,
+ controls.Select(ctrl => $"{ctrl.Name}({ctrl.GetType()})")),
+ ICollection list => string.Join(separator, list),
+ null => null,
+ _ => value.ToString()
+ };
+ // Convert to quote surrounded string or null with no quotes
+ return o is not null ? string.Format(wrap, o) : "null";
+ }
}
internal sealed class SetClipboardCommand : LocalizedCommands
diff --git a/Robust.Client/GameController/GameController.cs b/Robust.Client/GameController/GameController.cs
index cb21e096b75..f855d902ba2 100644
--- a/Robust.Client/GameController/GameController.cs
+++ b/Robust.Client/GameController/GameController.cs
@@ -612,6 +612,8 @@ private void Update(FrameEventArgs frameEventArgs)
{
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
}
+
+ _audio.FlushALDisposeQueues();
}
internal static void SetupLogging(
diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs
index e29d45c5853..f6467daa075 100644
--- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs
+++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs
@@ -6,13 +6,12 @@
using System.Numerics;
using System.Text;
using Robust.Client.Graphics;
+using Robust.Client.Graphics.Clyde;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
-using Robust.Shared;
using Robust.Shared.Animations;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
-using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -30,6 +29,7 @@
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer;
using Direction = Robust.Shared.Maths.Direction;
+using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.GameObjects
{
@@ -772,15 +772,7 @@ public void LayerSetData(int index, PrototypeLayerData layerDatum)
{
foreach (var keyString in layerDatum.MapKeys)
{
- object key;
- if (reflection.TryParseEnumReference(keyString, out var @enum))
- {
- key = @enum;
- }
- else
- {
- key = keyString;
- }
+ var key = ParseKey(keyString);
if (LayerMap.TryGetValue(key, out var mappedIndex))
{
@@ -806,9 +798,30 @@ public void LayerSetData(int index, PrototypeLayerData layerDatum)
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = layerDatum.Visible ?? layer.Visible;
+ if (layerDatum.CopyToShaderParameters is { } copyParameters)
+ {
+ layer.CopyToShaderParameters = new CopyToShaderParameters(ParseKey(copyParameters.LayerKey))
+ {
+ ParameterTexture = copyParameters.ParameterTexture,
+ ParameterUV = copyParameters.ParameterUV
+ };
+ }
+ else
+ {
+ layer.CopyToShaderParameters = null;
+ }
+
RebuildBounds();
}
+ private object ParseKey(string keyString)
+ {
+ if (reflection.TryParseEnumReference(keyString, out var @enum))
+ return @enum;
+
+ return keyString;
+ }
+
public void LayerSetData(object layerKey, PrototypeLayerData data)
{
if (!LayerMapTryGet(layerKey, out var layer, true))
@@ -1237,9 +1250,9 @@ public RSI.StateId LayerGetState(int layer)
public IEnumerable AllLayers => Layers;
// Lobby SpriteView rendering path
- public void Render(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Direction? overrideDirection = null)
+ public void Render(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Direction? overrideDirection = null, Vector2 position = default)
{
- RenderInternal(drawingHandle, eyeRotation, worldRotation, Vector2.Zero, overrideDirection);
+ RenderInternal(drawingHandle, eyeRotation, worldRotation, position, overrideDirection);
}
[DataField("noRot")] private bool _screenLock = false;
@@ -1637,6 +1650,9 @@ public Vector2 Offset
[ViewVariables]
public LayerRenderingStrategy RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy;
+ [ViewVariables(VVAccess.ReadWrite)]
+ public CopyToShaderParameters? CopyToShaderParameters;
+
public Layer(SpriteComponent parent)
{
_parent = parent;
@@ -2009,8 +2025,6 @@ internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3 spriteMatrix,
// Set the drawing transform for this layer
GetLayerDrawMatrix(dir, out var layerMatrix);
- Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
- drawingHandle.SetTransform(in transformMatrix);
// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
// due to direction overrides or offsets.
@@ -2020,7 +2034,41 @@ internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3 spriteMatrix,
// Get the correct directional texture from the state, and draw it!
var texture = GetRenderTexture(_actualState, dir);
- RenderTexture(drawingHandle, texture);
+
+ if (CopyToShaderParameters == null)
+ {
+ // Set the drawing transform for this layer
+ Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
+ drawingHandle.SetTransform(in transformMatrix);
+
+ RenderTexture(drawingHandle, texture);
+ }
+ else
+ {
+ // Multiple atrocities to god being committed right here.
+ var otherLayerIdx = _parent.LayerMap[CopyToShaderParameters.LayerKey!];
+ var otherLayer = _parent.Layers[otherLayerIdx];
+ if (otherLayer.Shader is not { } shader)
+ {
+ // No shader set apparently..?
+ return;
+ }
+
+ if (!shader.Mutable)
+ otherLayer.Shader = shader = shader.Duplicate();
+
+ var clydeTexture = Clyde.RenderHandle.ExtractTexture(texture, null, out var csr);
+ var sr = Clyde.RenderHandle.WorldTextureBoundsToUV(clydeTexture, csr);
+
+ if (CopyToShaderParameters.ParameterTexture is { } paramTexture)
+ shader.SetParameter(paramTexture, clydeTexture);
+
+ if (CopyToShaderParameters.ParameterUV is { } paramUV)
+ {
+ var uv = new Vector4(sr.Left, sr.Bottom, sr.Right, sr.Top);
+ shader.SetParameter(paramUV, uv);
+ }
+ }
}
private void RenderTexture(DrawingHandleWorld drawingHandle, Texture texture)
@@ -2098,6 +2146,17 @@ internal void AdvanceFrameAnimation(RSI.State state)
}
}
+ ///
+ /// Instantiated version of .
+ /// Has actually resolved to a a real key.
+ ///
+ public sealed class CopyToShaderParameters(object layerKey)
+ {
+ public object LayerKey = layerKey;
+ public string? ParameterTexture;
+ public string? ParameterUV;
+ }
+
void IAnimationProperties.SetAnimatableProperty(string name, object value)
{
if (!name.StartsWith("layer/"))
diff --git a/Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs b/Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs
index 973b601e86d..661d412cdfd 100644
--- a/Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs
+++ b/Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs
@@ -107,7 +107,7 @@ private void HandleComponentState(EntityUid uid, ContainerManagerComponent compo
toDelete.Add(id);
}
- foreach (var dead in toDelete)
+ foreach (var dead in toDelete.Span)
{
component.Containers.Remove(dead);
}
@@ -142,7 +142,7 @@ private void HandleComponentState(EntityUid uid, ContainerManagerComponent compo
toRemove.Add(entity);
}
- foreach (var entity in toRemove)
+ foreach (var entity in toRemove.Span)
{
Remove(
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity)),
@@ -162,7 +162,7 @@ private void HandleComponentState(EntityUid uid, ContainerManagerComponent compo
removedExpected.Add(netEntity);
}
- foreach (var entityUid in removedExpected)
+ foreach (var entityUid in removedExpected.Span)
{
RemoveExpectedEntity(entityUid, out _);
}
diff --git a/Robust.Client/GameObjects/EntitySystems/InputSystem.cs b/Robust.Client/GameObjects/EntitySystems/InputSystem.cs
index 1d4e87b5131..f73ad60aa49 100644
--- a/Robust.Client/GameObjects/EntitySystems/InputSystem.cs
+++ b/Robust.Client/GameObjects/EntitySystems/InputSystem.cs
@@ -26,6 +26,7 @@ public sealed class InputSystem : SharedInputSystem, IPostInjectInit
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ILogManager _logManager = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
private ISawmill _sawmillInputContext = default!;
@@ -151,7 +152,7 @@ private void GenerateInputCommand(IConsoleShell shell, string argstr, string[] a
var pxform = Transform(pent);
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
- var coords = EntityCoordinates.FromMap(EntityManager, pent, new MapCoordinates(wPos, pxform.MapID));
+ var coords = EntityCoordinates.FromMap(pent, new MapCoordinates(wPos, pxform.MapID), _transform, EntityManager);
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
diff --git a/Robust.Client/GameObjects/EntitySystems/MapSystem.cs b/Robust.Client/GameObjects/EntitySystems/MapSystem.cs
index 44ef2827552..9077957e04e 100644
--- a/Robust.Client/GameObjects/EntitySystems/MapSystem.cs
+++ b/Robust.Client/GameObjects/EntitySystems/MapSystem.cs
@@ -1,12 +1,9 @@
using Robust.Client.Graphics;
using Robust.Client.Map;
-using Robust.Client.Physics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Physics.Dynamics;
namespace Robust.Client.GameObjects;
@@ -16,6 +13,17 @@ public sealed class MapSystem : SharedMapSystem
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
+ protected override MapId GetNextMapId()
+ {
+ // Client-side map entities use negative map Ids to avoid conflict with server-side maps.
+ var id = new MapId(--LastMapId);
+ while (MapManager.MapExists(id))
+ {
+ id = new MapId(--LastMapId);
+ }
+ return id;
+ }
+
public override void Initialize()
{
base.Initialize();
@@ -27,9 +35,4 @@ public override void Shutdown()
base.Shutdown();
_overlayManager.RemoveOverlay();
}
-
- protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
- {
- EnsureComp(uid);
- }
}
diff --git a/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs b/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs
index f50db2892a8..20a0225a6a9 100644
--- a/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs
+++ b/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs
@@ -1,84 +1,8 @@
-using Robust.Client.Player;
using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Reflection;
-using System;
-using UserInterfaceComponent = Robust.Shared.GameObjects.UserInterfaceComponent;
-namespace Robust.Client.GameObjects
-{
- public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
- {
- [Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly IReflectionManager _reflectionManager = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeNetworkEvent(MessageReceived);
- }
-
- private void MessageReceived(BoundUIWrapMessage ev)
- {
- var uid = GetEntity(ev.Entity);
-
- if (!TryComp(uid, out var cmp))
- return;
-
- var uiKey = ev.UiKey;
- var message = ev.Message;
- message.Session = _playerManager.LocalSession!;
- message.Entity = GetNetEntity(uid);
- message.UiKey = uiKey;
-
- // Raise as object so the correct type is used.
- RaiseLocalEvent(uid, (object)message, true);
-
- switch (message)
- {
- case OpenBoundInterfaceMessage _:
- TryOpenUi(uid, uiKey, cmp);
- break;
-
- case CloseBoundInterfaceMessage _:
- TryCloseUi(message.Session, uid, uiKey, remoteCall: true, uiComp: cmp);
- break;
+namespace Robust.Client.GameObjects;
- default:
- if (cmp.OpenInterfaces.TryGetValue(uiKey, out var bui))
- bui.InternalReceiveMessage(message);
-
- break;
- }
- }
-
- private bool TryOpenUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? uiComp = null)
- {
- if (!Resolve(uid, ref uiComp))
- return false;
-
- if (uiComp.OpenInterfaces.ContainsKey(uiKey))
- return false;
-
- var data = uiComp.MappedInterfaceData[uiKey];
-
- // TODO: This type should be cached, but I'm too lazy.
- var type = _reflectionManager.LooseGetType(data.ClientType);
- var boundInterface =
- (BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new object[] {uid, uiKey});
-
- boundInterface.Open();
- uiComp.OpenInterfaces[uiKey] = boundInterface;
-
- if (_playerManager.LocalSession is { } playerSession)
- {
- uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
- RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);
- }
+public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
+{
- return true;
- }
- }
}
diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs
index 77174dc090d..472bf2b43a7 100644
--- a/Robust.Client/GameStates/ClientGameStateManager.cs
+++ b/Robust.Client/GameStates/ClientGameStateManager.cs
@@ -125,6 +125,8 @@ public sealed class ClientGameStateManager : IClientGameStateManager
#endif
private bool _resettingPredictedEntities;
+ private readonly List _brokenEnts = new();
+ private readonly List<(EntityUid, NetEntity)> _toStart = new();
///
public void Initialize()
@@ -667,7 +669,16 @@ private void MergeImplicitData(IEnumerable createdEntities)
foreach (var netEntity in createdEntities)
{
+#if EXCEPTION_TOLERANCE
+ if (!_entityManager.TryGetEntityData(netEntity, out _, out var meta))
+ {
+ _sawmill.Error($"Encountered deleted entity while merging implicit data! NetEntity: {netEntity}");
+ continue;
+ }
+#else
var (_, meta) = _entityManager.GetEntityData(netEntity);
+#endif
+
var compData = _compDataPool.Get();
_outputData.Add(netEntity, compData);
@@ -700,7 +711,7 @@ public IEnumerable ApplyGameState(GameState curState, GameState? next
{
using var _ = _timing.StartStateApplicationArea();
- // TODO repays optimize this.
+ // TODO replays optimize this.
// This currently just saves game states as they are applied.
// However this is inefficient and may have redundant data.
// E.g., we may record states: [10 to 15] [11 to 16] *error* [0 to 18] [18 to 19] [18 to 20] ...
@@ -1138,7 +1149,7 @@ private void Detach(GameTick maxTick,
if ((meta.Flags & MetaDataFlags.InContainer) != 0 &&
metas.TryGetComponent(xform.ParentUid, out var containerMeta) &&
(containerMeta.Flags & MetaDataFlags.Detached) == 0 &&
- containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container, null, true))
+ containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container))
{
containerSys.Remove((ent.Value, xform, meta), container, false, true);
}
@@ -1157,63 +1168,58 @@ private void Detach(GameTick maxTick,
private void InitializeAndStart(Dictionary toCreate)
{
- var metaQuery = _entityManager.GetEntityQuery();
+ _toStart.Clear();
-#if EXCEPTION_TOLERANCE
- var brokenEnts = new List();
-#endif
using (_prof.Group("Initialize Entity"))
{
+ EntityUid entity = default;
foreach (var netEntity in toCreate.Keys)
{
- var entity = _entityManager.GetEntity(netEntity);
-#if EXCEPTION_TOLERANCE
try
{
-#endif
- _entities.InitializeEntity(entity, metaQuery.GetComponent(entity));
-#if EXCEPTION_TOLERANCE
+ (entity, var meta) = _entityManager.GetEntityData(netEntity);
+ _entities.InitializeEntity(entity, meta);
+ _toStart.Add((entity, netEntity));
}
catch (Exception e)
{
- _sawmill.Error($"Server entity threw in Init: ent={_entities.ToPrettyString(entity)}");
+ _sawmill.Error($"Server entity threw in Init: nent={netEntity}, ent={_entities.ToPrettyString(entity)}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
- brokenEnts.Add(entity);
- toCreate.Remove(netEntity);
- }
+ _toCreate.Remove(netEntity);
+ _brokenEnts.Add(entity);
+#if !EXCEPTION_TOLERANCE
+ throw;
#endif
+ }
}
}
using (_prof.Group("Start Entity"))
{
- foreach (var netEntity in toCreate.Keys)
+ foreach (var (entity, netEntity) in _toStart)
{
- var entity = _entityManager.GetEntity(netEntity);
-#if EXCEPTION_TOLERANCE
try
{
-#endif
- _entities.StartEntity(entity);
-#if EXCEPTION_TOLERANCE
+ _entities.StartEntity(entity);
}
catch (Exception e)
{
- _sawmill.Error($"Server entity threw in Start: ent={_entityManager.ToPrettyString(entity)}");
+ _sawmill.Error($"Server entity threw in Start: nent={netEntity}, ent={_entityManager.ToPrettyString(entity)}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
- brokenEnts.Add(entity);
- toCreate.Remove(netEntity);
- }
+ _toCreate.Remove(netEntity);
+ _brokenEnts.Add(entity);
+#if !EXCEPTION_TOLERANCE
+ throw;
#endif
+ }
}
}
-#if EXCEPTION_TOLERANCE
- foreach (var entity in brokenEnts)
+ foreach (var entity in _brokenEnts)
{
_entityManager.DeleteEntity(entity);
}
-#endif
+ _brokenEnts.Clear();
}
private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataComponent meta, IEventBus bus, EntityState? curState,
@@ -1329,23 +1335,8 @@ private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataCompo
foreach (var (comp, cur, next) in _compStateWork.Values)
{
- try
- {
- var handleState = new ComponentHandleState(cur, next);
- bus.RaiseComponentEvent(comp, ref handleState);
- }
-#pragma warning disable CS0168 // Variable is declared but never used
- catch (Exception e)
-#pragma warning restore CS0168 // Variable is declared but never used
- {
-#if EXCEPTION_TOLERANCE
- _sawmill.Error($"Failed to apply comp state: entity={_entities.ToPrettyString(uid)}, comp={comp.GetType()}");
- _runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(HandleEntityState)}");
-#else
- _sawmill.Error($"Failed to apply comp state: entity={_entities.ToPrettyString(uid)}, comp={comp.GetType()}");
- throw;
-#endif
- }
+ var handleState = new ComponentHandleState(cur, next);
+ bus.RaiseComponentEvent(comp, ref handleState);
}
}
@@ -1414,7 +1405,7 @@ private void DetachEntCommand(IConsoleShell shell, string argStr, string[] args)
_entities.TryGetComponent(xform.ParentUid, out MetaDataComponent? containerMeta) &&
(containerMeta.Flags & MetaDataFlags.Detached) == 0)
{
- containerSys.TryGetContainingContainer(xform.ParentUid, uid, out container, null, true);
+ containerSys.TryGetContainingContainer(xform.ParentUid, uid, out container);
}
_entities.EntitySysManager.GetEntitySystem().DetachParentToNull(uid, xform);
diff --git a/Robust.Client/Graphics/Clyde/Clyde.Constants.cs b/Robust.Client/Graphics/Clyde/Clyde.Constants.cs
index 94b1fe3acf6..2ae93219693 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.Constants.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.Constants.cs
@@ -6,7 +6,8 @@ private static readonly (string, uint)[] BaseShaderAttribLocations =
{
("aPos", 0),
("tCoord", 1),
- ("modulate", 2)
+ ("tCoord2", 2),
+ ("modulate", 3)
};
private const int UniIModUV = 0;
diff --git a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs
index 724bde9b088..d718479778a 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs
@@ -124,7 +124,7 @@ private void CullEmptyChunks()
{
foreach (var (grid, chunks) in _mapChunkData)
{
- var gridComp = _mapManager.GetGridComp(grid);
+ var gridComp = _entityManager.GetComponent(grid);
foreach (var (index, chunk) in chunks)
{
if (!chunk.Dirty || gridComp.Chunks.ContainsKey(index))
diff --git a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
index 17e40bc9dbb..a4cc29756f7 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
@@ -11,6 +11,7 @@
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Utility;
@@ -250,10 +251,8 @@ private List GetOverlaysForSpace(OverlaySpace space)
private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye)
{
var mapId = eye.Position.MapId;
- if (mapId == MapId.Nullspace || !_mapManager.HasMapEntity(mapId))
- {
+ if (mapId == MapId.Nullspace)
return;
- }
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldAABB, worldBounds);
var worldOverlays = GetOverlaysForSpace(OverlaySpace.WorldSpaceEntities);
@@ -514,7 +513,9 @@ private void RenderViewport(Viewport viewport)
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawLight && eye.DrawFov)
{
- ApplyFovToBuffer(viewport, eye);
+ var mapUid = _mapManager.GetMapEntityId(eye.Position.MapId);
+ if (_entityManager.GetComponent(mapUid).LightingEnabled)
+ ApplyFovToBuffer(viewport, eye);
}
}
diff --git a/Robust.Client/Graphics/Clyde/Clyde.Layout.cs b/Robust.Client/Graphics/Clyde/Clyde.Layout.cs
index 07a0f145f76..7f6ab086b71 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.Layout.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.Layout.cs
@@ -23,9 +23,12 @@ private static unsafe void SetupVAOLayout()
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
- // Colour Modulation.
- GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
+ // Texture Coords (2).
+ GL.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
GL.EnableVertexAttribArray(2);
+ // Colour Modulation.
+ GL.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 6 * sizeof(float));
+ GL.EnableVertexAttribArray(3);
}
// NOTE: This is:
@@ -37,6 +40,7 @@ private readonly struct Vertex2D
{
public readonly Vector2 Position;
public readonly Vector2 TextureCoordinates;
+ public readonly Vector2 TextureCoordinates2;
// Note that this color is in linear space.
public readonly Color Modulate;
@@ -48,6 +52,15 @@ public Vertex2D(Vector2 position, Vector2 textureCoordinates, Color modulate)
Modulate = modulate;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vertex2D(Vector2 position, Vector2 textureCoordinates, Vector2 textureCoordinates2, Color modulate)
+ {
+ Position = position;
+ TextureCoordinates = textureCoordinates;
+ TextureCoordinates2 = textureCoordinates2;
+ Modulate = modulate;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vertex2D(float x, float y, float u, float v, float r, float g, float b, float a)
: this(new Vector2(x, y), new Vector2(u, v), new Color(r, g, b, a))
diff --git a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
index b3453a7574c..b51d70c0165 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
@@ -15,7 +15,7 @@ internal partial class Clyde
{
private RenderHandle _renderHandle = default!;
- private sealed class RenderHandle : IRenderHandle
+ internal sealed class RenderHandle : IRenderHandle
{
private readonly Clyde _clyde;
private readonly IEntityManager _entities;
@@ -88,16 +88,21 @@ public void DrawTextureWorld(Texture texture, Vector2 bl, Vector2 br, Vector2 tl
{
var clydeTexture = ExtractTexture(texture, in subRegion, out var csr);
- var (w, h) = clydeTexture.Size;
- var sr = new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
+ var sr = WorldTextureBoundsToUV(clydeTexture, csr);
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
}
+ internal static Box2 WorldTextureBoundsToUV(ClydeTexture texture, UIBox2 csr)
+ {
+ var (w, h) = texture.Size;
+ return new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
+ }
+
///
/// Converts a subRegion (px) into texture coords (0-1) of a given texture (cells of the textureAtlas).
///
- private static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr)
+ internal static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr)
{
if (texture is AtlasTexture atlas)
{
diff --git a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs
index 8a58541e575..da77262b139 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs
@@ -578,10 +578,10 @@ private void DrawTexture(ClydeHandle texture, Vector2 bl, Vector2 br, Vector2 tl
// TODO: split batch if necessary.
var vIdx = BatchVertexIndex;
- BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, modulate);
- BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, modulate);
- BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, modulate);
- BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, modulate);
+ BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, new Vector2(0, 0), modulate);
+ BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, new Vector2(1, 0), modulate);
+ BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, new Vector2(1, 1), modulate);
+ BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, new Vector2(0, 1), modulate);
BatchVertexIndex += 4;
QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx);
diff --git a/Robust.Client/Graphics/Clyde/Clyde.Textures.cs b/Robust.Client/Graphics/Clyde/Clyde.Textures.cs
index 8a453a248ee..4dc90a723e0 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.Textures.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.Textures.cs
@@ -601,7 +601,7 @@ private void FlushTextureDispose()
}
}
- private sealed class ClydeTexture : OwnedTexture
+ internal sealed class ClydeTexture : OwnedTexture
{
private readonly Clyde _clyde;
public readonly bool IsSrgb;
@@ -649,24 +649,30 @@ public override string ToString()
return $"ClydeTexture: ({TextureId})";
}
- public override Color GetPixel(int x, int y)
+ public override unsafe Color GetPixel(int x, int y)
{
if (!_clyde._loadedTextures.TryGetValue(TextureId, out var loaded))
{
throw new DataException("Texture not found");
}
- Span rgba = stackalloc byte[4*this.Size.X*this.Size.Y];
- unsafe
- {
- fixed (byte* p = rgba)
- {
+ var curTexture2D = GL.GetInteger(GetPName.TextureBinding2D);
+ var bufSize = 4 * loaded.Size.X * loaded.Size.Y;
+ var buffer = ArrayPool.Shared.Rent(bufSize);
+
+ GL.BindTexture(TextureTarget.Texture2D, loaded.OpenGLObject.Handle);
- GL.GetTextureImage(loaded.OpenGLObject.Handle, 0, PF.Rgba, PT.UnsignedByte, 4*this.Size.X*this.Size.Y, (IntPtr) p);
- }
+ fixed (byte* p = buffer)
+ {
+ GL.GetnTexImage(TextureTarget.Texture2D, 0, PF.Rgba, PT.UnsignedByte, bufSize, (IntPtr) p);
}
- int pixelPos = (this.Size.X*(this.Size.Y-y) + x)*4;
- return new Color(rgba[pixelPos+0], rgba[pixelPos+1], rgba[pixelPos+2], rgba[pixelPos+3]);
+
+ GL.BindTexture(TextureTarget.Texture2D, curTexture2D);
+
+ var pixelPos = (loaded.Size.X * (loaded.Size.Y - y - 1) + x) * 4;
+ var color = new Color(buffer[pixelPos+0], buffer[pixelPos+1], buffer[pixelPos+2], buffer[pixelPos+3]);
+ ArrayPool.Shared.Return(buffer);
+ return color;
}
}
diff --git a/Robust.Client/Graphics/Clyde/Shaders/base-default.frag b/Robust.Client/Graphics/Clyde/Shaders/base-default.frag
index 15bdb5b1252..a0830f17f4b 100644
--- a/Robust.Client/Graphics/Clyde/Shaders/base-default.frag
+++ b/Robust.Client/Graphics/Clyde/Shaders/base-default.frag
@@ -1,4 +1,5 @@
varying highp vec2 UV;
+varying highp vec2 UV2;
varying highp vec2 Pos;
varying highp vec4 VtxModulate;
diff --git a/Robust.Client/Graphics/Clyde/Shaders/base-default.vert b/Robust.Client/Graphics/Clyde/Shaders/base-default.vert
index 28d21c4f137..51ba6649c94 100644
--- a/Robust.Client/Graphics/Clyde/Shaders/base-default.vert
+++ b/Robust.Client/Graphics/Clyde/Shaders/base-default.vert
@@ -2,10 +2,12 @@
/*layout (location = 0)*/ attribute vec2 aPos;
// Texture coordinates.
/*layout (location = 1)*/ attribute vec2 tCoord;
+/*layout (location = 2)*/ attribute vec2 tCoord2;
// Colour modulation.
-/*layout (location = 2)*/ attribute vec4 modulate;
+/*layout (location = 3)*/ attribute vec4 modulate;
varying vec2 UV;
+varying vec2 UV2;
varying vec2 Pos;
varying vec4 VtxModulate;
@@ -36,5 +38,6 @@ void main()
gl_Position = vec4(VERTEX, 0.0, 1.0);
Pos = (VERTEX + 1.0) / 2.0;
UV = mix(modifyUV.xy, modifyUV.zw, tCoord);
+ UV2 = tCoord2;
VtxModulate = zFromSrgb(modulate);
}
diff --git a/Robust.Client/Graphics/Clyde/Shaders/base-raw.frag b/Robust.Client/Graphics/Clyde/Shaders/base-raw.frag
index e72eaeebbf1..b62afbb8d56 100644
--- a/Robust.Client/Graphics/Clyde/Shaders/base-raw.frag
+++ b/Robust.Client/Graphics/Clyde/Shaders/base-raw.frag
@@ -1,4 +1,5 @@
varying highp vec2 UV;
+varying highp vec2 UV2;
uniform sampler2D lightMap;
diff --git a/Robust.Client/Graphics/Clyde/Shaders/base-raw.vert b/Robust.Client/Graphics/Clyde/Shaders/base-raw.vert
index e5b8cf27fbc..11fee7a4f6c 100644
--- a/Robust.Client/Graphics/Clyde/Shaders/base-raw.vert
+++ b/Robust.Client/Graphics/Clyde/Shaders/base-raw.vert
@@ -2,10 +2,12 @@
/*layout (location = 0)*/ attribute vec2 aPos;
// Texture coordinates.
/*layout (location = 1)*/ attribute vec2 tCoord;
+/*layout (location = 2)*/ attribute vec2 tCoord2;
// Colour modulation.
-/*layout (location = 2)*/ attribute vec4 modulate;
+/*layout (location = 3)*/ attribute vec4 modulate;
varying vec2 UV;
+varying vec2 UV2;
// Maybe we should merge these CPU side.
// idk yet.
@@ -40,6 +42,7 @@ void main()
vec2 VERTEX = aPos;
UV = tCoord;
+ UV2 = tCoord2;
// [SHADER_CODE]
diff --git a/Robust.Client/Graphics/Drawing/DrawingHandleBase.cs b/Robust.Client/Graphics/Drawing/DrawingHandleBase.cs
index 525fa741f43..56259861687 100644
--- a/Robust.Client/Graphics/Drawing/DrawingHandleBase.cs
+++ b/Robust.Client/Graphics/Drawing/DrawingHandleBase.cs
@@ -114,43 +114,12 @@ public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ReadOnlySpan
DrawPrimitives(primitiveTopology, White, indices, drawVertices);
}
- private static void PadVerticesV2(ReadOnlySpan input, Span output, Color color)
+ private void PadVerticesV2(ReadOnlySpan input, Span output, Color color)
{
- if (input.Length == 0)
- return;
-
- if (input.Length != output.Length)
- {
- throw new InvalidOperationException("Invalid lengths!");
- }
-
- var colorLinear = Color.FromSrgb(color);
- var colorVec = Unsafe.As>(ref colorLinear);
- var uvVec = Vector128.Create(0, 0, 0.5f, 0.5f);
- var maskVec = Vector128.Create(0xFFFFFFFF, 0xFFFFFFFF, 0, 0).AsSingle();
-
- var simdVectors = (nuint)(input.Length / 2);
- ref readonly var srcBase = ref Unsafe.As(ref Unsafe.AsRef(in input[0]));
- ref var dstBase = ref Unsafe.As(ref output[0]);
-
- for (nuint i = 0; i < simdVectors; i++)
- {
- var positions = Vector128.LoadUnsafe(in srcBase, i * 4);
-
- var posColorLower = (positions & maskVec) | uvVec;
- var posColorUpper = (Vector128.Shuffle(positions, Vector128.Create(2, 3, 0, 0)) & maskVec) | uvVec;
-
- posColorLower.StoreUnsafe(ref dstBase, i * 16);
- colorVec.StoreUnsafe(ref dstBase, i * 16 + 4);
- posColorUpper.StoreUnsafe(ref dstBase, i * 16 + 8);
- colorVec.StoreUnsafe(ref dstBase, i * 16 + 12);
- }
-
- var lastPos = (int)simdVectors * 2;
- if (lastPos != output.Length)
+ Color colorLinear = Color.FromSrgb(color);
+ for (var i = 0; i < output.Length; i++)
{
- // Odd number of vertices. Handle the last manually.
- output[lastPos] = new DrawVertexUV2DColor(input[lastPos], new Vector2(0.5f, 0.5f), colorLinear);
+ output[i] = new DrawVertexUV2DColor(input[i], new Vector2(0.5f, 0.5f), colorLinear);
}
}
@@ -268,6 +237,8 @@ public struct DrawVertexUV2DColor
{
public Vector2 Position;
public Vector2 UV;
+ public Vector2 UV2;
+
///
/// Modulation colour for this vertex.
/// Note that this color is in linear space.
diff --git a/Robust.Client/Graphics/Shaders/ShaderPrototype.cs b/Robust.Client/Graphics/Shaders/ShaderPrototype.cs
index 0aed10c6130..8c8f255ae55 100644
--- a/Robust.Client/Graphics/Shaders/ShaderPrototype.cs
+++ b/Robust.Client/Graphics/Shaders/ShaderPrototype.cs
@@ -17,7 +17,7 @@
namespace Robust.Client.Graphics
{
[Prototype("shader")]
- public sealed class ShaderPrototype : IPrototype, ISerializationHooks
+ public sealed partial class ShaderPrototype : IPrototype, ISerializationHooks
{
[ViewVariables]
[IdDataField]
diff --git a/Robust.Client/Placement/Modes/AlignSimilar.cs b/Robust.Client/Placement/Modes/AlignSimilar.cs
index ae5bb28ab98..94a1eb191f8 100644
--- a/Robust.Client/Placement/Modes/AlignSimilar.cs
+++ b/Robust.Client/Placement/Modes/AlignSimilar.cs
@@ -34,7 +34,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
var snapToEntities = EntitySystem.Get().GetEntitiesInRange(MouseCoords, SnapToRange)
.Where(entity => pManager.EntityManager.GetComponent(entity).EntityPrototype == pManager.CurrentPrototype && pManager.EntityManager.GetComponent(entity).MapID == mapId)
- .OrderBy(entity => (pManager.EntityManager.GetComponent(entity).WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager)).LengthSquared())
+ .OrderBy(entity => (pManager.EntityManager.GetComponent(entity).WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager, pManager.EntityManager.System())).LengthSquared())
.ToList();
if (snapToEntities.Count == 0)
diff --git a/Robust.Client/Placement/Modes/AlignSnapgridBorder.cs b/Robust.Client/Placement/Modes/AlignSnapgridBorder.cs
index 1f84bc116b5..f3a4d3dff2d 100644
--- a/Robust.Client/Placement/Modes/AlignSnapgridBorder.cs
+++ b/Robust.Client/Placement/Modes/AlignSnapgridBorder.cs
@@ -2,6 +2,7 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
namespace Robust.Client.Placement.Modes
@@ -24,7 +25,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
SnapSize = 1f;
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
- Grid = pManager.MapManager.GetGrid(gridId);
+ Grid = pManager.EntityManager.GetComponent(gridId);
SnapSize = Grid.TileSize; //Find snap size for the grid.
}
else
diff --git a/Robust.Client/Placement/Modes/AlignSnapgridCenter.cs b/Robust.Client/Placement/Modes/AlignSnapgridCenter.cs
index eb12cddd234..7c3266b53d3 100644
--- a/Robust.Client/Placement/Modes/AlignSnapgridCenter.cs
+++ b/Robust.Client/Placement/Modes/AlignSnapgridCenter.cs
@@ -56,7 +56,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
SnapSize = 1f;
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
- Grid = pManager.MapManager.GetGrid(gridId);
+ Grid = pManager.EntityManager.GetComponent(gridId);
SnapSize = Grid.TileSize; //Find snap size for the grid.
}
else
diff --git a/Robust.Client/Placement/Modes/AlignTileAny.cs b/Robust.Client/Placement/Modes/AlignTileAny.cs
index b0774b65c6e..89e239f1398 100644
--- a/Robust.Client/Placement/Modes/AlignTileAny.cs
+++ b/Robust.Client/Placement/Modes/AlignTileAny.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
namespace Robust.Client.Placement.Modes
{
@@ -19,7 +20,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
var gridId = MouseCoords.GetGridUid(pManager.EntityManager);
- if (!pManager.MapManager.TryGetGrid(gridId, out var mapGrid))
+ if (!pManager.EntityManager.TryGetComponent(gridId, out var mapGrid))
return;
CurrentTile = mapGrid.GetTileRef(MouseCoords);
diff --git a/Robust.Client/Placement/Modes/AlignTileDense.cs b/Robust.Client/Placement/Modes/AlignTileDense.cs
index 86e2cf55c47..50af90644b8 100644
--- a/Robust.Client/Placement/Modes/AlignTileDense.cs
+++ b/Robust.Client/Placement/Modes/AlignTileDense.cs
@@ -1,6 +1,7 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
namespace Robust.Client.Placement.Modes
{
@@ -20,7 +21,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
- var mapGrid = pManager.MapManager.GetGrid(gridId);
+ var mapGrid = pManager.EntityManager.GetComponent(gridId);
tileSize = mapGrid.TileSize; //convert from ushort to float
}
diff --git a/Robust.Client/Placement/Modes/AlignTileEmpty.cs b/Robust.Client/Placement/Modes/AlignTileEmpty.cs
index 06461dd7587..c73e805758c 100644
--- a/Robust.Client/Placement/Modes/AlignTileEmpty.cs
+++ b/Robust.Client/Placement/Modes/AlignTileEmpty.cs
@@ -2,6 +2,7 @@
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
namespace Robust.Client.Placement.Modes
@@ -22,7 +23,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
- var mapGrid = pManager.MapManager.GetGrid(gridId);
+ var mapGrid = pManager.EntityManager.GetComponent(gridId);
CurrentTile = mapGrid.GetTileRef(MouseCoords);
tileSize = mapGrid.TileSize; //convert from ushort to float
}
diff --git a/Robust.Client/Placement/Modes/AlignTileNonDense.cs b/Robust.Client/Placement/Modes/AlignTileNonDense.cs
index 07915221292..ef601ef74df 100644
--- a/Robust.Client/Placement/Modes/AlignTileNonDense.cs
+++ b/Robust.Client/Placement/Modes/AlignTileNonDense.cs
@@ -1,6 +1,7 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
namespace Robust.Client.Placement.Modes
{
@@ -20,7 +21,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
- var mapGrid = pManager.MapManager.GetGrid(gridId);
+ var mapGrid = pManager.EntityManager.GetComponent(gridId);
tileSize = mapGrid.TileSize; //convert from ushort to float
}
diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs
index a6bc7221e9a..d28c90a1a05 100644
--- a/Robust.Client/Placement/PlacementManager.cs
+++ b/Robust.Client/Placement/PlacementManager.cs
@@ -21,6 +21,7 @@
using Robust.Shared.Utility;
using Robust.Shared.Log;
using Direction = Robust.Shared.Maths.Direction;
+using Robust.Shared.Map.Components;
namespace Robust.Client.Placement
{
@@ -79,6 +80,10 @@ public bool IsActive
private set
{
_isActive = value;
+
+ if (CurrentPermission?.UseEditorContext is false)
+ return;
+
SwitchEditorContext(value);
}
}
@@ -332,7 +337,7 @@ private void HandlePlacementMessage(MsgPlacement msg)
private void HandleTileChanged(ref TileChangedEvent args)
{
- var coords = MapManager.GetGrid(args.NewTile.GridUid).GridTileToLocal(args.NewTile.GridIndices);
+ var coords = EntityManager.GetComponent(args.NewTile.GridUid).GridTileToLocal(args.NewTile.GridIndices);
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
}
@@ -753,7 +758,7 @@ private void RequestPlacement(EntityCoordinates coordinates)
// If we have actually placed something on a valid grid...
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
- var grid = MapManager.GetGrid(gridId);
+ var grid = EntityManager.GetComponent(gridId);
// no point changing the tile to the same thing.
if (grid.GetTileRef(coordinates).Tile.TypeId == CurrentPermission.TileType)
diff --git a/Robust.Client/Placement/PlacementMode.cs b/Robust.Client/Placement/PlacementMode.cs
index bb367b36d60..5f8c9fd8684 100644
--- a/Robust.Client/Placement/PlacementMode.cs
+++ b/Robust.Client/Placement/PlacementMode.cs
@@ -11,6 +11,7 @@
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Utility;
@@ -115,11 +116,12 @@ public virtual void Render(DrawingHandleWorld handle)
var dirAng = pManager.Direction.ToAngle();
var spriteSys = pManager.EntityManager.System();
+ var transformSys = pManager.EntityManager.System();
foreach (var coordinate in locationcollection)
{
if (!coordinate.IsValid(pManager.EntityManager))
return; // Just some paranoia just in case
- var worldPos = coordinate.ToMapPos(pManager.EntityManager);
+ var worldPos = coordinate.ToMapPos(pManager.EntityManager, transformSys);
var worldRot = pManager.EntityManager.GetComponent(coordinate.EntityId).WorldRotation + dirAng;
sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
@@ -136,11 +138,12 @@ public IEnumerable LineCoordinates()
{
var mouseScreen = pManager.InputManager.MouseScreenPosition;
var mousePos = pManager.EyeManager.PixelToMap(mouseScreen);
+ var transformSys = pManager.EntityManager.System();
if (mousePos.MapId == MapId.Nullspace)
yield break;
- var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, pManager.EntityManager) - pManager.StartPoint;
+ var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
float iterations;
Vector2 distance;
if (Math.Abs(x) > Math.Abs(y))
@@ -167,11 +170,12 @@ public IEnumerable GridCoordinates()
{
var mouseScreen = pManager.InputManager.MouseScreenPosition;
var mousePos = pManager.EyeManager.PixelToMap(mouseScreen);
+ var transformSys = pManager.EntityManager.System();
if (mousePos.MapId == MapId.Nullspace)
yield break;
- var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, pManager.EntityManager) - pManager.StartPoint;
+ var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
var xSign = Math.Sign(placementdiff.X);
var ySign = Math.Sign(placementdiff.Y);
@@ -193,9 +197,9 @@ public IEnumerable GridCoordinates()
public TileRef GetTileRef(EntityCoordinates coordinates)
{
var gridUidOpt = coordinates.GetGridUid(pManager.EntityManager);
- return gridUidOpt is EntityUid gridUid && gridUid.IsValid() ? pManager.MapManager.GetGrid(gridUid).GetTileRef(MouseCoords)
+ return gridUidOpt is EntityUid gridUid && gridUid.IsValid() ? pManager.EntityManager.GetComponent(gridUid).GetTileRef(MouseCoords)
: new TileRef(gridUidOpt ?? EntityUid.Invalid,
- MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager), Tile.Empty);
+ MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager, pManager.EntityManager.System()), Tile.Empty);
}
public TextureResource GetSprite(string key)
@@ -223,7 +227,8 @@ public bool RangeCheck(EntityCoordinates coordinates)
}
var range = pManager.CurrentPermission!.Range;
- if (range > 0 && !pManager.EntityManager.GetComponent(controlled).Coordinates.InRange(pManager.EntityManager, coordinates, range))
+ var transformSys = pManager.EntityManager.System();
+ if (range > 0 && !pManager.EntityManager.GetComponent(controlled).Coordinates.InRange(pManager.EntityManager, transformSys, coordinates, range))
return false;
return true;
}
@@ -231,7 +236,8 @@ public bool RangeCheck(EntityCoordinates coordinates)
public bool IsColliding(EntityCoordinates coordinates)
{
var bounds = pManager.ColliderAABB;
- var mapCoords = coordinates.ToMap(pManager.EntityManager);
+ var transformSys = pManager.EntityManager.System();
+ var mapCoords = coordinates.ToMap(pManager.EntityManager, transformSys);
var (x, y) = mapCoords.Position;
var collisionBox = Box2.FromDimensions(
@@ -261,7 +267,8 @@ protected EntityCoordinates ScreenToCursorGrid(ScreenCoordinates coords)
return EntityCoordinates.FromMap(pManager.MapManager, mapCoords);
}
- return EntityCoordinates.FromMap(pManager.EntityManager, gridUid, mapCoords);
+ var transformSys = pManager.EntityManager.System();
+ return EntityCoordinates.FromMap(gridUid, mapCoords, transformSys, pManager.EntityManager);
}
}
}
diff --git a/Robust.Client/Replays/Loading/ReplayLoadManager.Checkpoints.cs b/Robust.Client/Replays/Loading/ReplayLoadManager.Checkpoints.cs
index ee11dfd2652..551fbc95a8d 100644
--- a/Robust.Client/Replays/Loading/ReplayLoadManager.Checkpoints.cs
+++ b/Robust.Client/Replays/Loading/ReplayLoadManager.Checkpoints.cs
@@ -88,7 +88,6 @@ public sealed partial class ReplayLoadManager
if (initMessages != null)
UpdateMessages(initMessages, uploadedFiles, prototypes, cvars, detachQueue, ref timeBase, true);
UpdateMessages(messages[0], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase, true);
- ProcessQueue(GameTick.MaxValue, detachQueue, detached);
var entSpan = state0.EntityStates.Value;
Dictionary entStates = new(entSpan.Count);
@@ -98,6 +97,8 @@ public sealed partial class ReplayLoadManager
entStates.Add(entState.NetEntity, modifiedState);
}
+ ProcessQueue(GameTick.MaxValue, detachQueue, detached, entStates);
+
await callback(0, states.Count, LoadingState.ProcessingFiles, true);
var playerSpan = state0.PlayerStates.Value;
Dictionary playerStates = new(playerSpan.Count);
@@ -144,7 +145,7 @@ TimeSpan GetTime(GameTick tick)
UpdatePlayerStates(curState.PlayerStates.Span, playerStates);
UpdateEntityStates(curState.EntityStates.Span, entStates, ref spawnedTracker, ref stateTracker, detached);
UpdateMessages(messages[i], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase);
- ProcessQueue(curState.ToSequence, detachQueue, detached);
+ ProcessQueue(curState.ToSequence, detachQueue, detached, entStates);
UpdateDeletions(curState.EntityDeletions, entStates, detached);
serverTime[i] = GetTime(curState.ToSequence) - initialTime;
ticksSinceLastCheckpoint++;
@@ -176,14 +177,28 @@ TimeSpan GetTime(GameTick tick)
private void ProcessQueue(
GameTick curTick,
Dictionary> detachQueue,
- HashSet detached)
+ HashSet detached,
+ Dictionary entStates)
{
foreach (var (tick, ents) in detachQueue)
{
if (tick > curTick)
continue;
detachQueue.Remove(tick);
- detached.UnionWith(ents);
+
+ foreach (var e in ents)
+ {
+ if (entStates.ContainsKey(e))
+ detached.Add(e);
+ else
+ {
+ // AFAIK this should only happen if the client skipped over some ticks, probably due to packet loss
+ // I.e., entity was created on tick n, then leaves PVS range on the tick n+1
+ // If the n-th tick gets dropped, the client only ever receives the pvs-leave message.
+ // In that case we should just ignore it.
+ _sawmill.Debug($"Received a PVS detach msg for entity {e} before it was received?");
+ }
+ }
}
}
diff --git a/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs b/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs
index 885501e85e9..023f2aab7ba 100644
--- a/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs
+++ b/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs
@@ -129,7 +129,7 @@ public async Task LoadReplayAsync(IReplayFileReader fileReader, Load
return parsed.FirstOrDefault()?.Root as MappingDataNode;
}
- private (MappingDataNode YamlData, HashSet CVars, TimeSpan Duration, TimeSpan StartTime, bool ClientSide)
+ private (MappingDataNode YamlData, HashSet CVars, TimeSpan? Duration, TimeSpan StartTime, bool ClientSide)
LoadMetadata(IReplayFileReader fileReader)
{
_sawmill.Info($"Reading replay metadata");
@@ -137,23 +137,16 @@ public async Task LoadReplayAsync(IReplayFileReader fileReader, Load
if (data == null)
throw new Exception("Failed to load yaml metadata");
- TimeSpan duration;
var finalData = LoadYamlFinalMetadata(fileReader);
- if (finalData == null)
- {
- var msg = "Failed to load final yaml metadata";
- if (!_confMan.GetCVar(CVars.ReplayIgnoreErrors))
- throw new Exception(msg);
+ TimeSpan? duration = finalData == null
+ ? null
+ : TimeSpan.Parse(((ValueDataNode) finalData[MetaFinalKeyDuration]).Value);
- _sawmill.Error(msg);
- duration = TimeSpan.FromDays(1);
- }
- else
- {
- duration = TimeSpan.Parse(((ValueDataNode) finalData[MetaFinalKeyDuration]).Value);
- }
+ if (finalData == null)
+ _sawmill.Warning("Failed to load final yaml metadata. Partial/incomplete replay?");
- var typeHash = Convert.FromHexString(((ValueDataNode) data[MetaKeyTypeHash]).Value);
+ var typeHashString = ((ValueDataNode) data[MetaKeyTypeHash]).Value;
+ var typeHash = Convert.FromHexString(typeHashString);
var stringHash = Convert.FromHexString(((ValueDataNode) data[MetaKeyStringHash]).Value);
var startTick = ((ValueDataNode) data[MetaKeyStartTick]).Value;
var timeBaseTick = ((ValueDataNode) data[MetaKeyBaseTick]).Value;
@@ -161,7 +154,12 @@ public async Task LoadReplayAsync(IReplayFileReader fileReader, Load
var clientSide = bool.Parse(((ValueDataNode) data[MetaKeyIsClientRecording]).Value);
if (!typeHash.SequenceEqual(_serializer.GetSerializableTypesHash()))
- throw new Exception($"{nameof(IRobustSerializer)} hashes do not match. Loading replays using a bad replay-client version?");
+ {
+ if (!_confMan.GetCVar(CVars.ReplayIgnoreErrors))
+ throw new Exception($"RobustSerializer hash mismatch. do not match. Client hash: {_serializer.GetSerializableTypesHashString()}, replay hash: {typeHashString}.");
+
+ _sawmill.Warning($"RobustSerializer hash mismatch. Replay may fail to load!");
+ }
using var stringFile = fileReader.Open(FileStrings);
var stringData = new byte[stringFile.Length];
diff --git a/Robust.Client/Replays/Playback/ReplayPlaybackManager.Checkpoint.cs b/Robust.Client/Replays/Playback/ReplayPlaybackManager.Checkpoint.cs
index adc8ed1248f..fa1cb57c46c 100644
--- a/Robust.Client/Replays/Playback/ReplayPlaybackManager.Checkpoint.cs
+++ b/Robust.Client/Replays/Playback/ReplayPlaybackManager.Checkpoint.cs
@@ -79,13 +79,14 @@ private void EnsureDetachedExist(CheckpointState checkpoint)
if (checkpoint.DetachedStates == null)
return;
- DebugTools.Assert(checkpoint.Detached.Count == checkpoint.DetachedStates.Length); ;
- var metas = _entMan.GetEntityQuery();
+ DebugTools.Assert(checkpoint.Detached.Count == checkpoint.DetachedStates.Length);
foreach (var es in checkpoint.DetachedStates)
{
- var uid = _entMan.GetEntity(es.NetEntity);
- if (metas.TryGetComponent(uid, out var meta) && !meta.EntityDeleted)
+ if (_entMan.TryGetEntityData(es.NetEntity, out var uid, out var meta))
+ {
+ DebugTools.Assert(!meta.EntityDeleted);
continue;
+ }
var metaState = (MetaDataComponentState?)es.ComponentChanges.Value?
.FirstOrDefault(c => c.NetID == _metaId).State;
@@ -93,18 +94,16 @@ private void EnsureDetachedExist(CheckpointState checkpoint)
if (metaState == null)
throw new MissingMetadataException(es.NetEntity);
- _entMan.CreateEntityUninitialized(metaState.PrototypeId, uid);
- meta = metas.GetComponent(uid);
+ uid = _entMan.CreateEntity(metaState.PrototypeId, out meta);
// Client creates a client-side net entity for the newly created entity.
// We need to clear this mapping before assigning the real net id.
// TODO NetEntity Jank: prevent the client from creating this in the first place.
_entMan.ClearNetEntity(meta.NetEntity);
+ _entMan.SetNetEntity(uid.Value, es.NetEntity, meta);
- _entMan.SetNetEntity(uid, es.NetEntity, meta);
-
- _entMan.InitializeEntity(uid, meta);
- _entMan.StartEntity(uid);
+ _entMan.InitializeEntity(uid.Value, meta);
+ _entMan.StartEntity(uid.Value);
meta.LastStateApplied = checkpoint.Tick;
}
}
diff --git a/Robust.Client/Replays/UI/ReplayControlWidget.cs b/Robust.Client/Replays/UI/ReplayControlWidget.cs
index dc8589733c1..3e9db292e30 100644
--- a/Robust.Client/Replays/UI/ReplayControlWidget.cs
+++ b/Robust.Client/Replays/UI/ReplayControlWidget.cs
@@ -90,6 +90,7 @@ protected override void FrameUpdate(FrameEventArgs args)
var maxIndex = Math.Max(1, replay.States.Count - 1);
var state = replay.States[index];
var replayTime = TimeSpan.FromSeconds(TickSlider.Value);
+ var end = replay.Duration == null ? "N/A" : replay.Duration.Value.ToString(TimeFormat);
IndexLabel.Text = Loc.GetString("replay-time-box-index-label",
("current", index), ("total", maxIndex), ("percentage", percentage));
@@ -98,10 +99,10 @@ protected override void FrameUpdate(FrameEventArgs args)
("current", state.ToSequence), ("total", replay.States[^1].ToSequence), ("percentage", percentage));
TimeLabel.Text = Loc.GetString("replay-time-box-replay-time-label",
- ("current", replayTime.ToString(TimeFormat)), ("end", replay.Duration.ToString(TimeFormat)), ("percentage", percentage));
+ ("current", replayTime.ToString(TimeFormat)), ("end", end), ("percentage", percentage));
var serverTime = (replayTime + replay.StartTime).ToString(TimeFormat);
- var duration = (replay.Duration + replay.StartTime).ToString(TimeFormat);
+ string duration = replay.Duration == null ? "N/A" : (replay.Duration + replay.StartTime).Value.ToString(TimeFormat);
ServerTimeLabel.Text = Loc.GetString("replay-time-box-server-time-label",
("current", serverTime), ("end", duration), ("percentage", percentage));
diff --git a/Robust.Client/ResourceManagement/ResourceCache.Preload.cs b/Robust.Client/ResourceManagement/ResourceCache.Preload.cs
index f698171adbf..cc567565ef5 100644
--- a/Robust.Client/ResourceManagement/ResourceCache.Preload.cs
+++ b/Robust.Client/ResourceManagement/ResourceCache.Preload.cs
@@ -10,6 +10,7 @@
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
+using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
@@ -142,6 +143,26 @@ private void PreloadRsis(ISawmill sawmill)
}
});
+ // Do not meta-atlas RSIs with custom load parameters.
+ var atlasList = rsiList.Where(x => x.LoadParameters == TextureLoadParameters.Default).ToArray();
+ var nonAtlasList = rsiList.Where(x => x.LoadParameters != TextureLoadParameters.Default).ToArray();
+
+ foreach (var data in nonAtlasList)
+ {
+ if (data.Bad)
+ continue;
+
+ try
+ {
+ RSIResource.LoadTexture(Clyde, data);
+ }
+ catch (Exception e)
+ {
+ sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
+ data.Bad = true;
+ }
+ }
+
// This combines individual RSI atlases into larger atlases to reduce draw batches. currently this is a VERY
// lazy bundling and is not at all compact, its basically an atlas of RSI atlases. Really what this should
// try to do is to have each RSI write directly to the atlas, rather than having each RSI write to its own
@@ -155,7 +176,7 @@ private void PreloadRsis(ISawmill sawmill)
// TODO allow RSIs to opt out (useful for very big & rare RSIs)
// TODO combine with (non-rsi) texture atlas?
- Array.Sort(rsiList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
+ Array.Sort(atlasList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
// Each RSI sub atlas has a different size.
// Even if we iterate through them once to estimate total area, I have NFI how to sanely estimate an optimal square-texture size.
@@ -167,9 +188,9 @@ private void PreloadRsis(ISawmill sawmill)
Vector2i offset = default;
int finalized = -1;
int atlasCount = 0;
- for (int i = 0; i < rsiList.Length; i++)
+ for (int i = 0; i < atlasList.Length; i++)
{
- var rsi = rsiList[i];
+ var rsi = atlasList[i];
if (rsi.Bad)
continue;
@@ -200,14 +221,14 @@ private void PreloadRsis(ISawmill sawmill)
var height = offset.Y + deltaY;
var croppedSheet = new Image(maxSize, height);
sheet.Blit(new UIBox2i(0, 0, maxSize, height), croppedSheet, default);
- FinalizeMetaAtlas(rsiList.Length - 1, croppedSheet);
+ FinalizeMetaAtlas(atlasList.Length - 1, croppedSheet);
void FinalizeMetaAtlas(int toIndex, Image sheet)
{
var atlas = Clyde.LoadTextureFromImage(sheet);
for (int i = finalized + 1; i <= toIndex; i++)
{
- var rsi = rsiList[i];
+ var rsi = atlasList[i];
rsi.AtlasTexture = atlas;
}
@@ -255,9 +276,10 @@ void FinalizeMetaAtlas(int toIndex, Image sheet)
}
sawmill.Debug(
- "Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountErrored} errored) in {LoadTime}",
+ "Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountNotAtlas} not atlassed, {CountErrored} errored) in {LoadTime}",
rsiList.Length,
atlasCount,
+ nonAtlasList.Length,
errors,
sw.Elapsed);
diff --git a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
index f207b583288..7f7b92525ca 100644
--- a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
+++ b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
@@ -40,17 +40,21 @@ public override void Load(IDependencyCollection dependencies, ResPath path)
var loadStepData = new LoadStepData {Path = path};
var manager = dependencies.Resolve();
LoadPreTexture(manager, loadStepData);
-
- loadStepData.AtlasTexture = dependencies.Resolve().LoadTextureFromImage(
- loadStepData.AtlasSheet,
- loadStepData.Path.ToString());
-
+ LoadTexture(dependencies.Resolve(), loadStepData);
LoadPostTexture(loadStepData);
LoadFinish(dependencies.Resolve(), loadStepData);
loadStepData.AtlasSheet.Dispose();
}
+ internal static void LoadTexture(IClyde clyde, LoadStepData loadStepData)
+ {
+ loadStepData.AtlasTexture = clyde.LoadTextureFromImage(
+ loadStepData.AtlasSheet,
+ loadStepData.Path.ToString(),
+ loadStepData.LoadParameters);
+ }
+
internal static void LoadPreTexture(IResourceManager manager, LoadStepData data)
{
var manifestPath = data.Path / "meta.json";
@@ -178,6 +182,7 @@ internal static void LoadPreTexture(IResourceManager manager, LoadStepData data)
data.FrameSize = frameSize;
data.DimX = dimensionX;
data.CallbackOffsets = callbackOffsets;
+ data.LoadParameters = metadata.LoadParameters;
}
internal static void LoadPostTexture(LoadStepData data)
@@ -380,6 +385,7 @@ internal sealed class LoadStepData
public Texture AtlasTexture = default!;
public Vector2i AtlasOffset;
public RSI Rsi = default!;
+ public TextureLoadParameters LoadParameters;
}
internal struct StateReg
diff --git a/Robust.Client/Robust.Client.csproj b/Robust.Client/Robust.Client.csproj
index 40c335746bf..5ea5e896458 100644
--- a/Robust.Client/Robust.Client.csproj
+++ b/Robust.Client/Robust.Client.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/Robust.Client/UserInterface/Control.Animations.cs b/Robust.Client/UserInterface/Control.Animations.cs
index 2db744627f2..0d94d0a155f 100644
--- a/Robust.Client/UserInterface/Control.Animations.cs
+++ b/Robust.Client/UserInterface/Control.Animations.cs
@@ -57,7 +57,7 @@ private void ProcessAnimations(FrameEventArgs args)
toRemove.Add(key);
}
- foreach (var key in toRemove)
+ foreach (var key in toRemove.Span)
{
_playingAnimations.Remove(key);
AnimationCompleted?.Invoke(key);
diff --git a/Robust.Client/UserInterface/Control.Layout.Styling.cs b/Robust.Client/UserInterface/Control.Layout.Styling.cs
new file mode 100644
index 00000000000..fdc1a4a4c83
--- /dev/null
+++ b/Robust.Client/UserInterface/Control.Layout.Styling.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Robust.Client.UserInterface;
+
+public partial class Control
+{
+ private LayoutStyleProperties _layoutStyleOverride;
+ private LayoutStyleProperties _layoutStyleSheet;
+
+ private void UpdateLayoutStyleProperties()
+ {
+ var propertiesSet = LayoutStyleProperties.None;
+
+ // Assumed most controls will have little or no style properties,
+ // so iterating once is less expensive overall then checking 10+ properties.
+ // C# switch statements are compiled efficiently anyways.
+ foreach (var (key, value) in _styleProperties)
+ {
+ switch (key)
+ {
+ case nameof(SizeFlagsStretchRatio):
+ UpdateField(ref _sizeFlagsStretchRatio, value, LayoutStyleProperties.StretchRatio);
+ break;
+ case nameof(MinWidth):
+ UpdateField(ref _minWidth, value, LayoutStyleProperties.MinWidth);
+ break;
+ case nameof(MinHeight):
+ UpdateField(ref _minHeight, value, LayoutStyleProperties.MinHeight);
+ break;
+ case nameof(SetWidth):
+ UpdateField(ref _setWidth, value, LayoutStyleProperties.SetWidth);
+ break;
+ case nameof(SetHeight):
+ UpdateField(ref _setHeight, value, LayoutStyleProperties.SetHeight);
+ break;
+ case nameof(MaxWidth):
+ UpdateField(ref _maxWidth, value, LayoutStyleProperties.MaxWidth);
+ break;
+ case nameof(MaxHeight):
+ UpdateField(ref _maxHeight, value, LayoutStyleProperties.MaxHeight);
+ break;
+ case nameof(HorizontalExpand):
+ UpdateField(ref _horizontalExpand, value, LayoutStyleProperties.HorizontalExpand);
+ break;
+ case nameof(VerticalExpand):
+ UpdateField(ref _verticalExpand, value, LayoutStyleProperties.VerticalExpand);
+ break;
+ case nameof(HorizontalAlignment):
+ UpdateField(ref _horizontalAlignment, value, LayoutStyleProperties.HorizontalAlignment);
+ break;
+ case nameof(VerticalAlignment):
+ UpdateField(ref _verticalAlignment, value, LayoutStyleProperties.VerticalAlignment);
+ break;
+ case nameof(Margin):
+ UpdateField(ref _margin, value, LayoutStyleProperties.Margin);
+ break;
+ }
+ }
+
+ // Reset cleared properties back to defaults.
+ var toClear = _layoutStyleSheet & ~propertiesSet;
+ if (toClear != 0)
+ {
+ ClearField(ref _sizeFlagsStretchRatio, DefaultStretchRatio, LayoutStyleProperties.StretchRatio);
+ ClearField(ref _minWidth, 0, LayoutStyleProperties.MinWidth);
+ ClearField(ref _minHeight, 0, LayoutStyleProperties.MinHeight);
+ ClearField(ref _setWidth, DefaultSetSize, LayoutStyleProperties.SetWidth);
+ ClearField(ref _setHeight, DefaultSetSize, LayoutStyleProperties.SetHeight);
+ ClearField(ref _maxWidth, DefaultMaxSize, LayoutStyleProperties.MaxWidth);
+ ClearField(ref _maxHeight, DefaultMaxSize, LayoutStyleProperties.MaxHeight);
+ ClearField(ref _horizontalExpand, false, LayoutStyleProperties.HorizontalExpand);
+ ClearField(ref _verticalExpand, false, LayoutStyleProperties.VerticalExpand);
+ ClearField(ref _horizontalAlignment, DefaultHAlignment, LayoutStyleProperties.HorizontalAlignment);
+ ClearField(ref _verticalAlignment, DefaultVAlignment, LayoutStyleProperties.VerticalAlignment);
+ ClearField(ref _margin, default, LayoutStyleProperties.Margin);
+ }
+
+ _layoutStyleSheet = propertiesSet;
+
+ return;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ void UpdateField(ref T field, object value, LayoutStyleProperties flag)
+ {
+ if ((_layoutStyleOverride & flag) != 0)
+ return;
+
+ // TODO: Probably need better error handling...
+ if (value is not T valueCast)
+ return;
+
+ field = valueCast;
+ propertiesSet |= flag;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ void ClearField(ref T field, T defaultValue, LayoutStyleProperties flag)
+ {
+ if ((toClear & flag) == 0)
+ return;
+
+ field = defaultValue;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void SetLayoutStyleProp(LayoutStyleProperties flag)
+ {
+ _layoutStyleOverride |= flag;
+ }
+
+ [Flags]
+ private enum LayoutStyleProperties : short
+ {
+ // @formatter:off
+ None = 0,
+ Margin = 1 << 0,
+ MinWidth = 1 << 1,
+ MinHeight = 1 << 2,
+ SetWidth = 1 << 3,
+ SetHeight = 1 << 4,
+ MaxWidth = 1 << 5,
+ MaxHeight = 1 << 6,
+ StretchRatio = 1 << 7,
+ HorizontalExpand = 1 << 8,
+ VerticalExpand = 1 << 9,
+ HorizontalAlignment = 1 << 10,
+ VerticalAlignment = 1 << 11,
+ // @formatter:on
+ }
+}
diff --git a/Robust.Client/UserInterface/Control.Layout.cs b/Robust.Client/UserInterface/Control.Layout.cs
index 88872de3cd4..7ae06d795fa 100644
--- a/Robust.Client/UserInterface/Control.Layout.cs
+++ b/Robust.Client/UserInterface/Control.Layout.cs
@@ -12,24 +12,30 @@ namespace Robust.Client.UserInterface
public partial class Control
{
+ private const float DefaultStretchRatio = 1;
+ private const float DefaultSetSize = float.NaN;
+ private const float DefaultMaxSize = float.PositiveInfinity;
+ private const HAlignment DefaultHAlignment = HAlignment.Stretch;
+ private const VAlignment DefaultVAlignment = VAlignment.Stretch;
+
private Vector2 _size;
[ViewVariables] internal Vector2? PreviousMeasure;
[ViewVariables] internal UIBox2? PreviousArrange;
- private float _sizeFlagsStretchRatio = 1;
+ private float _sizeFlagsStretchRatio = DefaultStretchRatio;
private float _minWidth;
private float _minHeight;
- private float _setWidth = float.NaN;
- private float _setHeight = float.NaN;
- private float _maxWidth = float.PositiveInfinity;
- private float _maxHeight = float.PositiveInfinity;
+ private float _setWidth = DefaultSetSize;
+ private float _setHeight = DefaultSetSize;
+ private float _maxWidth = DefaultMaxSize;
+ private float _maxHeight = DefaultMaxSize;
private bool _horizontalExpand;
private bool _verticalExpand;
- private HAlignment _horizontalAlignment = HAlignment.Stretch;
- private VAlignment _verticalAlignment = VAlignment.Stretch;
+ private HAlignment _horizontalAlignment = DefaultHAlignment;
+ private VAlignment _verticalAlignment = DefaultVAlignment;
private Thickness _margin;
private bool _measuring;
private bool _arranging;
@@ -46,6 +52,10 @@ public partial class Control
[ViewVariables] public bool IsMeasureValid { get; private set; }
[ViewVariables] public bool IsArrangeValid { get; private set; }
+ ///
+ /// Controls the amount of empty space in virtual pixels around the control.
+ ///
+ /// Values can be provided as "All" or "Horizontal, Vertical" or "Left, Top, Right, Bottom"
[ViewVariables]
public Thickness Margin
{
@@ -53,6 +63,7 @@ public Thickness Margin
set
{
_margin = value;
+ SetLayoutStyleProp(LayoutStyleProperties.Margin);
InvalidateMeasure();
}
}
@@ -242,6 +253,7 @@ public HAlignment HorizontalAlignment
set
{
_horizontalAlignment = value;
+ SetLayoutStyleProp(LayoutStyleProperties.HorizontalAlignment);
InvalidateArrange();
}
}
@@ -258,6 +270,7 @@ public VAlignment VerticalAlignment
set
{
_verticalAlignment = value;
+ SetLayoutStyleProp(LayoutStyleProperties.VerticalAlignment);
InvalidateArrange();
}
}
@@ -276,6 +289,7 @@ public bool HorizontalExpand
set
{
_horizontalExpand = value;
+ SetLayoutStyleProp(LayoutStyleProperties.HorizontalExpand);
Parent?.InvalidateMeasure();
}
}
@@ -294,6 +308,7 @@ public bool VerticalExpand
set
{
_verticalExpand = value;
+ SetLayoutStyleProp(LayoutStyleProperties.VerticalExpand);
Parent?.InvalidateArrange();
}
}
@@ -318,6 +333,7 @@ public float SizeFlagsStretchRatio
_sizeFlagsStretchRatio = value;
+ SetLayoutStyleProp(LayoutStyleProperties.StretchRatio);
Parent?.InvalidateArrange();
}
}
@@ -394,6 +410,7 @@ public float MinWidth
set
{
_minWidth = value;
+ SetLayoutStyleProp(LayoutStyleProperties.MinWidth);
InvalidateMeasure();
}
}
@@ -408,6 +425,7 @@ public float MinHeight
set
{
_minHeight = value;
+ SetLayoutStyleProp(LayoutStyleProperties.MinHeight);
InvalidateMeasure();
}
}
@@ -422,6 +440,7 @@ public float SetWidth
set
{
_setWidth = value;
+ SetLayoutStyleProp(LayoutStyleProperties.SetWidth);
InvalidateMeasure();
}
}
@@ -436,6 +455,7 @@ public float SetHeight
set
{
_setHeight = value;
+ SetLayoutStyleProp(LayoutStyleProperties.SetHeight);
InvalidateMeasure();
}
}
@@ -450,6 +470,7 @@ public float MaxWidth
set
{
_maxWidth = value;
+ SetLayoutStyleProp(LayoutStyleProperties.MaxWidth);
InvalidateMeasure();
}
}
@@ -464,6 +485,7 @@ public float MaxHeight
set
{
_maxHeight = value;
+ SetLayoutStyleProp(LayoutStyleProperties.MaxHeight);
InvalidateMeasure();
}
}
diff --git a/Robust.Client/UserInterface/Control.Styling.cs b/Robust.Client/UserInterface/Control.Styling.cs
index 4f54cf56444..fbe7a1e7004 100644
--- a/Robust.Client/UserInterface/Control.Styling.cs
+++ b/Robust.Client/UserInterface/Control.Styling.cs
@@ -239,6 +239,7 @@ internal void DoStyleUpdate()
protected virtual void StylePropertiesChanged()
{
+ UpdateLayoutStyleProperties();
InvalidateMeasure();
}
diff --git a/Robust.Client/UserInterface/Control.cs b/Robust.Client/UserInterface/Control.cs
index 82222b8177e..1eb9b7a444e 100644
--- a/Robust.Client/UserInterface/Control.cs
+++ b/Robust.Client/UserInterface/Control.cs
@@ -641,7 +641,11 @@ public void RemoveAllChildren()
foreach (var child in Children.ToArray())
{
- RemoveChild(child);
+ // This checks fails in some obscure cases like using the element inspector in the dev window.
+ // Why? Well I could probably spend 15 minutes in a debugger to find out,
+ // but I'd probably still end up with this fix.
+ if (child.Parent == this)
+ RemoveChild(child);
}
}
diff --git a/Robust.Client/UserInterface/Controllers/UIController.cs b/Robust.Client/UserInterface/Controllers/UIController.cs
index 59021d0dffe..ab186486acc 100644
--- a/Robust.Client/UserInterface/Controllers/UIController.cs
+++ b/Robust.Client/UserInterface/Controllers/UIController.cs
@@ -5,7 +5,6 @@
namespace Robust.Client.UserInterface.Controllers;
-// Notices your UIController, *UwU Whats this?*
///
/// Each is instantiated as a singleton by
/// can use for regular IoC dependencies
diff --git a/Robust.Client/UserInterface/Controls/CheckBox.cs b/Robust.Client/UserInterface/Controls/CheckBox.cs
index d7b93b7e732..4f12ae0c0b9 100644
--- a/Robust.Client/UserInterface/Controls/CheckBox.cs
+++ b/Robust.Client/UserInterface/Controls/CheckBox.cs
@@ -29,6 +29,7 @@ public CheckBox()
TextureRect = new TextureRect
{
StyleClasses = { StyleClassCheckBox },
+ VerticalAlignment = VAlignment.Center,
};
hBox.AddChild(TextureRect);
diff --git a/Robust.Client/UserInterface/Controls/LineEdit.cs b/Robust.Client/UserInterface/Controls/LineEdit.cs
index ec3850a1f3d..71ff8398f76 100644
--- a/Robust.Client/UserInterface/Controls/LineEdit.cs
+++ b/Robust.Client/UserInterface/Controls/LineEdit.cs
@@ -4,6 +4,8 @@
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Graphics;
+using Robust.Shared;
+using Robust.Shared.Configuration;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -20,6 +22,8 @@ namespace Robust.Client.UserInterface.Controls
public class LineEdit : Control
{
[Dependency] private readonly IClyde _clyde = default!;
+ [Dependency] private readonly IConfigurationManager _cfgManager = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
private const float MouseScrollDelay = 0.001f;
@@ -46,6 +50,9 @@ public class LineEdit : Control
private bool _mouseSelectingText;
private float _lastMousePosition;
+ private TimeSpan? _lastClickTime;
+ private Vector2? _lastClickPosition;
+
private bool IsPlaceHolderVisible => string.IsNullOrEmpty(_text) && _placeHolder != null;
public event Action? OnTextChanged;
@@ -685,8 +692,26 @@ async void DoPaste()
args.Handle();
}
}
+ // Double-clicking. Clicks delay should be <= 250ms and the distance < 10 pixels.
+ else if (args.Function == EngineKeyFunctions.UIClick && _lastClickPosition != null && _lastClickTime != null
+ && _timing.RealTime - _lastClickTime <= TimeSpan.FromMilliseconds(_cfgManager.GetCVar(CVars.DoubleClickDelay))
+ && (_lastClickPosition.Value - args.PointerLocation.Position).IsShorterThan(_cfgManager.GetCVar(CVars.DoubleClickRange)))
+ {
+ _lastClickTime = _timing.RealTime;
+ _lastClickPosition = args.PointerLocation.Position;
+
+ _lastMousePosition = args.RelativePosition.X;
+
+ _selectionStart = TextEditShared.PrevWordPosition(_text, GetIndexAtPos(args.RelativePosition.X));
+ _cursorPosition = TextEditShared.EndWordPosition(_text, GetIndexAtPos(args.RelativePosition.X));
+
+ args.Handle();
+ }
else
{
+ _lastClickTime = _timing.RealTime;
+ _lastClickPosition = args.PointerLocation.Position;
+
_mouseSelectingText = true;
_lastMousePosition = args.RelativePosition.X;
diff --git a/Robust.Client/UserInterface/Controls/RichTextLabel.cs b/Robust.Client/UserInterface/Controls/RichTextLabel.cs
index 8836b42e3c0..8e4f2e8c967 100644
--- a/Robust.Client/UserInterface/Controls/RichTextLabel.cs
+++ b/Robust.Client/UserInterface/Controls/RichTextLabel.cs
@@ -6,6 +6,7 @@
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
+using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface.Controls
{
@@ -16,6 +17,26 @@ public class RichTextLabel : Control
private FormattedMessage? _message;
private RichTextEntry _entry;
+ private float _lineHeightScale = 1;
+ private bool _lineHeightOverride;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float LineHeightScale
+ {
+ get
+ {
+ if (!_lineHeightOverride && TryGetStyleProperty(nameof(LineHeightScale), out float value))
+ return value;
+
+ return _lineHeightScale;
+ }
+ set
+ {
+ _lineHeightScale = value;
+ _lineHeightOverride = true;
+ InvalidateMeasure();
+ }
+ }
public RichTextLabel()
{
@@ -47,7 +68,7 @@ protected override Vector2 MeasureOverride(Vector2 availableSize)
}
var font = _getFont();
- _entry.Update(font, availableSize.X * UIScale, UIScale);
+ _entry.Update(font, availableSize.X * UIScale, UIScale, LineHeightScale);
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
}
@@ -61,7 +82,7 @@ protected internal override void Draw(DrawingHandleScreen handle)
return;
}
- _entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale);
+ _entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
}
[Pure]
diff --git a/Robust.Client/UserInterface/Controls/ScrollContainer.cs b/Robust.Client/UserInterface/Controls/ScrollContainer.cs
index 5733fab6b4f..4741855a22c 100644
--- a/Robust.Client/UserInterface/Controls/ScrollContainer.cs
+++ b/Robust.Client/UserInterface/Controls/ScrollContainer.cs
@@ -123,10 +123,10 @@ protected override Vector2 MeasureOverride(Vector2 availableSize)
if (!ReturnMeasure)
return Vector2.Zero;
- if (_vScrollEnabled)
+ if (_vScrollEnabled && size.Y >= availableSize.Y)
size.X += _vScrollBar.DesiredSize.X;
- if (_hScrollEnabled)
+ if (_hScrollEnabled && size.X >= availableSize.X)
size.Y += _hScrollBar.DesiredSize.Y;
return size;
diff --git a/Robust.Client/UserInterface/Controls/Slider.cs b/Robust.Client/UserInterface/Controls/Slider.cs
index 29840f1ea8d..f2f9c955257 100644
--- a/Robust.Client/UserInterface/Controls/Slider.cs
+++ b/Robust.Client/UserInterface/Controls/Slider.cs
@@ -2,6 +2,7 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Input;
+using Robust.Shared.Log;
using Robust.Shared.Maths;
using static Robust.Client.UserInterface.Controls.LayoutContainer;
@@ -32,6 +33,11 @@ public class Slider : Range
public bool Grabbed => _grabbed;
+ ///
+ /// Whether the slider can be adjusted.
+ ///
+ public bool Disabled { get; set; }
+
public StyleBox? ForegroundStyleBoxOverride
{
get => _foregroundStyleBoxOverride;
@@ -132,7 +138,7 @@ protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
- if (args.Function != EngineKeyFunctions.UIClick)
+ if (args.Function != EngineKeyFunctions.UIClick || Disabled)
{
return;
}
@@ -146,7 +152,7 @@ protected internal override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
- if (args.Function != EngineKeyFunctions.UIClick) return;
+ if (args.Function != EngineKeyFunctions.UIClick || !_grabbed) return;
_grabbed = false;
OnReleased?.Invoke(this);
diff --git a/Robust.Client/UserInterface/Controls/VerticalTabContainer.xaml b/Robust.Client/UserInterface/Controls/VerticalTabContainer.xaml
new file mode 100644
index 00000000000..09202d49c63
--- /dev/null
+++ b/Robust.Client/UserInterface/Controls/VerticalTabContainer.xaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Robust.Client/UserInterface/Controls/VerticalTabContainer.xaml.cs b/Robust.Client/UserInterface/Controls/VerticalTabContainer.xaml.cs
new file mode 100644
index 00000000000..0c9ce2dc1dd
--- /dev/null
+++ b/Robust.Client/UserInterface/Controls/VerticalTabContainer.xaml.cs
@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Maths;
+
+namespace Robust.Client.UserInterface.Controls;
+
+[GenerateTypedNameReferences]
+public sealed partial class VerticalTabContainer : BoxContainer
+{
+ private readonly Dictionary _tabs = new();
+
+ // Just used to order controls in case one gets removed.
+ private readonly List _controls = new();
+
+ private readonly ButtonGroup _tabGroup = new(false);
+
+ private Control? _currentControl;
+
+ public VerticalTabContainer()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public int AddTab(Control control, string title)
+ {
+ var button = new Button()
+ {
+ Text = title,
+ Group = _tabGroup,
+ };
+
+ TabContainer.AddChild(button);
+ ContentsContainer.AddChild(control);
+ var index = ChildCount - 1;
+ button.OnPressed += args =>
+ {
+ SelectTab(control);
+ };
+
+ _controls.Add(control);
+ _tabs.Add(control, button);
+
+ // Existing tabs
+ if (ContentsContainer.ChildCount > 1)
+ {
+ control.Visible = false;
+ }
+ // First tab
+ else
+ {
+ SelectTab(control);
+ }
+
+ return index;
+ }
+
+ protected override void ChildRemoved(Control child)
+ {
+ if (_tabs.Remove(child, out var button))
+ {
+ button.Dispose();
+ }
+
+ // Set the current tab to a different control
+ if (_currentControl == child)
+ {
+ var previous = _controls.IndexOf(child) - 1;
+
+ if (previous > -1)
+ {
+ var setControl = _controls[previous];
+ SelectTab(setControl);
+ }
+ else
+ {
+ _currentControl = null;
+ }
+ }
+
+ _controls.Remove(child);
+ base.ChildRemoved(child);
+ }
+
+ private void SelectTab(Control control)
+ {
+ if (_currentControl != null)
+ {
+ _currentControl.Visible = false;
+ }
+
+ var button = _tabs[control];
+ button.Pressed = true;
+ control.Visible = true;
+ _currentControl = control;
+ }
+}
diff --git a/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs b/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs
index a8415381918..3c6b728ffd8 100644
--- a/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs
+++ b/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs
@@ -20,6 +20,7 @@ internal sealed class DebugCoordsPanel : PanelContainer
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClyde _displayManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IBaseClient _baseClient = default!;
private readonly StringBuilder _textBuilder = new();
private readonly char[] _textBuffer = new char[1024];
@@ -58,30 +59,36 @@ protected override void FrameUpdate(FrameEventArgs args)
_textBuilder.Clear();
+ var isInGame = _baseClient.RunLevel.IsInGameLike();
var mouseScreenPos = _inputManager.MouseScreenPosition;
var screenSize = _displayManager.ScreenSize;
var screenScale = _displayManager.MainWindow.ContentScale;
- EntityCoordinates mouseGridPos;
- TileRef tile;
+ EntityCoordinates mouseGridPos = default;
+ TileRef tile = default;
+ MapCoordinates mouseWorldMap = default;
- var mouseWorldMap = _eyeManager.PixelToMap(mouseScreenPos);
- if (mouseWorldMap == MapCoordinates.Nullspace)
- return;
-
- var mapSystem = _entityManager.System();
- var xformSystem = _entityManager.System();
-
- if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
- {
- mouseGridPos = mapSystem.MapToGrid(mouseGridUid, mouseWorldMap);
- tile = mapSystem.GetTileRef(mouseGridUid, mouseGrid, mouseGridPos);
- }
- else
+ if (isInGame)
{
- mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
- mouseWorldMap.Position);
- tile = new TileRef(EntityUid.Invalid, mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
+ mouseWorldMap = _eyeManager.PixelToMap(mouseScreenPos);
+ if (mouseWorldMap != MapCoordinates.Nullspace)
+ {
+ var mapSystem = _entityManager.System();
+ var xformSystem = _entityManager.System();
+
+ if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
+ {
+ mouseGridPos = mapSystem.MapToGrid(mouseGridUid, mouseWorldMap);
+ tile = mapSystem.GetTileRef(mouseGridUid, mouseGrid, mouseGridPos);
+ }
+ else
+ {
+ mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
+ mouseWorldMap.Position);
+ tile = new TileRef(EntityUid.Invalid,
+ mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
+ }
+ }
}
var controlHovered = UserInterfaceManager.CurrentlyHovered;
@@ -95,32 +102,37 @@ protected override void FrameUpdate(FrameEventArgs args)
{tile}
GUI: {controlHovered}");
- _textBuilder.AppendLine("\nAttached NetEntity:");
- var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
-
- if (controlledEntity == EntityUid.Invalid)
- {
- _textBuilder.AppendLine("No attached netentity.");
- }
- else
+ if (isInGame)
{
- var entityTransform = _entityManager.GetComponent(controlledEntity);
- var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
- var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
-
- var playerCoordinates = entityTransform.Coordinates;
- var playerRotation = xformSystem.GetWorldRotation(entityTransform);
- var gridRotation = entityTransform.GridUid != null
- ? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
- : Angle.Zero;
-
- _textBuilder.Append($@" Screen: {playerScreen}
+ var xformSystem = _entityManager.System();
+
+ _textBuilder.AppendLine("\nAttached NetEntity:");
+ var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
+
+ if (controlledEntity == EntityUid.Invalid)
+ {
+ _textBuilder.AppendLine("No attached netentity.");
+ }
+ else
+ {
+ var entityTransform = _entityManager.GetComponent(controlledEntity);
+ var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
+ var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
+
+ var playerCoordinates = entityTransform.Coordinates;
+ var playerRotation = xformSystem.GetWorldRotation(entityTransform);
+ var gridRotation = entityTransform.GridUid != null
+ ? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
+ : Angle.Zero;
+
+ _textBuilder.Append($@" Screen: {playerScreen}
{playerWorldOffset}
{_entityManager.GetNetCoordinates(playerCoordinates)}
Rotation: {playerRotation.Degrees:F2}°
NEntId: {_entityManager.GetNetEntity(controlledEntity)}
Grid NEntId: {_entityManager.GetNetEntity(entityTransform.GridUid)}
Grid Rotation: {gridRotation.Degrees:F2}°");
+ }
}
_contents.TextMemory = FormatHelpers.BuilderToMemory(_textBuilder, _textBuffer);
diff --git a/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugNetPanel.cs b/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugNetPanel.cs
index ce06806b46d..70a6de84e23 100644
--- a/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugNetPanel.cs
+++ b/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugNetPanel.cs
@@ -90,7 +90,7 @@ protected override void FrameUpdate(FrameEventArgs args)
contents.TextMemory = FormatHelpers.FormatIntoMem(_textBuffer,
$@"UP: {sentBytes / ONE_KIBIBYTE:N} KiB/s, {sentPackets} pckt/s, {LastSentBytes / ONE_KIBIBYTE:N} KiB, {LastSentPackets} pckt
DOWN: {receivedBytes / ONE_KIBIBYTE:N} KiB/s, {receivedPackets} pckt/s, {LastReceivedBytes / ONE_KIBIBYTE:N} KiB, {LastReceivedPackets} pckt
-PING: {NetManager.ServerChannel?.Ping ?? -1} ms");
+PING: {NetManager.ServerChannel?.Ping ?? -1} ms, MTU: {NetManager.ServerChannel?.CurrentMtu} B");
}
}
}
diff --git a/Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml b/Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml
index d3136adff39..59a6aa4371a 100644
--- a/Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml
+++ b/Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml
@@ -20,10 +20,14 @@
-
+
+
+
+
+
diff --git a/Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml.cs b/Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml.cs
index aaea6a9d8c2..a7343f4eb31 100644
--- a/Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml.cs
+++ b/Robust.Client/UserInterface/DevWindow/DevWindowTabUI.xaml.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.Console.Commands;
using Robust.Client.Graphics;
@@ -208,25 +209,40 @@ private void Refresh()
});
foreach (var (prop, value) in values)
{
- ControlProperties.AddChild(new BoxContainer
+ var button = new ContainerButton
{
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- SeparationOverride = 3,
- Margin = new Thickness(3, 1),
Children =
{
new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ SeparationOverride = 3,
+ Margin = new Thickness(3, 1),
Children =
{
- new Label { Text = $"{prop}", FontColorOverride = Color.GreenYellow },
- new Label { Text = ":" }, // this is for the non colored ":", intentional
+ new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Children =
+ {
+ new Label { Text = $"{prop}", FontColorOverride = Color.GreenYellow },
+ new Label { Text = ":" }, // this is for the non colored ":", intentional
+ }
+ },
+ new Label { Text = $"{value}" },
}
- },
- new Label { Text = $"{value}" },
+ }
}
- });
+ };
+ button.OnPressed += _ =>
+ {
+ // TODO replace with parenting to popup container on WindowRoot
+ UIPopup.Text = GuiDumpCommand.PropertyValuesString(SelectedControl, prop);
+ var box = UIBox2.FromDimensions(UserInterfaceManager.MousePositionScaled.Position
+ - GlobalPosition, Vector2.One);
+ UIPopup.Open(box);
+ };
+ ControlProperties.AddChild(button);
}
}
}
diff --git a/Robust.Client/UserInterface/DevWindow/DevWindowTabUIPopup.xaml b/Robust.Client/UserInterface/DevWindow/DevWindowTabUIPopup.xaml
new file mode 100644
index 00000000000..a2cfba4c89e
--- /dev/null
+++ b/Robust.Client/UserInterface/DevWindow/DevWindowTabUIPopup.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/Robust.Client/UserInterface/DevWindow/DevWindowTabUIPopup.xaml.cs b/Robust.Client/UserInterface/DevWindow/DevWindowTabUIPopup.xaml.cs
new file mode 100644
index 00000000000..b22f66bd60a
--- /dev/null
+++ b/Robust.Client/UserInterface/DevWindow/DevWindowTabUIPopup.xaml.cs
@@ -0,0 +1,20 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Robust.Client.UserInterface;
+
+[GenerateTypedNameReferences]
+internal sealed partial class DevWindowTabUIPopup : Popup
+{
+ public string? Text
+ {
+ get => TextLabel.Text;
+ set => TextLabel.Text = value;
+ }
+
+ public DevWindowTabUIPopup()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
diff --git a/Robust.Client/UserInterface/RichText/FontPrototype.cs b/Robust.Client/UserInterface/RichText/FontPrototype.cs
index af058dbac62..4c549e2fd3d 100644
--- a/Robust.Client/UserInterface/RichText/FontPrototype.cs
+++ b/Robust.Client/UserInterface/RichText/FontPrototype.cs
@@ -5,7 +5,7 @@
namespace Robust.Client.UserInterface.RichText;
[Prototype("font")]
-public sealed class FontPrototype : IPrototype
+public sealed partial class FontPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
diff --git a/Robust.Client/UserInterface/RichTextEntry.cs b/Robust.Client/UserInterface/RichTextEntry.cs
index 529bdd15c0f..4c42084a59c 100644
--- a/Robust.Client/UserInterface/RichTextEntry.cs
+++ b/Robust.Client/UserInterface/RichTextEntry.cs
@@ -71,7 +71,8 @@ public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager
/// The font being used for display.
/// The maximum horizontal size of the container of this entry.
///
- public void Update(Font defaultFont, float maxSizeX, float uiScale)
+ ///
+ public void Update(Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
{
// This method is gonna suck due to complexity.
// Bear with me here.
@@ -159,7 +160,7 @@ void CheckLineBreak(ref RichTextEntry src, int? line)
if (!context.Font.TryPeek(out var font))
font = defaultFont;
- src.Height += font.GetLineHeight(uiScale);
+ src.Height += GetLineHeight(font, uiScale, lineHeightScale);
}
}
}
@@ -170,7 +171,8 @@ public readonly void Draw(
UIBox2 drawBox,
float verticalOffset,
MarkupDrawingContext context,
- float uiScale)
+ float uiScale,
+ float lineHeightScale = 1)
{
context.Clear();
context.Color.Push(_defaultColor);
@@ -197,7 +199,7 @@ public readonly void Draw(
if (lineBreakIndex < LineBreaks.Count &&
LineBreaks[lineBreakIndex] == globalBreakCounter)
{
- baseLine = new Vector2(drawBox.Left, baseLine.Y + font.GetLineHeight(uiScale) + controlYAdvance);
+ baseLine = new Vector2(drawBox.Left, baseLine.Y + GetLineHeight(font, uiScale, lineHeightScale) + controlYAdvance);
controlYAdvance = 0;
lineBreakIndex += 1;
}
@@ -216,7 +218,7 @@ public readonly void Draw(
control.Position = new Vector2(baseLine.X * invertedScale, (baseLine.Y - defaultFont.GetAscent(uiScale)) * invertedScale);
control.Measure(new Vector2(Width, Height));
var advanceX = control.DesiredPixelSize.X;
- controlYAdvance = Math.Max(0f, (control.DesiredPixelSize.Y - font.GetLineHeight(uiScale)) * invertedScale);
+ controlYAdvance = Math.Max(0f, (control.DesiredPixelSize.Y - GetLineHeight(font, uiScale, lineHeightScale)) * invertedScale);
baseLine += new Vector2(advanceX, 0);
}
}
@@ -242,5 +244,11 @@ private readonly string ProcessNode(MarkupNode node, MarkupDrawingContext contex
tag.PopDrawContext(node, context);
return tag.TextAfter(node);
}
+
+ private static int GetLineHeight(Font font, float uiScale, float lineHeightScale)
+ {
+ var height = font.GetLineHeight(uiScale);
+ return (int)(height * lineHeightScale);
+ }
}
}
diff --git a/Robust.Client/UserInterface/Themes/UiTheme.cs b/Robust.Client/UserInterface/Themes/UiTheme.cs
index f719c7ad4b3..50e16a1611c 100644
--- a/Robust.Client/UserInterface/Themes/UiTheme.cs
+++ b/Robust.Client/UserInterface/Themes/UiTheme.cs
@@ -1,12 +1,10 @@
using System;
using System.Collections.Frozen;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.ContentPack;
-using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
@@ -18,7 +16,7 @@
namespace Robust.Client.UserInterface.Themes;
[Prototype("uiTheme")]
-public sealed class UITheme : IPrototype
+public sealed partial class UITheme : IPrototype
{
private IResourceCache? _cache;
private IUserInterfaceManager? _uiMan;
diff --git a/Robust.Client/UserInterface/UserInterfaceManager.Themes.cs b/Robust.Client/UserInterface/UserInterfaceManager.Themes.cs
index 435e4579f56..e8198db21a4 100644
--- a/Robust.Client/UserInterface/UserInterfaceManager.Themes.cs
+++ b/Robust.Client/UserInterface/UserInterfaceManager.Themes.cs
@@ -2,6 +2,7 @@
using Robust.Client.UserInterface.Themes;
using Robust.Shared;
using Robust.Shared.Log;
+using Robust.Shared.Prototypes;
namespace Robust.Client.UserInterface;
@@ -18,11 +19,29 @@ private void _initThemes()
{
DefaultTheme = _protoManager.Index(UITheme.DefaultName);
CurrentTheme = DefaultTheme;
+ ReloadThemes();
+ _configurationManager.OnValueChanged(CVars.InterfaceTheme, SetThemeOrPrevious, true);
+ _protoManager.PrototypesReloaded += OnPrototypesReloaded;
+ }
+
+ private void OnPrototypesReloaded(PrototypesReloadedEventArgs eventArgs)
+ {
+ if (eventArgs.WasModified())
+ {
+ _sawmillUI.Debug("Reloading UI themes due to prototype reload");
+ ReloadThemes();
+ }
+ }
+
+ private void ReloadThemes()
+ {
+ _themes.Clear();
foreach (var proto in _protoManager.EnumeratePrototypes())
{
_themes.Add(proto.ID, proto);
}
- _configurationManager.OnValueChanged(CVars.InterfaceTheme, SetThemeOrPrevious, true);
+
+ SetThemeOrPrevious(CurrentTheme.ID);
}
//Try to set the current theme, if the theme is not found do nothing
diff --git a/Robust.Client/ViewVariables/ClientViewVariablesManager.cs b/Robust.Client/ViewVariables/ClientViewVariablesManager.cs
index f8045ec94d0..006042f4dd3 100644
--- a/Robust.Client/ViewVariables/ClientViewVariablesManager.cs
+++ b/Robust.Client/ViewVariables/ClientViewVariablesManager.cs
@@ -8,6 +8,8 @@
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.ViewVariables.Editors;
using Robust.Client.ViewVariables.Instances;
+using Robust.Shared.Audio;
+using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
@@ -29,6 +31,8 @@ internal sealed partial class ClientViewVariablesManager : ViewVariablesManager,
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IRobustSerializer _robustSerializer = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+ [Dependency] private readonly IResourceManager _resManager = default!;
private uint _nextReqId = 1;
private readonly Vector2i _defaultWindowSize = (640, 420);
@@ -126,8 +130,12 @@ public VVPropEditor PropertyFor(Type? type)
return new VVPropEditorString();
}
- if (type == typeof(EntProtoId) ||
- type == typeof(EntProtoId?))
+ if (type == typeof(EntProtoId?))
+ {
+ return new VVPropEditorNullableEntProtoId();
+ }
+
+ if (type == typeof(EntProtoId))
{
return new VVPropEditorEntProtoId();
}
@@ -222,6 +230,12 @@ public VVPropEditor PropertyFor(Type? type)
return new VVPropEditorTimeSpan();
}
+ if (typeof(SoundSpecifier).IsAssignableFrom(type))
+ {
+ var control = new VVPropEditorSoundSpecifier(_protoManager, _resManager);
+ return control;
+ }
+
if (type == typeof(ViewVariablesBlobMembers.ServerKeyValuePairToken) ||
type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
diff --git a/Robust.Client/ViewVariables/Editors/VVPropEditorNullableEntProtoId.cs b/Robust.Client/ViewVariables/Editors/VVPropEditorNullableEntProtoId.cs
new file mode 100644
index 00000000000..d0137f1f8c5
--- /dev/null
+++ b/Robust.Client/ViewVariables/Editors/VVPropEditorNullableEntProtoId.cs
@@ -0,0 +1,35 @@
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
+
+namespace Robust.Client.ViewVariables.Editors;
+
+internal sealed class VVPropEditorNullableEntProtoId : VVPropEditor
+{
+ protected override Control MakeUI(object? value)
+ {
+ var lineEdit = new LineEdit
+ {
+ Text = value is EntProtoId protoId ? protoId.Id : "",
+ Editable = !ReadOnly,
+ HorizontalExpand = true,
+ };
+
+ if (!ReadOnly)
+ {
+ lineEdit.OnTextEntered += e =>
+ {
+ if (string.IsNullOrWhiteSpace(e.Text))
+ {
+ ValueChanged(null);
+ }
+ else
+ {
+ ValueChanged((EntProtoId) e.Text);
+ }
+ };
+ }
+
+ return lineEdit;
+ }
+}
diff --git a/Robust.Client/ViewVariables/Editors/VVPropEditorSoundSpecifier.cs b/Robust.Client/ViewVariables/Editors/VVPropEditorSoundSpecifier.cs
new file mode 100644
index 00000000000..d4441dff995
--- /dev/null
+++ b/Robust.Client/ViewVariables/Editors/VVPropEditorSoundSpecifier.cs
@@ -0,0 +1,393 @@
+using System.Globalization;
+using System.Numerics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Audio;
+using Robust.Shared.ContentPack;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using Robust.Shared.ViewVariables;
+
+namespace Robust.Client.ViewVariables.Editors;
+
+public sealed class VVPropEditorSoundSpecifier : VVPropEditor
+{
+ private readonly IPrototypeManager _protoManager;
+ private readonly IResourceManager _resManager;
+
+ // Need to cache to some level just to make sure each edit doesn't reset the specifier to the default.
+
+ private SoundSpecifier? _specifier;
+
+ public VVPropEditorSoundSpecifier(IPrototypeManager protoManager, IResourceManager resManager)
+ {
+ _protoManager = protoManager;
+ _resManager = resManager;
+ }
+
+ protected override Control MakeUI(object? value)
+ {
+ var typeButton = new OptionButton()
+ {
+ Disabled = ReadOnly,
+ };
+
+ typeButton.AddItem(Loc.GetString("vv-sound-none"));
+ typeButton.AddItem(Loc.GetString("vv-sound-collection"), 1);
+ typeButton.AddItem(Loc.GetString("vv-sound-path"), 2);
+
+ var editBox = new LineEdit()
+ {
+ HorizontalExpand = true,
+ Editable = !ReadOnly,
+ };
+
+ var pathControls = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Children =
+ {
+ typeButton,
+ editBox
+ },
+ SetSize = new Vector2(384f, 32f)
+ };
+
+ if (value != null)
+ {
+ switch (value)
+ {
+ case SoundCollectionSpecifier collection:
+ typeButton.SelectId(1);
+ editBox.Text = collection.Collection ?? string.Empty;
+ _specifier = collection;
+ break;
+ case SoundPathSpecifier path:
+ typeButton.SelectId(2);
+ editBox.Text = path.Path.ToString();
+ _specifier = path;
+ break;
+ default:
+ _specifier = null;
+ break;
+ }
+ }
+
+ typeButton.OnItemSelected += args =>
+ {
+ typeButton.SelectId(args.Id);
+ editBox.Text = string.Empty;
+
+ editBox.Editable = !ReadOnly && typeButton.SelectedId > 0;
+
+ if (typeButton.SelectedId == 0)
+ {
+ // Dummy value
+ ValueChanged(new SoundPathSpecifier(""));
+ }
+ };
+
+ editBox.OnTextEntered += args =>
+ {
+ if (string.IsNullOrEmpty(args.Text))
+ return;
+
+ switch (typeButton.SelectedId)
+ {
+ case 1:
+ if (!_protoManager.HasIndex(args.Text))
+ return;
+
+ _specifier = new SoundCollectionSpecifier(args.Text)
+ {
+ Params = _specifier?.Params ?? AudioParams.Default,
+ };
+ ValueChanged(_specifier);
+ break;
+ case 2:
+ var path = new ResPath(args.Text);
+
+ if (!_resManager.ContentFileExists(path))
+ return;
+
+ _specifier = new SoundPathSpecifier(args.Text)
+ {
+ Params = _specifier?.Params ?? AudioParams.Default,
+ };
+
+ ValueChanged(_specifier);
+ break;
+ default:
+ return;
+ }
+ };
+
+ // Audio params
+
+ /* Volume */
+
+ var volumeEdit = new LineEdit()
+ {
+ Text = _specifier?.Params.Volume.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
+ HorizontalExpand = true,
+ Editable = !ReadOnly && _specifier != null,
+ };
+
+ volumeEdit.OnTextEntered += args =>
+ {
+ if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
+ return;
+
+ _specifier.Params = _specifier.Params.WithVolume(floatValue);
+ ValueChanged(_specifier);
+ };
+
+ var volumeContainer = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Children =
+ {
+ new Label()
+ {
+ Text = Loc.GetString("vv-sound-volume"),
+ },
+ volumeEdit,
+ }
+ };
+
+ /* Pitch */
+
+ var pitchEdit = new LineEdit()
+ {
+ Text = _specifier?.Params.Pitch.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
+ HorizontalExpand = true,
+ Editable = !ReadOnly && _specifier != null,
+ };
+
+ pitchEdit.OnTextEntered += args =>
+ {
+ if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
+ return;
+
+ _specifier.Params = _specifier.Params.WithPitchScale(floatValue);
+ ValueChanged(_specifier);
+ };
+
+ var pitchContainer = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Children =
+ {
+ new Label()
+ {
+ Text = Loc.GetString("vv-sound-pitch"),
+ },
+ pitchEdit,
+ }
+ };
+
+ /* MaxDistance */
+
+ var maxDistanceEdit = new LineEdit()
+ {
+ Text = _specifier?.Params.MaxDistance.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
+ HorizontalExpand = true,
+ Editable = !ReadOnly && _specifier != null,
+ };
+
+ maxDistanceEdit.OnTextEntered += args =>
+ {
+ if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
+ return;
+
+ _specifier.Params = _specifier.Params.WithMaxDistance(floatValue);
+ ValueChanged(_specifier);
+ };
+
+ var maxDistanceContainer = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Children =
+ {
+ new Label()
+ {
+ Text = Loc.GetString("vv-sound-max-distance"),
+ },
+ maxDistanceEdit,
+ }
+ };
+
+ /* RolloffFactor */
+
+ var rolloffFactorEdit = new LineEdit()
+ {
+ Text = _specifier?.Params.RolloffFactor.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
+ HorizontalExpand = true,
+ Editable = !ReadOnly && _specifier != null,
+ };
+
+ rolloffFactorEdit.OnTextEntered += args =>
+ {
+ if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
+ return;
+
+ _specifier.Params = _specifier.Params.WithRolloffFactor(floatValue);
+ ValueChanged(_specifier);
+ };
+
+ var rolloffFactorContainer = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Children =
+ {
+ new Label()
+ {
+ Text = Loc.GetString("vv-sound-rolloff-factor"),
+ },
+ rolloffFactorEdit,
+ }
+ };
+
+ /* ReferenceDistance */
+
+ var referenceDistanceEdit = new LineEdit()
+ {
+ Text = _specifier?.Params.ReferenceDistance.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
+ HorizontalExpand = true,
+ Editable = !ReadOnly && _specifier != null,
+ };
+
+ referenceDistanceEdit.OnTextEntered += args =>
+ {
+ if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
+ return;
+
+ _specifier.Params = _specifier.Params.WithReferenceDistance(floatValue);
+ ValueChanged(_specifier);
+ };
+
+ var referenceDistanceContainer = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Children =
+ {
+ new Label()
+ {
+ Text = Loc.GetString("vv-sound-reference-distance"),
+ },
+ referenceDistanceEdit,
+ }
+ };
+
+ /* Loop */
+
+ var loopButton = new Button()
+ {
+ Text = Loc.GetString("vv-sound-loop"),
+ Pressed = _specifier?.Params.Loop ?? false,
+ ToggleMode = true,
+ Disabled = ReadOnly || _specifier == null,
+ };
+
+ loopButton.OnPressed += args =>
+ {
+ if (_specifier == null)
+ return;
+
+ _specifier.Params = _specifier.Params.WithLoop(args.Button.Pressed);
+ ValueChanged(_specifier);
+ };
+
+ /* PlayOffsetSeconds */
+
+ var playOffsetEdit = new LineEdit()
+ {
+ Text = _specifier?.Params.PlayOffsetSeconds.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
+ HorizontalExpand = true,
+ Editable = !ReadOnly && _specifier != null,
+ };
+
+ playOffsetEdit.OnTextEntered += args =>
+ {
+ if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
+ return;
+
+ _specifier.Params = _specifier.Params.WithPlayOffset(floatValue);
+ ValueChanged(_specifier);
+ };
+
+ var playOffsetContainer = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Children =
+ {
+ new Label()
+ {
+ Text = Loc.GetString("vv-sound-play-offset"),
+ },
+ playOffsetEdit,
+ }
+ };
+
+ /* Variation */
+
+ var variationEdit = new LineEdit()
+ {
+ Text = _specifier?.Params.Variation.ToString() ?? string.Empty,
+ HorizontalExpand = true,
+ Editable = !ReadOnly && _specifier != null,
+ };
+
+ variationEdit.OnTextEntered += args =>
+ {
+ if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
+ return;
+
+ _specifier.Params = _specifier.Params.WithVariation(floatValue);
+ ValueChanged(_specifier);
+ };
+
+ var variationContainer = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Children =
+ {
+ new Label()
+ {
+ Text = Loc.GetString("vv-sound-variation"),
+ },
+ variationEdit,
+ }
+ };
+
+ var audioParamsControls = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ Children =
+ {
+ volumeContainer,
+ pitchContainer,
+ maxDistanceContainer,
+ rolloffFactorContainer,
+ referenceDistanceContainer,
+ loopButton,
+ playOffsetContainer,
+ variationContainer,
+ }
+ };
+
+ var controls = new BoxContainer()
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ Children =
+ {
+ pathControls,
+ audioParamsControls,
+ }
+ };
+
+ return controls;
+ }
+}
diff --git a/Robust.Roslyn.Shared/Diagnostics.cs b/Robust.Roslyn.Shared/Diagnostics.cs
index b829a2f4c30..94709a5f293 100644
--- a/Robust.Roslyn.Shared/Diagnostics.cs
+++ b/Robust.Roslyn.Shared/Diagnostics.cs
@@ -28,6 +28,7 @@ public static class Diagnostics
public const string IdComponentPauseNoFields = "RA0022";
public const string IdComponentPauseNoParentAttribute = "RA0023";
public const string IdComponentPauseWrongTypeAttribute = "RA0024";
+ public const string IdDependencyFieldAssigned = "RA0025";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");
diff --git a/Robust.Server/Audio/AudioSystem.cs b/Robust.Server/Audio/AudioSystem.cs
index c7ecb695dac..f89bc0c675e 100644
--- a/Robust.Server/Audio/AudioSystem.cs
+++ b/Robust.Server/Audio/AudioSystem.cs
@@ -68,7 +68,7 @@ private void AddAudioFilter(EntityUid uid, AudioComponent component, Filter filt
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
var entity = Spawn("Audio", MapCoordinates.Nullspace);
var audio = SetupAudio(entity, filename, audioParams);
@@ -78,8 +78,11 @@ public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
{
+ if (string.IsNullOrEmpty(filename))
+ return null;
+
if (TerminatingOrDeleted(uid))
{
Log.Error($"Tried to play audio on a terminating / deleted entity {ToPrettyString(uid)}. Trace: {Environment.StackTrace}");
@@ -94,8 +97,11 @@ public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
{
+ if (string.IsNullOrEmpty(filename))
+ return null;
+
if (TerminatingOrDeleted(uid))
{
Log.Error($"Tried to play audio on a terminating / deleted entity {ToPrettyString(uid)}. Trace: {Environment.StackTrace}");
@@ -109,8 +115,11 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string fil
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
+ if (string.IsNullOrEmpty(filename))
+ return null;
+
if (TerminatingOrDeleted(coordinates.EntityId))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}. Trace: {Environment.StackTrace}");
@@ -128,9 +137,12 @@ public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string
}
///
- public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
+ public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
+ if (string.IsNullOrEmpty(filename))
+ return null;
+
if (TerminatingOrDeleted(coordinates.EntityId))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}. Trace: {Environment.StackTrace}");
@@ -176,12 +188,12 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(Soun
return audio;
}
- public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, Filter.SinglePlayer(recipient), false, audioParams);
}
- public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayGlobal(filename, actor.PlayerSession, audioParams);
@@ -189,12 +201,12 @@ public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string
return null;
}
- public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.SinglePlayer(recipient), uid, false, audioParams);
}
- public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayEntity(filename, actor.PlayerSession, uid, audioParams);
@@ -202,12 +214,12 @@ public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string
return null;
}
- public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.SinglePlayer(recipient), coordinates, false, audioParams);
}
- public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
+ public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayStatic(filename, actor.PlayerSession, coordinates, audioParams);
diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs
index f8e92bc7cbc..8bc2cf55f47 100644
--- a/Robust.Server/BaseServer.cs
+++ b/Robust.Server/BaseServer.cs
@@ -89,7 +89,7 @@ internal sealed class BaseServer : IBaseServerInternal, IPostInjectInit
[Dependency] private readonly IWatchdogApi _watchdogApi = default!;
[Dependency] private readonly HubManager _hubManager = default!;
[Dependency] private readonly IScriptHost _scriptHost = default!;
- [Dependency] private readonly IMetricsManager _metricsManager = default!;
+ [Dependency] private readonly IMetricsManagerInternal _metricsManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustMappedStringSerializer _stringSerializer = default!;
[Dependency] private readonly ILocalizationManagerInternal _loc = default!;
@@ -749,6 +749,8 @@ private void FrameUpdate(FrameEventArgs frameEventArgs)
_hubManager.Heartbeat();
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
+
+ _metricsManager.FrameUpdate();
}
void IPostInjectInit.PostInject()
diff --git a/Robust.Server/Console/Commands/SpinCommand.cs b/Robust.Server/Console/Commands/SpinCommand.cs
index 9f48332a96a..f60369a4a67 100644
--- a/Robust.Server/Console/Commands/SpinCommand.cs
+++ b/Robust.Server/Console/Commands/SpinCommand.cs
@@ -66,7 +66,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
}
var physicsSystem = _entities.System();
- physicsSystem.SetAngularDamping(physics, drag);
+ physicsSystem.SetAngularDamping(target.Value, physics, drag);
physicsSystem.SetAngularVelocity(target.Value, speed, body: physics);
}
}
diff --git a/Robust.Server/Console/Commands/TestbedCommand.cs b/Robust.Server/Console/Commands/TestbedCommand.cs
index e18a22ad8ed..37a3f8c44c3 100644
--- a/Robust.Server/Console/Commands/TestbedCommand.cs
+++ b/Robust.Server/Console/Commands/TestbedCommand.cs
@@ -70,6 +70,11 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
}
var mapId = new MapId(mapInt);
+ if (!_map.MapExists(mapId))
+ {
+ shell.WriteError($"map {args[0]} does not exist");
+ return;
+ }
if (shell.Player == null)
{
@@ -110,13 +115,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
private void SetupPlayer(MapId mapId, IConsoleShell shell)
{
- if (mapId == MapId.Nullspace) return;
-
- if (!_map.MapExists(mapId))
- {
- _map.CreateMap(mapId);
- }
-
_map.SetMapPaused(mapId, false);
var mapUid = _map.GetMapEntityIdOrThrow(mapId);
_ent.System().SetGravity(mapUid, new Vector2(0, -9.8f));
diff --git a/Robust.Server/DataMetrics/MetricsManager.Factory.cs b/Robust.Server/DataMetrics/MetricsManager.Factory.cs
new file mode 100644
index 00000000000..f9543db929f
--- /dev/null
+++ b/Robust.Server/DataMetrics/MetricsManager.Factory.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Metrics;
+using System.Linq;
+using Robust.Shared.Utility;
+
+namespace Robust.Server.DataMetrics;
+
+internal sealed partial class MetricsManager : IMeterFactory
+{
+ private readonly Dictionary> _meterCache = new();
+ private readonly object _meterCacheLock = new();
+
+ Meter IMeterFactory.Create(MeterOptions options)
+ {
+ if (options.Scope != null && options.Scope != this)
+ throw new InvalidOperationException("Cannot specify a custom scope when creating a meter");
+
+ lock (_meterCacheLock)
+ {
+ if (LockedFindCachedMeter(options) is { } cached)
+ return cached.Meter;
+
+ var meter = new Meter(options.Name, options.Version, options.Tags, this);
+ var meterList = _meterCache.GetOrNew(options.Name);
+ meterList.Add(new CachedMeter(options.Version, TagsToDict(options.Tags), meter));
+ return meter;
+ }
+ }
+
+ private CachedMeter? LockedFindCachedMeter(MeterOptions options)
+ {
+ if (!_meterCache.TryGetValue(options.Name, out var metersList))
+ return null;
+
+ var tagsDict = TagsToDict(options.Tags);
+
+ foreach (var cachedMeter in metersList)
+ {
+ if (cachedMeter.Version == options.Version && TagsMatch(tagsDict, cachedMeter.Tags))
+ return cachedMeter;
+ }
+
+ return null;
+ }
+
+ private static bool TagsMatch(Dictionary a, Dictionary b)
+ {
+ if (a.Count != b.Count)
+ return false;
+
+ foreach (var (key, valueA) in a)
+ {
+ if (!b.TryGetValue(key, out var valueB))
+ return false;
+
+ if (!Equals(valueA, valueB))
+ return false;
+ }
+
+ return true;
+ }
+
+ private static Dictionary TagsToDict(IEnumerable>? tags)
+ {
+ return tags?.ToDictionary() ?? [];
+ }
+
+ private void DisposeMeters()
+ {
+ lock (_meterCacheLock)
+ {
+ foreach (var meters in _meterCache.Values)
+ {
+ foreach (var meter in meters)
+ {
+ meter.Meter.Dispose();
+ }
+ }
+ }
+ }
+
+ private sealed class CachedMeter(string? version, Dictionary tags, Meter meter)
+ {
+ public readonly string? Version = version;
+ public readonly Dictionary Tags = tags;
+ public readonly Meter Meter = meter;
+ }
+}
diff --git a/Robust.Server/DataMetrics/MetricsManager.MetricsServer.cs b/Robust.Server/DataMetrics/MetricsManager.MetricsServer.cs
index 958a3e903a6..35c6abbb66f 100644
--- a/Robust.Server/DataMetrics/MetricsManager.MetricsServer.cs
+++ b/Robust.Server/DataMetrics/MetricsManager.MetricsServer.cs
@@ -18,13 +18,20 @@ internal sealed partial class MetricsManager
private sealed class ManagedHttpListenerMetricsServer : MetricHandler
{
private readonly ISawmill _sawmill;
+ private readonly Func? _beforeCollect;
private readonly HttpListener _listener;
private readonly CollectorRegistry _registry;
- public ManagedHttpListenerMetricsServer(ISawmill sawmill, string host, int port, string url = "metrics/",
- CollectorRegistry? registry = null)
+ public ManagedHttpListenerMetricsServer(
+ ISawmill sawmill,
+ string host,
+ int port,
+ string url = "metrics/",
+ CollectorRegistry? registry = null,
+ Func? beforeCollect = null)
{
_sawmill = sawmill;
+ _beforeCollect = beforeCollect;
_listener = new HttpListener();
_listener.Prefixes.Add($"http://{host}:{port}/{url}");
_registry = registry ?? Metrics.DefaultRegistry;
@@ -57,6 +64,12 @@ private async Task ListenerThread(CancellationToken cancel)
{
MetricsEvents.Log.ScrapeStart();
+ // prometheus-net does have a "before collect" callback of its own.
+ // But it doesn't get ran before stuff like their System.Diagnostics.Metrics integration,
+ // So I'm just gonna make my own here.
+ if (_beforeCollect != null)
+ await _beforeCollect(cancel);
+
var stream = resp.OutputStream;
// prometheus-net is a terrible library and have to do all this insanity,
// just to handle the ScrapeFailedException correctly.
diff --git a/Robust.Server/DataMetrics/MetricsManager.UpdateMetrics.cs b/Robust.Server/DataMetrics/MetricsManager.UpdateMetrics.cs
new file mode 100644
index 00000000000..d91ffa1b895
--- /dev/null
+++ b/Robust.Server/DataMetrics/MetricsManager.UpdateMetrics.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Robust.Shared;
+using Robust.Shared.Asynchronous;
+using Robust.Shared.IoC;
+using Robust.Shared.Timing;
+
+namespace Robust.Server.DataMetrics;
+
+internal sealed partial class MetricsManager
+{
+ //
+ // Handles the implementation of the "UpdateMetrics" callback.
+ //
+
+ public event Action? UpdateMetrics;
+
+ private TimeSpan _fixedUpdateInterval;
+ private TimeSpan _nextFixedUpdate;
+
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+ private void InitializeUpdateMetrics()
+ {
+ _cfg.OnValueChanged(
+ CVars.MetricsUpdateInterval,
+ seconds =>
+ {
+ _fixedUpdateInterval = TimeSpan.FromSeconds(seconds);
+ _nextFixedUpdate = _gameTiming.RealTime + _fixedUpdateInterval;
+ },
+ true);
+ }
+
+ public void FrameUpdate()
+ {
+ if (_fixedUpdateInterval == TimeSpan.Zero)
+ return;
+
+ var time = _gameTiming.RealTime;
+
+ if (_nextFixedUpdate > time)
+ return;
+
+ _nextFixedUpdate = time + _fixedUpdateInterval;
+
+ _sawmill.Verbose("Running fixed metrics update");
+ UpdateMetrics?.Invoke();
+ }
+
+ private async Task BeforeCollectCallback(CancellationToken cancel)
+ {
+ if (UpdateMetrics == null)
+ return;
+
+ await _taskManager.TaskOnMainThread(() =>
+ {
+ UpdateMetrics?.Invoke();
+ });
+ }
+}
diff --git a/Robust.Server/DataMetrics/MetricsManager.cs b/Robust.Server/DataMetrics/MetricsManager.cs
index 29c0d29d103..804ea02fb61 100644
--- a/Robust.Server/DataMetrics/MetricsManager.cs
+++ b/Robust.Server/DataMetrics/MetricsManager.cs
@@ -1,26 +1,54 @@
using System;
+using System.Diagnostics.Metrics;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
+using Prometheus;
using Prometheus.DotNetRuntime;
using Prometheus.DotNetRuntime.Metrics.Producers;
using Robust.Shared;
+using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using EventSource = System.Diagnostics.Tracing.EventSource;
-#nullable enable
-
namespace Robust.Server.DataMetrics;
-internal sealed partial class MetricsManager : IMetricsManager, IDisposable
+///
+/// Manages OpenTelemetry metrics exposure.
+///
+///
+///
+/// If enabled via , metrics about the game server are exposed via a HTTP server
+/// in an OpenTelemetry-compatible format (Prometheus).
+///
+///
+/// Metrics can be added through the types in System.Diagnostics.Metrics or Prometheus.
+/// IoC contains an implementation of that can be used to instantiate meters.
+///
+///
+public interface IMetricsManager
+{
+ ///
+ /// An event that gets raised on the main thread when complex metrics should be updated.
+ ///
+ ///
+ /// This event is raised on the main thread before a Prometheus collection happens,
+ /// and also with a fixed interval if is set.
+ /// You can use it to update complex metrics that can't "just" be stuffed into a counter.
+ ///
+ event Action UpdateMetrics;
+}
+
+internal sealed partial class MetricsManager : IMetricsManagerInternal, IDisposable
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
+ [Dependency] private readonly ITaskManager _taskManager = default!;
private bool _initialized;
@@ -55,6 +83,8 @@ void ValueChanged(CVarDef cVar) where T : notnull
{
_cfg.OnValueChanged(cVar, _ => Reload());
}
+
+ InitializeUpdateMetrics();
}
private async Task Stop()
@@ -73,6 +103,8 @@ private async Task Stop()
async void IDisposable.Dispose()
{
+ DisposeMeters();
+
await Stop();
_initialized = false;
@@ -100,7 +132,12 @@ private async void Reload()
_sawmill.Info("Prometheus metrics enabled, host: {1} port: {0}", port, host);
var sawmill = Logger.GetSawmill("metrics.server");
- _metricServer = new ManagedHttpListenerMetricsServer(sawmill, host, port);
+ _metricServer = new ManagedHttpListenerMetricsServer(
+ sawmill,
+ host,
+ port,
+ registry: Metrics.DefaultRegistry,
+ beforeCollect: BeforeCollectCallback);
_metricServer.Start();
if (_cfg.GetCVar(CVars.MetricsRuntime))
@@ -190,7 +227,8 @@ private sealed class MetricsEvents : EventSource
}
}
-internal interface IMetricsManager
+internal interface IMetricsManagerInternal : IMetricsManager
{
void Initialize();
+ void FrameUpdate();
}
diff --git a/Robust.Server/GameObjects/EntitySystems/MapLoaderSystem.cs b/Robust.Server/GameObjects/EntitySystems/MapLoaderSystem.cs
index b112319ebc3..d39f90d4cd6 100644
--- a/Robust.Server/GameObjects/EntitySystems/MapLoaderSystem.cs
+++ b/Robust.Server/GameObjects/EntitySystems/MapLoaderSystem.cs
@@ -49,7 +49,6 @@ public sealed class MapLoaderSystem : EntitySystem
private ISawmill _logLoader = default!;
private ISawmill _logWriter = default!;
- private static readonly MapLoadOptions DefaultLoadOptions = new();
private const int MapFormatVersion = 6;
private const int BackwardsVersion = 2;
@@ -132,7 +131,7 @@ public void Load(MapId mapId, string path, MapLoadOptions? options = null)
public bool TryLoad(MapId mapId, string path, [NotNullWhen(true)] out IReadOnlyList? rootUids,
MapLoadOptions? options = null)
{
- options ??= DefaultLoadOptions;
+ options ??= new();
var resPath = new ResPath(path).ToRootedPath();
@@ -280,6 +279,9 @@ private bool Deserialize(MapData data)
// Load the prototype data onto entities, e.g. transform parents, etc.
LoadEntities(data);
+ // Assign MapSaveTileMapComponent to all read grids.
+ SaveGridTileMap(data);
+
// Build the scene graph / transform hierarchy to know the order to startup entities.
// This also allows us to swap out the root node up front if necessary.
BuildEntityHierarchy(data);
@@ -576,6 +578,19 @@ private void LoadEntity(EntityUid uid, MappingDataNode data, MetaDataComponent m
meta.LastComponentRemoved = _timing.CurTick;
}
+ private void SaveGridTileMap(MapData mapData)
+ {
+ DebugTools.Assert(_context.TileMap != null);
+
+ foreach (var entity in mapData.EntitiesToDeserialize.Keys)
+ {
+ if (HasComp(entity))
+ {
+ EnsureComp(entity).TileMap = _context.TileMap;
+ }
+ }
+ }
+
private void BuildEntityHierarchy(MapData mapData)
{
_stopwatch.Restart();
@@ -642,11 +657,13 @@ private void SwapRootNode(MapData data)
var xformQuery = GetEntityQuery();
// We just need to cache the old mapuid and point to the new mapuid.
- if (HasComp(rootNode))
+ if (TryComp(rootNode, out MapComponent? mapComp))
{
// If map exists swap out
- if (_mapManager.MapExists(data.TargetMap))
+ if (_mapSystem.TryGetMap(data.TargetMap, out var existing))
{
+ data.Options.DoMapInit |= _mapSystem.IsInitialized(data.TargetMap);
+ data.MapIsPaused = _mapSystem.IsPaused(existing.Value);
// Map exists but we also have a map file with stuff on it soooo swap out the old map.
if (data.Options.LoadMap)
{
@@ -659,26 +676,28 @@ private void SwapRootNode(MapData data)
data.Options.Rotation = Angle.Zero;
}
- _mapManager.SetMapEntity(data.TargetMap, rootNode);
+ Del(existing);
EnsureComp(rootNode);
+
+ mapComp.MapId = data.TargetMap;
+ DebugTools.Assert(mapComp.LifeStage < ComponentLifeStage.Initializing);
}
// Otherwise just ignore the map in the file.
else
{
var oldRootUid = data.Entities[0];
- var newRootUid = _mapManager.GetMapEntityId(data.TargetMap);
- data.Entities[0] = newRootUid;
+ data.Entities[0] = existing.Value;
foreach (var ent in data.Entities)
{
- if (ent == newRootUid)
+ if (ent == existing)
continue;
var xform = xformQuery.GetComponent(ent);
if (!xform.ParentUid.IsValid() || xform.ParentUid.Equals(oldRootUid))
{
- _transform.SetParent(ent, xform, newRootUid);
+ _transform.SetParent(ent, xform, existing.Value);
}
}
@@ -687,16 +706,9 @@ private void SwapRootNode(MapData data)
}
else
{
- // If we're loading a file with a map then swap out the entityuid
- // TODO: Mapmanager nonsense
- var AAAAA = _mapManager.CreateMap(data.TargetMap);
-
- if (!data.MapIsPostInit)
- {
- _mapManager.AddUninitializedMap(data.TargetMap);
- }
-
- _mapManager.SetMapEntity(data.TargetMap, rootNode);
+ data.MapIsPaused = !data.MapIsPostInit;
+ mapComp.MapId = data.TargetMap;
+ DebugTools.Assert(mapComp.LifeStage < ComponentLifeStage.Initializing);
EnsureComp(rootNode);
// Nothing should have invalid uid except for the root node.
@@ -705,17 +717,15 @@ private void SwapRootNode(MapData data)
else
{
// No map file root, in that case create a new map / get the one we're loading onto.
- var mapNode = _mapManager.GetMapEntityId(data.TargetMap);
-
- if (!mapNode.IsValid())
+ if (!_mapSystem.TryGetMap(data.TargetMap, out var mapNode))
{
// Map doesn't exist so we'll start it up now so we can re-attach the preinit entities to it for later.
- _mapManager.CreateMap(data.TargetMap);
- _mapManager.AddUninitializedMap(data.TargetMap);
- mapNode = _mapManager.GetMapEntityId(data.TargetMap);
- DebugTools.Assert(mapNode.IsValid());
+ mapNode = _mapSystem.CreateMap(data.TargetMap, false);
}
+ data.Options.DoMapInit |= _mapSystem.IsInitialized(data.TargetMap);
+ data.MapIsPaused = _mapSystem.IsPaused(mapNode.Value);
+
// If anything has an invalid parent (e.g. it's some form of root node) then parent it to the map.
foreach (var ent in data.Entities)
{
@@ -727,12 +737,11 @@ private void SwapRootNode(MapData data)
if (!xform.ParentUid.IsValid())
{
- _transform.SetParent(ent, xform, mapNode);
+ _transform.SetParent(ent, xform, mapNode.Value);
}
}
}
- data.MapIsPaused = _mapManager.IsMapPaused(data.TargetMap);
_logLoader.Debug($"Swapped out root node in {_stopwatch.Elapsed}");
}
@@ -880,7 +889,7 @@ private void StartupEntity(EntityUid uid, MetaDataComponent metadata, MapData da
{
EntityManager.SetLifeStage(metadata, EntityLifeStage.MapInitialized);
}
- else if (_mapManager.IsMapInitialized(data.TargetMap))
+ else if (data.Options.DoMapInit)
{
_serverEntityManager.RunMapInit(uid, metadata);
}
@@ -948,7 +957,7 @@ public MappingDataNode GetSaveData(EntityUid uid)
// Yes, post-init maps do not have EntityLifeStage >= EntityLifeStage.MapInitialized
bool postInit;
if (TryComp(uid, out MapComponent? mapComp))
- postInit = !mapComp.MapPreInit;
+ postInit = mapComp.MapInitialized;
else
postInit = metadata.EntityLifeStage >= EntityLifeStage.MapInitialized;
@@ -981,28 +990,74 @@ private void WriteTileMapSection(MappingDataNode rootNode, List entit
var gridQuery = GetEntityQuery();
var tileDefs = new HashSet();
+ Dictionary? origTileMap = null;
foreach (var ent in entities)
{
if (!gridQuery.TryGetComponent(ent, out var grid))
continue;
- var tileEnumerator = grid.GetAllTilesEnumerator(false);
-
+ var tileEnumerator = _mapSystem.GetAllTilesEnumerator(ent, grid, ignoreEmpty: false);
while (tileEnumerator.MoveNext(out var tileRef))
{
tileDefs.Add(tileRef.Value.Tile.TypeId);
}
+
+ if (TryComp(ent, out MapSaveTileMapComponent? saveTileMap))
+ origTileMap ??= saveTileMap.TileMap;
}
+ Dictionary tileIdMap;
+ if (origTileMap != null)
+ {
+ tileIdMap = new Dictionary();
+
+ // We are re-saving a map, so we have an original tile map we can preserve.
+ foreach (var (origId, prototypeId) in origTileMap)
+ {
+ // Skip removed tile definitions.
+ if (!_tileDefManager.TryGetDefinition(prototypeId, out var definition))
+ continue;
+
+ tileIdMap.Add(definition.TileId, origId);
+ }
+
+ // Assign new IDs for all new tile types.
+ var nextId = 0;
+ foreach (var tileId in tileDefs)
+ {
+ if (tileIdMap.ContainsKey(tileId))
+ continue;
+
+ // New tile, assign new ID that isn't taken by original tile map.
+ while (origTileMap.ContainsKey(nextId))
+ {
+ nextId += 1;
+ }
+
+ tileIdMap.Add(tileId, nextId);
+ nextId += 1;
+ }
+ }
+ else
+ {
+ // Make no-op tile ID map.
+ tileIdMap = tileDefs.ToDictionary(x => x, x => x);
+ }
+
+ DebugTools.Assert(
+ tileIdMap.Count == tileIdMap.Values.Distinct().Count(),
+ "Tile ID map has double mapped values??");
+
+ _context.TileWriteMap = tileIdMap;
+
var tileMap = new MappingDataNode();
rootNode.Add("tilemap", tileMap);
- var ordered = new List(tileDefs);
- ordered.Sort();
- foreach (var tyleId in ordered)
+ foreach (var (nativeId, mapId) in tileIdMap.OrderBy(x => x.Key))
{
- var tileDef = _tileDefManager[tyleId];
- tileMap.Add(tyleId.ToString(CultureInfo.InvariantCulture), tileDef.ID);
+ tileMap.Add(
+ mapId.ToString(CultureInfo.InvariantCulture),
+ _tileDefManager[nativeId].ID);
}
}
@@ -1036,17 +1091,17 @@ private void PopulateEntityList(EntityUid uid, List entities, Diction
}
}
- private bool IsSaveable(EntityUid uid, EntityQuery metaQuery, EntityQuery transformQuery)
+ private bool IsSaveable(EntityUid uid)
{
// Don't serialize things parented to un savable things.
// For example clothes inside a person.
while (uid.IsValid())
{
- var meta = metaQuery.GetComponent(uid);
+ var meta = MetaData(uid);
if (meta.EntityDeleted || meta.EntityPrototype?.MapSavable == false) break;
- uid = transformQuery.GetComponent(uid).ParentUid;
+ uid = Transform(uid).ParentUid;
}
// If we manage to get up to the map (root node) then it's saveable.
@@ -1061,7 +1116,7 @@ private void RecursivePopulate(EntityUid uid,
EntityQuery transformQuery,
EntityQuery saveCompQuery)
{
- if (!IsSaveable(uid, metaQuery, transformQuery))
+ if (!IsSaveable(uid))
return;
entities.Add(uid);
@@ -1176,11 +1231,12 @@ private void WriteEntitySection(MappingDataNode rootNode, Dictionary grid)
{
_pvs.GridParentChanged(grid);
@@ -31,11 +41,6 @@ public override void Initialize()
Subs.CVar(_cfg, CVars.GameDeleteEmptyGrids, SetGridDeletion, true);
}
- protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
- {
- EnsureComp(uid);
- }
-
private void SetGridDeletion(bool value)
{
_deleteEmptyGrids = value;
diff --git a/Robust.Server/GameObjects/EntitySystems/UserInterfaceSystem.cs b/Robust.Server/GameObjects/EntitySystems/UserInterfaceSystem.cs
index 3f87ff3fe0a..07fffe099df 100644
--- a/Robust.Server/GameObjects/EntitySystems/UserInterfaceSystem.cs
+++ b/Robust.Server/GameObjects/EntitySystems/UserInterfaceSystem.cs
@@ -1,416 +1,9 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using JetBrains.Annotations;
-using Robust.Server.Player;
-using Robust.Shared.Enums;
+using System.Collections;
using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Player;
-using Robust.Shared.Utility;
-namespace Robust.Server.GameObjects
-{
- public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
- {
- [Dependency] private readonly IPlayerManager _playerMan = default!;
- [Dependency] private readonly TransformSystem _xformSys = default!;
-
- private EntityQuery _ignoreUIRangeQuery;
-
- private readonly List _sessionCache = new();
-
- ///
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeNetworkEvent(OnMessageReceived);
- _playerMan.PlayerStatusChanged += OnPlayerStatusChanged;
-
- _ignoreUIRangeQuery = GetEntityQuery();
- }
-
- public override void Shutdown()
- {
- base.Shutdown();
-
- _playerMan.PlayerStatusChanged -= OnPlayerStatusChanged;
- }
-
- private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
- {
- if (args.NewStatus != SessionStatus.Disconnected)
- return;
-
- if (!OpenInterfaces.TryGetValue(args.Session, out var buis))
- return;
-
- foreach (var bui in buis.ToArray())
- {
- CloseShared(bui, args.Session);
- }
- }
-
- ///
- public override void Update(float frameTime)
- {
- var xformQuery = GetEntityQuery();
- var query = AllEntityQuery();
-
- while (query.MoveNext(out var uid, out var activeUis, out var xform))
- {
- foreach (var ui in activeUis.Interfaces)
- {
- CheckRange(uid, activeUis, ui, xform, xformQuery);
-
- if (!ui.StateDirty)
- continue;
-
- ui.StateDirty = false;
-
- foreach (var (player, state) in ui.PlayerStateOverrides)
- {
- RaiseNetworkEvent(state, player.Channel);
- }
-
- if (ui.LastStateMsg == null)
- continue;
-
- foreach (var session in ui.SubscribedSessions)
- {
- if (!ui.PlayerStateOverrides.ContainsKey(session))
- RaiseNetworkEvent(ui.LastStateMsg, session.Channel);
- }
- }
- }
- }
-
- ///
- /// Verify that the subscribed clients are still in range of the interface.
- ///
- private void CheckRange(EntityUid uid, ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery query)
- {
- if (ui.InteractionRange <= 0)
- return;
-
- // We have to cache the set of sessions because Unsubscribe modifies the original.
- _sessionCache.Clear();
- _sessionCache.AddRange(ui.SubscribedSessions);
-
- var uiPos = _xformSys.GetWorldPosition(transform, query);
- var uiMap = transform.MapID;
-
- foreach (var session in _sessionCache)
- {
- // The component manages the set of sessions, so this invalid session should be removed soon.
- if (!query.TryGetComponent(session.AttachedEntity, out var xform))
- continue;
-
- if (_ignoreUIRangeQuery.HasComponent(session.AttachedEntity))
- continue;
-
- // Handle pluggable BoundUserInterfaceCheckRangeEvent
- var checkRangeEvent = new BoundUserInterfaceCheckRangeEvent(uid, ui, session);
- RaiseLocalEvent(uid, ref checkRangeEvent, broadcast: true);
- if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Pass)
- continue;
-
- if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Fail)
- {
- CloseUi(ui, session, activeUis);
- continue;
- }
-
- DebugTools.Assert(checkRangeEvent.Result == BoundUserInterfaceRangeResult.Default);
-
- if (uiMap != xform.MapID)
- {
- CloseUi(ui, session, activeUis);
- continue;
- }
-
- var distanceSquared = (uiPos - _xformSys.GetWorldPosition(xform, query)).LengthSquared();
- if (distanceSquared > ui.InteractionRangeSqrd)
- CloseUi(ui, session, activeUis);
- }
- }
-
- #region Get BUI
-
- public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
- {
- if (!Resolve(uid, ref ui))
- return false;
-
- return ui.Interfaces.ContainsKey(uiKey);
- }
-
- public PlayerBoundUserInterface GetUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
- {
- if (!Resolve(uid, ref ui))
- throw new InvalidOperationException($"Cannot get {typeof(PlayerBoundUserInterface)} from an entity without {typeof(UserInterfaceComponent)}!");
-
- return ui.Interfaces[uiKey];
- }
-
- public PlayerBoundUserInterface? GetUiOrNull(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
- {
- return TryGetUi(uid, uiKey, out var bui, ui)
- ? bui
- : null;
- }
-
- ///
- /// Return UIs a session has open.
- /// Null if empty.
- ///
- public List? GetAllUIsForSession(ICommonSession session)
- {
- OpenInterfaces.TryGetValue(session, out var value);
- return value;
- }
- #endregion
-
- public bool IsUiOpen(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
- {
- if (!TryGetUi(uid, uiKey, out var bui, ui))
- return false;
+namespace Robust.Server.GameObjects;
- return bui.SubscribedSessions.Count > 0;
- }
-
- public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
- {
- if (!TryGetUi(uid, uiKey, out var bui, ui))
- return false;
-
- return bui.SubscribedSessions.Contains(session);
- }
-
- ///
- /// Sets a state. This can be used for stateful UI updating.
- /// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
- /// Pretty much how NanoUI did it back in ye olde BYOND.
- ///
- ///
- /// The state object that will be sent to all current and future client.
- /// This can be null.
- ///
- ///
- /// The player session to send this new state to.
- /// Set to null for sending it to every subscribed player session.
- ///
- public bool TrySetUiState(EntityUid uid,
- Enum uiKey,
- BoundUserInterfaceState state,
- ICommonSession? session = null,
- UserInterfaceComponent? ui = null,
- bool clearOverrides = true)
- {
- if (!TryGetUi(uid, uiKey, out var bui, ui))
- return false;
-
- SetUiState(bui, state, session, clearOverrides);
- return true;
- }
-
- ///
- /// Sets a state. This can be used for stateful UI updating.
- /// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
- /// Pretty much how NanoUI did it back in ye olde BYOND.
- ///
- ///
- /// The state object that will be sent to all current and future client.
- /// This can be null.
- ///
- ///
- /// The player session to send this new state to.
- /// Set to null for sending it to every subscribed player session.
- ///
- public void SetUiState(PlayerBoundUserInterface bui, BoundUserInterfaceState state, ICommonSession? session = null, bool clearOverrides = true)
- {
- var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), new UpdateBoundStateMessage(state), bui.UiKey);
- if (session == null)
- {
- bui.LastStateMsg = msg;
- if (clearOverrides)
- bui.PlayerStateOverrides.Clear();
- }
- else
- {
- bui.PlayerStateOverrides[session] = msg;
- }
-
- bui.StateDirty = true;
- }
-
- #region Close
- protected override void CloseShared(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
- {
- var owner = bui.Owner;
- bui._subscribedSessions.Remove(session);
- bui.PlayerStateOverrides.Remove(session);
-
- if (OpenInterfaces.TryGetValue(session, out var buis))
- buis.Remove(bui);
-
- RaiseLocalEvent(owner, new BoundUIClosedEvent(bui.UiKey, owner, session));
-
- if (bui._subscribedSessions.Count == 0)
- DeactivateInterface(bui.Owner, bui, activeUis);
- }
-
- ///
- /// Closes this all interface for any clients that have any open.
- ///
- public bool TryCloseAll(EntityUid uid, Shared.GameObjects.ActiveUserInterfaceComponent? aui = null)
- {
- if (!Resolve(uid, ref aui, false))
- return false;
-
- foreach (var ui in aui.Interfaces)
- {
- CloseAll(ui);
- }
-
- return true;
- }
-
- ///
- /// Closes this specific interface for any clients that have it open.
- ///
- public bool TryCloseAll(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
- {
- if (!TryGetUi(uid, uiKey, out var bui, ui))
- return false;
-
- CloseAll(bui);
- return true;
- }
-
- ///
- /// Closes this interface for any clients that have it open.
- ///
- public void CloseAll(PlayerBoundUserInterface bui)
- {
- foreach (var session in bui.SubscribedSessions.ToArray())
- {
- CloseUi(bui, session);
- }
- }
-
- #endregion
-
- #region SendMessage
-
- ///
- /// Send a BUI message to all connected player sessions.
- ///
- public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, UserInterfaceComponent? ui = null)
- {
- if (!TryGetUi(uid, uiKey, out var bui, ui))
- return false;
-
- SendUiMessage(bui, message);
- return true;
- }
-
- ///
- /// Send a BUI message to all connected player sessions.
- ///
- public void SendUiMessage(PlayerBoundUserInterface bui, BoundUserInterfaceMessage message)
- {
- var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey);
- foreach (var session in bui.SubscribedSessions)
- {
- RaiseNetworkEvent(msg, session.Channel);
- }
- }
-
- ///
- /// Send a BUI message to a specific player session.
- ///
- public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, ICommonSession session, UserInterfaceComponent? ui = null)
- {
- if (!TryGetUi(uid, uiKey, out var bui, ui))
- return false;
-
- return TrySendUiMessage(bui, message, session);
- }
-
- ///
- /// Send a BUI message to a specific player session.
- ///
- public bool TrySendUiMessage(PlayerBoundUserInterface bui, BoundUserInterfaceMessage message, ICommonSession session)
- {
- if (!bui.SubscribedSessions.Contains(session))
- return false;
-
- RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey), session.Channel);
- return true;
- }
-
- #endregion
- }
-
- ///
- /// Raised by to check whether an interface is still accessible by its user.
- ///
- [ByRefEvent]
- [PublicAPI]
- public struct BoundUserInterfaceCheckRangeEvent
- {
- ///
- /// The entity owning the UI being checked for.
- ///
- public readonly EntityUid Target;
-
- ///
- /// The UI itself.
- ///
- ///
- public readonly PlayerBoundUserInterface UserInterface;
-
- ///
- /// The player for which the UI is being checked.
- ///
- public readonly ICommonSession Player;
-
- ///
- /// The result of the range check.
- ///
- public BoundUserInterfaceRangeResult Result;
-
- public BoundUserInterfaceCheckRangeEvent(
- EntityUid target,
- PlayerBoundUserInterface userInterface,
- ICommonSession player)
- {
- Target = target;
- UserInterface = userInterface;
- Player = player;
- }
- }
-
- ///
- /// Possible results for a .
- ///
- public enum BoundUserInterfaceRangeResult : byte
- {
- ///
- /// Run built-in range check.
- ///
- Default,
-
- ///
- /// Range check passed, UI is accessible.
- ///
- Pass,
-
- ///
- /// Range check failed, UI is inaccessible.
- ///
- Fail
- }
+public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
+{
}
diff --git a/Robust.Server/GameObjects/MapSaveIdComponent.cs b/Robust.Server/GameObjects/MapSaveIdComponent.cs
index dd760880c89..790f9047294 100644
--- a/Robust.Server/GameObjects/MapSaveIdComponent.cs
+++ b/Robust.Server/GameObjects/MapSaveIdComponent.cs
@@ -9,7 +9,7 @@ namespace Robust.Server.GameObjects
/// This component stores the previous map UID of entities from map load.
/// This can then be used to re-serialize the entity with the same UID for the merge driver to recognize.
///
- [RegisterComponent]
+ [RegisterComponent, UnsavedComponent]
public sealed partial class MapSaveIdComponent : Component
{
public int Uid { get; set; }
diff --git a/Robust.Server/GameObjects/MapSaveTileMapComponent.cs b/Robust.Server/GameObjects/MapSaveTileMapComponent.cs
new file mode 100644
index 00000000000..bdd4cde887c
--- /dev/null
+++ b/Robust.Server/GameObjects/MapSaveTileMapComponent.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using Robust.Shared.GameObjects;
+
+namespace Robust.Server.GameObjects;
+
+///
+/// Used by to track the original tile map from when a map was loaded.
+///
+///
+///
+/// This component is used to reduce differences on map saving, by making it so that a tile map can be re-used between map saves even if internal engine IDs change.
+///
+///
+/// This component is created on every grid entity read during map load.
+/// This means loading a multi-grid map will create multiple of these components.
+/// When re-saving the map, the map loader will arbitrarily choose which available
+/// to use.
+///
+///
+[RegisterComponent, UnsavedComponent]
+internal sealed partial class MapSaveTileMapComponent : Component
+{
+ public Dictionary TileMap = [];
+}
diff --git a/Robust.Server/GameObjects/ServerEntityManager.cs b/Robust.Server/GameObjects/ServerEntityManager.cs
index 0e261de1218..7596e65e94b 100644
--- a/Robust.Server/GameObjects/ServerEntityManager.cs
+++ b/Robust.Server/GameObjects/ServerEntityManager.cs
@@ -86,7 +86,7 @@ void IServerEntityManagerInternal.FinishEntityStartup(EntityUid entity)
StartEntity(entity);
}
- private protected override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
+ internal override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
{
if (prototypeName == null)
return base.CreateEntity(prototypeName, out metadata, context);
diff --git a/Robust.Server/GameStates/PvsSystem.ToSendSet.cs b/Robust.Server/GameStates/PvsSystem.ToSendSet.cs
index 7ca5e51d4e2..a9f05606ef5 100644
--- a/Robust.Server/GameStates/PvsSystem.ToSendSet.cs
+++ b/Robust.Server/GameStates/PvsSystem.ToSendSet.cs
@@ -48,30 +48,17 @@ private void AddPvsChunk(PvsChunk chunk, float distance, PvsSession session)
// We add chunk-size here so that its consistent with the normal PVS range setting.
// I.e., distance here is the Chebyshev distance to the centre of each chunk, but the normal pvs range only
// required that the chunk be touching the box, not the centre.
- var limit = distance < (_lowLodDistance + ChunkSize) / 2
+ var count = distance <= (_viewSize + ChunkSize) / 2
? chunk.Contents.Count
: chunk.LodCounts[0];
- // If the PVS budget is exceeded, it should still be safe to send all of the chunk's direct children, though
- // after that we have no guarantee that an entity's parent got sent.
- var directChildren = Math.Min(limit, chunk.LodCounts[2]);
-
// Send entities on the chunk.
- var span = CollectionsMarshal.AsSpan(chunk.Contents);
- for (var i = 0; i < limit; i++)
+ var span = CollectionsMarshal.AsSpan(chunk.Contents)[..count];
+ foreach (ref var ent in span)
{
- var ent = span[i];
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
-
- if ((mask & meta.VisMask) != meta.VisMask)
- continue;
-
- // TODO PVS improve this somehow
- // Having entities "leave" pvs view just because the pvs entry budget was exceeded sucks.
- // This probably requires changing client game state manager to support receiving entities with unknown parents.
- // Probably needs to do something similar to pending net entity states, but for entity spawning.
- if (!AddEntity(session, ref ent, ref meta, fromTick))
- limit = directChildren;
+ if ((mask & meta.VisMask) == meta.VisMask)
+ AddEntity(session, ref ent, ref meta, fromTick);
}
}
@@ -80,26 +67,26 @@ private void AddPvsChunk(PvsChunk chunk, float distance, PvsSession session)
///
/// Returns false if the entity would exceed the client's PVS budget.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool AddEntity(PvsSession session, ref PvsChunk.ChunkEntity ent, ref PvsMetadata meta,
+ private void AddEntity(PvsSession session, ref PvsChunk.ChunkEntity ent, ref PvsMetadata meta,
GameTick fromTick)
{
DebugTools.Assert(fromTick < _gameTiming.CurTick);
ref var data = ref session.DataMemory.GetRef(ent.Ptr.Index);
if (data.LastSeen == _gameTiming.CurTick)
- return true;
+ return;
if (meta.LifeStage >= EntityLifeStage.Terminating)
{
Log.Error($"Attempted to send deleted entity: {ToPrettyString(ent.Uid)}");
EntityManager.QueueDeleteEntity(ent.Uid);
- return true;
+ return;
}
var (entered,budgetExceeded) = IsEnteringPvsRange(ref data, fromTick, ref session.Budget);
if (budgetExceeded)
- return false;
+ return;
data.LastSeen = _gameTiming.CurTick;
session.ToSend!.Add(ent.Ptr);
@@ -108,25 +95,23 @@ private bool AddEntity(PvsSession session, ref PvsChunk.ChunkEntity ent, ref Pvs
{
var state = GetFullEntityState(session.Session, ent.Uid, ent.Meta);
session.States.Add(state);
- return true;
+ return;
}
if (entered)
{
var state = GetEntityState(session.Session, ent.Uid, data.EntityLastAcked, ent.Meta);
session.States.Add(state);
- return true;
+ return;
}
if (meta.LastModifiedTick <= fromTick)
- return true;
+ return;
var entState = GetEntityState(session.Session, ent.Uid, fromTick , ent.Meta);
if (!entState.Empty)
session.States.Add(entState);
-
- return true;
}
///
diff --git a/Robust.Server/GameStates/PvsSystem.cs b/Robust.Server/GameStates/PvsSystem.cs
index a451b31a983..184eb2f28e8 100644
--- a/Robust.Server/GameStates/PvsSystem.cs
+++ b/Robust.Server/GameStates/PvsSystem.cs
@@ -62,12 +62,14 @@ internal sealed partial class PvsSystem : EntitySystem
public bool CullingEnabled { get; private set; }
///
- /// Size of the side of the view bounds square.
+ /// Size of the side of the view bounds square. Related to
///
private float _viewSize;
- // see CVars.NetLowLodDistance
- private float _lowLodDistance;
+ ///
+ /// Size of the side of the priority view bounds square. Related to
+ ///
+ private float _priorityViewSize;
///
/// Per-tick ack data to avoid re-allocating.
@@ -139,7 +141,7 @@ public override void Initialize()
Subs.CVar(_configManager, CVars.NetPVS, SetPvs, true);
Subs.CVar(_configManager, CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
- Subs.CVar(_configManager, CVars.NetLowLodRange, OnLodChanged, true);
+ Subs.CVar(_configManager, CVars.NetPvsPriorityRange, OnPriorityRangeChanged, true);
Subs.CVar(_configManager, CVars.NetForceAckThreshold, OnForceAckChanged, true);
Subs.CVar(_configManager, CVars.NetPvsAsync, OnAsyncChanged, true);
Subs.CVar(_configManager, CVars.NetPvsCompressLevel, ResetParallelism, true);
@@ -276,12 +278,13 @@ private void ForceFullState(PvsSession session)
private void OnViewsizeChanged(float value)
{
- _viewSize = value;
+ _viewSize = Math.Max(ChunkSize, value);
+ OnPriorityRangeChanged(_configManager.GetCVar(CVars.NetPvsPriorityRange));
}
- private void OnLodChanged(float value)
+ private void OnPriorityRangeChanged(float value)
{
- _lowLodDistance = Math.Clamp(value, ChunkSize, 100f);
+ _priorityViewSize = Math.Max(_viewSize, value);
}
private void OnForceAckChanged(int value)
@@ -372,13 +375,6 @@ private void VerifySessionData(PvsSession pvsSession)
{
ref var data = ref pvsSession.DataMemory.GetRef(intPtr.Index);
DebugTools.AssertEqual(data.LastSeen, _gameTiming.CurTick);
-
- // if an entity is visible, its parents should always be visible.
- if (_xformQuery.GetComponent(GetEntity(IndexToNetEntity(intPtr))).ParentUid is not {Valid: true} pUid)
- continue;
-
- DebugTools.Assert(toSendSet.Contains(GetNetEntity(pUid)),
- $"Attempted to send an entity without sending it's parents. Entity: {ToPrettyString(pUid)}.");
}
pvsSession.PreviouslySent.TryGetValue(_gameTiming.CurTick - 1, out var lastSent);
@@ -394,7 +390,7 @@ private void VerifySessionData(PvsSession pvsSession)
private (Vector2 worldPos, float range, EntityUid? map) CalcViewBounds(Entity eye)
{
- var size = Math.Max(eye.Comp2?.PvsSize ?? _viewSize, 1);
+ var size = Math.Max(eye.Comp2?.PvsSize ?? _priorityViewSize, 1);
return (_transform.GetWorldPosition(eye.Comp1), size / 2f, eye.Comp1.MapUid);
}
diff --git a/Robust.Server/Maps/MapChunkSerializer.cs b/Robust.Server/Maps/MapChunkSerializer.cs
index d06e45679b5..cabf4435c23 100644
--- a/Robust.Server/Maps/MapChunkSerializer.cs
+++ b/Robust.Server/Maps/MapChunkSerializer.cs
@@ -104,12 +104,16 @@ public DataNode Write(ISerializationManager serializationManager, MapChunk value
root.Add("version", new ValueDataNode("6"));
- gridNode.Value = SerializeTiles(value);
+ Dictionary? tileWriteMap = null;
+ if (context is MapSerializationContext mapContext)
+ tileWriteMap = mapContext.TileWriteMap;
+
+ gridNode.Value = SerializeTiles(value, tileWriteMap);
return root;
}
- private static string SerializeTiles(MapChunk chunk)
+ private static string SerializeTiles(MapChunk chunk, Dictionary? tileWriteMap)
{
// number of bytes written per tile, because sizeof(Tile) is useless.
const int structSize = 6;
@@ -125,7 +129,11 @@ private static string SerializeTiles(MapChunk chunk)
for (ushort x = 0; x < chunk.ChunkSize; x++)
{
var tile = chunk.GetTile(x, y);
- writer.Write(tile.TypeId);
+ var typeId = tile.TypeId;
+ if (tileWriteMap != null)
+ typeId = tileWriteMap[typeId];
+
+ writer.Write(typeId);
writer.Write((byte)tile.Flags);
writer.Write(tile.Variant);
}
diff --git a/Robust.Server/Maps/MapLoadOptions.cs b/Robust.Server/Maps/MapLoadOptions.cs
index d04dde8130f..85f9034b176 100644
--- a/Robust.Server/Maps/MapLoadOptions.cs
+++ b/Robust.Server/Maps/MapLoadOptions.cs
@@ -53,5 +53,7 @@ public Angle Rotation
/// This should be set to false if you want to load a map file onto an existing map and do not wish to overwrite the existing entity.
///
public bool LoadMap { get; set; } = true;
+
+ public bool DoMapInit = false;
}
}
diff --git a/Robust.Server/Physics/GridFixtureSystem.cs b/Robust.Server/Physics/GridFixtureSystem.cs
index 5453599fa6b..28622aa8b68 100644
--- a/Robust.Server/Physics/GridFixtureSystem.cs
+++ b/Robust.Server/Physics/GridFixtureSystem.cs
@@ -236,7 +236,7 @@ private void CheckSplits(EntityUid uid, HashSet dirtyNodes)
grids.Add(foundSplits);
}
- var oldGrid = _mapManager.GetGrid(uid);
+ var oldGrid = Comp(uid);
var oldGridUid = uid;
// Split time
@@ -605,7 +605,7 @@ private HashSet GenerateSplitNode(EntityUid gridEuid, MapChunk c
DebugTools.Assert(chunk.FilledTiles > 0);
- var grid = _mapManager.GetGrid(gridEuid);
+ var grid = Comp(gridEuid);
var group = CreateNodes(gridEuid, grid, chunk);
_nodes[gridEuid][chunk.Indices] = group;
diff --git a/Robust.Server/Robust.Server.csproj b/Robust.Server/Robust.Server.csproj
index 30fcd66ca8e..cea71c5afdf 100644
--- a/Robust.Server/Robust.Server.csproj
+++ b/Robust.Server/Robust.Server.csproj
@@ -29,7 +29,7 @@
-
+
diff --git a/Robust.Server/ServerIoC.cs b/Robust.Server/ServerIoC.cs
index 8d9ca24fc86..016e8271ee1 100644
--- a/Robust.Server/ServerIoC.cs
+++ b/Robust.Server/ServerIoC.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.Metrics;
using Robust.Server.Configuration;
using Robust.Server.Console;
using Robust.Server.DataMetrics;
@@ -80,6 +81,8 @@ internal static void RegisterIoC(IDependencyCollection deps)
deps.Register();
deps.Register();
deps.Register();
+ deps.Register();
+ deps.Register();
deps.Register();
deps.Register();
deps.Register();
diff --git a/Robust.Server/ServerStatus/StatusHost.cs b/Robust.Server/ServerStatus/StatusHost.cs
index abd75472725..062c1dd8c07 100644
--- a/Robust.Server/ServerStatus/StatusHost.cs
+++ b/Robust.Server/ServerStatus/StatusHost.cs
@@ -256,7 +256,7 @@ public ContextImpl(HttpListenerContext context)
_context = context;
RequestMethod = new HttpMethod(context.Request.HttpMethod!);
- var headers = new Dictionary();
+ var headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (string? key in context.Request.Headers.Keys)
{
if (key == null)
diff --git a/Robust.Server/ServerStatus/WatchdogApi.cs b/Robust.Server/ServerStatus/WatchdogApi.cs
index 8677d40ed60..55afe1ea08e 100644
--- a/Robust.Server/ServerStatus/WatchdogApi.cs
+++ b/Robust.Server/ServerStatus/WatchdogApi.cs
@@ -157,11 +157,14 @@ public async void Heartbeat()
try
{
// Passing null as content works so...
- await _httpClient.PostAsync(new Uri(_baseUri, $"server_api/{_watchdogKey}/ping"), null!);
+ _sawmill.Debug("Sending ping to watchdog...");
+ using var resp = await _httpClient.PostAsync(new Uri(_baseUri, $"server_api/{_watchdogKey}/ping"), null!);
+ resp.EnsureSuccessStatusCode();
+ _sawmill.Debug("Succeeded in sending ping to watchdog");
}
catch (HttpRequestException e)
{
- _sawmill.Warning("Failed to send ping to watchdog:\n{0}", e);
+ _sawmill.Error("Failed to send ping to watchdog:\n{0}", e);
}
}
diff --git a/Robust.Server/Toolshed/Commands/Players/PlayersCommand.cs b/Robust.Server/Toolshed/Commands/Players/PlayersCommand.cs
index fab35bdaf83..6ff6c1040a4 100644
--- a/Robust.Server/Toolshed/Commands/Players/PlayersCommand.cs
+++ b/Robust.Server/Toolshed/Commands/Players/PlayersCommand.cs
@@ -69,6 +69,12 @@ public EntityUid GetPlayerEntity([PipedArgument] ICommonSession sessions)
{
return sessions.AttachedEntity ?? default;
}
+
+ [CommandImplementation("entity")]
+ public EntityUid GetPlayerEntity([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] string username)
+ {
+ return GetPlayerEntity(Immediate(ctx, username));
+ }
}
public record struct NoSuchPlayerError(string Username) : IConError
diff --git a/Robust.Server/ViewVariables/ServerViewVariablesManager.cs b/Robust.Server/ViewVariables/ServerViewVariablesManager.cs
index 38406613e2a..610efe4c47d 100644
--- a/Robust.Server/ViewVariables/ServerViewVariablesManager.cs
+++ b/Robust.Server/ViewVariables/ServerViewVariablesManager.cs
@@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Server.Console;
using Robust.Server.Player;
+using Robust.Shared.Audio;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -296,7 +297,6 @@ private bool TryReinterpretValue(object? input, [NotNullWhen(true)] out object?
output = prototype;
return true;
-
default:
return false;
}
diff --git a/Robust.Server/ViewVariables/ViewVariablesTrait.cs b/Robust.Server/ViewVariables/ViewVariablesTrait.cs
index 15880adbf80..2c65f4bdf3e 100644
--- a/Robust.Server/ViewVariables/ViewVariablesTrait.cs
+++ b/Robust.Server/ViewVariables/ViewVariablesTrait.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Robust.Server.ViewVariables.Traits;
+using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network.Messages;
@@ -96,6 +97,9 @@ public virtual bool TryModifyProperty(object[] property, object value)
if (value is EntityUid uid)
return IoCManager.Resolve().GetComponentOrNull(uid)?.NetEntity ?? NetEntity.Invalid;
+ if (value is SoundSpecifier)
+ return value;
+
var valType = value.GetType();
if (!valType.IsValueType)
{
diff --git a/Robust.Shared.Maths/Color.cs b/Robust.Shared.Maths/Color.cs
index 0d341552db5..4e56b73cb9e 100644
--- a/Robust.Shared.Maths/Color.cs
+++ b/Robust.Shared.Maths/Color.cs
@@ -592,7 +592,11 @@ public static Vector4 ToHsv(Color rgb)
if (c != 0)
{
if (max == rgb.R)
+ {
h = (rgb.G - rgb.B) / c % 6.0f;
+ if (h < 0f)
+ h += 6.0f;
+ }
else if (max == rgb.G)
h = (rgb.B - rgb.R) / c + 2.0f;
else if (max == rgb.B)
@@ -774,7 +778,11 @@ public static Vector4 ToHcy(Color rgb)
var h = 0.0f;
if (max == rgb.R)
+ {
h = (rgb.G - rgb.B) / c % 6.0f;
+ if (h < 0f)
+ h += 6.0f;
+ }
else if (max == rgb.G)
h = (rgb.B - rgb.R) / c + 2.0f;
else if (max == rgb.B)
diff --git a/Robust.Shared.Maths/Vector2Helpers.cs b/Robust.Shared.Maths/Vector2Helpers.cs
index 726e77c1a9b..3744c178b1d 100644
--- a/Robust.Shared.Maths/Vector2Helpers.cs
+++ b/Robust.Shared.Maths/Vector2Helpers.cs
@@ -59,6 +59,192 @@ public static float Normalize(ref this Vector2 vec)
return length;
}
+ ///
+ /// Compares the lengths of two vectors.
+ ///
+ ///
+ /// Avoids square root computations by using squared lengths.
+ ///
+ ///
+ /// a positive value if is longer than ,
+ /// a negative value if is longer than ,
+ /// or 0 if and have equal lengths.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float CompareLength(Vector2 a, Vector2 b)
+ {
+ return a.LengthSquared() - b.LengthSquared();
+ }
+
+ ///
+ /// Compares the length of a vector with a scalar.
+ ///
+ ///
+ /// Avoids a square root computation by using squared length.
+ ///
+ ///
+ /// a positive value if is longer than ,
+ /// a negative value if is shorter than ,
+ /// or 0 if has a length equal to .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float CompareLength(Vector2 vec, float scalar)
+ {
+ return vec.LengthSquared() - (scalar * scalar);
+ }
+
+ ///
+ /// Compares the length of this vector with a scalar.
+ ///
+ ///
+ /// Avoids a square root computation by using squared length.
+ ///
+ ///
+ /// a positive value if this vector is longer than ,
+ /// a negative value if this vector is shorter than ,
+ /// or 0 if this vector has a length equal to .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float CompareLengthTo(this Vector2 vec, float scalar)
+ {
+ return CompareLength(vec, scalar);
+ }
+
+ ///
+ /// Compares the length of this vector with another.
+ ///
+ ///
+ /// Avoids square root computations by using squared lengths.
+ ///
+ ///
+ /// a positive value if this vector is longer than ,
+ /// a negative value if this vector is shorter than ,
+ /// or 0 if this vector and have equal lengths.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float CompareLengthTo(this Vector2 thisVec, Vector2 otherVec)
+ {
+ return CompareLength(thisVec, otherVec);
+ }
+
+ ///
+ /// Is the length of this vector greater than ?
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsLongerThan(this Vector2 vec, float scalar)
+ {
+ return CompareLength(vec, scalar) > 0;
+ }
+
+ ///
+ /// Is the length of this vector greater than the length of ?
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsLongerThan(this Vector2 thisVec, Vector2 otherVec)
+ {
+ return CompareLength(thisVec, otherVec) > 0;
+ }
+
+ ///
+ /// Is the length of this vector greater than or equal to ?
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsLongerThanOrEqualTo(this Vector2 vec, float scalar)
+ {
+ return CompareLength(vec, scalar) >= 0;
+ }
+
+ ///
+ /// Is the length of this vector greater than or equal to the length of ?
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsLongerThanOrEqualTo(this Vector2 thisVec, Vector2 otherVec)
+ {
+ return CompareLength(thisVec, otherVec) >= 0;
+ }
+
+ ///
+ /// Is the length of this vector less than ?
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsShorterThan(this Vector2 vec, float scalar)
+ {
+ return CompareLength(vec, scalar) < 0;
+ }
+
+ ///
+ /// Is the length of this vector less than the length of ?
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsShorterThan(this Vector2 thisVec, Vector2 otherVec)
+ {
+ return CompareLength(thisVec, otherVec) < 0;
+ }
+
+ ///
+ /// Is the length of this vector less than or equal to ?
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsShorterThanOrEqualTo(this Vector2 vec, float scalar)
+ {
+ return CompareLength(vec, scalar) <= 0;
+ }
+
+ ///
+ /// Is the length of this vector less than or equal to the length of ?
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsShorterThanOrEqualTo(this Vector2 thisVec, Vector2 otherVec)
+ {
+ return CompareLength(thisVec, otherVec) <= 0;
+ }
+
+ ///
+ /// Returns true if this vector's length is equal to .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool LengthEquals(this Vector2 thisVec, float scalar)
+ {
+ return CompareLength(thisVec, scalar) == 0;
+ }
+
+ ///
+ /// Is this vector the same length as ?
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsEqualLengthTo(this Vector2 thisVec, Vector2 otherVec)
+ {
+ return CompareLength(thisVec, otherVec) == 0;
+ }
+
+ ///
+ /// Compares the length of a vector with a scalar.
+ ///
+ ///
+ /// Avoids a square root computation by using squared length.
+ ///
+ ///
+ /// a positive value if is shorter than ,
+ /// a negative value if is longer than ,
+ /// or 0 if has a length equal to .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float CompareLength(float scalar, Vector2 vec)
+ {
+ return (scalar * scalar) - vec.LengthSquared();
+ }
+
+ ///
+ /// Is the length of this vector zero?
+ ///
+ public static bool IsLengthZero(this Vector2 vec)
+ {
+ return vec.LengthSquared() == 0;
+ }
+
///
/// Perform the cross product on a scalar and a vector. In 2D this produces
/// a vector.
diff --git a/Robust.Shared.Scripting/ScriptGlobalsShared.cs b/Robust.Shared.Scripting/ScriptGlobalsShared.cs
index 63da98c273c..c494f0d2ab2 100644
--- a/Robust.Shared.Scripting/ScriptGlobalsShared.cs
+++ b/Robust.Shared.Scripting/ScriptGlobalsShared.cs
@@ -67,12 +67,12 @@ public EntityUid eid(int i)
public MapGridComponent getgrid(int i)
{
- return map.GetGrid(new EntityUid(i));
+ return ent.GetComponent(new EntityUid(i));
}
public MapGridComponent getgrid(EntityUid mapId)
{
- return map.GetGrid(mapId);
+ return ent.GetComponent(mapId);
}
public T res()
diff --git a/Robust.Shared/Analyzers/AccessAttribute.cs b/Robust.Shared/Analyzers/AccessAttribute.cs
index 94e2a91db2b..aa84ce7711a 100644
--- a/Robust.Shared/Analyzers/AccessAttribute.cs
+++ b/Robust.Shared/Analyzers/AccessAttribute.cs
@@ -1,6 +1,6 @@
using System;
-#if NETSTANDARD2_0
+#if ROBUST_ANALYZERS_IMPL
namespace Robust.Shared.Analyzers.Implementation;
#else
namespace Robust.Shared.Analyzers;
diff --git a/Robust.Shared/Analyzers/AccessPermissions.cs b/Robust.Shared/Analyzers/AccessPermissions.cs
index 88ebd1a5e2a..13ea5c503a3 100644
--- a/Robust.Shared/Analyzers/AccessPermissions.cs
+++ b/Robust.Shared/Analyzers/AccessPermissions.cs
@@ -1,6 +1,6 @@
using System;
-#if NETSTANDARD2_0
+#if ROBUST_ANALYZERS_IMPL
namespace Robust.Shared.Analyzers.Implementation;
#else
namespace Robust.Shared.Analyzers;
diff --git a/Robust.Shared/Analyzers/NotNullableFlagAttribute.cs b/Robust.Shared/Analyzers/NotNullableFlagAttribute.cs
index 518f1a18c94..5b4a1722a2c 100644
--- a/Robust.Shared/Analyzers/NotNullableFlagAttribute.cs
+++ b/Robust.Shared/Analyzers/NotNullableFlagAttribute.cs
@@ -1,6 +1,6 @@
using System;
-#if NETSTANDARD2_0
+#if ROBUST_ANALYZERS_IMPL
namespace Robust.Shared.Analyzers.Implementation;
#else
namespace Robust.Shared.Analyzers;
diff --git a/Robust.Shared/Analyzers/PreferGenericVariantAttribute.cs b/Robust.Shared/Analyzers/PreferGenericVariantAttribute.cs
index 2b713f121fe..426148795cc 100644
--- a/Robust.Shared/Analyzers/PreferGenericVariantAttribute.cs
+++ b/Robust.Shared/Analyzers/PreferGenericVariantAttribute.cs
@@ -1,6 +1,6 @@
using System;
-#if NETSTANDARD2_0
+#if ROBUST_ANALYZERS_IMPL
namespace Robust.Shared.Analyzers.Implementation;
#else
namespace Robust.Shared.Analyzers;
diff --git a/Robust.Shared/Asynchronous/TaskManager.cs b/Robust.Shared/Asynchronous/TaskManager.cs
index 2662a1ace64..709b46df57c 100644
--- a/Robust.Shared/Asynchronous/TaskManager.cs
+++ b/Robust.Shared/Asynchronous/TaskManager.cs
@@ -74,4 +74,31 @@ public interface ITaskManager
///
void BlockWaitOnTask(Task task);
}
+
+ internal static class TaskManagerExt
+ {
+ ///
+ /// Run a callback on the main thread, returning a task that represents its completion.
+ ///
+ ///
+ public static Task TaskOnMainThread(this ITaskManager taskManager, Action callback)
+ {
+ var tcs = new TaskCompletionSource();
+
+ taskManager.RunOnMainThread(() =>
+ {
+ try
+ {
+ callback();
+ tcs.SetResult();
+ }
+ catch (Exception e)
+ {
+ tcs.TrySetException(e);
+ }
+ });
+
+ return tcs.Task;
+ }
+ }
}
diff --git a/Robust.Shared/Audio/AudioMetadataPrototype.cs b/Robust.Shared/Audio/AudioMetadataPrototype.cs
index 49188c4d82e..65a67840fad 100644
--- a/Robust.Shared/Audio/AudioMetadataPrototype.cs
+++ b/Robust.Shared/Audio/AudioMetadataPrototype.cs
@@ -10,7 +10,7 @@ namespace Robust.Shared.Audio;
/// to allow the server to know audio lengths without shipping the large audio files themselves.
///
[Prototype(ProtoName)]
-public sealed class AudioMetadataPrototype : IPrototype
+public sealed partial class AudioMetadataPrototype : IPrototype
{
public const string ProtoName = "audioMetadata";
diff --git a/Robust.Shared/Audio/AudioParams.cs b/Robust.Shared/Audio/AudioParams.cs
index a8db8910cc3..fa7557dece7 100644
--- a/Robust.Shared/Audio/AudioParams.cs
+++ b/Robust.Shared/Audio/AudioParams.cs
@@ -29,12 +29,6 @@ public enum Attenuation : int
[DataDefinition]
public partial struct AudioParams
{
- ///
- /// The DistanceModel to use for this specific source.
- ///
- [DataField]
- public Attenuation Attenuation { get; set; } = Attenuation.LinearDistanceClamped;
-
///
/// Base volume to play the audio at, in dB.
///
@@ -45,13 +39,13 @@ public partial struct AudioParams
/// Scale for the audio pitch.
///
[DataField]
- public float Pitch { get; set; } = Default.Pitch;
+ public float Pitch
+ {
+ get => _pitch;
+ set => _pitch = MathF.Max(0f, value);
+ }
- ///
- /// Audio bus to play on.
- ///
- [DataField]
- public string BusName { get; set; } = Default.BusName;
+ private float _pitch = Default.Pitch;
///
/// Only applies to positional audio.
@@ -89,7 +83,7 @@ public partial struct AudioParams
///
/// The "default" audio configuration.
///
- public static readonly AudioParams Default = new(0, 1, "Master", SharedAudioSystem.DefaultSoundRange, 1, 1, false, 0f);
+ public static readonly AudioParams Default = new(0, 1, SharedAudioSystem.DefaultSoundRange, 1, 1, false, 0f);
// explicit parameterless constructor required so that default values get set properly.
public AudioParams() { }
@@ -97,21 +91,19 @@ public AudioParams() { }
public AudioParams(
float volume,
float pitch,
- string busName,
float maxDistance,
float refDistance,
bool loop,
float playOffsetSeconds,
float? variation = null)
- : this(volume, pitch, busName, maxDistance, 1, refDistance, loop, playOffsetSeconds, variation)
+ : this(volume, pitch, maxDistance, 1, refDistance, loop, playOffsetSeconds, variation)
{
}
- public AudioParams(float volume, float pitch, string busName, float maxDistance,float rolloffFactor, float refDistance, bool loop, float playOffsetSeconds, float? variation = null) : this()
+ public AudioParams(float volume, float pitch, float maxDistance,float rolloffFactor, float refDistance, bool loop, float playOffsetSeconds, float? variation = null) : this()
{
Volume = volume;
Pitch = pitch;
- BusName = busName;
MaxDistance = maxDistance;
RolloffFactor = rolloffFactor;
ReferenceDistance = refDistance;
@@ -167,18 +159,6 @@ public readonly AudioParams WithPitchScale(float pitch)
return me;
}
- ///
- /// Returns a copy of this instance with a new bus name set, for easy chaining.
- ///
- /// The new bus name.
- [Pure]
- public readonly AudioParams WithBusName(string bus)
- {
- var me = this;
- me.BusName = bus;
- return me;
- }
-
///
/// Returns a copy of this instance with a new max distance set, for easy chaining.
///
@@ -227,18 +207,6 @@ public readonly AudioParams WithLoop(bool loop)
return me;
}
- ///
- /// Returns a copy of this instance with attenuation set, for easy chaining.
- ///
- /// The new attenuation.
- [Pure]
- public readonly AudioParams WithAttenuation(Attenuation attenuation)
- {
- var me = this;
- me.Attenuation = attenuation;
- return me;
- }
-
[Pure]
public readonly AudioParams WithPlayOffset(float offset)
{
diff --git a/Robust.Shared/Audio/AudioPresetPrototype.cs b/Robust.Shared/Audio/AudioPresetPrototype.cs
index 9e5fe916d3c..64cee87d631 100644
--- a/Robust.Shared/Audio/AudioPresetPrototype.cs
+++ b/Robust.Shared/Audio/AudioPresetPrototype.cs
@@ -9,7 +9,7 @@ namespace Robust.Shared.Audio;
/// This can be used by to apply an audio preset.
///
[Prototype("audioPreset")]
-public sealed class AudioPresetPrototype : IPrototype
+public sealed partial class AudioPresetPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = default!;
diff --git a/Robust.Shared/Audio/Components/AudioComponent.cs b/Robust.Shared/Audio/Components/AudioComponent.cs
index 2b1162617b8..c2b104670c6 100644
--- a/Robust.Shared/Audio/Components/AudioComponent.cs
+++ b/Robust.Shared/Audio/Components/AudioComponent.cs
@@ -6,6 +6,7 @@
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.ViewVariables;
@@ -90,11 +91,24 @@ public sealed partial class AudioComponent : Component, IAudioSource
public void StartPlaying() => Source.StartPlaying();
///
- public void StopPlaying() => Source.StopPlaying();
+ public void StopPlaying()
+ {
+ PlaybackPosition = 0f;
+ Source.StopPlaying();
+ }
///
public void Restart() => Source.Restart();
+ [DataField, AutoNetworkedField]
+ public AudioState State = AudioState.Playing;
+
+ ///
+ /// Time when the audio was paused so we can offset it later if relevant.
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan? PauseTime;
+
///
///
///
@@ -208,6 +222,7 @@ public float Occlusion
///
///
[ViewVariables]
+ [Access(Other = AccessPermissions.ReadWriteExecute)]
public float PlaybackPosition
{
get => Source.PlaybackPosition;
@@ -240,6 +255,14 @@ public void Dispose()
}
}
+[Serializable, NetSerializable]
+public enum AudioState : byte
+{
+ Stopped,
+ Playing,
+ Paused,
+}
+
[Flags]
public enum AudioFlags : byte
{
diff --git a/Robust.Shared/Audio/SoundCollectionPrototype.cs b/Robust.Shared/Audio/SoundCollectionPrototype.cs
index 574dc56366c..350bfb85dcb 100644
--- a/Robust.Shared/Audio/SoundCollectionPrototype.cs
+++ b/Robust.Shared/Audio/SoundCollectionPrototype.cs
@@ -1,13 +1,13 @@
+using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
-using System.Collections.Generic;
namespace Robust.Shared.Audio;
[Prototype("soundCollection")]
-public sealed class SoundCollectionPrototype : IPrototype
+public sealed partial class SoundCollectionPrototype : IPrototype
{
[ViewVariables]
[IdDataField]
diff --git a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs
index e01a6cd0bea..1013554cfbf 100644
--- a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs
+++ b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs
@@ -55,6 +55,141 @@ public override void Initialize()
SubscribeLocalEvent(OnAudioUnpaused);
}
+ ///
+ /// Sets the playback position of audio to the specified spot.
+ ///
+ public void SetPlaybackPosition(Entity? nullEntity, float position)
+ {
+ if (nullEntity == null)
+ return;
+
+ var entity = nullEntity.Value;
+
+ if (!Resolve(entity.Owner, ref entity.Comp, false))
+ return;
+
+ var audioLength = GetAudioLength(entity.Comp.FileName);
+
+ if (audioLength.TotalSeconds < position)
+ {
+ // Just stop it and return
+ if (!_netManager.IsClient)
+ QueueDel(nullEntity.Value);
+
+ entity.Comp.StopPlaying();
+ return;
+ }
+
+ if (position < 0f)
+ {
+ Log.Error($"Tried to set playback position for {ToPrettyString(entity.Owner)} / {entity.Comp.FileName} outside of bounds");
+ return;
+ }
+
+ // If we're paused then the current position is , else it's
+ var currentPos = (entity.Comp.PauseTime ?? Timing.CurTime) - entity.Comp.AudioStart;
+ var timeOffset = TimeSpan.FromSeconds(position - currentPos.TotalSeconds);
+
+ DebugTools.Assert(currentPos > TimeSpan.Zero);
+
+ // Rounding.
+ if (Math.Abs(timeOffset.TotalSeconds) <= 0.01)
+ {
+ return;
+ }
+
+ if (entity.Comp.PauseTime != null)
+ {
+ entity.Comp.PauseTime = entity.Comp.PauseTime.Value + timeOffset;
+
+ // Paused audio doesn't have TimedDespawn so.
+ }
+ else
+ {
+ // Bump it back so the actual playback positions moves forward
+ entity.Comp.AudioStart -= timeOffset;
+
+ // need to ensure it doesn't despawn too early.
+ if (TryComp(entity.Owner, out TimedDespawnComponent? despawn))
+ {
+ despawn.Lifetime -= (float) timeOffset.TotalSeconds;
+ }
+ }
+
+ entity.Comp.PlaybackPosition = position;
+ // Network the new playback position.
+ Dirty(entity);
+ }
+
+ ///
+ /// Calculates playback position considering length paused.
+ ///
+ ///
+ ///
+ private float GetPlaybackPosition(AudioComponent component)
+ {
+ return (float) (Timing.CurTime - (component.PauseTime ?? TimeSpan.Zero) - component.AudioStart).TotalSeconds;
+ }
+
+ ///
+ /// Sets the shared state for an audio entity.
+ ///
+ public void SetState(EntityUid? entity, AudioState state, bool force = false, AudioComponent? component = null)
+ {
+ if (entity == null || !Resolve(entity.Value, ref component, false))
+ return;
+
+ if (component.State == state && !force)
+ return;
+
+ // Unpause it
+ if (component.State == AudioState.Paused && state == AudioState.Playing)
+ {
+ var pauseOffset = Timing.CurTime - component.PauseTime;
+ component.AudioStart += pauseOffset ?? TimeSpan.Zero;
+ component.PlaybackPosition = (float) (Timing.CurTime - component.AudioStart).TotalSeconds;
+ }
+
+ // If we were stopped then played then restart audiostart to now.
+ if (component.State == AudioState.Stopped && state == AudioState.Playing)
+ {
+ component.AudioStart = Timing.CurTime;
+ component.PauseTime = null;
+ }
+
+ switch (state)
+ {
+ case AudioState.Stopped:
+ component.AudioStart = Timing.CurTime;
+ component.PauseTime = null;
+ component.StopPlaying();
+ RemComp(entity.Value);
+ break;
+ case AudioState.Paused:
+ // Set it to current time so we can easily unpause it later.
+ component.PauseTime = Timing.CurTime;
+ component.Pause();
+ RemComp(entity.Value);
+ break;
+ case AudioState.Playing:
+ component.PauseTime = null;
+ component.StartPlaying();
+
+ // Reset TimedDespawn so the audio still gets cleaned up.
+
+ if (!component.Looping)
+ {
+ var timed = EnsureComp(entity.Value);
+ var audioLength = GetAudioLength(component.FileName);
+ timed.Lifetime = (float) audioLength.TotalSeconds + 0.01f;
+ }
+ break;
+ }
+
+ component.State = state;
+ Dirty(entity.Value, component);
+ }
+
protected void SetZOffset(float value)
{
ZOffset = value;
@@ -234,7 +369,7 @@ public TimeSpan GetAudioLength(string filename)
/// The resource path to the OGG Vorbis file to play.
/// The set of players that will hear the sound.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null);
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null);
///
/// Play an audio file globally, without position.
@@ -253,7 +388,7 @@ public TimeSpan GetAudioLength(string filename)
/// The resource path to the OGG Vorbis file to play.
/// The player that will hear the sound.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null);
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null);
///
/// Play an audio file globally, without position.
@@ -274,7 +409,7 @@ public TimeSpan GetAudioLength(string filename)
/// The resource path to the OGG Vorbis file to play.
/// The player that will hear the sound.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null);
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null);
///
/// Play an audio file globally, without position.
@@ -294,7 +429,7 @@ public TimeSpan GetAudioLength(string filename)
/// The set of players that will hear the sound.
/// The UID of the entity "emitting" the audio.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null);
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null);
///
/// Play an audio file following an entity.
@@ -303,7 +438,7 @@ public TimeSpan GetAudioLength(string filename)
/// The player that will hear the sound.
/// The UID of the entity "emitting" the audio.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null);
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null);
///
/// Play an audio file following an entity.
@@ -312,7 +447,7 @@ public TimeSpan GetAudioLength(string filename)
/// The player that will hear the sound.
/// The UID of the entity "emitting" the audio.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null);
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null);
///
/// Play an audio file following an entity.
@@ -378,7 +513,7 @@ public TimeSpan GetAudioLength(string filename)
/// The sound specifier that points the audio file(s) that should be played.
/// The EntityCoordinates to attach the audio source to.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string filename,
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string? filename,
EntityCoordinates coordinates, AudioParams? audioParams = null);
///
@@ -387,7 +522,7 @@ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs
/// The resource path to the OGG Vorbis file to play.
/// The UID of the entity "emitting" the audio.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string filename, EntityUid uid,
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string? filename, EntityUid uid,
AudioParams? audioParams = null);
///
@@ -419,7 +554,7 @@ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs
/// The set of players that will hear the sound.
/// The coordinates at which to play the audio.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null);
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null);
///
/// Play an audio file at a static position.
@@ -428,7 +563,7 @@ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs
/// The player that will hear the sound.
/// The coordinates at which to play the audio.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
///
/// Play an audio file at a static position.
@@ -437,7 +572,7 @@ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs
/// The player that will hear the sound.
/// The coordinates at which to play the audio.
[return: NotNullIfNotNull("filename")]
- public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
+ public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
///
/// Play an audio file at a static position.
@@ -505,4 +640,12 @@ protected sealed class PlayAudioEntityMessage : AudioMessage
{
public NetEntity NetEntity;
}
+
+ public bool IsPlaying(EntityUid? stream, AudioComponent? component = null)
+ {
+ if (stream == null || !Resolve(stream.Value, ref component, false))
+ return false;
+
+ return component.State == AudioState.Playing;
+ }
}
diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs
index da908fd9e6e..01ac866d4b8 100644
--- a/Robust.Shared/CVars.cs
+++ b/Robust.Shared/CVars.cs
@@ -65,16 +65,26 @@ protected CVars()
CVarDef.Create("net.pool_size", 512, CVar.CLIENT | CVar.SERVER);
///
- /// Maximum UDP payload size to send.
+ /// Maximum UDP payload size to send by default, for IPv4.
///
///
+ ///
public static readonly CVarDef NetMtu =
- CVarDef.Create("net.mtu", 1000, CVar.ARCHIVE);
+ CVarDef.Create("net.mtu", 900, CVar.ARCHIVE);
+
+ ///
+ /// Maximum UDP payload size to send by default, for IPv6.
+ ///
+ ///
+ ///
+ public static readonly CVarDef NetMtuIpv6 =
+ CVarDef.Create("net.mtu_ipv6", NetPeerConfiguration.kDefaultMTUV6, CVar.ARCHIVE);
///
/// If set, automatically try to detect MTU above .
///
///
+ ///
///
///
public static readonly CVarDef NetMtuExpand =
@@ -215,18 +225,24 @@ protected CVars()
CVarDef.Create("net.pvs_async", true, CVar.ARCHIVE | CVar.SERVERONLY);
///
- /// View size to take for PVS calculations,
- /// as the size of the sides of a square centered on the view points of clients.
+ /// View size to take for PVS calculations, as the size of the sides of a square centered on the view points of
+ /// clients. See also .
///
public static readonly CVarDef NetMaxUpdateRange =
CVarDef.Create("net.pvs_range", 25f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
///
- /// Chunks whose centre is further than this distance away from a player's eye will contain fewer entities.
- /// This has no effect if it is smaller than
+ /// A variant of that is used to limit the view-distance of entities with the
+ /// flag set. This can be used to extend the range at which certain
+ /// entities become visible.
///
- public static readonly CVarDef NetLowLodRange =
- CVarDef.Create("net.low_lod_distance", 100f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
+ ///
+ /// This is useful for entities like lights and occluders to try and prevent noticeable pop-in as players
+ /// move around. Note that this has no effect if it is less than , and that this
+ /// only works for entities that are directly parented to a grid or map.
+ ///
+ public static readonly CVarDef NetPvsPriorityRange =
+ CVarDef.Create("net.pvs_priority_range", 32.5f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
///
/// Maximum allowed delay between the current tick and a client's last acknowledged tick before we send the
@@ -416,6 +432,35 @@ protected CVars()
public static readonly CVarDef MetricsPort =
CVarDef.Create("metrics.port", 44880, CVar.SERVERONLY);
+ ///
+ /// Sets a fixed interval (seconds) for internal collection of certain metrics,
+ /// when not using the Prometheus metrics server.
+ ///
+ ///
+ ///
+ /// Most metrics are internally implemented directly via the prometheus-net library.
+ /// These metrics can only be scraped by the Prometheus metrics server ().
+ /// However, newer metrics are implemented with the System.Diagnostics.Metrics library in the .NET runtime.
+ /// These metrics can be scraped through more means, such as dotnet counters.
+ ///
+ ///
+ /// While many metrics are simple counters that can "just" be reported,
+ /// some metrics require more advanced internal work and need some code to be ran internally
+ /// before their values are made current. When collecting metrics via a
+ /// method other than the Prometheus metrics server, these metrics pose a problem,
+ /// as there is no way for the game to update them before collection properly.
+ ///
+ ///
+ /// This CVar acts as a fallback: if set to a value other than 0 (disabled),
+ /// these metrics will be internally updated at the interval provided.
+ ///
+ ///
+ /// This does not need to be enabled if metrics are collected exclusively via the Prometheus metrics server.
+ ///
+ ///
+ public static readonly CVarDef MetricsUpdateInterval =
+ CVarDef.Create("metrics.update_interval", 0f, CVar.SERVERONLY);
+
///
/// Enable detailed runtime metrics. Empty to disable.
///
@@ -806,7 +851,7 @@ protected CVars()
/// See the documentation of the enum for values.
///
public static readonly CVarDef AuthMode =
- CVarDef.Create("auth.mode", (int) Network.AuthMode.Optional, CVar.SERVERONLY);
+ CVarDef.Create("auth.mode", (int) Network.AuthMode.Required, CVar.SERVERONLY);
///
/// Allow unauthenticated localhost connections, even if the auth mode is set to required.
@@ -841,6 +886,22 @@ protected CVars()
public static readonly CVarDef RenderFOVColor =
CVarDef.Create("render.fov_color", Color.Black.ToHex(), CVar.ARCHIVE | CVar.CLIENTONLY);
+ /*
+ * CONTROLS
+ */
+
+ ///
+ /// Milliseconds to wait to consider double-click delays.
+ ///
+ public static readonly CVarDef DoubleClickDelay =
+ CVarDef.Create("controls.double_click_delay", 250, CVar.ARCHIVE | CVar.CLIENTONLY);
+
+ ///
+ /// Range in pixels for double-clicks
+ ///
+ public static readonly CVarDef DoubleClickRange =
+ CVarDef.Create("controls.double_click_range", 10, CVar.ARCHIVE | CVar.CLIENTONLY);
+
/*
* DISPLAY
*/
@@ -1384,7 +1445,7 @@ protected CVars()
/// Comma-separated list of URLs of hub servers to advertise to.
///
public static readonly CVarDef HubUrls =
- CVarDef.Create("hub.hub_urls", "https://central.spacestation14.io/hub/", CVar.SERVERONLY);
+ CVarDef.Create("hub.hub_urls", "https://hub.spacestation14.com/", CVar.SERVERONLY);
///
/// URL of this server to advertise.
@@ -1611,7 +1672,8 @@ protected CVars()
/// original exception rather than sending people on a wild-goose chase to find a non-existent bug.
///
public static readonly CVarDef ReplayIgnoreErrors =
- CVarDef.Create("replay.ignore_errors", false, CVar.CLIENTONLY | CVar.ARCHIVE);
+ CVarDef.Create("replay.ignore_errors", false, CVar.CLIENTONLY);
+
/*
* CFG
*/
diff --git a/Robust.Shared/Collections/ValueList.cs b/Robust.Shared/Collections/ValueList.cs
index 6892778ab6c..22b31f2f1f1 100644
--- a/Robust.Shared/Collections/ValueList.cs
+++ b/Robust.Shared/Collections/ValueList.cs
@@ -607,4 +607,12 @@ public void EnsureLength(int newCount)
region.Clear();
Count = newCount;
}
+
+ public void AddRange(IEnumerable select)
+ {
+ foreach (var result in select)
+ {
+ Add(result);
+ }
+ }
}
diff --git a/Robust.Shared/Configuration/ConfigurationManager.cs b/Robust.Shared/Configuration/ConfigurationManager.cs
index 725bb746ea9..b9750a7f7f6 100644
--- a/Robust.Shared/Configuration/ConfigurationManager.cs
+++ b/Robust.Shared/Configuration/ConfigurationManager.cs
@@ -558,17 +558,21 @@ public void OverrideDefault(CVarDef def, T value) where T : notnull
OverrideDefault(def.Name, value);
}
- ///
- public T GetCVar(string name)
+ public object GetCVar(string name)
{
using var _ = Lock.ReadGuard();
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
- //TODO: Make flags work, required non-derpy net system.
- return (T)(GetConfigVarValue(cVar))!;
+ return GetConfigVarValue(cVar);
throw new InvalidConfigurationException($"Trying to get unregistered variable '{name}'");
}
+ ///
+ public T GetCVar(string name)
+ {
+ return (T)GetCVar(name);
+ }
+
public T GetCVar(CVarDef def) where T : notnull
{
return GetCVar(def.Name);
diff --git a/Robust.Shared/Configuration/EnvironmentVariables.cs b/Robust.Shared/Configuration/EnvironmentVariables.cs
index 0bb7fc169a9..e642abfe197 100644
--- a/Robust.Shared/Configuration/EnvironmentVariables.cs
+++ b/Robust.Shared/Configuration/EnvironmentVariables.cs
@@ -1,9 +1,10 @@
using System;
+using System.Collections;
using System.Collections.Generic;
namespace Robust.Shared.Configuration
{
- public static class EnvironmentVariables
+ internal static class EnvironmentVariables
{
///
/// The environment variable for configuring CVar overrides. The value
@@ -12,23 +13,38 @@ public static class EnvironmentVariables
///
public const string ConfigVarEnvironmentVariable = "ROBUST_CVARS";
+ public const string SingleVarPrefix = "ROBUST_CVAR_";
+
///
/// Get the CVar overrides defined in the relevant environment variable.
///
- public static IEnumerable<(string, string)> GetEnvironmentCVars()
+ internal static IEnumerable<(string, string)> GetEnvironmentCVars()
{
- var eVarString = Environment.GetEnvironmentVariable(ConfigVarEnvironmentVariable);
-
- if (eVarString == null)
- {
- yield break;
- }
+ // Handle ROBUST_CVARS.
+ var eVarString = Environment.GetEnvironmentVariable(ConfigVarEnvironmentVariable) ?? "";
foreach (var cVarPair in eVarString.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
var pairParts = cVarPair.Split('=', 2);
yield return (pairParts[0], pairParts[1]);
}
+
+ // Handle ROBUST_CVAR_*
+
+ foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
+ {
+ var key = (string)entry.Key;
+ var value = (string?)entry.Value;
+
+ if (value == null)
+ continue;
+
+ if (!key.StartsWith(SingleVarPrefix))
+ continue;
+
+ var varName = key[SingleVarPrefix.Length..].Replace("__", ".");
+ yield return (varName, value);
+ }
}
}
}
diff --git a/Robust.Shared/Configuration/IConfigurationManager.cs b/Robust.Shared/Configuration/IConfigurationManager.cs
index aababead8b5..6412e566ef4 100644
--- a/Robust.Shared/Configuration/IConfigurationManager.cs
+++ b/Robust.Shared/Configuration/IConfigurationManager.cs
@@ -118,6 +118,13 @@ void RegisterCVar(string name, T defaultValue, CVar flags = CVar.NONE, Action
/// The new default value of the CVar.
void OverrideDefault(CVarDef def, T value) where T : notnull;
+ ///
+ /// Get the value of a CVar.
+ ///
+ /// The name of the CVar.
+ ///
+ object GetCVar(string name);
+
///
/// Get the value of a CVar.
///
diff --git a/Robust.Shared/Console/Commands/DumpSerializerTypeMapCommand.cs b/Robust.Shared/Console/Commands/DumpSerializerTypeMapCommand.cs
index 3ab807ee602..1f24ab33f28 100644
--- a/Robust.Shared/Console/Commands/DumpSerializerTypeMapCommand.cs
+++ b/Robust.Shared/Console/Commands/DumpSerializerTypeMapCommand.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.IO;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
@@ -12,11 +12,16 @@ internal sealed class DumpSerializerTypeMapCommand : LocalizedCommands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
- foreach (var (type, index) in _robustSerializer.GetTypeMap().OrderBy(x => x.Value))
- {
- shell.WriteLine($"{index}: {type}");
- }
+ var stream = new MemoryStream();
+ ((RobustSerializer)_robustSerializer).GetHashManifest(stream, true);
+ stream.Position = 0;
+ using var streamReader = new StreamReader(stream);
shell.WriteLine($"Hash: {_robustSerializer.GetSerializableTypesHashString()}");
+ shell.WriteLine("Manifest:");
+ while (streamReader.ReadLine() is { } line)
+ {
+ shell.WriteLine(line);
+ }
}
}
diff --git a/Robust.Shared/Console/Commands/MapCommands.cs b/Robust.Shared/Console/Commands/MapCommands.cs
index b9dfe0be647..517dcf938d6 100644
--- a/Robust.Shared/Console/Commands/MapCommands.cs
+++ b/Robust.Shared/Console/Commands/MapCommands.cs
@@ -10,7 +10,8 @@ namespace Robust.Shared.Console.Commands;
sealed class AddMapCommand : LocalizedCommands
{
- [Dependency] private readonly IMapManager _map = default!;
+ [Dependency] private readonly IMapManagerInternal _map = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
public override string Command => "addmap";
public override bool RequireServerOrSingleplayer => true;
@@ -24,11 +25,8 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
if (!_map.MapExists(mapId))
{
- _map.CreateMap(mapId);
- if (args.Length >= 2 && args[1] == "false")
- {
- _map.AddUninitializedMap(mapId);
- }
+ var init = args.Length < 2 || !bool.Parse(args[1]);
+ _entMan.System().CreateMap(mapId, runMapInit: init);
shell.WriteLine($"Map with ID {mapId} created.");
return;
@@ -84,7 +82,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
var gridIdNet = NetEntity.Parse(args[0]);
- if (!_entManager.TryGetEntity(gridIdNet, out var gridId) || !_map.GridExists(gridId))
+ if (!_entManager.TryGetEntity(gridIdNet, out var gridId) || !_entManager.HasComponent(gridId))
{
shell.WriteError($"Grid {gridId} does not exist.");
return;
diff --git a/Robust.Shared/Console/CompletionHelper.cs b/Robust.Shared/Console/CompletionHelper.cs
index 440e66e6156..ff0b483993b 100644
--- a/Robust.Shared/Console/CompletionHelper.cs
+++ b/Robust.Shared/Console/CompletionHelper.cs
@@ -1,6 +1,9 @@
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using JetBrains.Annotations;
+using Robust.Shared.Audio;
+using Robust.Shared.Collections;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -23,7 +26,34 @@ public static class CompletionHelper
public static IEnumerable Booleans => new[]
{ new CompletionOption(bool.FalseString), new CompletionOption(bool.TrueString) };
- public static IEnumerable ContentFilePath(string arg, IResourceManager res)
+ ///
+ /// Special-cased file handler for audio that accounts for serverside completion.
+ ///
+ public static IEnumerable AudioFilePath(string arg, IPrototypeManager protoManager,
+ IResourceManager res)
+ {
+ var resPath = GetUpdatedPath(arg);
+ var paths = new HashSet();
+
+ foreach (var path in res.ContentGetDirectoryEntries(resPath))
+ {
+ paths.Add(path);
+ }
+
+ foreach (var audioProto in protoManager.EnumeratePrototypes())
+ {
+ var hero = new ResPath(audioProto.ID);
+
+ if (!hero.TryRelativeTo(resPath, out _))
+ continue;
+
+ paths.Add(hero.GetNextSegment(resPath).ToString());
+ }
+
+ return GetPaths(resPath, paths, res);
+ }
+
+ private static ResPath GetUpdatedPath(string arg)
{
var curPath = arg;
if (!curPath.StartsWith("/"))
@@ -31,12 +61,18 @@ public static IEnumerable ContentFilePath(string arg, IResourc
var resPath = new ResPath(curPath);
- if (!curPath.EndsWith("/")){
+ if (!curPath.EndsWith("/"))
+ {
resPath /= "..";
resPath = resPath.Clean();
}
- var options = res.ContentGetDirectoryEntries(resPath)
+ return resPath;
+ }
+
+ private static IEnumerable GetPaths(ResPath resPath, IEnumerable inputs, IResourceManager res)
+ {
+ var options = inputs
.OrderBy(c => c)
.Select(c =>
{
@@ -51,6 +87,12 @@ public static IEnumerable ContentFilePath(string arg, IResourc
return options;
}
+ public static IEnumerable ContentFilePath(string arg, IResourceManager res)
+ {
+ var resPath = GetUpdatedPath(arg);
+ return GetPaths(resPath, res.ContentGetDirectoryEntries(resPath), res);
+ }
+
public static IEnumerable ContentDirPath(string arg, IResourceManager res)
{
var curPath = arg;
diff --git a/Robust.Shared/Console/CompletionResult.cs b/Robust.Shared/Console/CompletionResult.cs
index f3e2c37a13f..64eab32568d 100644
--- a/Robust.Shared/Console/CompletionResult.cs
+++ b/Robust.Shared/Console/CompletionResult.cs
@@ -38,7 +38,7 @@ private static CompletionOption[] ConvertOptions(IEnumerable stringOpts)
///
/// Possible option to tab-complete in a .
///
-public record struct CompletionOption(string Value, string? Hint = null, CompletionOptionFlags Flags = default)
+public record struct CompletionOption(string Value, string? Hint = null, CompletionOptionFlags Flags = default) : IComparable
{
///
/// The value that will be filled in if completed.
@@ -54,6 +54,12 @@ public record struct CompletionOption(string Value, string? Hint = null, Complet
/// Flags that control how this completion is used.
///
public CompletionOptionFlags Flags { get; set; } = Flags;
+
+ public int CompareTo(CompletionOption other)
+ {
+ var valueComparison = string.Compare(Value, other.Value, StringComparison.CurrentCultureIgnoreCase);
+ return valueComparison;
+ }
}
///
diff --git a/Robust.Shared/Containers/SharedContainerSystem.Insert.cs b/Robust.Shared/Containers/SharedContainerSystem.Insert.cs
index 5d18b7f9f31..ca1b0234cb7 100644
--- a/Robust.Shared/Containers/SharedContainerSystem.Insert.cs
+++ b/Robust.Shared/Containers/SharedContainerSystem.Insert.cs
@@ -135,6 +135,25 @@ public bool Insert(Entity
+ /// Attempts to insert an entity into a container. If it fails, it will instead drop the entity next to the
+ /// container entity.
+ ///
+ /// Whether or not the entity was successfully inserted
+ public bool InsertOrDrop(Entity toInsert,
+ BaseContainer container,
+ TransformComponent? containerXform = null)
+ {
+ if (!Resolve(toInsert.Owner, ref toInsert.Comp1) || !Resolve(container.Owner, ref containerXform))
+ return false;
+
+ if (Insert(toInsert, container, containerXform))
+ return true;
+
+ _transform.DropNextTo(toInsert, (container.Owner, containerXform));
+ return false;
+ }
+
///
/// Checks if the entity can be inserted into the given container.
///
diff --git a/Robust.Shared/Containers/SharedContainerSystem.cs b/Robust.Shared/Containers/SharedContainerSystem.cs
index 707f8edff8e..91b909264a8 100644
--- a/Robust.Shared/Containers/SharedContainerSystem.cs
+++ b/Robust.Shared/Containers/SharedContainerSystem.cs
@@ -168,9 +168,16 @@ public bool TryGetContainer(EntityUid uid, string id, [NotNullWhen(true)] out Ba
return containerManager.Containers.TryGetValue(id, out container);
}
- public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [NotNullWhen(true)] out BaseContainer? container, ContainerManagerComponent? containerManager = null, bool skipExistCheck = false)
+ [Obsolete("Use variant without skipExistCheck argument")]
+ public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [NotNullWhen(true)] out BaseContainer? container, bool skipExistCheck)
{
- if (!Resolve(uid, ref containerManager, false) || !(skipExistCheck || Exists(containedUid)))
+ return TryGetContainingContainer(uid, containedUid, out container);
+ }
+
+ public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [NotNullWhen(true)] out BaseContainer? container, ContainerManagerComponent? containerManager = null)
+ {
+ DebugTools.Assert(Exists(containedUid));
+ if (!Resolve(uid, ref containerManager, false))
{
container = null;
return false;
@@ -191,7 +198,8 @@ public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [No
public bool ContainsEntity(EntityUid uid, EntityUid containedUid, ContainerManagerComponent? containerManager = null)
{
- if (!Resolve(uid, ref containerManager, false) || !Exists(containedUid))
+ DebugTools.Assert(Exists(containedUid));
+ if (!Resolve(uid, ref containerManager, false))
return false;
foreach (var container in containerManager.Containers.Values)
@@ -251,7 +259,7 @@ public bool TryGetContainingContainer(EntityUid uid, [NotNullWhen(true)] out Bas
if (!Resolve(uid, ref transform, false))
return false;
- return TryGetContainingContainer(transform.ParentUid, uid, out container, skipExistCheck: true);
+ return TryGetContainingContainer(transform.ParentUid, uid, out container);
}
///
diff --git a/Robust.Shared/ContentPack/Sandbox.yml b/Robust.Shared/ContentPack/Sandbox.yml
index 897e2a97672..201244c6ea1 100644
--- a/Robust.Shared/ContentPack/Sandbox.yml
+++ b/Robust.Shared/ContentPack/Sandbox.yml
@@ -951,7 +951,9 @@ Types:
IComparable: { All: True }
IComparable`1: { All: True }
IDisposable: { All: True }
- IEquatable`1: { }
+ IEquatable`1:
+ Methods:
+ - "bool Equals(!0)"
IFormatProvider: { All: True }
IFormattable: { All: True }
Index: { All: True }
diff --git a/Robust.Shared/Enums/PlacementInformation.cs b/Robust.Shared/Enums/PlacementInformation.cs
index 1062d62f085..4e13aba1030 100644
--- a/Robust.Shared/Enums/PlacementInformation.cs
+++ b/Robust.Shared/Enums/PlacementInformation.cs
@@ -1,15 +1,46 @@
-using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects;
-namespace Robust.Shared.Enums
+namespace Robust.Shared.Enums;
+
+public sealed class PlacementInformation
{
- public sealed class PlacementInformation
- {
- public string? EntityType { get; set; }
- public bool IsTile { get; set; }
- public EntityUid MobUid { get; set; }
- public string? PlacementOption { get; set; }
- public int Range { get; set; }
- public int TileType { get; set; }
- public int Uses { get; set; } = 1;
- }
+ ///
+ /// Entity prototype to be placed
+ ///
+ public string? EntityType { get; set; }
+
+ ///
+ /// Indiciates if the entity prototype to be placed is in fact a tile
+ ///
+ public bool IsTile { get; set; }
+
+ ///
+ /// ID of the mob that has permission to place the prototype
+ ///
+ public EntityUid MobUid { get; set; }
+
+ ///
+ /// Specifies the placement alignment
+ ///
+ public string? PlacementOption { get; set; }
+
+ ///
+ /// Determines the max range at which the entity prototype can be placed
+ ///
+ public int Range { get; set; }
+
+ ///
+ ///
+ ///
+ public int TileType { get; set; }
+
+ ///
+ /// Number of times the entity can be placed
+ ///
+ public int Uses { get; set; } = 1;
+
+ ///
+ /// Sets whether the input context should switch to 'editor' mode
+ ///
+ public bool UseEditorContext { get; set; } = true;
}
diff --git a/Robust.Shared/GameObjects/ComponentAttributes.cs b/Robust.Shared/GameObjects/ComponentAttributes.cs
new file mode 100644
index 00000000000..7a538ec0b0c
--- /dev/null
+++ b/Robust.Shared/GameObjects/ComponentAttributes.cs
@@ -0,0 +1,28 @@
+using System;
+using JetBrains.Annotations;
+
+namespace Robust.Shared.GameObjects;
+
+///
+/// Marks a component as being automatically registered by
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false)]
+[BaseTypeRequired(typeof(IComponent))]
+[MeansImplicitUse]
+public sealed class RegisterComponentAttribute : Attribute;
+
+///
+/// Defines Name that this component is represented with in prototypes.
+///
+[AttributeUsage(AttributeTargets.Class)]
+public sealed class ComponentProtoNameAttribute(string prototypeName) : Attribute
+{
+ public string PrototypeName { get; } = prototypeName;
+}
+
+///
+/// Marks a component as not being saved when saving maps/grids.
+///
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false)]
+public sealed class UnsavedComponentAttribute : Attribute;
diff --git a/Robust.Shared/GameObjects/ComponentFactory.cs b/Robust.Shared/GameObjects/ComponentFactory.cs
index d0437ada609..3a2b836d92d 100644
--- a/Robust.Shared/GameObjects/ComponentFactory.cs
+++ b/Robust.Shared/GameObjects/ComponentFactory.cs
@@ -121,9 +121,11 @@ private ComponentRegistration Register(Type type,
if (!overwrite && lowerCaseNames.TryGetValue(lowerCaseName, out var prevName))
throw new InvalidOperationException($"{lowerCaseName} is already registered, previous: {prevName}");
+ var unsaved = type.HasCustomAttribute();
+
var idx = CompIdx.Index(type);
- var registration = new ComponentRegistration(name, type, idx);
+ var registration = new ComponentRegistration(name, type, idx, unsaved);
idxToType[idx] = type;
names[name] = registration;
diff --git a/Robust.Shared/GameObjects/ComponentProtoNameAttribute.cs b/Robust.Shared/GameObjects/ComponentProtoNameAttribute.cs
deleted file mode 100644
index e1fa90d39e7..00000000000
--- a/Robust.Shared/GameObjects/ComponentProtoNameAttribute.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-
-namespace Robust.Shared.GameObjects
-{
- ///
- /// Defines Name that this component is represented with in prototypes.
- ///
- [AttributeUsage(AttributeTargets.Class)]
- public sealed class ComponentProtoNameAttribute : Attribute
- {
- public string PrototypeName { get; }
-
- public ComponentProtoNameAttribute(string prototypeName)
- {
- PrototypeName = prototypeName;
- }
- }
-}
diff --git a/Robust.Shared/GameObjects/ComponentRegistration.cs b/Robust.Shared/GameObjects/ComponentRegistration.cs
index 7e6006f8fb5..84947c40d5f 100644
--- a/Robust.Shared/GameObjects/ComponentRegistration.cs
+++ b/Robust.Shared/GameObjects/ComponentRegistration.cs
@@ -21,6 +21,12 @@ public sealed class ComponentRegistration
public CompIdx Idx { get; }
+ ///
+ /// If this is true, the component will not be saved when saving a map/grid.
+ ///
+ ///
+ public bool Unsaved { get; }
+
///
/// ID used to reference the component type across the network.
/// If null, no network synchronization will be available for this component.
@@ -35,11 +41,12 @@ public sealed class ComponentRegistration
// Internal for sandboxing.
// Avoid content passing an instance of this to ComponentFactory to get any type they want instantiated.
- internal ComponentRegistration(string name, Type type, CompIdx idx)
+ internal ComponentRegistration(string name, Type type, CompIdx idx, bool unsaved = false)
{
Name = name;
Type = type;
Idx = idx;
+ Unsaved = unsaved;
}
public override string ToString()
diff --git a/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs b/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs
index 68920261139..531a28a19c3 100644
--- a/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs
+++ b/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs
@@ -29,9 +29,50 @@ public sealed partial class PrototypeLayerData
[DataField("map")] public HashSet? MapKeys;
[DataField("renderingStrategy")] public LayerRenderingStrategy? RenderingStrategy;
+ ///
+ /// If set, indicates that this sprite layer should instead be used to copy into shader parameters on another layer.
+ ///
+ ///
+ ///
+ /// If set, this sprite layer is not rendered. Instead, the "result" of rendering it (exact sprite layer and such)
+ /// are copied into the shader parameters of another object,
+ /// specified by the .
+ ///
+ ///
+ /// The specified layer must have a shader set. When it does, the shader's
+ ///
+ ///
+ /// Note that sprite layers are processed in-order, so to avoid 1-frame delays,
+ /// the layer doing the copying should occur BEFORE the layer being copied into.
+ ///
+ ///
+ [DataField] public PrototypeCopyToShaderParameters? CopyToShaderParameters;
+
[DataField] public bool Cycle;
}
+///
+/// Stores parameters for .
+///
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class PrototypeCopyToShaderParameters
+{
+ ///
+ /// The map key of the layer that will have its shader modified.
+ ///
+ [DataField(required: true)] public string LayerKey;
+
+ ///
+ /// The name of the shader parameter that will receive the actual selected texture.
+ ///
+ [DataField] public string? ParameterTexture;
+
+ ///
+ /// The name of the shader parameter that will receive UVs to select the sprite in .
+ ///
+ [DataField] public string? ParameterUV;
+}
+
[Serializable, NetSerializable]
public enum LayerRenderingStrategy
{
diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs
index 1ff62c83220..97db539045a 100644
--- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs
+++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs
@@ -101,7 +101,6 @@ public Matrix3 InvLocalMatrix
internal bool _mapIdInitialized;
internal bool _gridInitialized;
- // TODO: Cache this.
///
/// The EntityUid of the map which this object is on, if any.
///
diff --git a/Robust.Shared/GameObjects/Components/UserInterface/BoundUserInterface.cs b/Robust.Shared/GameObjects/Components/UserInterface/BoundUserInterface.cs
index b6c6fb6bd3c..00aa9f33a53 100644
--- a/Robust.Shared/GameObjects/Components/UserInterface/BoundUserInterface.cs
+++ b/Robust.Shared/GameObjects/Components/UserInterface/BoundUserInterface.cs
@@ -41,14 +41,14 @@ protected internal virtual void Open()
///
/// Invoked when the server uses SetState.
///
- protected virtual void UpdateState(BoundUserInterfaceState state)
+ protected internal virtual void UpdateState(BoundUserInterfaceState state)
{
}
///
/// Invoked when the server sends an arbitrary message.
///
- protected virtual void ReceiveMessage(BoundUserInterfaceMessage message)
+ protected internal virtual void ReceiveMessage(BoundUserInterfaceMessage message)
{
}
@@ -57,7 +57,7 @@ protected virtual void ReceiveMessage(BoundUserInterfaceMessage message)
///
public void Close()
{
- UiSystem.TryCloseUi(_playerManager.LocalSession, Owner, UiKey);
+ UiSystem.CloseUi(Owner, UiKey, _playerManager.LocalEntity, predicted: true);
}
///
@@ -65,7 +65,7 @@ public void Close()
///
public void SendMessage(BoundUserInterfaceMessage message)
{
- UiSystem.SendUiMessage(this, message);
+ UiSystem.ClientSendUiMessage(Owner, UiKey, message);
}
public void SendPredictedMessage(BoundUserInterfaceMessage message)
@@ -73,20 +73,6 @@ public void SendPredictedMessage(BoundUserInterfaceMessage message)
UiSystem.SendPredictedUiMessage(this, message);
}
- internal void InternalReceiveMessage(BoundUserInterfaceMessage message)
- {
- switch (message)
- {
- case UpdateBoundStateMessage updateBoundStateMessage:
- State = updateBoundStateMessage.State;
- UpdateState(State);
- break;
- default:
- ReceiveMessage(message);
- break;
- }
- }
-
~BoundUserInterface()
{
Dispose(false);
diff --git a/Robust.Server/GameObjects/Components/UserInterface/IgnoreUIRangeComponent.cs b/Robust.Shared/GameObjects/Components/UserInterface/IgnoreUIRangeComponent.cs
similarity index 67%
rename from Robust.Server/GameObjects/Components/UserInterface/IgnoreUIRangeComponent.cs
rename to Robust.Shared/GameObjects/Components/UserInterface/IgnoreUIRangeComponent.cs
index db00a0c068c..b6fa9aaf91a 100644
--- a/Robust.Server/GameObjects/Components/UserInterface/IgnoreUIRangeComponent.cs
+++ b/Robust.Shared/GameObjects/Components/UserInterface/IgnoreUIRangeComponent.cs
@@ -1,12 +1,12 @@
-using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
-namespace Robust.Server.GameObjects;
+namespace Robust.Shared.GameObjects;
///
/// Lets any entities with this component ignore user interface range checks that would normally
/// close the UI automatically.
///
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent]
public sealed partial class IgnoreUIRangeComponent : Component
{
}
diff --git a/Robust.Shared/GameObjects/Components/UserInterface/PlayerBoundUserInterface.cs b/Robust.Shared/GameObjects/Components/UserInterface/PlayerBoundUserInterface.cs
deleted file mode 100644
index f8b0790b31d..00000000000
--- a/Robust.Shared/GameObjects/Components/UserInterface/PlayerBoundUserInterface.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.Collections.Generic;
-using JetBrains.Annotations;
-using Robust.Shared.Player;
-using Robust.Shared.ViewVariables;
-
-namespace Robust.Shared.GameObjects;
-
-///
-/// Represents an entity-bound interface that can be opened by multiple players at once.
-///
-[PublicAPI]
-public sealed class PlayerBoundUserInterface
-{
- [ViewVariables]
- public float InteractionRange;
-
- [ViewVariables]
- public float InteractionRangeSqrd => InteractionRange * InteractionRange;
-
- [ViewVariables]
- public Enum UiKey { get; }
- [ViewVariables]
- public EntityUid Owner { get; }
-
- internal readonly HashSet _subscribedSessions = new();
- [ViewVariables]
- internal BoundUIWrapMessage? LastStateMsg;
- [ViewVariables(VVAccess.ReadWrite)]
- public bool RequireInputValidation;
-
- [ViewVariables]
- internal bool StateDirty;
-
- [ViewVariables]
- internal readonly Dictionary PlayerStateOverrides =
- new();
-
- ///
- /// All of the sessions currently subscribed to this UserInterface.
- ///
- [ViewVariables]
- public IReadOnlySet SubscribedSessions => _subscribedSessions;
-
- public PlayerBoundUserInterface(PrototypeData data, EntityUid owner)
- {
- RequireInputValidation = data.RequireInputValidation;
- UiKey = data.UiKey;
- Owner = owner;
-
- InteractionRange = data.InteractionRange;
- }
-}
diff --git a/Robust.Shared/GameObjects/Components/UserInterface/ServerBoundUserInterfaceMessage.cs b/Robust.Shared/GameObjects/Components/UserInterface/ServerBoundUserInterfaceMessage.cs
index f89fcd13e00..27afadc4425 100644
--- a/Robust.Shared/GameObjects/Components/UserInterface/ServerBoundUserInterfaceMessage.cs
+++ b/Robust.Shared/GameObjects/Components/UserInterface/ServerBoundUserInterfaceMessage.cs
@@ -1,29 +1,26 @@
-using System.Collections.Generic;
using JetBrains.Annotations;
+using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
-namespace Robust.Shared.GameObjects
+namespace Robust.Shared.GameObjects;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class ActiveUserInterfaceComponent : Component
{
- [RegisterComponent]
- public sealed partial class ActiveUserInterfaceComponent : Component
- {
- [ViewVariables]
- public HashSet Interfaces = new();
- }
+}
- [PublicAPI]
- public sealed class ServerBoundUserInterfaceMessage
- {
- [ViewVariables]
- public BoundUserInterfaceMessage Message { get; }
- [ViewVariables]
- public ICommonSession Session { get; }
+[PublicAPI]
+public sealed class ServerBoundUserInterfaceMessage
+{
+ [ViewVariables]
+ public BoundUserInterfaceMessage Message { get; }
+ [ViewVariables]
+ public ICommonSession Session { get; }
- public ServerBoundUserInterfaceMessage(BoundUserInterfaceMessage message, ICommonSession session)
- {
- Message = message;
- Session = session;
- }
+ public ServerBoundUserInterfaceMessage(BoundUserInterfaceMessage message, ICommonSession session)
+ {
+ Message = message;
+ Session = session;
}
}
diff --git a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs
index 2e8808e8b42..42cc789c21e 100644
--- a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs
+++ b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs
@@ -8,36 +8,51 @@
namespace Robust.Shared.GameObjects
{
- [RegisterComponent, NetworkedComponent]
+ [RegisterComponent, NetworkedComponent, Access(typeof(SharedUserInterfaceSystem))]
public sealed partial class UserInterfaceComponent : Component
{
- // TODO: Obviously clean this shit up, I just moved it into shared.
-
- [ViewVariables] public readonly Dictionary OpenInterfaces = new();
+ ///
+ /// The currently open interfaces. Used clientside to store the UI.
+ ///
+ [ViewVariables, Access(Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.ReadWriteExecute)]
+ public readonly Dictionary ClientOpenInterfaces = new();
- [ViewVariables] public readonly Dictionary Interfaces = new();
+ [DataField]
+ internal Dictionary Interfaces = new();
- public Dictionary MappedInterfaceData = new();
+ ///
+ /// Actors that currently have interfaces open.
+ ///
+ [DataField]
+ public Dictionary> Actors = new();
///
- /// Loaded on Init from serialized data.
+ /// Legacy data, new BUIs should be using comp states.
///
- [DataField("interfaces")] internal List InterfaceData = new();
+ public Dictionary States = new();
+
+ [Serializable, NetSerializable]
+ internal sealed class UserInterfaceComponentState(
+ Dictionary> actors,
+ Dictionary states)
+ : IComponentState
+ {
+ public Dictionary> Actors = actors;
+
+ public Dictionary States = states;
+ }
}
[DataDefinition]
- public sealed partial class PrototypeData
+ public sealed partial class InterfaceData
{
- [DataField("key", required: true)]
- public Enum UiKey { get; private set; } = default!;
-
[DataField("type", required: true)]
public string ClientType { get; private set; } = default!;
///
/// Maximum range before a BUI auto-closes. A non-positive number means there is no limit.
///
- [DataField("range")]
+ [DataField]
public float InteractionRange = 2f;
// TODO BUI move to content?
@@ -48,7 +63,7 @@ public sealed partial class PrototypeData
///
/// Avoids requiring each system to individually validate client inputs. However, perhaps some BUIs are supposed to be bypass accessibility checks
///
- [DataField("requireInputValidation")]
+ [DataField]
public bool RequireInputValidation = true;
}
@@ -56,18 +71,12 @@ public sealed partial class PrototypeData
/// Raised whenever the server receives a BUI message from a client relating to a UI that requires input
/// validation.
///
- public sealed class BoundUserInterfaceMessageAttempt : CancellableEntityEventArgs
+ public sealed class BoundUserInterfaceMessageAttempt(EntityUid actor, EntityUid target, Enum uiKey)
+ : CancellableEntityEventArgs
{
- public readonly ICommonSession Sender;
- public readonly EntityUid Target;
- public readonly Enum UiKey;
-
- public BoundUserInterfaceMessageAttempt(ICommonSession sender, EntityUid target, Enum uiKey)
- {
- Sender = sender;
- Target = target;
- UiKey = uiKey;
- }
+ public readonly EntityUid Actor = actor;
+ public readonly EntityUid Target = target;
+ public readonly Enum UiKey = uiKey;
}
[NetSerializable, Serializable]
@@ -104,7 +113,7 @@ public abstract class BaseBoundUserInterfaceEvent : EntityEventArgs
/// Only set when the message is raised as a directed event.
///
[NonSerialized]
- public ICommonSession Session = default!;
+ public EntityUid Actor = default!;
}
///
@@ -120,17 +129,6 @@ public abstract class BoundUserInterfaceMessage : BaseBoundUserInterfaceEvent
public NetEntity Entity { get; set; } = NetEntity.Invalid;
}
- [NetSerializable, Serializable]
- internal sealed class UpdateBoundStateMessage : BoundUserInterfaceMessage
- {
- public readonly BoundUserInterfaceState State;
-
- public UpdateBoundStateMessage(BoundUserInterfaceState state)
- {
- State = state;
- }
- }
-
[NetSerializable, Serializable]
internal sealed class OpenBoundInterfaceMessage : BoundUserInterfaceMessage
{
@@ -142,59 +140,38 @@ internal sealed class CloseBoundInterfaceMessage : BoundUserInterfaceMessage
}
[Serializable, NetSerializable]
- internal abstract class BaseBoundUIWrapMessage : EntityEventArgs
+ internal abstract class BaseBoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey)
+ : EntityEventArgs
{
- public readonly NetEntity Entity;
- public readonly BoundUserInterfaceMessage Message;
- public readonly Enum UiKey;
-
- public BaseBoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey)
- {
- Message = message;
- UiKey = uiKey;
- Entity = entity;
- }
+ public readonly NetEntity Entity = entity;
+ public readonly BoundUserInterfaceMessage Message = message;
+ public readonly Enum UiKey = uiKey;
}
///
/// Helper message raised from client to server.
///
[Serializable, NetSerializable]
- internal sealed class BoundUIWrapMessage : BaseBoundUIWrapMessage
- {
- public BoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey) : base(entity, message, uiKey)
- {
- }
- }
-
- ///
- /// Helper message raised from client to server.
- ///
- [Serializable, NetSerializable]
- internal sealed class PredictedBoundUIWrapMessage : BaseBoundUIWrapMessage
- {
- public PredictedBoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey) : base(entity, message, uiKey)
- {
- }
- }
+ internal sealed class BoundUIWrapMessage(NetEntity entity, BoundUserInterfaceMessage message, Enum uiKey)
+ : BaseBoundUIWrapMessage(entity, message, uiKey);
public sealed class BoundUIOpenedEvent : BaseLocalBoundUserInterfaceEvent
{
- public BoundUIOpenedEvent(Enum uiKey, EntityUid uid, ICommonSession session)
+ public BoundUIOpenedEvent(Enum uiKey, EntityUid uid, EntityUid actor)
{
UiKey = uiKey;
Entity = uid;
- Session = session;
+ Actor = actor;
}
}
public sealed class BoundUIClosedEvent : BaseLocalBoundUserInterfaceEvent
{
- public BoundUIClosedEvent(Enum uiKey, EntityUid uid, ICommonSession session)
+ public BoundUIClosedEvent(Enum uiKey, EntityUid uid, EntityUid actor)
{
UiKey = uiKey;
Entity = uid;
- Session = session;
+ Actor = actor;
}
}
}
diff --git a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceUserComponent.cs b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceUserComponent.cs
new file mode 100644
index 00000000000..f2e09def547
--- /dev/null
+++ b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceUserComponent.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Robust.Shared.GameObjects;
+
+///
+/// Stores data about this entity and what BUIs they have open.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class UserInterfaceUserComponent : Component
+{
+ public override bool SessionSpecific => true;
+
+ [DataField]
+ public Dictionary> OpenInterfaces = new();
+}
+
+[Serializable, NetSerializable]
+internal sealed class UserInterfaceUserComponentState : IComponentState
+{
+ public Dictionary> OpenInterfaces = new();
+}
diff --git a/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs b/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs
index 736209cd9e2..bfafd0098d0 100644
--- a/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs
+++ b/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs
@@ -329,7 +329,7 @@ private static void ProcessSingleEventCore(
ref Unit unitRef,
EventData subs)
{
- foreach (var handler in subs.BroadcastRegistrations)
+ foreach (var handler in subs.BroadcastRegistrations.Span)
{
if ((handler.Mask & source) != 0)
handler.Handler(ref unitRef);
diff --git a/Robust.Shared/GameObjects/EntityEventBus.Ordering.cs b/Robust.Shared/GameObjects/EntityEventBus.Ordering.cs
index 5f9837c96ac..6bb026526a7 100644
--- a/Robust.Shared/GameObjects/EntityEventBus.Ordering.cs
+++ b/Robust.Shared/GameObjects/EntityEventBus.Ordering.cs
@@ -13,7 +13,7 @@ private static void CollectBroadcastOrdered(
EventData sub,
ref ValueList found)
{
- foreach (var handler in sub.BroadcastRegistrations)
+ foreach (var handler in sub.BroadcastRegistrations.Span)
{
if ((handler.Mask & source) != 0)
found.Add(new OrderedEventDispatch(handler.Handler, handler.Order));
@@ -44,7 +44,7 @@ private static void DispatchOrderedEvents(ref Unit eventArgs, ref ValueList
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddComponents(EntityUid target, EntityPrototype prototype, bool removeExisting = true)
+ {
+ AddComponents(target, prototype.Components, removeExisting);
+ }
+
+ ///
+ public void AddComponents(EntityUid target, ComponentRegistry registry, bool removeExisting = true)
+ {
+ if (registry.Count == 0)
+ return;
+
+ var metadata = MetaQuery.GetComponent(target);
+
+ foreach (var (name, entry) in registry)
+ {
+ var reg = _componentFactory.GetRegistration(name);
+
+ if (HasComponent(target, reg.Type))
+ {
+ if (!removeExisting)
+ continue;
+
+ RemoveComponent(target, reg.Type, metadata);
+ }
+
+ var comp = _componentFactory.GetComponent(reg);
+ _serManager.CopyTo(entry.Component, ref comp, notNullableOverride: true);
+ AddComponent(target, comp, metadata: metadata);
+ }
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void RemoveComponents(EntityUid target, EntityPrototype prototype)
+ {
+ RemoveComponents(target, prototype.Components);
+ }
+
+ ///
+ public void RemoveComponents(EntityUid target, ComponentRegistry registry)
+ {
+ if (registry.Count == 0)
+ return;
+
+ var metadata = MetaQuery.GetComponent(target);
+
+ foreach (var entry in registry.Values)
+ {
+ RemoveComponent(target, entry.Component.GetType(), metadata);
+ }
+ }
+
public IComponent AddComponent(EntityUid uid, ushort netId, MetaDataComponent? meta = null)
{
var newComponent = _componentFactory.GetComponent(netId);
@@ -958,6 +1013,11 @@ public IEnumerable GetComponents(EntityUid uid)
}
}
+ ///
+ /// Internal variant of that directly returns the actual component set.
+ ///
+ internal IReadOnlyCollection GetComponentsInternal(EntityUid uid) => _entCompIndex[uid];
+
///
public int ComponentCount(EntityUid uid)
{
@@ -1452,6 +1512,24 @@ public bool TryGetComponent(EntityUid uid, [NotNullWhen(true)] out TComp1? compo
return false;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [Pure]
+ public bool TryComp(EntityUid uid, [NotNullWhen(true)] out TComp1? component)
+ => TryGetComponent(uid, out component);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [Pure]
+ public bool TryComp(EntityUid? uid, [NotNullWhen(true)] out TComp1? component)
+ => TryGetComponent(uid, out component);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [Pure]
+ public bool HasComp(EntityUid uid) => HasComponent(uid);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [Pure]
+ public bool HasComp(EntityUid? uid) => HasComponent(uid);
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent(EntityUid uid)
diff --git a/Robust.Shared/GameObjects/EntityManager.Spawn.cs b/Robust.Shared/GameObjects/EntityManager.Spawn.cs
index 7eb98c9c52f..ca96a731661 100644
--- a/Robust.Shared/GameObjects/EntityManager.Spawn.cs
+++ b/Robust.Shared/GameObjects/EntityManager.Spawn.cs
@@ -5,16 +5,17 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.Containers;
+using Robust.Shared.Maths;
namespace Robust.Shared.GameObjects;
public partial class EntityManager
{
- // This method will soon be marked as obsolete.
+ // This method will soon(TM) be marked as obsolete.
public EntityUid SpawnEntity(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
=> SpawnAttachedTo(protoName, coordinates, overrides);
- // This method will soon be marked as obsolete.
+ // This method will soon(TM) be marked as obsolete.
public EntityUid SpawnEntity(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null)
=> Spawn(protoName, coordinates, overrides);
@@ -83,12 +84,16 @@ public virtual EntityUid SpawnAttachedTo(string? protoName, EntityCoordinates co
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public EntityUid Spawn(string? protoName = null, ComponentRegistry? overrides = null)
- => Spawn(protoName, MapCoordinates.Nullspace, overrides);
+ public EntityUid Spawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true)
+ {
+ var entity = CreateEntityUninitialized(protoName, MapCoordinates.Nullspace, overrides);
+ InitializeAndStartEntity(entity, doMapInit);
+ return entity;
+ }
- public virtual EntityUid Spawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null)
+ public virtual EntityUid Spawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
{
- var entity = CreateEntityUninitialized(protoName, coordinates, overrides);
+ var entity = CreateEntityUninitialized(protoName, coordinates, overrides, rotation);
InitializeAndStartEntity(entity, coordinates.MapId);
return entity;
}
@@ -111,32 +116,19 @@ public bool TrySpawnNextTo(
if (!xform.ParentUid.IsValid())
return false;
- if (!MetaQuery.TryGetComponent(target, out var meta))
- return false;
-
- if ((meta.Flags & MetaDataFlags.InContainer) == 0)
+ if (!_containers.TryGetContainingContainer(target, out var container))
{
- uid = SpawnAttachedTo(protoName, xform.Coordinates, overrides);
+ uid = SpawnNextToOrDrop(protoName, target, xform, overrides);
return true;
}
- if (!TryGetComponent(xform.ParentUid, out ContainerManagerComponent? containerComp))
- return false;
-
- foreach (var container in containerComp.Containers.Values)
- {
- if (!container.Contains(target))
- continue;
-
- uid = Spawn(protoName, overrides);
- if (_containers.Insert(uid.Value, container))
- return true;
-
- DeleteEntity(uid.Value);
- uid = null;
- return false;
- }
+ var doMapInit = _mapSystem.IsInitialized(xform.MapUid);
+ uid = Spawn(protoName, overrides, doMapInit);
+ if (_containers.Insert(uid.Value, container))
+ return true;
+ DeleteEntity(uid.Value);
+ uid = null;
return false;
}
@@ -155,7 +147,8 @@ public bool TrySpawnInContainer(
if (!containerComp.Containers.TryGetValue(containerId, out var container))
return false;
- uid = Spawn(protoName, overrides);
+ var doMapInit = _mapSystem.IsInitialized(TransformQuery.GetComponent(containerUid).MapUid);
+ uid = Spawn(protoName, overrides, doMapInit);
if (_containers.Insert(uid.Value, container))
return true;
@@ -171,7 +164,8 @@ public EntityUid SpawnNextToOrDrop(string? protoName, EntityUid target, Transfor
if (!xform.ParentUid.IsValid())
return Spawn(protoName);
- var uid = Spawn(protoName, overrides);
+ var doMapInit = _mapSystem.IsInitialized(xform.MapUid);
+ var uid = Spawn(protoName, overrides, doMapInit);
_xforms.DropNextTo(uid, target);
return uid;
}
@@ -184,14 +178,28 @@ public EntityUid SpawnInContainerOrDrop(
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
- var uid = Spawn(protoName, overrides);
+ return SpawnInContainerOrDrop(protoName, containerUid, containerId, out _, xform, containerComp, overrides);
+ }
+
+ public EntityUid SpawnInContainerOrDrop(
+ string? protoName,
+ EntityUid containerUid,
+ string containerId,
+ out bool inserted,
+ TransformComponent? xform = null,
+ ContainerManagerComponent? containerComp = null,
+ ComponentRegistry? overrides = null)
+ {
+ inserted = true;
+ xform ??= TransformQuery.GetComponent(containerUid);
+ var doMapInit = _mapSystem.IsInitialized(xform.MapUid);
+ var uid = Spawn(protoName, overrides, doMapInit);
if ((containerComp == null && !TryGetComponent(containerUid, out containerComp))
|| !containerComp.Containers.TryGetValue(containerId, out var container)
|| !_containers.Insert(uid, container))
{
-
- xform ??= TransformQuery.GetComponent(containerUid);
+ inserted = false;
if (xform.ParentUid.IsValid())
_xforms.DropNextTo(uid, (containerUid, xform));
}
diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs
index 2ea4e71cfb4..a540494d157 100644
--- a/Robust.Shared/GameObjects/EntityManager.cs
+++ b/Robust.Shared/GameObjects/EntityManager.cs
@@ -9,6 +9,7 @@
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
+using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Profiling;
@@ -297,14 +298,13 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, Entity
}
///
- public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates, ComponentRegistry? overrides = null)
+ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
{
var newEntity = CreateEntity(prototypeName, out _, overrides);
var transform = TransformQuery.GetComponent(newEntity);
if (coordinates.MapId == MapId.Nullspace)
{
- DebugTools.Assert(_mapManager.GetMapEntityId(coordinates.MapId) == EntityUid.Invalid);
transform._parent = EntityUid.Invalid;
transform.Anchored = false;
return newEntity;
@@ -323,7 +323,7 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoo
else
{
coords = new EntityCoordinates(mapEnt, coordinates.Position);
- _xforms.SetCoordinates(newEntity, transform, coords, null, newParent: mapXform);
+ _xforms.SetCoordinates(newEntity, transform, coords, rotation, newParent: mapXform);
}
return newEntity;
@@ -354,7 +354,7 @@ public virtual void DirtyEntity(EntityUid uid, MetaDataComponent? metadata = nul
}
///
- [Obsolete("use override with an EntityUid")]
+ [Obsolete("use override with an EntityUid or Entity")]
public void Dirty(IComponent component, MetaDataComponent? meta = null)
{
Dirty(component.Owner, component, meta);
@@ -365,6 +365,7 @@ public virtual void Dirty(EntityUid uid, IComponent component, MetaDataComponent
{
DebugTools.Assert(component.GetType().HasCustomAttribute(),
$"Attempted to dirty a non-networked component: {component.GetType()}");
+ DebugTools.AssertOwner(uid, component);
if (component.LifeStage >= ComponentLifeStage.Removing || !component.NetSyncEnabled)
return;
@@ -775,7 +776,7 @@ private EntityUid AllocEntity(out MetaDataComponent metadata)
///
/// Allocates an entity and loads components but does not do initialization.
///
- private protected virtual EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
+ internal virtual EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
{
if (prototypeName == null)
return AllocEntity(out metadata);
@@ -820,15 +821,22 @@ private protected void LoadEntity(EntityUid entity, IEntityLoadContext? context,
public void InitializeAndStartEntity(EntityUid entity, MapId? mapId = null)
{
+ var doMapInit = _mapManager.IsMapInitialized(mapId ?? TransformQuery.GetComponent(entity).MapID);
+ InitializeAndStartEntity(entity, doMapInit);
+ }
+
+ public void InitializeAndStartEntity(Entity entity, bool doMapInit)
+ {
+ if (!MetaQuery.Resolve(entity.Owner, ref entity.Comp))
+ return;
+
try
{
- var meta = MetaQuery.GetComponent(entity);
- InitializeEntity(entity, meta);
- StartEntity(entity);
+ InitializeEntity(entity.Owner, entity.Comp);
+ StartEntity(entity.Owner);
- // If the map we're initializing the entity on is initialized, run map init on it.
- if (_mapManager.IsMapInitialized(mapId ?? TransformQuery.GetComponent(entity).MapID))
- RunMapInit(entity, meta);
+ if (doMapInit)
+ RunMapInit(entity.Owner, entity.Comp);
}
catch (Exception e)
{
@@ -858,7 +866,7 @@ public void RunMapInit(EntityUid entity, MetaDataComponent meta)
DebugTools.Assert(meta.EntityLifeStage == EntityLifeStage.Initialized, $"Expected entity {ToPrettyString(entity)} to be initialized, was {meta.EntityLifeStage}");
SetLifeStage(meta, EntityLifeStage.MapInitialized);
- EventBus.RaiseLocalEvent(entity, MapInitEventInstance, false);
+ EventBus.RaiseLocalEvent(entity, MapInitEventInstance);
}
///
diff --git a/Robust.Shared/GameObjects/EntityManagerExt.cs b/Robust.Shared/GameObjects/EntityManagerExt.cs
index f99ad91ea5d..f9ee17da860 100644
--- a/Robust.Shared/GameObjects/EntityManagerExt.cs
+++ b/Robust.Shared/GameObjects/EntityManagerExt.cs
@@ -1,4 +1,7 @@
-namespace Robust.Shared.GameObjects
+using Robust.Shared.Collections;
+using Robust.Shared.Random;
+
+namespace Robust.Shared.GameObjects
{
public static class EntityManagerExt
{
@@ -19,5 +22,79 @@ public static class EntityManagerExt
return default;
}
+
+ ///
+ /// Picks an entity at random with the supplied component.
+ ///
+ public static bool TryGetRandom(this IEntityManager entManager, IRobustRandom random, out EntityUid entity, bool includePaused = false) where TComp1 : IComponent
+ {
+ var entities = new ValueList();
+
+ if (includePaused)
+ {
+ var query = entManager.AllEntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out _))
+ {
+ entities.Add(uid);
+ }
+ }
+ else
+ {
+ var query = entManager.EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out _))
+ {
+ entities.Add(uid);
+ }
+ }
+
+ if (entities.Count == 0)
+ {
+ entity = EntityUid.Invalid;
+ return false;
+ }
+
+ entity = random.Pick(entities);
+ return true;
+ }
+
+ ///
+ /// Picks an entity at random with the supplied components.
+ ///
+ public static bool TryGetRandom(this IEntityManager entManager, IRobustRandom random, out EntityUid entity, bool includePaused = false)
+ where TComp1 : IComponent
+ where TComp2 : IComponent
+ {
+ var entities = new ValueList();
+
+ if (includePaused)
+ {
+ var query = entManager.AllEntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out _, out _))
+ {
+ entities.Add(uid);
+ }
+ }
+ else
+ {
+ var query = entManager.EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out _, out _))
+ {
+ entities.Add(uid);
+ }
+ }
+
+ if (entities.Count == 0)
+ {
+ entity = EntityUid.Invalid;
+ return false;
+ }
+
+ entity = random.Pick(entities);
+ return true;
+ }
}
}
diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs
index 392d0f8cc29..c70bb05a362 100644
--- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs
+++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs
@@ -5,8 +5,10 @@
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Map;
+using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
+using TerraFX.Interop.Windows;
namespace Robust.Shared.GameObjects;
@@ -699,32 +701,32 @@ protected void QueueDel(EntityUid? uid)
#region Entity Spawning
- // This method will be obsoleted soon.
+ // This method will be obsoleted soon(TM).
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid Spawn(string? prototype, EntityCoordinates coordinates)
{
return ((IEntityManager)EntityManager).SpawnEntity(prototype, coordinates);
}
- ///
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected EntityUid Spawn(string? prototype, MapCoordinates coordinates)
- => EntityManager.Spawn(prototype, coordinates);
+ protected EntityUid Spawn(string? prototype, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
+ => EntityManager.Spawn(prototype, coordinates, overrides, rotation);
- ///
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected EntityUid Spawn(string? prototype = null)
- => EntityManager.Spawn(prototype);
+ protected EntityUid Spawn(string? prototype = null, ComponentRegistry? overrides = null, bool doMapInit = true)
+ => EntityManager.Spawn(prototype, overrides, doMapInit);
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected EntityUid SpawnAttachedTo(string? prototype, EntityCoordinates coordinates)
- => EntityManager.SpawnAttachedTo(prototype, coordinates);
+ protected EntityUid SpawnAttachedTo(string? prototype, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
+ => EntityManager.SpawnAttachedTo(prototype, coordinates, overrides);
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected EntityUid SpawnAtPosition(string? prototype, EntityCoordinates coordinates)
- => EntityManager.SpawnAtPosition(prototype, coordinates);
+ protected EntityUid SpawnAtPosition(string? prototype, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
+ => EntityManager.SpawnAtPosition(prototype, coordinates, overrides);
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/Robust.Shared/GameObjects/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs
index 75e1472b32a..550a9f372f2 100644
--- a/Robust.Shared/GameObjects/IEntityManager.Components.cs
+++ b/Robust.Shared/GameObjects/IEntityManager.Components.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Robust.Shared.GameObjects
@@ -38,6 +39,26 @@ public partial interface IEntityManager
///
int Count(Type component);
+ ///
+ /// Adds the specified components from the
+ ///
+ void AddComponents(EntityUid target, EntityPrototype prototype, bool removeExisting = true);
+
+ ///
+ /// Adds the specified registry components to the target entity.
+ ///
+ void AddComponents(EntityUid target, ComponentRegistry registry, bool removeExisting = true);
+
+ ///
+ /// Removes the specified entity prototype components from the target entity.
+ ///
+ void RemoveComponents(EntityUid target, EntityPrototype prototype);
+
+ ///
+ /// Removes the specified registry components from the target entity.
+ ///
+ void RemoveComponents(EntityUid target, ComponentRegistry registry);
+
///
/// Adds a Component type to an entity. If the entity is already Initialized, the component will
/// automatically be Initialized and Started.
diff --git a/Robust.Shared/GameObjects/IEntityManager.Spawn.cs b/Robust.Shared/GameObjects/IEntityManager.Spawn.cs
index 11b1d9edc74..29f744366a4 100644
--- a/Robust.Shared/GameObjects/IEntityManager.Spawn.cs
+++ b/Robust.Shared/GameObjects/IEntityManager.Spawn.cs
@@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Containers;
using Robust.Shared.Map;
+using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Robust.Shared.GameObjects;
@@ -27,12 +28,12 @@ EntityUid[] SpawnEntities(EntityCoordinates coordinates, List protoName
///
/// Spawns an entity in nullspace.
///
- EntityUid Spawn(string? protoName = null, ComponentRegistry? overrides = null);
+ EntityUid Spawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true);
///
/// Spawns an entity at a specific world position. The entity will either be parented to the map or a grid.
///
- EntityUid Spawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null);
+ EntityUid Spawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!);
///
/// Spawns an entity and then parents it to the entity that the given entity coordinates are relative to.
@@ -46,7 +47,7 @@ EntityUid[] SpawnEntities(EntityCoordinates coordinates, List protoName
EntityUid SpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null);
///
- /// Attempts to spawn an entity inside of a container.
+ /// Attempt to spawn an entity and insert it into a container. If the insertion fails, the entity gets deleted.
///
bool TrySpawnInContainer(
string? protoName,
@@ -58,9 +59,9 @@ bool TrySpawnInContainer(
///
/// Attempts to spawn an entity inside of a container. If it fails to insert into the container, it will
- /// instead attempt to spawn the entity next to the target.
+ /// instead drop the entity next to the target (see ).
///
- public EntityUid SpawnInContainerOrDrop(
+ EntityUid SpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
@@ -68,9 +69,20 @@ public EntityUid SpawnInContainerOrDrop(
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null);
+ ///
+ EntityUid SpawnInContainerOrDrop(
+ string? protoName,
+ EntityUid containerUid,
+ string containerId,
+ out bool inserted,
+ TransformComponent? xform = null,
+ ContainerManagerComponent? containerComp = null,
+ ComponentRegistry? overrides = null);
+
///
- /// Attempts to spawn an entity adjacent to some other entity. If the other entity is in a container, this will
- /// attempt to insert the new entity into the same container.
+ /// Attempts to spawn an entity adjacent to some other target entity. If the target entity is in
+ /// a container, this will attempt to insert the spawned entity into the same container. If the insertion fails,
+ /// the entity is deleted. If the entity is not in a container, this behaves like .
///
bool TrySpawnNextTo(
string? protoName,
diff --git a/Robust.Shared/GameObjects/IEntityManager.cs b/Robust.Shared/GameObjects/IEntityManager.cs
index fc052dc8bfc..3d1210277d4 100644
--- a/Robust.Shared/GameObjects/IEntityManager.cs
+++ b/Robust.Shared/GameObjects/IEntityManager.cs
@@ -4,6 +4,7 @@
using JetBrains.Annotations;
using Prometheus;
using Robust.Shared.Map;
+using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -76,10 +77,12 @@ public partial interface IEntityManager
EntityUid CreateEntityUninitialized(string? prototypeName, EntityCoordinates coordinates, ComponentRegistry? overrides = null);
- EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates, ComponentRegistry? overrides = null);
+ EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!);
void InitializeAndStartEntity(EntityUid entity, MapId? mapId = null);
+ void InitializeAndStartEntity(Entity entity, bool doMapInit);
+
void InitializeEntity(EntityUid entity, MetaDataComponent? meta = null);
void StartEntity(EntityUid entity);
diff --git a/Robust.Shared/GameObjects/NetEntity.cs b/Robust.Shared/GameObjects/NetEntity.cs
index 22a7be1630a..857fd0878fa 100644
--- a/Robust.Shared/GameObjects/NetEntity.cs
+++ b/Robust.Shared/GameObjects/NetEntity.cs
@@ -68,7 +68,7 @@ public static bool TryParse(ReadOnlySpan uid, out NetEntity entity)
entity = Parse(uid);
return true;
}
- catch (FormatException)
+ catch (Exception ex) when (ex is FormatException or OverflowException)
{
entity = Invalid;
return false;
diff --git a/Robust.Shared/GameObjects/RegisterComponentAttribute.cs b/Robust.Shared/GameObjects/RegisterComponentAttribute.cs
deleted file mode 100644
index 22b1986cb35..00000000000
--- a/Robust.Shared/GameObjects/RegisterComponentAttribute.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using JetBrains.Annotations;
-
-namespace Robust.Shared.GameObjects
-{
- ///
- /// Marks a component as being automatically registered by
- ///
- [AttributeUsage(AttributeTargets.Class, Inherited = false)]
- [BaseTypeRequired(typeof(IComponent))]
- [MeansImplicitUse]
- public sealed class RegisterComponentAttribute : Attribute
- {
-
- }
-}
diff --git a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs
index e91450344bf..6048531d220 100644
--- a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs
+++ b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs
@@ -67,7 +67,7 @@ private void AddContained(HashSet intersecting, LookupFlags flags)
}
}
- foreach (var uid in toAdd)
+ foreach (var uid in toAdd.Span)
{
intersecting.Add(uid);
}
diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs
index e787c64b329..dd4306971ba 100644
--- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs
+++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs
@@ -58,7 +58,7 @@ private void AddContained(HashSet> intersecting, LookupFlags flags,
}
}
- foreach (var uid in toAdd)
+ foreach (var uid in toAdd.Span)
{
intersecting.Add(uid);
}
diff --git a/Robust.Shared/GameObjects/Systems/SharedAppearanceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedAppearanceSystem.cs
index e6bd0b12793..76bb98f5b52 100644
--- a/Robust.Shared/GameObjects/Systems/SharedAppearanceSystem.cs
+++ b/Robust.Shared/GameObjects/Systems/SharedAppearanceSystem.cs
@@ -74,6 +74,28 @@ public bool TryGetData(EntityUid uid, Enum key, [NotNullWhen(true)] out object?
return component.AppearanceData.TryGetValue(key, out value);
}
+
+ ///
+ /// Copies appearance data from src to dest.
+ /// If src has no nothing is done.
+ /// If dest has no AppearanceComponent then it is created.
+ ///
+ public void CopyData(Entity src, Entity dest)
+ {
+ if (!Resolve(src, ref src.Comp, false))
+ return;
+
+ dest.Comp ??= EnsureComp(dest);
+ dest.Comp.AppearanceData.Clear();
+
+ foreach (var (key, value) in src.Comp.AppearanceData)
+ {
+ dest.Comp.AppearanceData[key] = value;
+ }
+
+ Dirty(dest, dest.Comp);
+ QueueUpdate(dest, dest.Comp);
+ }
}
[Serializable, NetSerializable]
diff --git a/Robust.Shared/GameObjects/Systems/SharedGridFixtureSystem.cs b/Robust.Shared/GameObjects/Systems/SharedGridFixtureSystem.cs
index 2d3bda26421..a60f78c84c1 100644
--- a/Robust.Shared/GameObjects/Systems/SharedGridFixtureSystem.cs
+++ b/Robust.Shared/GameObjects/Systems/SharedGridFixtureSystem.cs
@@ -180,7 +180,7 @@ private bool UpdateFixture(EntityUid uid, MapChunk chunk, List rectangles
toRemove.Add((oldId, oldFixture));
}
- foreach (var (id, fixture) in toRemove)
+ foreach (var (id, fixture) in toRemove.Span)
{
// TODO add a DestroyFixture() override that takes in a list.
// reduced broadphase lookups
@@ -194,7 +194,7 @@ private bool UpdateFixture(EntityUid uid, MapChunk chunk, List rectangles
}
// Anything remaining is a new fixture (or at least, may have not serialized onto the chunk yet).
- foreach (var (id, fixture) in newFixtures)
+ foreach (var (id, fixture) in newFixtures.Span)
{
var existingFixture = _fixtures.GetFixtureOrNull(uid, id, manager: manager);
// Check if it's the same (otherwise remove anyway).
diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Light.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Light.cs
index 9cd9bcf4532..d7052777404 100644
--- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Light.cs
+++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Light.cs
@@ -11,13 +11,14 @@ public abstract partial class SharedMapSystem
{
public void SetAmbientLight(MapId mapId, Color color)
{
- var mapComp = EnsureComp(MapManager.GetMapEntityId(mapId));
+ var mapUid = MapManager.GetMapEntityId(mapId);
+ var mapComp = EnsureComp(mapUid);
if (mapComp.AmbientLightColor.Equals(color))
return;
mapComp.AmbientLightColor = color;
- Dirty(mapComp);
+ Dirty(mapUid, mapComp);
}
private void OnMapLightGetState(EntityUid uid, MapLightComponent component, ref ComponentGetState args)
diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Map.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Map.cs
index b8f1d4ea19f..6b848b50197 100644
--- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Map.cs
+++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Map.cs
@@ -1,68 +1,188 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameStates;
-using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects;
public abstract partial class SharedMapSystem
{
+ protected int LastMapId;
+
private void InitializeMap()
{
- SubscribeLocalEvent(OnMapAdd);
- SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnComponentAdd);
+ SubscribeLocalEvent(OnCompInit);
+ SubscribeLocalEvent(OnCompStartup);
+ SubscribeLocalEvent(OnMapInit);
SubscribeLocalEvent(OnMapRemoved);
SubscribeLocalEvent(OnMapHandleState);
SubscribeLocalEvent(OnMapGetState);
}
+ public bool MapExists([NotNullWhen(true)] MapId? mapId)
+ {
+ return mapId != null && Maps.ContainsKey(mapId.Value);
+ }
+
+ public EntityUid GetMap(MapId mapId)
+ {
+ return Maps[mapId];
+ }
+
+ public bool TryGetMap([NotNullWhen(true)] MapId? mapId, [NotNullWhen(true)] out EntityUid? uid)
+ {
+ if (mapId == null || !Maps.TryGetValue(mapId.Value, out var map))
+ {
+ uid = null;
+ return false;
+ }
+
+ uid = map;
+ return true;
+ }
+
private void OnMapHandleState(EntityUid uid, MapComponent component, ref ComponentHandleState args)
{
if (args.Current is not MapComponentState state)
return;
- component.MapId = state.MapId;
-
- if (!MapManager.MapExists(state.MapId))
+ if (component.MapId == MapId.Nullspace)
{
- var mapInternal = (IMapManagerInternal)MapManager;
- mapInternal.CreateMap(state.MapId, uid);
+ if (state.MapId == MapId.Nullspace)
+ throw new Exception($"Received invalid map state? {ToPrettyString(uid)}");
+
+ component.MapId = state.MapId;
+ Maps.Add(component.MapId, uid);
+ RecursiveMapIdUpdate(uid, uid, component.MapId);
}
+ DebugTools.AssertEqual(component.MapId, state.MapId);
component.LightingEnabled = state.LightingEnabled;
- var xformQuery = GetEntityQuery();
+ component.MapInitialized = state.Initialized;
- xformQuery.GetComponent(uid).ChangeMapId(state.MapId, xformQuery);
+ if (LifeStage(uid) >= EntityLifeStage.Initialized)
+ SetPaused(uid, state.MapPaused);
+ else
+ component.MapPaused = state.MapPaused;
+ }
+
+ private void RecursiveMapIdUpdate(EntityUid uid, EntityUid mapUid, MapId mapId)
+ {
+ // This is required only in the event where an entity becomes a map AFTER children have already been attached to it.
+ // AFAIK, this currently only happens when the client applies entity states out of order (i.e., ignoring transform hierarchy),
+ // which itself only happens if PVS is disabled.
+ // TODO MAPS remove this
- MapManager.SetMapPaused(state.MapId, state.MapPaused);
+ var xform = Transform(uid);
+ xform.MapUid = mapUid;
+ xform.MapID = mapId;
+ xform._mapIdInitialized = true;
+ foreach (var child in xform._children)
+ {
+ RecursiveMapIdUpdate(child, mapUid, mapId);
+ }
}
private void OnMapGetState(EntityUid uid, MapComponent component, ref ComponentGetState args)
{
- args.State = new MapComponentState(component.MapId, component.LightingEnabled, component.MapPaused);
+ args.State = new MapComponentState(component.MapId, component.LightingEnabled, component.MapPaused, component.MapInitialized);
}
- protected abstract void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args);
+ protected abstract MapId GetNextMapId();
- private void OnMapInit(EntityUid uid, MapComponent component, ComponentInit args)
+ private void OnComponentAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
+ // ordered startups when
+ EnsureComp(uid);
EnsureComp(uid);
EnsureComp(uid);
+ }
+
+ private void OnCompInit(EntityUid uid, MapComponent component, ComponentInit args)
+ {
+ if (component.MapId == MapId.Nullspace)
+ component.MapId = GetNextMapId();
+
+ DebugTools.AssertEqual(component.MapId.IsClientSide, IsClientSide(uid));
+ if (!Maps.TryAdd(component.MapId, uid))
+ {
+ if (Maps[component.MapId] != uid)
+ throw new Exception($"Attempted to initialize a map {ToPrettyString(uid)} with a duplicate map id {component.MapId}");
+ }
var msg = new MapChangedEvent(uid, component.MapId, true);
RaiseLocalEvent(uid, msg, true);
}
+ private void OnCompStartup(EntityUid uid, MapComponent component, ComponentStartup args)
+ {
+ if (component.MapPaused)
+ RecursiveSetPaused(uid, true);
+ }
+
private void OnMapRemoved(EntityUid uid, MapComponent component, ComponentShutdown args)
{
DebugTools.Assert(component.MapId != MapId.Nullspace);
- Log.Info($"Deleting map {component.MapId}");
-
- var iMap = (IMapManagerInternal)MapManager;
- iMap.RemoveMapId(component.MapId);
+ Maps.Remove(component.MapId);
var msg = new MapChangedEvent(uid, component.MapId, false);
RaiseLocalEvent(uid, msg, true);
}
+
+ ///
+ /// Creates a new map, automatically assigning a map id.
+ ///
+ public EntityUid CreateMap(out MapId mapId, bool runMapInit = true)
+ {
+ mapId = GetNextMapId();
+ var uid = CreateMap(mapId, runMapInit);
+ return uid;
+ }
+
+ ///
+ public EntityUid CreateMap(bool runMapInit = true) => CreateMap(out _, runMapInit);
+
+ ///
+ /// Creates a new map with the specified map id.
+ ///
+ /// Throws if an invalid or already existing map id is provided.
+ public EntityUid CreateMap(MapId mapId, bool runMapInit = true)
+ {
+ if (Maps.ContainsKey(mapId))
+ throw new ArgumentException($"Map with id {mapId} already exists");
+
+ if (mapId == MapId.Nullspace)
+ throw new ArgumentException($"Cannot create a null-space map");
+
+ if (_netManager.IsServer && mapId.IsClientSide)
+ throw new ArgumentException($"Attempted to create a client-side map on the server?");
+
+ if (_netManager.IsClient && _netManager.IsConnected && !mapId.IsClientSide)
+ throw new ArgumentException($"Attempted to create a client-side map entity with a non client-side map ID?");
+
+ var uid = EntityManager.CreateEntityUninitialized(null);
+ var map = _factory.GetComponent();
+ map.MapId = mapId;
+ AddComp(uid, map);
+
+ // Give the entity a name, mainly for debugging. Content can always override this with a localized name.
+ var meta = MetaData(uid);
+ _meta.SetEntityName(uid, $"Map Entity", meta);
+
+ // Initialize components. this should add the map id to the collections.
+ EntityManager.InitializeComponents(uid, meta);
+ EntityManager.StartComponents(uid);
+ DebugTools.Assert(Maps[mapId] == uid);
+
+ if (runMapInit)
+ InitializeMap((uid, map));
+ else
+ SetPaused((uid, map), true);
+
+ return uid;
+ }
}
diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.MapInit.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.MapInit.cs
new file mode 100644
index 00000000000..e7b326e039c
--- /dev/null
+++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.MapInit.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Utility;
+
+namespace Robust.Shared.GameObjects;
+
+public abstract partial class SharedMapSystem
+{
+ public bool IsInitialized(MapId mapId)
+ {
+ if (mapId == MapId.Nullspace)
+ return true; // Nullspace is always initialized
+
+ if(!Maps.TryGetValue(mapId, out var uid))
+ throw new ArgumentException($"Map {mapId} does not exist.");
+
+ return IsInitialized(uid);
+ }
+ public bool IsInitialized(EntityUid? map)
+ {
+ if (map == null)
+ return true; // Nullspace is always initialized
+
+ return IsInitialized(map.Value);
+ }
+
+ public bool IsInitialized(Entity map)
+ {
+ if (!_mapQuery.Resolve(map, ref map.Comp))
+ return false;
+
+ return map.Comp.MapInitialized;
+ }
+
+ private void OnMapInit(EntityUid uid, MapComponent component, MapInitEvent args)
+ {
+ DebugTools.Assert(!component.MapInitialized);
+ component.MapInitialized = true;
+ EntityManager.Dirty(uid, component);
+ }
+
+ public void InitializeMap(MapId mapId, bool unpause = true)
+ {
+ if(!Maps.TryGetValue(mapId, out var uid))
+ throw new ArgumentException($"Map {mapId} does not exist.");
+
+ InitializeMap(uid, unpause);
+ }
+
+ public void InitializeMap(Entity map, bool unpause = true)
+ {
+ if (!_mapQuery.Resolve(map, ref map.Comp))
+ return;
+
+ if (map.Comp.MapInitialized)
+ throw new ArgumentException($"Map {ToPrettyString(map)} is already initialized.");
+
+ RecursiveMapInit(map.Owner);
+
+ if (unpause)
+ SetPaused(map, false);
+ }
+
+ private void RecursiveMapInit(EntityUid entity)
+ {
+ var toInitialize = new List {entity};
+ for (var i = 0; i < toInitialize.Count; i++)
+ {
+ var uid = toInitialize[i];
+ // toInitialize might contain deleted entities.
+ if(!_metaQuery.TryComp(uid, out var meta))
+ continue;
+
+ if (meta.EntityLifeStage == EntityLifeStage.MapInitialized)
+ continue;
+
+ toInitialize.AddRange(Transform(uid)._children);
+ EntityManager.RunMapInit(uid, meta);
+ }
+ }
+}
diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Pause.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Pause.cs
new file mode 100644
index 00000000000..cd62f2c2dfe
--- /dev/null
+++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Pause.cs
@@ -0,0 +1,60 @@
+using System;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+namespace Robust.Shared.GameObjects;
+
+public abstract partial class SharedMapSystem
+{
+ public bool IsPaused(MapId mapId)
+ {
+ if (mapId == MapId.Nullspace)
+ return false;
+
+ if(!Maps.TryGetValue(mapId, out var uid))
+ throw new ArgumentException($"Map {mapId} does not exist.");
+
+ return IsPaused(uid);
+ }
+
+ public bool IsPaused(Entity map)
+ {
+ if (!_mapQuery.Resolve(map, ref map.Comp))
+ return false;
+
+ return map.Comp.MapPaused;
+ }
+
+ public void SetPaused(MapId mapId, bool paused)
+ {
+ if(!Maps.TryGetValue(mapId, out var uid))
+ throw new ArgumentException($"Map {mapId} does not exist.");
+
+ SetPaused(uid, paused);
+ }
+
+ public void SetPaused(Entity map, bool paused)
+ {
+ if (!_mapQuery.Resolve(map, ref map.Comp))
+ return;
+
+ if (map.Comp.MapPaused == paused)
+ return;
+
+ map.Comp.MapPaused = paused;
+ if (map.Comp.LifeStage < ComponentLifeStage.Initializing)
+ return;
+
+ Dirty(map);
+ RecursiveSetPaused(map, paused);
+ }
+
+ private void RecursiveSetPaused(EntityUid entity, bool paused)
+ {
+ _meta.SetEntityPaused(entity, paused);
+ foreach (var child in Transform(entity)._children)
+ {
+ RecursiveSetPaused(child, paused);
+ }
+ }
+}
diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs
index 8045aee7e8b..6267c1ecaa8 100644
--- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs
+++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs
@@ -20,17 +20,23 @@ public abstract partial class SharedMapSystem : EntitySystem
[Dependency] private readonly FixtureSystem _fixtures = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly IComponentFactory _factory = default!;
+ [Dependency] private readonly MetaDataSystem _meta = default!;
private EntityQuery _mapQuery;
private EntityQuery _gridQuery;
+ private EntityQuery _metaQuery;
private EntityQuery _xformQuery;
+ internal Dictionary Maps { get; } = new();
+
public override void Initialize()
{
base.Initialize();
_mapQuery = GetEntityQuery();
_gridQuery = GetEntityQuery();
+ _metaQuery = GetEntityQuery();
_xformQuery = GetEntityQuery();
InitializeMap();
diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs
index 32be8c70fde..2b797ca4ed6 100644
--- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs
+++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs
@@ -199,48 +199,35 @@ public bool IsParentOf(TransformComponent parent, EntityUid child)
#region Component Lifetime
- private void OnCompInit(EntityUid uid, TransformComponent component, ComponentInit args)
+ private (EntityUid?, MapId) InitializeMapUid(EntityUid uid, TransformComponent xform)
{
- // 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 MapComponent every iteration.
- static MapId FindMapIdAndSet(EntityUid uid, TransformComponent xform, IEntityManager entMan, EntityQuery xformQuery, IMapManager mapManager)
- {
- if (xform._mapIdInitialized)
- return xform.MapID;
+ if (xform._mapIdInitialized)
+ return (xform.MapUid, xform.MapID);
- MapId value;
-
- if (xform.ParentUid.IsValid())
- {
- value = FindMapIdAndSet(xform.ParentUid, xformQuery.GetComponent(xform.ParentUid), entMan, xformQuery, mapManager);
- }
- else
- {
- // second level node, terminates recursion up the branch of the tree
- if (entMan.TryGetComponent(uid, out MapComponent? mapComp))
- {
- value = mapComp.MapId;
- }
- else
- {
- // We allow entities to be spawned directly into null-space.
- value = MapId.Nullspace;
- }
- }
-
- xform.MapUid = value == MapId.Nullspace ? null : mapManager.GetMapEntityId(value);
- xform.MapID = value;
- xform._mapIdInitialized = true;
- return value;
+ if (xform.ParentUid.IsValid())
+ {
+ (xform.MapUid, xform.MapID) = InitializeMapUid(xform.ParentUid, Transform(xform.ParentUid));
}
-
- if (!component._mapIdInitialized)
+ else if (_mapQuery.TryComp(uid, out var mapComp))
+ {
+ DebugTools.AssertNotEqual(mapComp.MapId, MapId.Nullspace);
+ xform.MapUid = uid;
+ xform.MapID = mapComp.MapId;
+ }
+ else
{
- FindMapIdAndSet(uid, component, EntityManager, XformQuery, _mapManager);
- component._mapIdInitialized = true;
+ xform.MapUid = null;
+ xform.MapID = MapId.Nullspace;
}
+ xform._mapIdInitialized = true;
+ return (xform.MapUid, xform.MapID);
+ }
+
+ private void OnCompInit(EntityUid uid, TransformComponent component, ComponentInit args)
+ {
+ InitializeMapUid(uid, component);
+
// Has to be done if _parent is set from ExposeData.
if (component.ParentUid.IsValid())
{
@@ -522,6 +509,8 @@ public void SetCoordinates(
throw new InvalidOperationException($"Attempted to re-parent to a terminating object. Entity: {ToPrettyString(uid)}, new parent: {ToPrettyString(value.EntityId)}");
}
+ InitializeMapUid(value.EntityId, newParent);
+
// Check for recursive/circular transform hierarchies.
if (xform.MapUid == newParent.MapUid)
{
@@ -722,10 +711,7 @@ internal void OnHandleState(EntityUid uid, TransformComponent xform, ref Compone
{
if (args.Current is TransformComponentState newState)
{
- var parent = GetEntity(newState.ParentID);
- if (!parent.IsValid() && newState.ParentID.IsValid())
- Log.Error($"Received transform component state with an unknown parent Id. Entity: {ToPrettyString(uid)}. Net parent: {newState.ParentID}");
-
+ var parent = EnsureEntity(newState.ParentID, uid);
var oldAnchored = xform.Anchored;
// update actual position data, if required
@@ -884,6 +870,28 @@ public MapCoordinates GetMapCoordinates(Entity entity)
return GetMapCoordinates(entity.Comp);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetMapCoordinates(EntityUid entity, MapCoordinates coordinates)
+ {
+ var xform = XformQuery.GetComponent(entity);
+ SetMapCoordinates((entity, xform), coordinates);
+ }
+
+ public void SetMapCoordinates(Entity entity, MapCoordinates coordinates)
+ {
+ var mapUid = _map.GetMap(coordinates.MapId);
+ if (!_gridQuery.HasComponent(entity) &&
+ _mapManager.TryFindGridAt(mapUid, coordinates.Position, out var targetGrid, out _))
+ {
+ var invWorldMatrix = GetInvWorldMatrix(targetGrid);
+ SetCoordinates(entity, new EntityCoordinates(targetGrid, invWorldMatrix.Transform(coordinates.Position)));
+ }
+ else
+ {
+ SetCoordinates(entity, new EntityCoordinates(mapUid, coordinates.Position));
+ }
+ }
+
[Pure]
public (Vector2 WorldPosition, Angle WorldRotation) GetWorldPositionRotation(EntityUid uid)
{
@@ -978,36 +986,25 @@ public Vector2 GetRelativePosition(
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPosition(EntityUid uid, Vector2 worldPos)
{
- var xform = Transform(uid);
+ var xform = XformQuery.GetComponent(uid);
SetWorldPosition(xform, worldPos);
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void SetWorldPosition(EntityUid uid, Vector2 worldPos, EntityQuery xformQuery)
- {
- var component = xformQuery.GetComponent(uid);
- SetWorldPosition(component, worldPos, xformQuery);
- }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPosition(TransformComponent component, Vector2 worldPos)
{
- SetWorldPosition(component, worldPos, XformQuery);
+ SetWorldPosition((component.Owner, component), worldPos);
}
+ ///
+ /// Sets the position of the entity in world-terms to the specified position.
+ /// May also de-parent the entity.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void SetWorldPosition(TransformComponent component, Vector2 worldPos, EntityQuery xformQuery)
+ public void SetWorldPosition(Entity entity, Vector2 worldPos)
{
- if (!component._parent.IsValid())
- {
- DebugTools.Assert("Parent is invalid while attempting to set WorldPosition - did you try to move root node?");
- return;
- }
-
- var (curWorldPos, curWorldRot) = GetWorldPositionRotation(component, xformQuery);
- var negativeParentWorldRot = component._localRotation - curWorldRot;
- var newLocalPos = component._localPosition + negativeParentWorldRot.RotateVec(worldPos - curWorldPos);
- SetLocalPosition(component, newLocalPos);
+ SetWorldPositionRotationInternal(entity.Owner, worldPos, null, entity.Comp);
}
#endregion
@@ -1093,24 +1090,35 @@ public void SetWorldPositionRotation(TransformComponent component, Vector2 world
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPositionRotation(EntityUid uid, Vector2 worldPos, Angle worldRot, TransformComponent? component = null)
+ {
+ SetWorldPositionRotationInternal(uid, worldPos, worldRot, component);
+ }
+
+ private void SetWorldPositionRotationInternal(EntityUid uid, Vector2 worldPos, Angle? worldRot = null, TransformComponent? component = null)
{
if (!XformQuery.Resolve(uid, ref component))
return;
- if (!component._parent.IsValid())
+ // If no worldRot supplied then default the new rotation to 0.
+
+ if (!component._parent.IsValid() || component.MapUid == null)
{
DebugTools.Assert("Parent is invalid while attempting to set WorldPosition - did you try to move root node?");
return;
}
- var (curWorldPos, curWorldRot) = GetWorldPositionRotation(component);
-
- var negativeParentWorldRot = component.LocalRotation - curWorldRot;
-
- var newLocalPos = component.LocalPosition + negativeParentWorldRot.RotateVec(worldPos - curWorldPos);
- var newLocalRot = component.LocalRotation + worldRot - curWorldRot;
-
- SetLocalPositionRotation(uid, newLocalPos, newLocalRot, component);
+ if (component.GridUid != uid && _mapManager.TryFindGridAt(component.MapUid.Value, worldPos, out var targetGrid, out _))
+ {
+ var targetGridXform = XformQuery.GetComponent(targetGrid);
+ var invLocalMatrix = targetGridXform.InvLocalMatrix;
+ var gridRot = targetGridXform.LocalRotation;
+ var localRot = worldRot - gridRot;
+ SetCoordinates(uid, component, new EntityCoordinates(targetGrid, invLocalMatrix.Transform(worldPos)), rotation: localRot);
+ }
+ else
+ {
+ SetCoordinates(uid, component, new EntityCoordinates(component.MapUid.Value, worldPos), rotation: worldRot);
+ }
}
[Obsolete("Use override with EntityUid")]
@@ -1463,8 +1471,7 @@ public void DropNextTo(Entity entity, Entity _mapQuery;
private EntityQuery _gridQuery;
private EntityQuery _metaQuery;
protected EntityQuery XformQuery;
@@ -50,6 +51,7 @@ public override void Initialize()
UpdatesOutsidePrediction = true;
+ _mapQuery = GetEntityQuery();
_gridQuery = GetEntityQuery();
_metaQuery = GetEntityQuery();
XformQuery = GetEntityQuery();
@@ -80,7 +82,7 @@ private void MapManagerOnTileChanged(ref TileChangedEvent e)
///
private void DeparentAllEntsOnTile(EntityUid gridId, Vector2i tileIndices)
{
- if (!TryComp(gridId, out BroadphaseComponent? lookup) || !_mapManager.TryGetGrid(gridId, out var grid))
+ if (!TryComp(gridId, out BroadphaseComponent? lookup) || !TryComp(gridId, out var grid))
return;
if (!XformQuery.TryGetComponent(gridId, out var gridXform))
diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs
index 4c5cac1af69..97cf4227200 100644
--- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs
+++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs
@@ -1,79 +1,119 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using Robust.Shared.Enums;
+using System.Linq;
+using System.Runtime.InteropServices;
+using JetBrains.Annotations;
+using Robust.Shared.Collections;
+using Robust.Shared.GameStates;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Network;
using Robust.Shared.Player;
+using Robust.Shared.Reflection;
+using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects;
public abstract class SharedUserInterfaceSystem : EntitySystem
{
- protected readonly Dictionary> OpenInterfaces = new();
+ [Dependency] private readonly IDynamicTypeFactory _factory = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly INetManager _netManager = default!;
+ [Dependency] private readonly IReflectionManager _reflection = default!;
+ [Dependency] private readonly ISharedPlayerManager _player = default!;
+ [Dependency] private readonly SharedTransformSystem _transforms = default!;
+
+ private EntityQuery _ignoreUIRangeQuery;
+ private EntityQuery _xformQuery;
+ private EntityQuery _uiQuery;
+ private EntityQuery _userQuery;
public override void Initialize()
{
base.Initialize();
- SubscribeAllEvent(OnMessageReceived);
- SubscribeLocalEvent(OnUserInterfaceInit);
- SubscribeLocalEvent(OnUserInterfaceShutdown);
- }
- private void OnUserInterfaceInit(EntityUid uid, UserInterfaceComponent component, ComponentInit args)
- {
- component.Interfaces.Clear();
+ _ignoreUIRangeQuery = GetEntityQuery();
+ _xformQuery = GetEntityQuery();
+ _uiQuery = GetEntityQuery();
+ _userQuery = GetEntityQuery();
- foreach (var prototypeData in component.InterfaceData)
+ SubscribeAllEvent((msg, args) =>
{
- component.Interfaces[prototypeData.UiKey] = new PlayerBoundUserInterface(prototypeData, uid);
- component.MappedInterfaceData[prototypeData.UiKey] = prototypeData;
- }
+ if (args.SenderSession.AttachedEntity is not { } player)
+ return;
+
+ OnMessageReceived(msg, player);
+ });
+
+ SubscribeLocalEvent(OnUserInterfaceOpen);
+ SubscribeLocalEvent(OnUserInterfaceClosed);
+ SubscribeLocalEvent(OnUserInterfaceShutdown);
+ SubscribeLocalEvent(OnUserInterfaceGetState);
+ SubscribeLocalEvent(OnUserInterfaceHandleState);
+
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ SubscribeLocalEvent(OnGetStateAttempt);
+ SubscribeLocalEvent(OnActorGetState);
+ SubscribeLocalEvent(OnActorHandleState);
+
+ _player.PlayerStatusChanged += OnStatusChange;
}
- private void OnUserInterfaceShutdown(EntityUid uid, UserInterfaceComponent component, ComponentShutdown args)
+ private void OnStatusChange(object? sender, SessionStatusEventArgs e)
{
- if (!TryComp(uid, out ActiveUserInterfaceComponent? activeUis))
+ var attachedEnt = e.Session.AttachedEntity;
+
+ if (attachedEnt == null)
return;
- foreach (var bui in activeUis.Interfaces)
- {
- DeactivateInterface(uid, bui, activeUis);
- }
+ // Content can't handle it yet sadly :(
+ CloseUserUis(attachedEnt.Value);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _player.PlayerStatusChanged -= OnStatusChange;
}
///
- /// Validates the received message, and then pass it onto systems/components
+ /// Validates the received message, and then pass it onto systems/components
///
- internal void OnMessageReceived(BaseBoundUIWrapMessage msg, EntitySessionEventArgs args)
+ private void OnMessageReceived(BoundUIWrapMessage msg, EntityUid sender)
{
- var uid = GetEntity(msg.Entity);
+ // This is more or less the main BUI method that handles all messages.
- if (!TryComp(uid, out UserInterfaceComponent? uiComp) || args.SenderSession is not { } session)
- return;
+ var uid = GetEntity(msg.Entity);
- if (!uiComp.Interfaces.TryGetValue(msg.UiKey, out var ui))
+ if (!_uiQuery.TryComp(uid, out var uiComp))
{
- Log.Debug($"Got BoundInterfaceMessageWrapMessage for unknown UI key: {msg.UiKey}");
return;
}
- if (!ui.SubscribedSessions.Contains(session))
+ if (!uiComp.Interfaces.TryGetValue(msg.UiKey, out var ui))
{
- Log.Debug($"UI {msg.UiKey} got BoundInterfaceMessageWrapMessage from a client who was not subscribed: {session}");
+ Log.Debug($"Got BoundInterfaceMessageWrapMessage for unknown UI key: {msg.UiKey}");
return;
}
- // if they want to close the UI, we can go home early.
- if (msg.Message is CloseBoundInterfaceMessage)
+ // If it's not an open message check we're even a subscriber.
+ if (msg.Message is not OpenBoundInterfaceMessage &&
+ (!uiComp.Actors.TryGetValue(msg.UiKey, out var actors) ||
+ !actors.Contains(sender)))
{
- CloseShared(ui, session);
+ Log.Debug($"UI {msg.UiKey} got BoundInterfaceMessageWrapMessage from a client who was not subscribed: {ToPrettyString(sender)}");
return;
}
// verify that the user is allowed to press buttons on this UI:
- if (ui.RequireInputValidation)
+ // If it's a close message something else might try to cancel it but we want to force it.
+ if (msg.Message is not CloseBoundInterfaceMessage && ui.RequireInputValidation)
{
- var attempt = new BoundUserInterfaceMessageAttempt(args.SenderSession, uid, msg.UiKey);
+ var attempt = new BoundUserInterfaceMessageAttempt(sender, uid, msg.UiKey);
RaiseLocalEvent(attempt);
if (attempt.Cancelled)
return;
@@ -81,159 +121,892 @@ internal void OnMessageReceived(BaseBoundUIWrapMessage msg, EntitySessionEventAr
// get the wrapped message and populate it with the sender & UI key information.
var message = msg.Message;
- message.Session = args.SenderSession;
+ message.Actor = sender;
message.Entity = msg.Entity;
message.UiKey = msg.UiKey;
+ if (uiComp.ClientOpenInterfaces.TryGetValue(msg.UiKey, out var cBui))
+ {
+ cBui.ReceiveMessage(message);
+ }
+
// Raise as object so the correct type is used.
RaiseLocalEvent(uid, (object)message, true);
}
- protected void DeactivateInterface(EntityUid entityUid, PlayerBoundUserInterface ui,
- ActiveUserInterfaceComponent? activeUis = null)
+ #region User
+
+ private void OnGetStateAttempt(Entity ent, ref ComponentGetStateAttemptEvent args)
{
- if (!Resolve(entityUid, ref activeUis, false))
+ if (args.Cancelled || args.Player?.AttachedEntity != ent.Owner)
+ args.Cancelled = true;
+ }
+
+ private void OnActorGetState(Entity ent, ref ComponentGetState args)
+ {
+ var interfaces = new Dictionary>();
+
+ foreach (var (buid, data) in ent.Comp.OpenInterfaces)
+ {
+ interfaces[GetNetEntity(buid)] = data;
+ }
+
+ args.State = new UserInterfaceUserComponentState()
+ {
+ OpenInterfaces = interfaces,
+ };
+ }
+
+ private void OnActorHandleState(Entity ent, ref ComponentHandleState args)
+ {
+ if (args.Current is not UserInterfaceUserComponentState state)
return;
- activeUis.Interfaces.Remove(ui);
- if (activeUis.Interfaces.Count == 0)
- RemCompDeferred(entityUid, activeUis);
+ // TODO: Allocate less.
+ ent.Comp.OpenInterfaces.Clear();
+
+ foreach (var (nent, data) in state.OpenInterfaces)
+ {
+ var openEnt = EnsureEntity(nent, ent.Owner);
+ ent.Comp.OpenInterfaces[openEnt] = data;
+ }
}
- protected virtual void CloseShared(PlayerBoundUserInterface bui, ICommonSession session,
- ActiveUserInterfaceComponent? activeUis = null)
+ #endregion
+
+ private void OnPlayerAttached(PlayerAttachedEvent ev)
{
+ if (!_userQuery.TryGetComponent(ev.Entity, out var actor))
+ return;
+
+ // Open BUIs upon attachment
+ foreach (var (uid, keys) in actor.OpenInterfaces)
+ {
+ if (!_uiQuery.TryGetComponent(uid, out var uiComp))
+ continue;
+
+ foreach (var key in keys)
+ {
+ if (!uiComp.Interfaces.TryGetValue(key, out var data))
+ continue;
+
+ EnsureClientBui((uid, uiComp), key, data);
+ }
+ }
}
- public bool TryGetUi(EntityUid uid, Enum uiKey, [NotNullWhen(true)] out PlayerBoundUserInterface? bui, UserInterfaceComponent? ui = null)
+ private void OnPlayerDetached(PlayerDetachedEvent ev)
{
- bui = null;
+ if (!_userQuery.TryGetComponent(ev.Entity, out var actor))
+ return;
+
+ // Close BUIs open detachment.
+ foreach (var (uid, keys) in actor.OpenInterfaces)
+ {
+ if (!_uiQuery.TryGetComponent(uid, out var uiComp))
+ continue;
+
+ foreach (var key in keys)
+ {
+ if (!uiComp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
+ continue;
+
+ cBui.Dispose();
+ uiComp.ClientOpenInterfaces.Remove(key);
+ }
+ }
+ }
+
+ private void OnUserInterfaceClosed(Entity ent, ref CloseBoundInterfaceMessage args)
+ {
+ // This handles all of the actually closing BUI.
+ // This is because CloseUi just relays the event so client sending message to server vs just server
+ // go through the same path.
+
+ var actor = args.Actor;
+
+ var actors = ent.Comp.Actors[args.UiKey];
+ actors.Remove(actor);
+
+ if (actors.Count == 0)
+ ent.Comp.Actors.Remove(args.UiKey);
+
+ Dirty(ent);
+
+ // If the actor is also deleting then don't worry about updating what they have open.
+ if (!TerminatingOrDeleted(actor))
+ {
+ var actorComp = EnsureComp(actor);
+
+ if (actorComp.OpenInterfaces.TryGetValue(ent.Owner, out var keys))
+ {
+ keys.Remove(args.UiKey);
+
+ if (keys.Count == 0)
+ actorComp.OpenInterfaces.Remove(ent.Owner);
+
+ Dirty(actor, actorComp);
+ }
+ }
+
+ // If we're client we want this handled immediately.
+ if (ent.Comp.ClientOpenInterfaces.Remove(args.UiKey, out var cBui))
+ {
+ cBui.Dispose();
+ }
+
+ if (ent.Comp.Actors.Count == 0)
+ RemCompDeferred(ent.Owner);
+
+ var ev = new BoundUIClosedEvent(args.UiKey, ent.Owner, args.Actor);
+ RaiseLocalEvent(ent.Owner, ev);
+ }
+
+ private void OnUserInterfaceOpen(Entity ent, ref OpenBoundInterfaceMessage args)
+ {
+ // Similar to the close method this handles actually opening a UI, it just gets relayed here
+ EnsureComp(ent.Owner);
+
+ var actor = args.Actor;
+ var actorComp = EnsureComp(actor);
+
+ // Let state handling open the UI clientside.
+ actorComp.OpenInterfaces.GetOrNew(ent.Owner).Add(args.UiKey);
+ ent.Comp.Actors.GetOrNew(args.UiKey).Add(actor);
+ Dirty(ent);
+ Dirty(actor, actorComp);
+
+ var ev = new BoundUIOpenedEvent(args.UiKey, ent.Owner, args.Actor);
+ RaiseLocalEvent(ent.Owner, ev);
+
+ // If we're client we want this handled immediately.
+ EnsureClientBui(ent, args.UiKey, ent.Comp.Interfaces[args.UiKey]);
+ }
+
+ private void OnUserInterfaceShutdown(EntityUid uid, UserInterfaceComponent component, ComponentShutdown args)
+ {
+ foreach (var bui in component.ClientOpenInterfaces.Values)
+ {
+ bui.Dispose();
+ }
+
+ component.ClientOpenInterfaces.Clear();
+ }
+
+ private void OnUserInterfaceGetState(Entity ent, ref ComponentGetState args)
+ {
+ var actors = new Dictionary>();
+ var states = new Dictionary();
+
+ foreach (var (key, acts) in ent.Comp.Actors)
+ {
+ actors[key] = GetNetEntityList(acts);
+ }
+
+ foreach (var (key, state) in ent.Comp.States)
+ {
+ states[key] = state;
+ }
+
+ args.State = new UserInterfaceComponent.UserInterfaceComponentState(actors, states);
+ }
+
+ private void OnUserInterfaceHandleState(Entity ent, ref ComponentHandleState args)
+ {
+ if (args.Current is not UserInterfaceComponent.UserInterfaceComponentState state)
+ return;
+
+ var toRemove = new ValueList();
+
+ foreach (var (key, actors) in state.Actors)
+ {
+ ref var existing = ref CollectionsMarshal.GetValueRefOrAddDefault(ent.Comp.Actors, key, out _);
+
+ existing ??= new List();
+
+ existing.Clear();
+ existing.AddRange(EnsureEntityList(actors, ent.Owner));
+ }
+
+ foreach (var key in ent.Comp.Actors.Keys)
+ {
+ if (state.Actors.ContainsKey(key))
+ continue;
+
+ toRemove.Add(key);
+ }
+
+ foreach (var key in toRemove)
+ {
+ ent.Comp.Actors.Remove(key);
+ }
+
+ toRemove.Clear();
+
+ // State handling
+ foreach (var key in ent.Comp.States.Keys)
+ {
+ if (state.States.ContainsKey(key))
+ continue;
+
+ toRemove.Add(key);
+ }
+
+ foreach (var key in toRemove)
+ {
+ ent.Comp.States.Remove(key);
+ }
+
+ toRemove.Clear();
+
+ // Check if the UI is still open, otherwise call close.
+ foreach (var (key, bui) in ent.Comp.ClientOpenInterfaces)
+ {
+ if (ent.Comp.Actors.ContainsKey(key))
+ continue;
+
+ bui.Dispose();
+ toRemove.Add(key);
+ }
+
+ foreach (var key in toRemove)
+ {
+ ent.Comp.ClientOpenInterfaces.Remove(key);
+ }
+
+ // update any states we have open
+ foreach (var (key, buiState) in state.States)
+ {
+ if (ent.Comp.States.TryGetValue(key, out var existing) &&
+ existing.Equals(buiState))
+ {
+ continue;
+ }
+
+ ent.Comp.States[key] = buiState;
+
+ if (!ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
+ continue;
+
+ cBui.UpdateState(buiState);
+ }
+
+ // If UI not open then open it
+ var attachedEnt = _player.LocalEntity;
+
+ if (attachedEnt != null)
+ {
+ foreach (var (key, value) in ent.Comp.Interfaces)
+ {
+ EnsureClientBui(ent, key, value);
+ }
+ }
+ }
+
+ ///
+ /// Opens a client's BUI if not already open and applies the state to it.
+ ///
+ private void EnsureClientBui(Entity entity, Enum key, InterfaceData data)
+ {
+ // If it's out BUI open it up and apply the state, otherwise do nothing.
+ var player = _player.LocalEntity;
+
+ if (player == null ||
+ !entity.Comp.Actors.TryGetValue(key, out var actors) ||
+ !actors.Contains(player.Value))
+ {
+ return;
+ }
+
+ DebugTools.Assert(_netManager.IsClient);
+
+ if (entity.Comp.ClientOpenInterfaces.ContainsKey(key))
+ {
+ return;
+ }
+
+ var type = _reflection.LooseGetType(data.ClientType);
+ var boundUserInterface = (BoundUserInterface) _factory.CreateInstance(type, [entity.Owner, key]);
+
+ entity.Comp.ClientOpenInterfaces[key] = boundUserInterface;
+ boundUserInterface.Open();
+
+ if (entity.Comp.States.TryGetValue(key, out var buiState))
+ {
+ boundUserInterface.UpdateState(buiState);
+ }
+ }
+
+ ///
+ /// Yields all the entities + keys currently open by this entity.
+ ///
+ public IEnumerable<(EntityUid Entity, Enum Key)> GetActorUis(Entity entity)
+ {
+ if (!_userQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ yield break;
+
+ foreach (var berry in entity.Comp.OpenInterfaces)
+ {
+ foreach (var key in berry.Value)
+ {
+ yield return (berry.Key, key);
+ }
+ }
+ }
+
+ ///
+ /// Gets the actors that have the specified key attached to this entity open.
+ ///
+ public IEnumerable GetActors(Entity entity, Enum key)
+ {
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false) || !entity.Comp.Actors.TryGetValue(key, out var actors))
+ yield break;
+
+ foreach (var actorUid in actors)
+ {
+ yield return actorUid;
+ }
+ }
+
+ ///
+ /// Closes the attached UI for all entities.
+ ///
+ public void CloseUi(Entity entity, Enum key)
+ {
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return;
+
+ if (!entity.Comp.Actors.TryGetValue(key, out var actors))
+ return;
+
+ for (var i = actors.Count - 1; i >= 0; i--)
+ {
+ var actor = actors[i];
+ CloseUi(entity, key, actor);
+ }
+
+ DebugTools.Assert(actors.Count == 0);
+ }
+
+ ///
+ /// Closes the attached UI only for the specified actor.
+ ///
+ public void CloseUi(Entity entity, Enum key, ICommonSession? actor, bool predicted = false)
+ {
+ var actorEnt = actor?.AttachedEntity;
+
+ if (actorEnt == null)
+ return;
+
+ CloseUi(entity, key, actorEnt.Value, predicted);
+ }
+
+ ///
+ /// Closes the attached Ui only for the specified actor.
+ ///
+ public void CloseUi(Entity entity, Enum key, EntityUid? actor, bool predicted = false)
+ {
+ if (actor == null)
+ return;
- return Resolve(uid, ref ui, false) && ui.Interfaces.TryGetValue(uiKey, out bui);
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return;
+
+ // Short-circuit if no UI.
+ if (!entity.Comp.Interfaces.ContainsKey(key))
+ return;
+
+ if (!entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(actor.Value))
+ return;
+
+ // Rely upon the client telling us.
+ if (predicted)
+ {
+ if (_timing.IsFirstTimePredicted)
+ {
+ // Not guaranteed to open so rely upon the event handling it.
+ // Also lets client request it to be opened remotely too.
+ EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key));
+ }
+ }
+ else
+ {
+ OnMessageReceived(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key), actor.Value);
+ }
}
///
- /// Switches between closed and open for a specific client.
+ /// Tries to call OpenUi and return false if it isn't open.
///
- public virtual bool TryToggleUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
+ public bool TryOpenUi(Entity entity, Enum key, EntityUid actor, bool predicted = false)
{
- if (!TryGetUi(uid, uiKey, out var bui, ui))
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return false;
+
+ OpenUi(entity, key, actor, predicted);
+
+ // Due to the event actually handling the UI open / closed we can't
+ if (!entity.Comp.Actors.TryGetValue(key, out var actors) ||
+ !actors.Contains(actor))
+ {
return false;
+ }
- ToggleUi(bui, session);
return true;
}
+ public void OpenUi(Entity entity, Enum key, EntityUid? actor, bool predicted = false)
+ {
+ if (actor == null || !_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return;
+
+ // No implementation for that UI key on this ent so short-circuit.
+ if (!entity.Comp.Interfaces.ContainsKey(key))
+ return;
+
+ if (entity.Comp.Actors.TryGetValue(key, out var actors) && actors.Contains(actor.Value))
+ return;
+
+ if (predicted)
+ {
+ if (_timing.IsFirstTimePredicted)
+ {
+ // Not guaranteed to open so rely upon the event handling it.
+ // Also lets client request it to be opened remotely too.
+ EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key));
+ }
+ }
+ else
+ {
+ OnMessageReceived(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key), actor.Value);
+ }
+ }
+
+ public void OpenUi(Entity entity, Enum key, ICommonSession actor, bool predicted = false)
+ {
+ var actorEnt = actor.AttachedEntity;
+
+ if (actorEnt == null)
+ return;
+
+ OpenUi(entity, key, actorEnt.Value, predicted);
+ }
+
///
- /// Switches between closed and open for a specific client.
+ /// Sets a BUI state and networks it to all clients.
///
- public void ToggleUi(PlayerBoundUserInterface bui, ICommonSession session)
+ public void SetUiState(Entity entity, Enum key, BoundUserInterfaceState? state)
{
- if (bui._subscribedSessions.Contains(session))
- CloseUi(bui, session);
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return;
+
+ if (!entity.Comp.Interfaces.ContainsKey(key))
+ return;
+
+ // Null state
+ if (state == null)
+ {
+ if (!entity.Comp.States.Remove(key))
+ return;
+
+ Dirty(entity);
+ }
+ // Non-null state, check if it matches existing.
else
- OpenUi(bui, session);
+ {
+ ref var stateRef = ref CollectionsMarshal.GetValueRefOrAddDefault(entity.Comp.States, key, out var exists);
+
+ if (exists && stateRef?.Equals(state) == true)
+ return;
+
+ stateRef = state;
+ }
+
+ Dirty(entity);
}
- public bool TryOpen(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
+ ///
+ /// Returns true if this entity has the specified Ui key available, even if not currently open.
+ ///
+ public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
{
- if (!TryGetUi(uid, uiKey, out var bui, ui))
+ if (!Resolve(uid, ref ui, false))
return false;
- return OpenUi(bui, session);
+ return ui.Interfaces.ContainsKey(uiKey);
}
///
- /// Opens this interface for a specific client.
+ /// Returns true if the specified UI key is open for this entity by anyone.
///
- public bool OpenUi(PlayerBoundUserInterface bui, ICommonSession session)
+ public bool IsUiOpen(Entity entity, Enum uiKey)
{
- if (session.Status == SessionStatus.Connecting || session.Status == SessionStatus.Disconnected)
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
return false;
- if (!bui._subscribedSessions.Add(session))
+ if (!entity.Comp.Actors.TryGetValue(uiKey, out var actors))
return false;
- OpenInterfaces.GetOrNew(session).Add(bui);
- RaiseLocalEvent(bui.Owner, new BoundUIOpenedEvent(bui.UiKey, bui.Owner, session));
- if (!bui._subscribedSessions.Contains(session))
- {
- // This can happen if Content closed a BUI from inside the event handler.
- // This will already have caused a redundant close event to be sent to the client, but whatever.
- // Just avoid doing the rest to avoid any state corruption shit.
+ DebugTools.Assert(actors.Count > 0);
+ return actors.Count > 0;
+ }
+
+ public bool IsUiOpen(Entity entity, Enum uiKey, EntityUid actor)
+ {
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return false;
+
+ if (!entity.Comp.Actors.TryGetValue(uiKey, out var actors))
return false;
+
+ return actors.Contains(actor);
+ }
+
+ ///
+ /// Raises a BUI message locally (on client or server) without networking it.
+ ///
+ [PublicAPI]
+ public void RaiseUiMessage(Entity entity, Enum key, BoundUserInterfaceMessage message)
+ {
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return;
+
+ if (!entity.Comp.Actors.TryGetValue(key, out var actors))
+ return;
+
+ OnMessageReceived(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), message.Actor);
+ }
+
+ #region Server messages
+
+ ///
+ /// Sends a BUI message to any actors who have the specified Ui key open.
+ ///
+ public void ServerSendUiMessage(Entity entity, Enum key, BoundUserInterfaceMessage message)
+ {
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return;
+
+ if (!entity.Comp.Actors.TryGetValue(key, out var actors))
+ return;
+
+ var filter = Filter.Entities(actors.ToArray());
+ RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), filter);
+ }
+
+ ///
+ /// Sends a Bui message to the specified actor only.
+ ///
+ public void ServerSendUiMessage(Entity entity, Enum key, BoundUserInterfaceMessage message, EntityUid actor)
+ {
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return;
+
+ if (!entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(actor))
+ return;
+
+ RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), actor);
+ }
+
+ ///
+ /// Sends a Bui message to the specified actor only.
+ ///
+ public void ServerSendUiMessage(Entity entity, Enum key, BoundUserInterfaceMessage message, ICommonSession actor)
+ {
+ if (!_netManager.IsClient)
+ return;
+
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false) || actor.AttachedEntity is not { } attachedEntity)
+ return;
+
+ if (!entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(attachedEntity))
+ return;
+
+ RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), actor);
+ }
+
+ #endregion
+
+ ///
+ /// Raises a BUI message from the client to the server.
+ ///
+ public void ClientSendUiMessage(Entity entity, Enum key, BoundUserInterfaceMessage message)
+ {
+ var player = _player.LocalEntity;
+
+ // Don't send it if we're not a valid actor for it just in case.
+ if (player == null ||
+ !_uiQuery.Resolve(entity.Owner, ref entity.Comp, false) ||
+ !entity.Comp.Actors.TryGetValue(key, out var actors) ||
+ !actors.Contains(player.Value))
+ {
+ return;
}
- RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), new OpenBoundInterfaceMessage(), bui.UiKey), session.Channel);
+ RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key));
+ }
- // Fun fact, clients needs to have BUIs open before they can receive the state.....
- if (bui.LastStateMsg != null)
- RaiseNetworkEvent(bui.LastStateMsg, session.Channel);
+ ///
+ /// Closes all Uis for the actor.
+ ///
+ public void CloseUserUis(Entity actor)
+ {
+ if (!_userQuery.Resolve(actor.Owner, ref actor.Comp, false))
+ return;
- ActivateInterface(bui);
- return true;
+ if (actor.Comp.OpenInterfaces.Count == 0)
+ return;
+
+ var copied = new Dictionary>(actor.Comp.OpenInterfaces);
+ var enumCopy = new ValueList();
+
+ foreach (var (uid, enums) in copied)
+ {
+ enumCopy.Clear();
+ enumCopy.AddRange(enums);
+
+ foreach (var key in enumCopy)
+ {
+ CloseUi(uid, key, actor.Owner);
+ }
+ }
}
- private void ActivateInterface(PlayerBoundUserInterface ui)
+ ///
+ /// Closes all Uis for the entity.
+ ///
+ public void CloseUis(Entity entity)
{
- EnsureComp(ui.Owner).Interfaces.Add(ui);
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return;
+
+ entity.Comp.Actors.Clear();
+ entity.Comp.States.Clear();
+ Dirty(entity);
}
- internal bool TryCloseUi(ICommonSession? session, EntityUid uid, Enum uiKey, bool remoteCall = false, UserInterfaceComponent? uiComp = null)
+ ///
+ /// Closes all Uis for the entity that the specified actor has open.
+ ///
+ public void CloseUis(Entity entity, EntityUid actor)
{
- if (!Resolve(uid, ref uiComp))
- return false;
+ if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return;
- if (!uiComp.OpenInterfaces.TryGetValue(uiKey, out var boundUserInterface))
- return false;
+ foreach (var key in entity.Comp.Interfaces.Keys)
+ {
+ CloseUi(entity, key, actor);
+ }
+ }
- if (!remoteCall)
- SendUiMessage(boundUserInterface, new CloseBoundInterfaceMessage());
+ ///
+ /// Closes all Uis for the entity that the specified actor has open.
+ ///
+ public void CloseUis(Entity entity, ICommonSession actor)
+ {
+ if (actor.AttachedEntity is not { } attachedEnt || !_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
+ return;
- uiComp.OpenInterfaces.Remove(uiKey);
- boundUserInterface.Dispose();
+ CloseUis(entity, attachedEnt);
+ }
- if (session != null)
- RaiseLocalEvent(uid, new BoundUIClosedEvent(uiKey, uid, session), true);
+ ///
+ /// Tries to get the BUI if it is currently open.
+ ///
+ public bool TryGetOpenUi(Entity entity, Enum uiKey, [NotNullWhen(true)] out BoundUserInterface? bui)
+ {
+ bui = null;
+ return _uiQuery.Resolve(entity.Owner, ref entity.Comp, false) && entity.Comp.ClientOpenInterfaces.TryGetValue(uiKey, out bui);
+ }
+
+ ///
+ /// Tries to get the BUI if it is currently open.
+ ///
+ public bool TryGetOpenUi(Entity