mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
111 Commits
v215.2.0
...
fix/bui-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
318548a4d6 | ||
|
|
dde4dd06f6 | ||
|
|
7cb3aeccc2 | ||
|
|
ae83e606d6 | ||
|
|
d9d5ef7471 | ||
|
|
0f97f366a6 | ||
|
|
35ab0b8cc8 | ||
|
|
5a14e939bf | ||
|
|
ccba6b5d1c | ||
|
|
254a5987c7 | ||
|
|
8550056e68 | ||
|
|
25211e3781 | ||
|
|
3500abfd47 | ||
|
|
7d1915096a | ||
|
|
4504731588 | ||
|
|
701fa95a82 | ||
|
|
40a9048704 | ||
|
|
cee8d42776 | ||
|
|
3330d96177 | ||
|
|
4033d96327 | ||
|
|
6e0205d1a8 | ||
|
|
7cd95351c3 | ||
|
|
2a102f048f | ||
|
|
16bab1bc03 | ||
|
|
123d0ae6ac | ||
|
|
d72de032fa | ||
|
|
0fdba836ee | ||
|
|
eb63809999 | ||
|
|
4c3c74865c | ||
|
|
b624f5b70f | ||
|
|
6566a7658a | ||
|
|
9e3e1cc929 | ||
|
|
4e87d93009 | ||
|
|
1031ae4cc5 | ||
|
|
73da147b88 | ||
|
|
0ab59d70b1 | ||
|
|
8e8470ac7e | ||
|
|
15f94bd094 | ||
|
|
68888c4370 | ||
|
|
19f87dfbb3 | ||
|
|
68e5b6924d | ||
|
|
9f913cd2d9 | ||
|
|
ec37d1c137 | ||
|
|
ea58924495 | ||
|
|
c5aa735506 | ||
|
|
f5a6e52c7f | ||
|
|
d5c4981648 | ||
|
|
8c6170661d | ||
|
|
1901059755 | ||
|
|
8cdec92be6 | ||
|
|
0a00e7ec29 | ||
|
|
8c25a83066 | ||
|
|
0fadfc2d9b | ||
|
|
a6e7224672 | ||
|
|
37796f4806 | ||
|
|
a5494d1df2 | ||
|
|
e2525a2103 | ||
|
|
b50f68866f | ||
|
|
03a4d3e0a0 | ||
|
|
af8fb52a4f | ||
|
|
fd60dc2887 | ||
|
|
cd24fd46b6 | ||
|
|
44cc7127fa | ||
|
|
af36d24892 | ||
|
|
caa8ff0f2d | ||
|
|
cd67c67a5c | ||
|
|
57b328e8c2 | ||
|
|
4874b1db68 | ||
|
|
814ad08884 | ||
|
|
8c4deb2067 | ||
|
|
f6a5120e56 | ||
|
|
c1b8bf8e52 | ||
|
|
4b193bad26 | ||
|
|
8db3da4852 | ||
|
|
0c271fc2f8 | ||
|
|
ed406c06b7 | ||
|
|
b31940b489 | ||
|
|
84360c653d | ||
|
|
6764ed56b0 | ||
|
|
2b55d39e51 | ||
|
|
fdc1de2430 | ||
|
|
99c5b0ad08 | ||
|
|
9e2ab2a917 | ||
|
|
36eb857b55 | ||
|
|
92d7f2723a | ||
|
|
91d3f67a94 | ||
|
|
b28b5ed09b | ||
|
|
30eed7957f | ||
|
|
73c1449811 | ||
|
|
958b5dd06d | ||
|
|
4002cbddb9 | ||
|
|
c38a14e78f | ||
|
|
5164d99996 | ||
|
|
c79217ab66 | ||
|
|
9ef7f7cb37 | ||
|
|
02ac314b1a | ||
|
|
8607ba1f16 | ||
|
|
2a9de462d5 | ||
|
|
c59ef5ab2d | ||
|
|
eba1d866fb | ||
|
|
ab2bff8f40 | ||
|
|
536fca4115 | ||
|
|
7a0d02463c | ||
|
|
6df53d60ed | ||
|
|
ff38e9f12a | ||
|
|
00c58c76a8 | ||
|
|
0bf99e173c | ||
|
|
eee771c5f1 | ||
|
|
2946cd866c | ||
|
|
9a2a3d658d | ||
|
|
d933f03a54 |
7
.github/workflows/build-test.yml
vendored
7
.github/workflows/build-test.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
os: [ubuntu-latest, windows-latest ] # , macos-latest] - temporarily disabled due to libfreetype.dll errors.
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
297
RELEASE-NOTES.md
297
RELEASE-NOTES.md
@@ -1,4 +1,4 @@
|
||||
# Release notes for RobustToolbox.
|
||||
# Release notes for RobustToolbox.
|
||||
|
||||
<!--
|
||||
NOTE: automatically updated sometimes by version.py.
|
||||
@@ -54,6 +54,301 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 221.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add SetMapAudio helper to SharedAudioSystem to setup map-wide audio entities.
|
||||
* Add SetWorldRotNoLerp method to SharedTransformSystem to avoid client lerping.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* `SpriteComponent.CopyFrom` now copies `CopyToShaderParameters` configuration.
|
||||
|
||||
|
||||
## 221.1.0
|
||||
|
||||
|
||||
## 221.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `EntParentChangedMessage.OldMapId` is now an `EntityUid` instead of `MapId`
|
||||
* `TransformSystem.DetachParentToNull()` is being renamed to `DetachEntity`
|
||||
* The order in which `MoveEvent` handlers are invoked has been changed to prioritise engine subscriptions
|
||||
|
||||
### New features
|
||||
|
||||
* Added `UpdateHovered()` and `SetHovered()` to `IUserInterfaceManager`, for updating or modifying the currently hovered control.
|
||||
* Add SwapPositions to TransformSystem to swap two entity's transforms.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Improve client gamestate exception tolerance.
|
||||
|
||||
### Other
|
||||
|
||||
* If the currently hovered control is disposed, `UserInterfaceManager` will now look for a new control, rather than just setting the hovered control to null.
|
||||
|
||||
### Internal
|
||||
|
||||
* Use more `EntityQuery<T>` internally in EntityManager and PhysicsSystem.
|
||||
|
||||
|
||||
## 220.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* RSIs can now specify load parameters, mimicking the ones from `.png.yml`. Currently only disabling sRGB is supported.
|
||||
* Added a second UV channel to Clyde's vertex format. On regular batched sprite draws, this goes 0 -> 1 across the sprite quad.
|
||||
* Added a new `CopyToShaderParameters` system for `SpriteComponent` layers.
|
||||
|
||||
|
||||
## 220.1.0
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix client-side replay exceptions due to dropped states when recording.
|
||||
|
||||
### Other
|
||||
|
||||
* Remove IP + HWId from ViewVariables.
|
||||
* Close BUIs upon disconnect.
|
||||
|
||||
|
||||
## 220.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Refactor UserInterfaceSystem.
|
||||
- The API has been significantly cleaned up and standardised, most noticeably callers don't need to worry about TryGetUi and can rely on either HasUi, SetUiState, CloseUi, or OpenUi to handle their code as appropriate.
|
||||
- Interface data is now stored via key rather than as a flat list which is a breaking change for YAML.
|
||||
- BoundUserInterfaces can now be completely handled via Shared code. Existing Server-side callers will behave similarly to before.
|
||||
- BoundUserInterfaces now properly close in many more situations, additionally they are now attached to the entity so reconnecting can re-open them and they can be serialized properly.
|
||||
|
||||
|
||||
## 219.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add SetMapCoordinates to TransformSystem.
|
||||
* Improve YAML Linter and validation of static fields.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix DebugCoordsPanel freezing when hovering a control.
|
||||
|
||||
### Other
|
||||
|
||||
* Optimise physics networking to not dirty every tick of movement.
|
||||
|
||||
|
||||
## 219.1.3
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix map-loader not pausing pre-init maps when not actively overwriting an existing map.
|
||||
|
||||
|
||||
## 219.1.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix map-loader not map-initialising grids when loading into a post-init map.
|
||||
|
||||
|
||||
## 219.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix map-loader not map-initialising maps when overwriting a post-init map.
|
||||
|
||||
|
||||
## 219.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added a new optional arguments to various entity spawning methods, including a new argument to set the entity's rotation.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixes map initialisation not always initialising all entities on a map.
|
||||
|
||||
### Other
|
||||
|
||||
* The default value of the `auth.mode` cvar has changed
|
||||
|
||||
|
||||
## 219.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* 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<T>(Span<T>, 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<T>` 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
vv-sound-none = None
|
||||
vv-sound-path = Path
|
||||
vv-sound-collection = Collection
|
||||
@@ -20,11 +20,16 @@ public sealed class AccessAnalyzer_Test
|
||||
{
|
||||
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);
|
||||
|
||||
|
||||
58
Robust.Analyzers.Tests/DependencyAssignAnalyzerTest.cs
Normal file
58
Robust.Analyzers.Tests/DependencyAssignAnalyzerTest.cs
Normal file
@@ -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<Robust.Analyzers.DependencyAssignAnalyzer>;
|
||||
|
||||
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<DependencyAssignAnalyzer, NUnitVerifier>()
|
||||
{
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,13 @@
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets"/>
|
||||
<Import Project="..\MSBuild\Robust.Engine.props"/>
|
||||
|
||||
<!-- Engine source files needed to make the tests work -->
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" LogicalName="Robust.Shared.Analyzers.AccessAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" LogicalName="Robust.Shared.Analyzers.AccessPermissions.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
22
Robust.Analyzers.Tests/TestHelper.cs
Normal file
22
Robust.Analyzers.Tests/TestHelper.cs
Normal file
@@ -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<string>) embeddedFiles);
|
||||
}
|
||||
|
||||
public static void AddEmbeddedSources(SolutionState state, IEnumerable<string> embeddedFiles)
|
||||
{
|
||||
foreach (var fileName in embeddedFiles)
|
||||
{
|
||||
using var stream = typeof(AccessAnalyzer_Test).Assembly.GetManifestResourceStream(fileName)!;
|
||||
state.Sources.Add((fileName, SourceText.From(stream)));
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Robust.Analyzers/DependencyAssignAnalyzer.cs
Normal file
61
Robust.Analyzers/DependencyAssignAnalyzer.cs
Normal file
@@ -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<DiagnosticDescriptor> 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<AttributeData> attributes, ISymbol symbol)
|
||||
{
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, symbol))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,29 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Needed for NotNullableFlagAnalyzer. -->
|
||||
<Compile Include="..\Robust.Shared\Analyzers\NotNullableFlagAttribute.cs" />
|
||||
<Compile Include="..\Robust.Shared\Analyzers\NotNullableFlagAttribute.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Needed for FriendAnalyzer. -->
|
||||
<Compile Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" />
|
||||
<Compile Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" />
|
||||
<Compile Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" LinkBase="Implementations" />
|
||||
<Compile Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Needed for PreferGenericVariantAnalyzer. -->
|
||||
<Compile Include="..\Robust.Shared\Analyzers\PreferGenericVariantAttribute.cs" />
|
||||
<Compile Include="..\Robust.Shared\Analyzers\PreferGenericVariantAttribute.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>disable</Nullable>
|
||||
<!--
|
||||
Rider seems to get really confused with hot reload if we directly compile in the above-linked classes.
|
||||
As such, they have an #if to change their namespace in this project.
|
||||
-->
|
||||
<DefineConstants>$(DefineConstants);ROBUST_ANALYZERS_IMPL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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<Data> _valueList;
|
||||
private Data[] _array = default!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var list = new List<Data>(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;
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,8 @@ public partial class AddRemoveComponentBenchmark
|
||||
.InitializeInstance();
|
||||
|
||||
_entityManager = _simulation.Resolve<IEntityManager>();
|
||||
|
||||
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++)
|
||||
{
|
||||
|
||||
@@ -29,8 +29,8 @@ public partial class ComponentIteratorBenchmark
|
||||
|
||||
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++)
|
||||
{
|
||||
|
||||
@@ -31,8 +31,8 @@ public partial class GetComponentBenchmark
|
||||
|
||||
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++)
|
||||
{
|
||||
|
||||
@@ -29,10 +29,9 @@ public partial class SpawnDeleteEntityBenchmark
|
||||
.InitializeInstance();
|
||||
|
||||
_entityManager = _simulation.Resolve<IEntityManager>();
|
||||
|
||||
_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)]
|
||||
|
||||
@@ -91,8 +91,7 @@ public class RecursiveMoveBenchmark : RobustIntegrationTest
|
||||
// Set up map and spawn player
|
||||
server.WaitPost(() =>
|
||||
{
|
||||
var mapId = mapMan.CreateMap();
|
||||
var map = mapMan.GetMapEntityId(mapId);
|
||||
var map = server.ResolveDependency<SharedMapSystem>().CreateMap(out var mapId);
|
||||
var gridComp = mapMan.CreateGridEntity(mapId);
|
||||
var grid = gridComp.Owner;
|
||||
mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1));
|
||||
|
||||
@@ -74,11 +74,13 @@ public sealed class AudioOverlay : Overlay
|
||||
output.Clear();
|
||||
output.AppendLine("Audio Source");
|
||||
output.AppendLine("Runtime:");
|
||||
output.AppendLine($"- Distance: {_audio.GetAudioDistance(distance.Length()):0.00}");
|
||||
output.AppendLine($"- Occlusion: {posOcclusion:0.0000}");
|
||||
output.AppendLine("Params:");
|
||||
output.AppendLine($"- RolloffFactor: {comp.RolloffFactor:0.0000}");
|
||||
output.AppendLine($"- Volume: {comp.Volume:0.0000}");
|
||||
output.AppendLine($"- Reference distance: {comp.ReferenceDistance}");
|
||||
output.AppendLine($"- Max distance: {comp.MaxDistance}");
|
||||
output.AppendLine($"- Reference distance: {comp.ReferenceDistance:0.00}");
|
||||
output.AppendLine($"- Max distance: {comp.MaxDistance:0.00}");
|
||||
var outputText = output.ToString().Trim();
|
||||
var dimensions = screenHandle.GetDimensions(_font, outputText, 1f);
|
||||
var buffer = new Vector2(3f, 3f);
|
||||
|
||||
@@ -126,6 +126,33 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -361,7 +388,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
var distance = delta.Length();
|
||||
|
||||
// Out of range so just clip it for us.
|
||||
if (distance > component.MaxDistance)
|
||||
if (GetAudioDistance(distance) > component.MaxDistance)
|
||||
{
|
||||
// Still keeps the source playing, just with no volume.
|
||||
component.Gain = 0f;
|
||||
|
||||
@@ -314,6 +314,8 @@ public abstract class BaseAudioSource : IAudioSource
|
||||
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}");
|
||||
}
|
||||
|
||||
@@ -285,6 +285,7 @@ namespace Robust.Client
|
||||
/// <summary>
|
||||
/// Enumeration of the run levels of the BaseClient.
|
||||
/// </summary>
|
||||
/// <seealso cref="ClientRunLevelExt"/>
|
||||
public enum ClientRunLevel : byte
|
||||
{
|
||||
Error = 0,
|
||||
@@ -315,6 +316,21 @@ namespace Robust.Client
|
||||
SinglePlayerGame,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper functions for working with <see cref="ClientRunLevel"/>.
|
||||
/// </summary>
|
||||
public static class ClientRunLevelExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if a <see cref="ClientRunLevel"/> is <see cref="ClientRunLevel.InGame"/>
|
||||
/// or <see cref="ClientRunLevel.SinglePlayerGame"/>.
|
||||
/// </summary>
|
||||
public static bool IsInGameLike(this ClientRunLevel runLevel)
|
||||
{
|
||||
return runLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for when something changed with the player.
|
||||
/// </summary>
|
||||
|
||||
@@ -612,6 +612,8 @@ namespace Robust.Client
|
||||
{
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
|
||||
}
|
||||
|
||||
_audio.FlushALDisposeQueues();
|
||||
}
|
||||
|
||||
internal static void SetupLogging(
|
||||
|
||||
@@ -6,13 +6,12 @@ using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
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 static Robust.Client.ComponentTrees.SpriteTreeSystem;
|
||||
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
|
||||
using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer;
|
||||
using Direction = Robust.Shared.Maths.Direction;
|
||||
using Vector4 = Robust.Shared.Maths.Vector4;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
@@ -772,15 +772,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
foreach (var keyString in layerDatum.MapKeys)
|
||||
{
|
||||
object key;
|
||||
if (reflection.TryParseEnumReference(keyString, out var @enum))
|
||||
{
|
||||
key = @enum;
|
||||
}
|
||||
else
|
||||
{
|
||||
key = keyString;
|
||||
}
|
||||
var key = ParseKey(keyString);
|
||||
|
||||
if (LayerMap.TryGetValue(key, out var mappedIndex))
|
||||
{
|
||||
@@ -806,9 +798,30 @@ namespace Robust.Client.GameObjects
|
||||
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
|
||||
layer.Visible = layerDatum.Visible ?? layer.Visible;
|
||||
|
||||
if (layerDatum.CopyToShaderParameters is { } copyParameters)
|
||||
{
|
||||
layer.CopyToShaderParameters = new CopyToShaderParameters(ParseKey(copyParameters.LayerKey))
|
||||
{
|
||||
ParameterTexture = copyParameters.ParameterTexture,
|
||||
ParameterUV = copyParameters.ParameterUV
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
layer.CopyToShaderParameters = null;
|
||||
}
|
||||
|
||||
RebuildBounds();
|
||||
}
|
||||
|
||||
private object ParseKey(string keyString)
|
||||
{
|
||||
if (reflection.TryParseEnumReference(keyString, out var @enum))
|
||||
return @enum;
|
||||
|
||||
return keyString;
|
||||
}
|
||||
|
||||
public void LayerSetData(object layerKey, PrototypeLayerData data)
|
||||
{
|
||||
if (!LayerMapTryGet(layerKey, out var layer, true))
|
||||
@@ -1237,9 +1250,9 @@ namespace Robust.Client.GameObjects
|
||||
public IEnumerable<ISpriteLayer> 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 @@ namespace Robust.Client.GameObjects
|
||||
[ViewVariables]
|
||||
public LayerRenderingStrategy RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public CopyToShaderParameters? CopyToShaderParameters;
|
||||
|
||||
public Layer(SpriteComponent parent)
|
||||
{
|
||||
_parent = parent;
|
||||
@@ -1665,6 +1681,8 @@ namespace Robust.Client.GameObjects
|
||||
DirOffset = toClone.DirOffset;
|
||||
_autoAnimated = toClone._autoAnimated;
|
||||
RenderingStrategy = toClone.RenderingStrategy;
|
||||
if (toClone.CopyToShaderParameters is { } copyToShaderParameters)
|
||||
CopyToShaderParameters = new CopyToShaderParameters(copyToShaderParameters);
|
||||
}
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
@@ -2009,8 +2027,6 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
// Set the drawing transform for this layer
|
||||
GetLayerDrawMatrix(dir, out var layerMatrix);
|
||||
Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
|
||||
drawingHandle.SetTransform(in transformMatrix);
|
||||
|
||||
// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
|
||||
// due to direction overrides or offsets.
|
||||
@@ -2020,7 +2036,41 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
// Get the correct directional texture from the state, and draw it!
|
||||
var texture = GetRenderTexture(_actualState, dir);
|
||||
RenderTexture(drawingHandle, texture);
|
||||
|
||||
if (CopyToShaderParameters == null)
|
||||
{
|
||||
// Set the drawing transform for this layer
|
||||
Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
|
||||
drawingHandle.SetTransform(in transformMatrix);
|
||||
|
||||
RenderTexture(drawingHandle, texture);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multiple atrocities to god being committed right here.
|
||||
var otherLayerIdx = _parent.LayerMap[CopyToShaderParameters.LayerKey!];
|
||||
var otherLayer = _parent.Layers[otherLayerIdx];
|
||||
if (otherLayer.Shader is not { } shader)
|
||||
{
|
||||
// No shader set apparently..?
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shader.Mutable)
|
||||
otherLayer.Shader = shader = shader.Duplicate();
|
||||
|
||||
var clydeTexture = Clyde.RenderHandle.ExtractTexture(texture, null, out var csr);
|
||||
var sr = Clyde.RenderHandle.WorldTextureBoundsToUV(clydeTexture, csr);
|
||||
|
||||
if (CopyToShaderParameters.ParameterTexture is { } paramTexture)
|
||||
shader.SetParameter(paramTexture, clydeTexture);
|
||||
|
||||
if (CopyToShaderParameters.ParameterUV is { } paramUV)
|
||||
{
|
||||
var uv = new Vector4(sr.Left, sr.Bottom, sr.Right, sr.Top);
|
||||
shader.SetParameter(paramUV, uv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderTexture(DrawingHandleWorld drawingHandle, Texture texture)
|
||||
@@ -2098,6 +2148,23 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiated version of <see cref="PrototypeCopyToShaderParameters"/>.
|
||||
/// Has <see cref="LayerKey"/> actually resolved to a a real key.
|
||||
/// </summary>
|
||||
public sealed class CopyToShaderParameters(object layerKey)
|
||||
{
|
||||
public object LayerKey = layerKey;
|
||||
public string? ParameterTexture;
|
||||
public string? ParameterUV;
|
||||
|
||||
public CopyToShaderParameters(CopyToShaderParameters toClone) : this(toClone.LayerKey)
|
||||
{
|
||||
ParameterTexture = toClone.ParameterTexture;
|
||||
ParameterUV = toClone.ParameterUV;
|
||||
}
|
||||
}
|
||||
|
||||
void IAnimationProperties.SetAnimatableProperty(string name, object value)
|
||||
{
|
||||
if (!name.StartsWith("layer/"))
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace Robust.Client.GameObjects
|
||||
toDelete.Add(id);
|
||||
}
|
||||
|
||||
foreach (var dead in toDelete)
|
||||
foreach (var dead in toDelete.Span)
|
||||
{
|
||||
component.Containers.Remove(dead);
|
||||
}
|
||||
@@ -142,7 +142,7 @@ namespace Robust.Client.GameObjects
|
||||
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 @@ namespace Robust.Client.GameObjects
|
||||
removedExpected.Add(netEntity);
|
||||
}
|
||||
|
||||
foreach (var entityUid in removedExpected)
|
||||
foreach (var entityUid in removedExpected.Span)
|
||||
{
|
||||
RemoveExpectedEntity(entityUid, out _);
|
||||
}
|
||||
|
||||
@@ -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 sealed class MapSystem : SharedMapSystem
|
||||
base.Shutdown();
|
||||
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
|
||||
}
|
||||
|
||||
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
|
||||
{
|
||||
EnsureComp<PhysicsMapComponent>(uid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,8 @@
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
using System;
|
||||
using UserInterfaceComponent = Robust.Shared.GameObjects.UserInterfaceComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
{
|
||||
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
{
|
||||
[Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<BoundUIWrapMessage>(MessageReceived);
|
||||
}
|
||||
|
||||
private void MessageReceived(BoundUIWrapMessage ev)
|
||||
{
|
||||
var uid = GetEntity(ev.Entity);
|
||||
|
||||
if (!TryComp<UserInterfaceComponent>(uid, out var cmp))
|
||||
return;
|
||||
|
||||
var uiKey = ev.UiKey;
|
||||
var message = ev.Message;
|
||||
message.Session = _playerManager.LocalSession!;
|
||||
message.Entity = GetNetEntity(uid);
|
||||
message.UiKey = uiKey;
|
||||
|
||||
// Raise as object so the correct type is used.
|
||||
RaiseLocalEvent(uid, (object)message, true);
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case OpenBoundInterfaceMessage _:
|
||||
TryOpenUi(uid, uiKey, cmp);
|
||||
break;
|
||||
|
||||
case CloseBoundInterfaceMessage _:
|
||||
TryCloseUi(message.Session, uid, uiKey, remoteCall: true, uiComp: cmp);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (cmp.OpenInterfaces.TryGetValue(uiKey, out var bui))
|
||||
bui.InternalReceiveMessage(message);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryOpenUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? uiComp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref uiComp))
|
||||
return false;
|
||||
|
||||
if (uiComp.OpenInterfaces.ContainsKey(uiKey))
|
||||
return false;
|
||||
|
||||
var data = uiComp.MappedInterfaceData[uiKey];
|
||||
|
||||
// TODO: This type should be cached, but I'm too lazy.
|
||||
var type = _reflectionManager.LooseGetType(data.ClientType);
|
||||
var boundInterface =
|
||||
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new object[] {uid, uiKey});
|
||||
|
||||
boundInterface.Open();
|
||||
uiComp.OpenInterfaces[uiKey] = boundInterface;
|
||||
|
||||
if (_playerManager.LocalSession is { } playerSession)
|
||||
{
|
||||
uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
|
||||
RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,8 @@ namespace Robust.Client.GameStates
|
||||
#endif
|
||||
|
||||
private bool _resettingPredictedEntities;
|
||||
private readonly List<EntityUid> _brokenEnts = new();
|
||||
private readonly List<(EntityUid, NetEntity)> _toStart = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
@@ -667,7 +669,16 @@ namespace Robust.Client.GameStates
|
||||
|
||||
foreach (var netEntity in createdEntities)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
if (!_entityManager.TryGetEntityData(netEntity, out _, out var meta))
|
||||
{
|
||||
_sawmill.Error($"Encountered deleted entity while merging implicit data! NetEntity: {netEntity}");
|
||||
continue;
|
||||
}
|
||||
#else
|
||||
var (_, meta) = _entityManager.GetEntityData(netEntity);
|
||||
#endif
|
||||
|
||||
var compData = _compDataPool.Get();
|
||||
_outputData.Add(netEntity, compData);
|
||||
|
||||
@@ -876,9 +887,22 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
foreach (var (entity, data) in _toApply)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
HandleEntityState(entity, data.NetEntity, data.Meta, _entities.EventBus, data.curState,
|
||||
data.nextState, data.LastApplied, curState.ToSequence, data.EnteringPvs);
|
||||
|
||||
data.nextState, data.LastApplied, curState.ToSequence, data.EnteringPvs);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Caught exception while applying entity state. Entity: {_entities.ToPrettyString(entity)}. Exception: {e}");
|
||||
_entityManager.DeleteEntity(entity);
|
||||
RequestFullState();
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
if (!data.EnteringPvs)
|
||||
continue;
|
||||
|
||||
@@ -917,7 +941,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessDeletions(delSpan, xforms, xformSys);
|
||||
ProcessDeletions(delSpan, xforms, metas, xformSys);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -962,6 +986,7 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
|
||||
var xforms = _entities.GetEntityQuery<TransformComponent>();
|
||||
var metas = _entities.GetEntityQuery<MetaDataComponent>();
|
||||
var xformSys = _entitySystemManager.GetEntitySystem<SharedTransformSystem>();
|
||||
|
||||
_toDelete.Clear();
|
||||
@@ -990,12 +1015,12 @@ namespace Robust.Client.GameStates
|
||||
|
||||
// This entity is going to get deleted, but maybe some if its children won't be, so lets detach them to
|
||||
// null. First we will detach the parent in order to reduce the number of broadphase/lookup updates.
|
||||
xformSys.DetachParentToNull(ent, xform);
|
||||
xformSys.DetachEntity(ent, xform);
|
||||
|
||||
// Then detach all children.
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
|
||||
xformSys.DetachEntity(child, xforms.Get(child), metas.Get(child), xform);
|
||||
|
||||
if (deleteClientChildren
|
||||
&& !deleteClientEntities // don't add duplicates
|
||||
@@ -1014,9 +1039,9 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessDeletions(
|
||||
ReadOnlySpan<NetEntity> delSpan,
|
||||
private void ProcessDeletions(ReadOnlySpan<NetEntity> delSpan,
|
||||
EntityQuery<TransformComponent> xforms,
|
||||
EntityQuery<MetaDataComponent> metas,
|
||||
SharedTransformSystem xformSys)
|
||||
{
|
||||
// Processing deletions is non-trivial, because by default deletions will also delete all child entities.
|
||||
@@ -1043,13 +1068,13 @@ namespace Robust.Client.GameStates
|
||||
continue; // Already deleted? or never sent to us?
|
||||
|
||||
// First, a single recursive map change
|
||||
xformSys.DetachParentToNull(id.Value, xform);
|
||||
xformSys.DetachEntity(id.Value, xform);
|
||||
|
||||
// Then detach all children.
|
||||
var childEnumerator = xform.ChildEnumerator;
|
||||
while (childEnumerator.MoveNext(out var child))
|
||||
{
|
||||
xformSys.DetachParentToNull(child, xforms.GetComponent(child), xform);
|
||||
xformSys.DetachEntity(child, xforms.Get(child), metas.Get(child), xform);
|
||||
}
|
||||
|
||||
// Finally, delete the entity.
|
||||
@@ -1138,13 +1163,13 @@ namespace Robust.Client.GameStates
|
||||
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);
|
||||
}
|
||||
|
||||
meta._flags |= MetaDataFlags.Detached;
|
||||
xformSys.DetachParentToNull(ent.Value, xform);
|
||||
xformSys.DetachEntity(ent.Value, xform);
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
|
||||
|
||||
if (container != null)
|
||||
@@ -1157,63 +1182,58 @@ namespace Robust.Client.GameStates
|
||||
|
||||
private void InitializeAndStart(Dictionary<NetEntity, EntityState> toCreate)
|
||||
{
|
||||
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
|
||||
_toStart.Clear();
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
var brokenEnts = new List<EntityUid>();
|
||||
#endif
|
||||
using (_prof.Group("Initialize Entity"))
|
||||
{
|
||||
EntityUid entity = default;
|
||||
foreach (var netEntity in toCreate.Keys)
|
||||
{
|
||||
var entity = _entityManager.GetEntity(netEntity);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
_entities.InitializeEntity(entity, metaQuery.GetComponent(entity));
|
||||
#if EXCEPTION_TOLERANCE
|
||||
(entity, var meta) = _entityManager.GetEntityData(netEntity);
|
||||
_entities.InitializeEntity(entity, meta);
|
||||
_toStart.Add((entity, netEntity));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Server entity threw in Init: ent={_entities.ToPrettyString(entity)}");
|
||||
_sawmill.Error($"Server entity threw in Init: nent={netEntity}, ent={_entities.ToPrettyString(entity)}");
|
||||
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
|
||||
brokenEnts.Add(entity);
|
||||
toCreate.Remove(netEntity);
|
||||
}
|
||||
_toCreate.Remove(netEntity);
|
||||
_brokenEnts.Add(entity);
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (_prof.Group("Start Entity"))
|
||||
{
|
||||
foreach (var netEntity in toCreate.Keys)
|
||||
foreach (var (entity, netEntity) in _toStart)
|
||||
{
|
||||
var entity = _entityManager.GetEntity(netEntity);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
_entities.StartEntity(entity);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
_entities.StartEntity(entity);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Server entity threw in Start: ent={_entityManager.ToPrettyString(entity)}");
|
||||
_sawmill.Error($"Server entity threw in Start: nent={netEntity}, ent={_entityManager.ToPrettyString(entity)}");
|
||||
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
|
||||
brokenEnts.Add(entity);
|
||||
toCreate.Remove(netEntity);
|
||||
}
|
||||
_toCreate.Remove(netEntity);
|
||||
_brokenEnts.Add(entity);
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
foreach (var entity in brokenEnts)
|
||||
foreach (var entity in _brokenEnts)
|
||||
{
|
||||
_entityManager.DeleteEntity(entity);
|
||||
}
|
||||
#endif
|
||||
_brokenEnts.Clear();
|
||||
}
|
||||
|
||||
private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataComponent meta, IEventBus bus, EntityState? curState,
|
||||
@@ -1329,23 +1349,8 @@ namespace Robust.Client.GameStates
|
||||
|
||||
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,10 +1419,10 @@ namespace Robust.Client.GameStates
|
||||
_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<TransformSystem>().DetachParentToNull(uid, xform);
|
||||
_entities.EntitySysManager.GetEntitySystem<TransformSystem>().DetachEntity(uid, xform);
|
||||
|
||||
if (container != null)
|
||||
containerSys.AddExpectedEntity(_entities.GetNetEntity(uid), container);
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
("aPos", 0),
|
||||
("tCoord", 1),
|
||||
("modulate", 2)
|
||||
("tCoord2", 2),
|
||||
("modulate", 3)
|
||||
};
|
||||
|
||||
private const int UniIModUV = 0;
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
foreach (var (grid, chunks) in _mapChunkData)
|
||||
{
|
||||
var gridComp = _mapManager.GetGridComp(grid);
|
||||
var gridComp = _entityManager.GetComponent<MapGridComponent>(grid);
|
||||
foreach (var (index, chunk) in chunks)
|
||||
{
|
||||
if (!chunk.Dirty || gridComp.Chunks.ContainsKey(index))
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Shared;
|
||||
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 @@ namespace Robust.Client.Graphics.Clyde
|
||||
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 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawLight && eye.DrawFov)
|
||||
{
|
||||
ApplyFovToBuffer(viewport, eye);
|
||||
var mapUid = _mapManager.GetMapEntityId(eye.Position.MapId);
|
||||
if (_entityManager.GetComponent<MapComponent>(mapUid).LightingEnabled)
|
||||
ApplyFovToBuffer(viewport, eye);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Texture Coords.
|
||||
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 2 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(1);
|
||||
// Colour Modulation.
|
||||
GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
|
||||
// Texture Coords (2).
|
||||
GL.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(2);
|
||||
// Colour Modulation.
|
||||
GL.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 6 * sizeof(float));
|
||||
GL.EnableVertexAttribArray(3);
|
||||
}
|
||||
|
||||
// NOTE: This is:
|
||||
@@ -37,6 +40,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
public readonly Vector2 Position;
|
||||
public readonly Vector2 TextureCoordinates;
|
||||
public readonly Vector2 TextureCoordinates2;
|
||||
// Note that this color is in linear space.
|
||||
public readonly Color Modulate;
|
||||
|
||||
@@ -48,6 +52,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
Modulate = modulate;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vertex2D(Vector2 position, Vector2 textureCoordinates, Vector2 textureCoordinates2, Color modulate)
|
||||
{
|
||||
Position = position;
|
||||
TextureCoordinates = textureCoordinates;
|
||||
TextureCoordinates2 = textureCoordinates2;
|
||||
Modulate = modulate;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vertex2D(float x, float y, float u, float v, float r, float g, float b, float a)
|
||||
: this(new Vector2(x, y), new Vector2(u, v), new Color(r, g, b, a))
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
private RenderHandle _renderHandle = default!;
|
||||
|
||||
private sealed class RenderHandle : IRenderHandle
|
||||
internal sealed class RenderHandle : IRenderHandle
|
||||
{
|
||||
private readonly Clyde _clyde;
|
||||
private readonly IEntityManager _entities;
|
||||
@@ -88,16 +88,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
var clydeTexture = ExtractTexture(texture, in subRegion, out var csr);
|
||||
|
||||
var (w, h) = clydeTexture.Size;
|
||||
var sr = new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
|
||||
var sr = WorldTextureBoundsToUV(clydeTexture, csr);
|
||||
|
||||
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
|
||||
}
|
||||
|
||||
internal static Box2 WorldTextureBoundsToUV(ClydeTexture texture, UIBox2 csr)
|
||||
{
|
||||
var (w, h) = texture.Size;
|
||||
return new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a subRegion (px) into texture coords (0-1) of a given texture (cells of the textureAtlas).
|
||||
/// </summary>
|
||||
private static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr)
|
||||
internal static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 sr)
|
||||
{
|
||||
if (texture is AtlasTexture atlas)
|
||||
{
|
||||
|
||||
@@ -578,10 +578,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// TODO: split batch if necessary.
|
||||
var vIdx = BatchVertexIndex;
|
||||
BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, modulate);
|
||||
BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, modulate);
|
||||
BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, modulate);
|
||||
BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, modulate);
|
||||
BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, new Vector2(0, 0), modulate);
|
||||
BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, new Vector2(1, 0), modulate);
|
||||
BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, new Vector2(1, 1), modulate);
|
||||
BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, new Vector2(0, 1), modulate);
|
||||
BatchVertexIndex += 4;
|
||||
QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx);
|
||||
|
||||
|
||||
@@ -601,7 +601,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ClydeTexture : OwnedTexture
|
||||
internal sealed class ClydeTexture : OwnedTexture
|
||||
{
|
||||
private readonly Clyde _clyde;
|
||||
public readonly bool IsSrgb;
|
||||
@@ -649,24 +649,30 @@ namespace Robust.Client.Graphics.Clyde
|
||||
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<byte> 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<byte>.Shared.Rent(bufSize);
|
||||
|
||||
GL.GetTextureImage(loaded.OpenGLObject.Handle, 0, PF.Rgba, PT.UnsignedByte, 4*this.Size.X*this.Size.Y, (IntPtr) p);
|
||||
}
|
||||
GL.BindTexture(TextureTarget.Texture2D, loaded.OpenGLObject.Handle);
|
||||
|
||||
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<byte>.Shared.Return(buffer);
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
varying highp vec2 UV;
|
||||
varying highp vec2 UV2;
|
||||
varying highp vec2 Pos;
|
||||
varying highp vec4 VtxModulate;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
varying highp vec2 UV;
|
||||
varying highp vec2 UV2;
|
||||
|
||||
uniform sampler2D lightMap;
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -114,43 +114,12 @@ namespace Robust.Client.Graphics
|
||||
DrawPrimitives(primitiveTopology, White, indices, drawVertices);
|
||||
}
|
||||
|
||||
private static void PadVerticesV2(ReadOnlySpan<Vector2> input, Span<DrawVertexUV2DColor> output, Color color)
|
||||
private void PadVerticesV2(ReadOnlySpan<Vector2> input, Span<DrawVertexUV2DColor> output, Color color)
|
||||
{
|
||||
if (input.Length == 0)
|
||||
return;
|
||||
|
||||
if (input.Length != output.Length)
|
||||
Color colorLinear = Color.FromSrgb(color);
|
||||
for (var i = 0; i < output.Length; i++)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid lengths!");
|
||||
}
|
||||
|
||||
var colorLinear = Color.FromSrgb(color);
|
||||
var colorVec = Unsafe.As<Color, Vector128<float>>(ref colorLinear);
|
||||
var uvVec = Vector128.Create(0, 0, 0.5f, 0.5f);
|
||||
var maskVec = Vector128.Create(0xFFFFFFFF, 0xFFFFFFFF, 0, 0).AsSingle();
|
||||
|
||||
var simdVectors = (nuint)(input.Length / 2);
|
||||
ref readonly var srcBase = ref Unsafe.As<Vector2, float>(ref Unsafe.AsRef(in input[0]));
|
||||
ref var dstBase = ref Unsafe.As<DrawVertexUV2DColor, float>(ref output[0]);
|
||||
|
||||
for (nuint i = 0; i < simdVectors; i++)
|
||||
{
|
||||
var positions = Vector128.LoadUnsafe(in srcBase, i * 4);
|
||||
|
||||
var posColorLower = (positions & maskVec) | uvVec;
|
||||
var posColorUpper = (Vector128.Shuffle(positions, Vector128.Create(2, 3, 0, 0)) & maskVec) | uvVec;
|
||||
|
||||
posColorLower.StoreUnsafe(ref dstBase, i * 16);
|
||||
colorVec.StoreUnsafe(ref dstBase, i * 16 + 4);
|
||||
posColorUpper.StoreUnsafe(ref dstBase, i * 16 + 8);
|
||||
colorVec.StoreUnsafe(ref dstBase, i * 16 + 12);
|
||||
}
|
||||
|
||||
var lastPos = (int)simdVectors * 2;
|
||||
if (lastPos != output.Length)
|
||||
{
|
||||
// Odd number of vertices. Handle the last manually.
|
||||
output[lastPos] = new DrawVertexUV2DColor(input[lastPos], new Vector2(0.5f, 0.5f), colorLinear);
|
||||
output[i] = new DrawVertexUV2DColor(input[i], new Vector2(0.5f, 0.5f), colorLinear);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +237,8 @@ namespace Robust.Client.Graphics
|
||||
{
|
||||
public Vector2 Position;
|
||||
public Vector2 UV;
|
||||
public Vector2 UV2;
|
||||
|
||||
/// <summary>
|
||||
/// Modulation colour for this vertex.
|
||||
/// Note that this color is in linear space.
|
||||
|
||||
@@ -17,7 +17,7 @@ using Vector4 = Robust.Shared.Maths.Vector4;
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
[Prototype("shader")]
|
||||
public sealed class ShaderPrototype : IPrototype, ISerializationHooks
|
||||
public sealed partial class ShaderPrototype : IPrototype, ISerializationHooks
|
||||
{
|
||||
[ViewVariables]
|
||||
[IdDataField]
|
||||
|
||||
@@ -80,6 +80,10 @@ namespace Robust.Client.Placement
|
||||
private set
|
||||
{
|
||||
_isActive = value;
|
||||
|
||||
if (CurrentPermission?.UseEditorContext is false)
|
||||
return;
|
||||
|
||||
SwitchEditorContext(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@ public sealed partial class ReplayLoadManager
|
||||
if (initMessages != null)
|
||||
UpdateMessages(initMessages, uploadedFiles, prototypes, cvars, detachQueue, ref timeBase, true);
|
||||
UpdateMessages(messages[0], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase, true);
|
||||
ProcessQueue(GameTick.MaxValue, detachQueue, detached);
|
||||
|
||||
var entSpan = state0.EntityStates.Value;
|
||||
Dictionary<NetEntity, EntityState> entStates = new(entSpan.Count);
|
||||
@@ -98,6 +97,8 @@ public sealed partial class ReplayLoadManager
|
||||
entStates.Add(entState.NetEntity, modifiedState);
|
||||
}
|
||||
|
||||
ProcessQueue(GameTick.MaxValue, detachQueue, detached, entStates);
|
||||
|
||||
await callback(0, states.Count, LoadingState.ProcessingFiles, true);
|
||||
var playerSpan = state0.PlayerStates.Value;
|
||||
Dictionary<NetUserId, SessionState> playerStates = new(playerSpan.Count);
|
||||
@@ -144,7 +145,7 @@ public sealed partial class ReplayLoadManager
|
||||
UpdatePlayerStates(curState.PlayerStates.Span, playerStates);
|
||||
UpdateEntityStates(curState.EntityStates.Span, entStates, ref spawnedTracker, ref stateTracker, detached);
|
||||
UpdateMessages(messages[i], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase);
|
||||
ProcessQueue(curState.ToSequence, detachQueue, detached);
|
||||
ProcessQueue(curState.ToSequence, detachQueue, detached, entStates);
|
||||
UpdateDeletions(curState.EntityDeletions, entStates, detached);
|
||||
serverTime[i] = GetTime(curState.ToSequence) - initialTime;
|
||||
ticksSinceLastCheckpoint++;
|
||||
@@ -176,14 +177,28 @@ public sealed partial class ReplayLoadManager
|
||||
private void ProcessQueue(
|
||||
GameTick curTick,
|
||||
Dictionary<GameTick, List<NetEntity>> detachQueue,
|
||||
HashSet<NetEntity> detached)
|
||||
HashSet<NetEntity> detached,
|
||||
Dictionary<NetEntity, EntityState> entStates)
|
||||
{
|
||||
foreach (var (tick, ents) in detachQueue)
|
||||
{
|
||||
if (tick > curTick)
|
||||
continue;
|
||||
detachQueue.Remove(tick);
|
||||
detached.UnionWith(ents);
|
||||
|
||||
foreach (var e in ents)
|
||||
{
|
||||
if (entStates.ContainsKey(e))
|
||||
detached.Add(e);
|
||||
else
|
||||
{
|
||||
// AFAIK this should only happen if the client skipped over some ticks, probably due to packet loss
|
||||
// I.e., entity was created on tick n, then leaves PVS range on the tick n+1
|
||||
// If the n-th tick gets dropped, the client only ever receives the pvs-leave message.
|
||||
// In that case we should just ignore it.
|
||||
_sawmill.Debug($"Received a PVS detach msg for entity {e} before it was received?");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,13 +79,14 @@ internal sealed partial class ReplayPlaybackManager
|
||||
if (checkpoint.DetachedStates == null)
|
||||
return;
|
||||
|
||||
DebugTools.Assert(checkpoint.Detached.Count == checkpoint.DetachedStates.Length); ;
|
||||
var metas = _entMan.GetEntityQuery<MetaDataComponent>();
|
||||
DebugTools.Assert(checkpoint.Detached.Count == checkpoint.DetachedStates.Length);
|
||||
foreach (var es in checkpoint.DetachedStates)
|
||||
{
|
||||
var uid = _entMan.GetEntity(es.NetEntity);
|
||||
if (metas.TryGetComponent(uid, out var meta) && !meta.EntityDeleted)
|
||||
if (_entMan.TryGetEntityData(es.NetEntity, out var uid, out var meta))
|
||||
{
|
||||
DebugTools.Assert(!meta.EntityDeleted);
|
||||
continue;
|
||||
}
|
||||
|
||||
var metaState = (MetaDataComponentState?)es.ComponentChanges.Value?
|
||||
.FirstOrDefault(c => c.NetID == _metaId).State;
|
||||
@@ -93,18 +94,16 @@ internal sealed partial class ReplayPlaybackManager
|
||||
if (metaState == null)
|
||||
throw new MissingMetadataException(es.NetEntity);
|
||||
|
||||
_entMan.CreateEntityUninitialized(metaState.PrototypeId, uid);
|
||||
meta = metas.GetComponent(uid);
|
||||
uid = _entMan.CreateEntity(metaState.PrototypeId, out meta);
|
||||
|
||||
// Client creates a client-side net entity for the newly created entity.
|
||||
// We need to clear this mapping before assigning the real net id.
|
||||
// TODO NetEntity Jank: prevent the client from creating this in the first place.
|
||||
_entMan.ClearNetEntity(meta.NetEntity);
|
||||
_entMan.SetNetEntity(uid.Value, es.NetEntity, meta);
|
||||
|
||||
_entMan.SetNetEntity(uid, es.NetEntity, meta);
|
||||
|
||||
_entMan.InitializeEntity(uid, meta);
|
||||
_entMan.StartEntity(uid);
|
||||
_entMan.InitializeEntity(uid.Value, meta);
|
||||
_entMan.StartEntity(uid.Value);
|
||||
meta.LastStateApplied = checkpoint.Tick;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -142,6 +143,26 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
});
|
||||
|
||||
// Do not meta-atlas RSIs with custom load parameters.
|
||||
var atlasList = rsiList.Where(x => x.LoadParameters == TextureLoadParameters.Default).ToArray();
|
||||
var nonAtlasList = rsiList.Where(x => x.LoadParameters != TextureLoadParameters.Default).ToArray();
|
||||
|
||||
foreach (var data in nonAtlasList)
|
||||
{
|
||||
if (data.Bad)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
RSIResource.LoadTexture(Clyde, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
|
||||
data.Bad = true;
|
||||
}
|
||||
}
|
||||
|
||||
// This combines individual RSI atlases into larger atlases to reduce draw batches. currently this is a VERY
|
||||
// lazy bundling and is not at all compact, its basically an atlas of RSI atlases. Really what this should
|
||||
// try to do is to have each RSI write directly to the atlas, rather than having each RSI write to its own
|
||||
@@ -155,7 +176,7 @@ namespace Robust.Client.ResourceManagement
|
||||
// TODO allow RSIs to opt out (useful for very big & rare RSIs)
|
||||
// TODO combine with (non-rsi) texture atlas?
|
||||
|
||||
Array.Sort(rsiList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
|
||||
Array.Sort(atlasList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
|
||||
|
||||
// Each RSI sub atlas has a different size.
|
||||
// Even if we iterate through them once to estimate total area, I have NFI how to sanely estimate an optimal square-texture size.
|
||||
@@ -167,9 +188,9 @@ namespace Robust.Client.ResourceManagement
|
||||
Vector2i offset = default;
|
||||
int finalized = -1;
|
||||
int atlasCount = 0;
|
||||
for (int i = 0; i < rsiList.Length; i++)
|
||||
for (int i = 0; i < atlasList.Length; i++)
|
||||
{
|
||||
var rsi = rsiList[i];
|
||||
var rsi = atlasList[i];
|
||||
if (rsi.Bad)
|
||||
continue;
|
||||
|
||||
@@ -200,14 +221,14 @@ namespace Robust.Client.ResourceManagement
|
||||
var height = offset.Y + deltaY;
|
||||
var croppedSheet = new Image<Rgba32>(maxSize, height);
|
||||
sheet.Blit(new UIBox2i(0, 0, maxSize, height), croppedSheet, default);
|
||||
FinalizeMetaAtlas(rsiList.Length - 1, croppedSheet);
|
||||
FinalizeMetaAtlas(atlasList.Length - 1, croppedSheet);
|
||||
|
||||
void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
|
||||
{
|
||||
var atlas = Clyde.LoadTextureFromImage(sheet);
|
||||
for (int i = finalized + 1; i <= toIndex; i++)
|
||||
{
|
||||
var rsi = rsiList[i];
|
||||
var rsi = atlasList[i];
|
||||
rsi.AtlasTexture = atlas;
|
||||
}
|
||||
|
||||
@@ -255,9 +276,10 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
|
||||
sawmill.Debug(
|
||||
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountErrored} errored) in {LoadTime}",
|
||||
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountNotAtlas} not atlassed, {CountErrored} errored) in {LoadTime}",
|
||||
rsiList.Length,
|
||||
atlasCount,
|
||||
nonAtlasList.Length,
|
||||
errors,
|
||||
sw.Elapsed);
|
||||
|
||||
|
||||
@@ -40,17 +40,21 @@ namespace Robust.Client.ResourceManagement
|
||||
var loadStepData = new LoadStepData {Path = path};
|
||||
var manager = dependencies.Resolve<IResourceManager>();
|
||||
LoadPreTexture(manager, loadStepData);
|
||||
|
||||
loadStepData.AtlasTexture = dependencies.Resolve<IClyde>().LoadTextureFromImage(
|
||||
loadStepData.AtlasSheet,
|
||||
loadStepData.Path.ToString());
|
||||
|
||||
LoadTexture(dependencies.Resolve<IClyde>(), loadStepData);
|
||||
LoadPostTexture(loadStepData);
|
||||
LoadFinish(dependencies.Resolve<IResourceCacheInternal>(), loadStepData);
|
||||
|
||||
loadStepData.AtlasSheet.Dispose();
|
||||
}
|
||||
|
||||
internal static void LoadTexture(IClyde clyde, LoadStepData loadStepData)
|
||||
{
|
||||
loadStepData.AtlasTexture = clyde.LoadTextureFromImage(
|
||||
loadStepData.AtlasSheet,
|
||||
loadStepData.Path.ToString(),
|
||||
loadStepData.LoadParameters);
|
||||
}
|
||||
|
||||
internal static void LoadPreTexture(IResourceManager manager, LoadStepData data)
|
||||
{
|
||||
var manifestPath = data.Path / "meta.json";
|
||||
@@ -178,6 +182,7 @@ namespace Robust.Client.ResourceManagement
|
||||
data.FrameSize = frameSize;
|
||||
data.DimX = dimensionX;
|
||||
data.CallbackOffsets = callbackOffsets;
|
||||
data.LoadParameters = metadata.LoadParameters;
|
||||
}
|
||||
|
||||
internal static void LoadPostTexture(LoadStepData data)
|
||||
@@ -380,6 +385,7 @@ namespace Robust.Client.ResourceManagement
|
||||
public Texture AtlasTexture = default!;
|
||||
public Vector2i AtlasOffset;
|
||||
public RSI Rsi = default!;
|
||||
public TextureLoadParameters LoadParameters;
|
||||
}
|
||||
|
||||
internal struct StateReg
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Robust.Client.UserInterface
|
||||
toRemove.Add(key);
|
||||
}
|
||||
|
||||
foreach (var key in toRemove)
|
||||
foreach (var key in toRemove.Span)
|
||||
{
|
||||
_playingAnimations.Remove(key);
|
||||
AnimationCompleted?.Invoke(key);
|
||||
|
||||
132
Robust.Client/UserInterface/Control.Layout.Styling.cs
Normal file
132
Robust.Client/UserInterface/Control.Layout.Styling.cs
Normal file
@@ -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<T>(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<T>(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
|
||||
}
|
||||
}
|
||||
@@ -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 @@ namespace Robust.Client.UserInterface
|
||||
[ViewVariables] public bool IsMeasureValid { get; private set; }
|
||||
[ViewVariables] public bool IsArrangeValid { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls the amount of empty space in virtual pixels around the control.
|
||||
/// </summary>
|
||||
/// <remarks>Values can be provided as "All" or "Horizontal, Vertical" or "Left, Top, Right, Bottom"</remarks>
|
||||
[ViewVariables]
|
||||
public Thickness Margin
|
||||
{
|
||||
@@ -53,6 +63,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_margin = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.Margin);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -242,6 +253,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_horizontalAlignment = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.HorizontalAlignment);
|
||||
InvalidateArrange();
|
||||
}
|
||||
}
|
||||
@@ -258,6 +270,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_verticalAlignment = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.VerticalAlignment);
|
||||
InvalidateArrange();
|
||||
}
|
||||
}
|
||||
@@ -276,6 +289,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_horizontalExpand = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.HorizontalExpand);
|
||||
Parent?.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -294,6 +308,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_verticalExpand = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.VerticalExpand);
|
||||
Parent?.InvalidateArrange();
|
||||
}
|
||||
}
|
||||
@@ -318,6 +333,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
_sizeFlagsStretchRatio = value;
|
||||
|
||||
SetLayoutStyleProp(LayoutStyleProperties.StretchRatio);
|
||||
Parent?.InvalidateArrange();
|
||||
}
|
||||
}
|
||||
@@ -394,6 +410,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_minWidth = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.MinWidth);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -408,6 +425,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_minHeight = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.MinHeight);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -422,6 +440,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_setWidth = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.SetWidth);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -436,6 +455,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_setHeight = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.SetHeight);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -450,6 +470,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_maxWidth = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.MaxWidth);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -464,6 +485,7 @@ namespace Robust.Client.UserInterface
|
||||
set
|
||||
{
|
||||
_maxHeight = value;
|
||||
SetLayoutStyleProp(LayoutStyleProperties.MaxHeight);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
protected virtual void StylePropertiesChanged()
|
||||
{
|
||||
UpdateLayoutStyleProperties();
|
||||
InvalidateMeasure();
|
||||
}
|
||||
|
||||
|
||||
@@ -641,7 +641,11 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controllers;
|
||||
|
||||
// Notices your UIController, *UwU Whats this?*
|
||||
/// <summary>
|
||||
/// Each <see cref="UIController"/> is instantiated as a singleton by <see cref="UserInterfaceManager"/>
|
||||
/// <see cref="UIController"/> can use <see cref="DependencyAttribute"/> for regular IoC dependencies
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Numerics;
|
||||
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 @@ namespace Robust.Client.UserInterface.Controls
|
||||
private bool _mouseSelectingText;
|
||||
private float _lastMousePosition;
|
||||
|
||||
private TimeSpan? _lastClickTime;
|
||||
private Vector2? _lastClickPosition;
|
||||
|
||||
private bool IsPlaceHolderVisible => string.IsNullOrEmpty(_text) && _placeHolder != null;
|
||||
|
||||
public event Action<LineEditEventArgs>? OnTextChanged;
|
||||
@@ -685,8 +692,26 @@ namespace Robust.Client.UserInterface.Controls
|
||||
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;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using Robust.Client.UserInterface.RichText;
|
||||
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 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
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 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
|
||||
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 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return;
|
||||
}
|
||||
|
||||
_entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale);
|
||||
_entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
|
||||
@@ -123,10 +123,10 @@ namespace Robust.Client.UserInterface.Controls
|
||||
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;
|
||||
|
||||
@@ -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 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public bool Grabbed => _grabbed;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the slider can be adjusted.
|
||||
/// </summary>
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
public StyleBox? ForegroundStyleBoxOverride
|
||||
{
|
||||
get => _foregroundStyleBoxOverride;
|
||||
@@ -132,7 +138,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
if (args.Function != EngineKeyFunctions.UIClick || Disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -146,7 +152,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
base.KeyBindUp(args);
|
||||
|
||||
if (args.Function != EngineKeyFunctions.UIClick) return;
|
||||
if (args.Function != EngineKeyFunctions.UIClick || !_grabbed) return;
|
||||
|
||||
_grabbed = false;
|
||||
OnReleased?.Invoke(this);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<!-- Organised with tabs in a vertical list to the left and the contents to the right -->
|
||||
<BoxContainer Name="Container" xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics"
|
||||
Orientation="Horizontal"
|
||||
MouseFilter="Pass">
|
||||
<ScrollContainer VerticalExpand="True"
|
||||
HScrollEnabled="False"
|
||||
ReturnMeasure="True"
|
||||
Margin="5 5 0 5">
|
||||
<PanelContainer Margin="5" VerticalExpand="False"
|
||||
VerticalAlignment="Top">
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Name="TabContainer" Margin="5" Orientation="Vertical"/>
|
||||
</PanelContainer>
|
||||
</ScrollContainer>
|
||||
<ScrollContainer VerticalExpand="True"
|
||||
HScrollEnabled="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalExpand="True"
|
||||
ReturnMeasure="True"
|
||||
Margin="0 5 5 5">
|
||||
<PanelContainer Margin="5" HorizontalExpand="True" HorizontalAlignment="Stretch">
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Margin="5"
|
||||
Orientation="Vertical"
|
||||
Name="ContentsContainer"/>
|
||||
</PanelContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
@@ -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<Control, BaseButton> _tabs = new();
|
||||
|
||||
// Just used to order controls in case one gets removed.
|
||||
private readonly List<Control> _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;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IClyde _displayManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IBaseClient _baseClient = default!;
|
||||
|
||||
private readonly StringBuilder _textBuilder = new();
|
||||
private readonly char[] _textBuffer = new char[1024];
|
||||
@@ -58,30 +59,36 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
|
||||
|
||||
_textBuilder.Clear();
|
||||
|
||||
var isInGame = _baseClient.RunLevel.IsInGameLike();
|
||||
var mouseScreenPos = _inputManager.MouseScreenPosition;
|
||||
var screenSize = _displayManager.ScreenSize;
|
||||
var screenScale = _displayManager.MainWindow.ContentScale;
|
||||
|
||||
EntityCoordinates mouseGridPos;
|
||||
TileRef tile;
|
||||
EntityCoordinates mouseGridPos = default;
|
||||
TileRef tile = default;
|
||||
MapCoordinates mouseWorldMap = default;
|
||||
|
||||
var mouseWorldMap = _eyeManager.PixelToMap(mouseScreenPos);
|
||||
if (mouseWorldMap == MapCoordinates.Nullspace)
|
||||
return;
|
||||
|
||||
var mapSystem = _entityManager.System<SharedMapSystem>();
|
||||
var xformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
|
||||
if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
|
||||
if (isInGame)
|
||||
{
|
||||
mouseGridPos = mapSystem.MapToGrid(mouseGridUid, mouseWorldMap);
|
||||
tile = mapSystem.GetTileRef(mouseGridUid, mouseGrid, mouseGridPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
|
||||
mouseWorldMap.Position);
|
||||
tile = new TileRef(EntityUid.Invalid, mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
|
||||
mouseWorldMap = _eyeManager.PixelToMap(mouseScreenPos);
|
||||
if (mouseWorldMap != MapCoordinates.Nullspace)
|
||||
{
|
||||
var mapSystem = _entityManager.System<SharedMapSystem>();
|
||||
var xformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
|
||||
if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
|
||||
{
|
||||
mouseGridPos = mapSystem.MapToGrid(mouseGridUid, mouseWorldMap);
|
||||
tile = mapSystem.GetTileRef(mouseGridUid, mouseGrid, mouseGridPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
|
||||
mouseWorldMap.Position);
|
||||
tile = new TileRef(EntityUid.Invalid,
|
||||
mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var controlHovered = UserInterfaceManager.CurrentlyHovered;
|
||||
@@ -95,32 +102,37 @@ Mouse Pos:
|
||||
{tile}
|
||||
GUI: {controlHovered}");
|
||||
|
||||
_textBuilder.AppendLine("\nAttached NetEntity:");
|
||||
var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
|
||||
|
||||
if (controlledEntity == EntityUid.Invalid)
|
||||
if (isInGame)
|
||||
{
|
||||
_textBuilder.AppendLine("No attached netentity.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var entityTransform = _entityManager.GetComponent<TransformComponent>(controlledEntity);
|
||||
var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
|
||||
var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
|
||||
var xformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
|
||||
var playerCoordinates = entityTransform.Coordinates;
|
||||
var playerRotation = xformSystem.GetWorldRotation(entityTransform);
|
||||
var gridRotation = entityTransform.GridUid != null
|
||||
? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
|
||||
: Angle.Zero;
|
||||
_textBuilder.AppendLine("\nAttached NetEntity:");
|
||||
var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
|
||||
|
||||
_textBuilder.Append($@" Screen: {playerScreen}
|
||||
if (controlledEntity == EntityUid.Invalid)
|
||||
{
|
||||
_textBuilder.AppendLine("No attached netentity.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var entityTransform = _entityManager.GetComponent<TransformComponent>(controlledEntity);
|
||||
var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
|
||||
var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
|
||||
|
||||
var playerCoordinates = entityTransform.Coordinates;
|
||||
var playerRotation = xformSystem.GetWorldRotation(entityTransform);
|
||||
var gridRotation = entityTransform.GridUid != null
|
||||
? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
|
||||
: Angle.Zero;
|
||||
|
||||
_textBuilder.Append($@" Screen: {playerScreen}
|
||||
{playerWorldOffset}
|
||||
{_entityManager.GetNetCoordinates(playerCoordinates)}
|
||||
Rotation: {playerRotation.Degrees:F2}°
|
||||
NEntId: {_entityManager.GetNetEntity(controlledEntity)}
|
||||
Grid NEntId: {_entityManager.GetNetEntity(entityTransform.GridUid)}
|
||||
Grid Rotation: {gridRotation.Degrees:F2}°");
|
||||
}
|
||||
}
|
||||
|
||||
_contents.TextMemory = FormatHelpers.BuilderToMemory(_textBuilder, _textBuffer);
|
||||
|
||||
@@ -135,6 +135,17 @@ namespace Robust.Client.UserInterface
|
||||
/// Plays the UI hover sound if relevant.
|
||||
/// </summary>
|
||||
void HoverSound();
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="CurrentlyHovered"/> to the given control.
|
||||
/// </summary>
|
||||
void SetHovered(Control? control);
|
||||
|
||||
/// <summary>
|
||||
/// Forces <see cref="CurrentlyHovered"/> to get updated. This is done automatically when the mouse is moved,
|
||||
/// but not necessarily a new or existing control is rearranged.
|
||||
/// </summary>
|
||||
void UpdateHovered();
|
||||
}
|
||||
|
||||
public readonly struct PostDrawUIRootEventArgs
|
||||
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.Utility;
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
[Prototype("font")]
|
||||
public sealed class FontPrototype : IPrototype
|
||||
public sealed partial class FontPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
@@ -71,7 +71,8 @@ namespace Robust.Client.UserInterface
|
||||
/// <param name="defaultFont">The font being used for display.</param>
|
||||
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
|
||||
/// <param name="uiScale"></param>
|
||||
public void Update(Font defaultFont, float maxSizeX, float uiScale)
|
||||
/// <param name="lineHeightScale"></param>
|
||||
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 @@ namespace Robust.Client.UserInterface
|
||||
if (!context.Font.TryPeek(out var font))
|
||||
font = defaultFont;
|
||||
|
||||
src.Height += font.GetLineHeight(uiScale);
|
||||
src.Height += GetLineHeight(font, uiScale, lineHeightScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +171,8 @@ namespace Robust.Client.UserInterface
|
||||
UIBox2 drawBox,
|
||||
float verticalOffset,
|
||||
MarkupDrawingContext context,
|
||||
float uiScale)
|
||||
float uiScale,
|
||||
float lineHeightScale = 1)
|
||||
{
|
||||
context.Clear();
|
||||
context.Color.Push(_defaultColor);
|
||||
@@ -197,7 +199,7 @@ namespace Robust.Client.UserInterface
|
||||
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 @@ namespace Robust.Client.UserInterface
|
||||
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 @@ namespace Robust.Client.UserInterface
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -18,7 +16,7 @@ using Robust.Shared.ViewVariables;
|
||||
namespace Robust.Client.UserInterface.Themes;
|
||||
|
||||
[Prototype("uiTheme")]
|
||||
public sealed class UITheme : IPrototype
|
||||
public sealed partial class UITheme : IPrototype
|
||||
{
|
||||
private IResourceCache? _cache;
|
||||
private IUserInterfaceManager? _uiMan;
|
||||
|
||||
@@ -9,6 +9,7 @@ using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
@@ -20,9 +21,10 @@ internal partial class UserInterfaceManager
|
||||
private bool _needUpdateActiveCursor;
|
||||
[ViewVariables] public Control? KeyboardFocused { get; private set; }
|
||||
|
||||
[ViewVariables] public Control? CurrentlyHovered { get; private set; } = default!;
|
||||
[ViewVariables] public Control? CurrentlyHovered { get; private set; }
|
||||
|
||||
private Control? _controlFocused;
|
||||
|
||||
[ViewVariables]
|
||||
public Control? ControlFocused
|
||||
{
|
||||
@@ -100,6 +102,7 @@ internal partial class UserInterfaceManager
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
|
||||
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
|
||||
args.PointerLocation.Position - control.GlobalPixelPosition);
|
||||
@@ -111,16 +114,18 @@ internal partial class UserInterfaceManager
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
// Attempt to ensure that keybind-up events only get raised after a single keybind-down.
|
||||
DebugTools.Assert(!_focusedControls.ContainsKey(args.Function));
|
||||
_focusedControls[args.Function] = control;
|
||||
|
||||
OnKeyBindDown?.Invoke(control);
|
||||
}
|
||||
|
||||
public void KeyBindUp(BoundKeyEventArgs args)
|
||||
{
|
||||
if (!_focusedControls.TryGetValue(args.Function, out var control))
|
||||
{
|
||||
// Only raise keybind-up for the control on which we previously raised keybind-down
|
||||
if (!_focusedControls.Remove(args.Function, out var control) || control.Disposed)
|
||||
return;
|
||||
}
|
||||
|
||||
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
|
||||
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
|
||||
@@ -131,7 +136,6 @@ internal partial class UserInterfaceManager
|
||||
// Always mark this as handled.
|
||||
// The only case it should not be is if we do not have a control to click on,
|
||||
// in which case we never reach this.
|
||||
_focusedControls.Remove(args.Function);
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
@@ -140,23 +144,7 @@ internal partial class UserInterfaceManager
|
||||
_resetTooltipTimer();
|
||||
// Update which control is considered hovered.
|
||||
var newHovered = MouseGetControl(mouseMoveEventArgs.Position);
|
||||
if (newHovered != CurrentlyHovered)
|
||||
{
|
||||
_clearTooltip();
|
||||
CurrentlyHovered?.MouseExited();
|
||||
CurrentlyHovered = newHovered;
|
||||
CurrentlyHovered?.MouseEntered();
|
||||
if (CurrentlyHovered != null)
|
||||
{
|
||||
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
|
||||
}
|
||||
else
|
||||
{
|
||||
_tooltipDelay = null;
|
||||
}
|
||||
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
SetHovered(newHovered);
|
||||
|
||||
var target = ControlFocused ?? newHovered;
|
||||
if (target != null)
|
||||
@@ -172,6 +160,33 @@ internal partial class UserInterfaceManager
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateHovered()
|
||||
{
|
||||
var ctrl = MouseGetControl(_inputManager.MouseScreenPosition);
|
||||
SetHovered(ctrl);
|
||||
}
|
||||
|
||||
public void SetHovered(Control? control)
|
||||
{
|
||||
if (control == CurrentlyHovered)
|
||||
return;
|
||||
|
||||
_clearTooltip();
|
||||
CurrentlyHovered?.MouseExited();
|
||||
CurrentlyHovered = control;
|
||||
CurrentlyHovered?.MouseEntered();
|
||||
if (CurrentlyHovered != null)
|
||||
{
|
||||
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
|
||||
}
|
||||
else
|
||||
{
|
||||
_tooltipDelay = null;
|
||||
}
|
||||
|
||||
_needUpdateActiveCursor = true;
|
||||
}
|
||||
|
||||
private void UpdateActiveCursor()
|
||||
{
|
||||
// Consider mouse input focus first so that dragging windows don't act up etc.
|
||||
|
||||
@@ -77,15 +77,12 @@ internal sealed partial class UserInterfaceManager
|
||||
|
||||
ReleaseKeyboardFocus(control);
|
||||
RemoveModal(control);
|
||||
if (control == CurrentlyHovered)
|
||||
{
|
||||
control.MouseExited();
|
||||
CurrentlyHovered = null;
|
||||
_clearTooltip();
|
||||
}
|
||||
|
||||
if (control != ControlFocused) return;
|
||||
ControlFocused = null;
|
||||
if (control == ControlFocused)
|
||||
ControlFocused = null;
|
||||
|
||||
if (control == CurrentlyHovered)
|
||||
UpdateHovered();
|
||||
}
|
||||
|
||||
public void PushModal(Control modal)
|
||||
|
||||
@@ -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 @@ internal partial class UserInterfaceManager
|
||||
{
|
||||
DefaultTheme = _protoManager.Index<UITheme>(UITheme.DefaultName);
|
||||
CurrentTheme = DefaultTheme;
|
||||
ReloadThemes();
|
||||
_configurationManager.OnValueChanged(CVars.InterfaceTheme, SetThemeOrPrevious, true);
|
||||
_protoManager.PrototypesReloaded += OnPrototypesReloaded;
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.WasModified<UITheme>())
|
||||
{
|
||||
_sawmillUI.Debug("Reloading UI themes due to prototype reload");
|
||||
ReloadThemes();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReloadThemes()
|
||||
{
|
||||
_themes.Clear();
|
||||
foreach (var proto in _protoManager.EnumeratePrototypes<UITheme>())
|
||||
{
|
||||
_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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -16,6 +17,10 @@ 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;
|
||||
@@ -39,7 +44,7 @@ public sealed class VVPropEditorSoundSpecifier : VVPropEditor
|
||||
Editable = !ReadOnly,
|
||||
};
|
||||
|
||||
var controls = new BoxContainer()
|
||||
var pathControls = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
@@ -57,10 +62,15 @@ public sealed class VVPropEditorSoundSpecifier : VVPropEditor
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -90,7 +100,11 @@ public sealed class VVPropEditorSoundSpecifier : VVPropEditor
|
||||
if (!_protoManager.HasIndex<SoundCollectionPrototype>(args.Text))
|
||||
return;
|
||||
|
||||
ValueChanged(new SoundCollectionSpecifier(args.Text));
|
||||
_specifier = new SoundCollectionSpecifier(args.Text)
|
||||
{
|
||||
Params = _specifier?.Params ?? AudioParams.Default,
|
||||
};
|
||||
ValueChanged(_specifier);
|
||||
break;
|
||||
case 2:
|
||||
var path = new ResPath(args.Text);
|
||||
@@ -98,13 +112,282 @@ public sealed class VVPropEditorSoundSpecifier : VVPropEditor
|
||||
if (!_resManager.ContentFileExists(path))
|
||||
return;
|
||||
|
||||
ValueChanged(new SoundPathSpecifier(args.Text));
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -42,6 +42,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
component.Source = new DummyAudioSource();
|
||||
}
|
||||
|
||||
public override void SetMapAudio(Entity<AudioComponent>? audio)
|
||||
{
|
||||
if (audio == null)
|
||||
return;
|
||||
|
||||
base.SetMapAudio(audio);
|
||||
|
||||
// Also need a global override because clients not near 0,0 won't get the audio.
|
||||
_pvs.AddGlobalOverride(audio.Value);
|
||||
}
|
||||
|
||||
private void AddAudioFilter(EntityUid uid, AudioComponent component, Filter filter)
|
||||
{
|
||||
var count = filter.Count;
|
||||
|
||||
@@ -70,6 +70,11 @@ namespace Robust.Server.Console.Commands
|
||||
}
|
||||
|
||||
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 @@ namespace Robust.Server.Console.Commands
|
||||
|
||||
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<Gravity2DController>().SetGravity(mapUid, new Vector2(0, -9.8f));
|
||||
|
||||
@@ -49,7 +49,6 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
private ISawmill _logLoader = default!;
|
||||
private ISawmill _logWriter = default!;
|
||||
|
||||
private static readonly MapLoadOptions DefaultLoadOptions = new();
|
||||
private const int MapFormatVersion = 6;
|
||||
private const int BackwardsVersion = 2;
|
||||
|
||||
@@ -132,7 +131,7 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
public bool TryLoad(MapId mapId, string path, [NotNullWhen(true)] out IReadOnlyList<EntityUid>? rootUids,
|
||||
MapLoadOptions? options = null)
|
||||
{
|
||||
options ??= DefaultLoadOptions;
|
||||
options ??= new();
|
||||
|
||||
var resPath = new ResPath(path).ToRootedPath();
|
||||
|
||||
@@ -280,6 +279,9 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
// 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 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
meta.LastComponentRemoved = _timing.CurTick;
|
||||
}
|
||||
|
||||
private void SaveGridTileMap(MapData mapData)
|
||||
{
|
||||
DebugTools.Assert(_context.TileMap != null);
|
||||
|
||||
foreach (var entity in mapData.EntitiesToDeserialize.Keys)
|
||||
{
|
||||
if (HasComp<MapGridComponent>(entity))
|
||||
{
|
||||
EnsureComp<MapSaveTileMapComponent>(entity).TileMap = _context.TileMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildEntityHierarchy(MapData mapData)
|
||||
{
|
||||
_stopwatch.Restart();
|
||||
@@ -642,11 +657,13 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
// We just need to cache the old mapuid and point to the new mapuid.
|
||||
|
||||
if (HasComp<MapComponent>(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 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
data.Options.Rotation = Angle.Zero;
|
||||
}
|
||||
|
||||
_mapManager.SetMapEntity(data.TargetMap, rootNode);
|
||||
Del(existing);
|
||||
EnsureComp<LoadedMapComponent>(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 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
}
|
||||
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<LoadedMapComponent>(rootNode);
|
||||
|
||||
// Nothing should have invalid uid except for the root node.
|
||||
@@ -705,17 +717,15 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
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 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
|
||||
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 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
{
|
||||
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 sealed class MapLoaderSystem : EntitySystem
|
||||
// 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 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
var gridQuery = GetEntityQuery<MapGridComponent>();
|
||||
var tileDefs = new HashSet<int>();
|
||||
|
||||
Dictionary<int, string>? 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<int, int> tileIdMap;
|
||||
if (origTileMap != null)
|
||||
{
|
||||
tileIdMap = new Dictionary<int, int>();
|
||||
|
||||
// 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<int>(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 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsSaveable(EntityUid uid, EntityQuery<MetaDataComponent> metaQuery, EntityQuery<TransformComponent> transformQuery)
|
||||
private bool IsSaveable(EntityUid uid)
|
||||
{
|
||||
// Don't serialize things parented to un savable things.
|
||||
// For example clothes inside a person.
|
||||
while (uid.IsValid())
|
||||
{
|
||||
var meta = metaQuery.GetComponent(uid);
|
||||
var meta = MetaData(uid);
|
||||
|
||||
if (meta.EntityDeleted || meta.EntityPrototype?.MapSavable == false) break;
|
||||
|
||||
uid = transformQuery.GetComponent(uid).ParentUid;
|
||||
uid = Transform(uid).ParentUid;
|
||||
}
|
||||
|
||||
// If we manage to get up to the map (root node) then it's saveable.
|
||||
@@ -1061,7 +1116,7 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
EntityQuery<TransformComponent> transformQuery,
|
||||
EntityQuery<MapSaveIdComponent> saveCompQuery)
|
||||
{
|
||||
if (!IsSaveable(uid, metaQuery, transformQuery))
|
||||
if (!IsSaveable(uid))
|
||||
return;
|
||||
|
||||
entities.Add(uid);
|
||||
@@ -1176,11 +1231,12 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
|
||||
foreach (var component in EntityManager.GetComponents(entityUid))
|
||||
{
|
||||
if (component is MapSaveIdComponent)
|
||||
var compType = component.GetType();
|
||||
var registration = _factory.GetRegistration(compType);
|
||||
if (registration.Unsaved)
|
||||
continue;
|
||||
|
||||
var compType = component.GetType();
|
||||
var compName = _factory.GetComponentName(compType);
|
||||
var compName = registration.Name;
|
||||
_context.CurrentComponent = compName;
|
||||
MappingDataNode? compMapping;
|
||||
MappingDataNode? protMapping = null;
|
||||
|
||||
@@ -5,9 +5,9 @@ using Robust.Shared.Collections;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Map.Events;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
@@ -18,6 +18,16 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
private bool _deleteEmptyGrids;
|
||||
|
||||
protected override MapId GetNextMapId()
|
||||
{
|
||||
var id = new MapId(++LastMapId);
|
||||
while (MapManager.MapExists(id))
|
||||
{
|
||||
id = new MapId(++LastMapId);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
protected override void UpdatePvsChunks(Entity<TransformComponent, MetaDataComponent> grid)
|
||||
{
|
||||
_pvs.GridParentChanged(grid);
|
||||
@@ -31,11 +41,6 @@ namespace Robust.Server.GameObjects
|
||||
Subs.CVar(_cfg, CVars.GameDeleteEmptyGrids, SetGridDeletion, true);
|
||||
}
|
||||
|
||||
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
|
||||
{
|
||||
EnsureComp<PhysicsMapComponent>(uid);
|
||||
}
|
||||
|
||||
private void SetGridDeletion(bool value)
|
||||
{
|
||||
_deleteEmptyGrids = value;
|
||||
|
||||
@@ -1,416 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using System.Collections;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
namespace Robust.Server.GameObjects;
|
||||
|
||||
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
{
|
||||
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerMan = default!;
|
||||
[Dependency] private readonly TransformSystem _xformSys = default!;
|
||||
|
||||
private EntityQuery<IgnoreUIRangeComponent> _ignoreUIRangeQuery;
|
||||
|
||||
private readonly List<ICommonSession> _sessionCache = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<BoundUIWrapMessage>(OnMessageReceived);
|
||||
_playerMan.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
|
||||
_ignoreUIRangeQuery = GetEntityQuery<IgnoreUIRangeComponent>();
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
_playerMan.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args)
|
||||
{
|
||||
if (args.NewStatus != SessionStatus.Disconnected)
|
||||
return;
|
||||
|
||||
if (!OpenInterfaces.TryGetValue(args.Session, out var buis))
|
||||
return;
|
||||
|
||||
foreach (var bui in buis.ToArray())
|
||||
{
|
||||
CloseShared(bui, args.Session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var query = AllEntityQuery<ActiveUserInterfaceComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var activeUis, out var xform))
|
||||
{
|
||||
foreach (var ui in activeUis.Interfaces)
|
||||
{
|
||||
CheckRange(uid, activeUis, ui, xform, xformQuery);
|
||||
|
||||
if (!ui.StateDirty)
|
||||
continue;
|
||||
|
||||
ui.StateDirty = false;
|
||||
|
||||
foreach (var (player, state) in ui.PlayerStateOverrides)
|
||||
{
|
||||
RaiseNetworkEvent(state, player.Channel);
|
||||
}
|
||||
|
||||
if (ui.LastStateMsg == null)
|
||||
continue;
|
||||
|
||||
foreach (var session in ui.SubscribedSessions)
|
||||
{
|
||||
if (!ui.PlayerStateOverrides.ContainsKey(session))
|
||||
RaiseNetworkEvent(ui.LastStateMsg, session.Channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that the subscribed clients are still in range of the interface.
|
||||
/// </summary>
|
||||
private void CheckRange(EntityUid uid, ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery<TransformComponent> query)
|
||||
{
|
||||
if (ui.InteractionRange <= 0)
|
||||
return;
|
||||
|
||||
// We have to cache the set of sessions because Unsubscribe modifies the original.
|
||||
_sessionCache.Clear();
|
||||
_sessionCache.AddRange(ui.SubscribedSessions);
|
||||
|
||||
var uiPos = _xformSys.GetWorldPosition(transform, query);
|
||||
var uiMap = transform.MapID;
|
||||
|
||||
foreach (var session in _sessionCache)
|
||||
{
|
||||
// The component manages the set of sessions, so this invalid session should be removed soon.
|
||||
if (!query.TryGetComponent(session.AttachedEntity, out var xform))
|
||||
continue;
|
||||
|
||||
if (_ignoreUIRangeQuery.HasComponent(session.AttachedEntity))
|
||||
continue;
|
||||
|
||||
// Handle pluggable BoundUserInterfaceCheckRangeEvent
|
||||
var checkRangeEvent = new BoundUserInterfaceCheckRangeEvent(uid, ui, session);
|
||||
RaiseLocalEvent(uid, ref checkRangeEvent, broadcast: true);
|
||||
if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Pass)
|
||||
continue;
|
||||
|
||||
if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Fail)
|
||||
{
|
||||
CloseUi(ui, session, activeUis);
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugTools.Assert(checkRangeEvent.Result == BoundUserInterfaceRangeResult.Default);
|
||||
|
||||
if (uiMap != xform.MapID)
|
||||
{
|
||||
CloseUi(ui, session, activeUis);
|
||||
continue;
|
||||
}
|
||||
|
||||
var distanceSquared = (uiPos - _xformSys.GetWorldPosition(xform, query)).LengthSquared();
|
||||
if (distanceSquared > ui.InteractionRangeSqrd)
|
||||
CloseUi(ui, session, activeUis);
|
||||
}
|
||||
}
|
||||
|
||||
#region Get BUI
|
||||
|
||||
public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!Resolve(uid, ref ui))
|
||||
return false;
|
||||
|
||||
return ui.Interfaces.ContainsKey(uiKey);
|
||||
}
|
||||
|
||||
public PlayerBoundUserInterface GetUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!Resolve(uid, ref ui))
|
||||
throw new InvalidOperationException($"Cannot get {typeof(PlayerBoundUserInterface)} from an entity without {typeof(UserInterfaceComponent)}!");
|
||||
|
||||
return ui.Interfaces[uiKey];
|
||||
}
|
||||
|
||||
public PlayerBoundUserInterface? GetUiOrNull(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
return TryGetUi(uid, uiKey, out var bui, ui)
|
||||
? bui
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return UIs a session has open.
|
||||
/// Null if empty.
|
||||
/// </summary>
|
||||
public List<PlayerBoundUserInterface>? GetAllUIsForSession(ICommonSession session)
|
||||
{
|
||||
OpenInterfaces.TryGetValue(session, out var value);
|
||||
return value;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public bool IsUiOpen(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
return bui.SubscribedSessions.Count > 0;
|
||||
}
|
||||
|
||||
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
return bui.SubscribedSessions.Contains(session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a state. This can be used for stateful UI updating.
|
||||
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
|
||||
/// Pretty much how NanoUI did it back in ye olde BYOND.
|
||||
/// </summary>
|
||||
/// <param name="state">
|
||||
/// The state object that will be sent to all current and future client.
|
||||
/// This can be null.
|
||||
/// </param>
|
||||
/// <param name="session">
|
||||
/// The player session to send this new state to.
|
||||
/// Set to null for sending it to every subscribed player session.
|
||||
/// </param>
|
||||
public bool TrySetUiState(EntityUid uid,
|
||||
Enum uiKey,
|
||||
BoundUserInterfaceState state,
|
||||
ICommonSession? session = null,
|
||||
UserInterfaceComponent? ui = null,
|
||||
bool clearOverrides = true)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
SetUiState(bui, state, session, clearOverrides);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a state. This can be used for stateful UI updating.
|
||||
/// This state is sent to all clients, and automatically sent to all new clients when they open the UI.
|
||||
/// Pretty much how NanoUI did it back in ye olde BYOND.
|
||||
/// </summary>
|
||||
/// <param name="state">
|
||||
/// The state object that will be sent to all current and future client.
|
||||
/// This can be null.
|
||||
/// </param>
|
||||
/// <param name="session">
|
||||
/// The player session to send this new state to.
|
||||
/// Set to null for sending it to every subscribed player session.
|
||||
/// </param>
|
||||
public void SetUiState(PlayerBoundUserInterface bui, BoundUserInterfaceState state, ICommonSession? session = null, bool clearOverrides = true)
|
||||
{
|
||||
var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), new UpdateBoundStateMessage(state), bui.UiKey);
|
||||
if (session == null)
|
||||
{
|
||||
bui.LastStateMsg = msg;
|
||||
if (clearOverrides)
|
||||
bui.PlayerStateOverrides.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
bui.PlayerStateOverrides[session] = msg;
|
||||
}
|
||||
|
||||
bui.StateDirty = true;
|
||||
}
|
||||
|
||||
#region Close
|
||||
protected override void CloseShared(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
|
||||
{
|
||||
var owner = bui.Owner;
|
||||
bui._subscribedSessions.Remove(session);
|
||||
bui.PlayerStateOverrides.Remove(session);
|
||||
|
||||
if (OpenInterfaces.TryGetValue(session, out var buis))
|
||||
buis.Remove(bui);
|
||||
|
||||
RaiseLocalEvent(owner, new BoundUIClosedEvent(bui.UiKey, owner, session));
|
||||
|
||||
if (bui._subscribedSessions.Count == 0)
|
||||
DeactivateInterface(bui.Owner, bui, activeUis);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes this all interface for any clients that have any open.
|
||||
/// </summary>
|
||||
public bool TryCloseAll(EntityUid uid, Shared.GameObjects.ActiveUserInterfaceComponent? aui = null)
|
||||
{
|
||||
if (!Resolve(uid, ref aui, false))
|
||||
return false;
|
||||
|
||||
foreach (var ui in aui.Interfaces)
|
||||
{
|
||||
CloseAll(ui);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes this specific interface for any clients that have it open.
|
||||
/// </summary>
|
||||
public bool TryCloseAll(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
CloseAll(bui);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes this interface for any clients that have it open.
|
||||
/// </summary>
|
||||
public void CloseAll(PlayerBoundUserInterface bui)
|
||||
{
|
||||
foreach (var session in bui.SubscribedSessions.ToArray())
|
||||
{
|
||||
CloseUi(bui, session);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SendMessage
|
||||
|
||||
/// <summary>
|
||||
/// Send a BUI message to all connected player sessions.
|
||||
/// </summary>
|
||||
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
SendUiMessage(bui, message);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a BUI message to all connected player sessions.
|
||||
/// </summary>
|
||||
public void SendUiMessage(PlayerBoundUserInterface bui, BoundUserInterfaceMessage message)
|
||||
{
|
||||
var msg = new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey);
|
||||
foreach (var session in bui.SubscribedSessions)
|
||||
{
|
||||
RaiseNetworkEvent(msg, session.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a BUI message to a specific player session.
|
||||
/// </summary>
|
||||
public bool TrySendUiMessage(EntityUid uid, Enum uiKey, BoundUserInterfaceMessage message, ICommonSession session, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
return TrySendUiMessage(bui, message, session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a BUI message to a specific player session.
|
||||
/// </summary>
|
||||
public bool TrySendUiMessage(PlayerBoundUserInterface bui, BoundUserInterfaceMessage message, ICommonSession session)
|
||||
{
|
||||
if (!bui.SubscribedSessions.Contains(session))
|
||||
return false;
|
||||
|
||||
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), message, bui.UiKey), session.Channel);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised by <see cref="UserInterfaceSystem"/> to check whether an interface is still accessible by its user.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
[PublicAPI]
|
||||
public struct BoundUserInterfaceCheckRangeEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity owning the UI being checked for.
|
||||
/// </summary>
|
||||
public readonly EntityUid Target;
|
||||
|
||||
/// <summary>
|
||||
/// The UI itself.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public readonly PlayerBoundUserInterface UserInterface;
|
||||
|
||||
/// <summary>
|
||||
/// The player for which the UI is being checked.
|
||||
/// </summary>
|
||||
public readonly ICommonSession Player;
|
||||
|
||||
/// <summary>
|
||||
/// The result of the range check.
|
||||
/// </summary>
|
||||
public BoundUserInterfaceRangeResult Result;
|
||||
|
||||
public BoundUserInterfaceCheckRangeEvent(
|
||||
EntityUid target,
|
||||
PlayerBoundUserInterface userInterface,
|
||||
ICommonSession player)
|
||||
{
|
||||
Target = target;
|
||||
UserInterface = userInterface;
|
||||
Player = player;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible results for a <see cref="BoundUserInterfaceCheckRangeEvent"/>.
|
||||
/// </summary>
|
||||
public enum BoundUserInterfaceRangeResult : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Run built-in range check.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// Range check passed, UI is accessible.
|
||||
/// </summary>
|
||||
Pass,
|
||||
|
||||
/// <summary>
|
||||
/// Range check failed, UI is inaccessible.
|
||||
/// </summary>
|
||||
Fail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
/// </remarks>
|
||||
[RegisterComponent]
|
||||
[RegisterComponent, UnsavedComponent]
|
||||
public sealed partial class MapSaveIdComponent : Component
|
||||
{
|
||||
public int Uid { get; set; }
|
||||
|
||||
24
Robust.Server/GameObjects/MapSaveTileMapComponent.cs
Normal file
24
Robust.Server/GameObjects/MapSaveTileMapComponent.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Used by <see cref="MapLoaderSystem"/> to track the original tile map from when a map was loaded.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 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 <see cref="MapSaveTileMapComponent"/>
|
||||
/// to use.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RegisterComponent, UnsavedComponent]
|
||||
internal sealed partial class MapSaveTileMapComponent : Component
|
||||
{
|
||||
public Dictionary<int, string> TileMap = [];
|
||||
}
|
||||
@@ -86,7 +86,7 @@ namespace Robust.Server.GameObjects
|
||||
StartEntity(entity);
|
||||
}
|
||||
|
||||
private protected override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
|
||||
internal override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
|
||||
{
|
||||
if (prototypeName == null)
|
||||
return base.CreateEntity(prototypeName, out metadata, context);
|
||||
|
||||
@@ -55,13 +55,10 @@ internal sealed partial class PvsSystem
|
||||
return;
|
||||
}
|
||||
|
||||
var root = (xform.GridUid ?? xform.MapUid);
|
||||
DebugTools.AssertNotNull(root);
|
||||
|
||||
if (xform.ParentUid != root)
|
||||
if (xform.ParentUid != xform.GridUid && xform.ParentUid != xform.MapUid)
|
||||
return;
|
||||
|
||||
var location = new PvsChunkLocation(root.Value, GetChunkIndices(xform._localPosition));
|
||||
var location = new PvsChunkLocation(xform.ParentUid, GetChunkIndices(xform._localPosition));
|
||||
if (meta.LastPvsLocation == location)
|
||||
return;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ internal sealed partial class PvsSystem
|
||||
// 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 count = distance < (_lowLodDistance + ChunkSize) / 2
|
||||
var count = distance <= (_viewSize + ChunkSize) / 2
|
||||
? chunk.Contents.Count
|
||||
: chunk.LodCounts[0];
|
||||
|
||||
|
||||
@@ -62,12 +62,14 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
public bool CullingEnabled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the side of the view bounds square.
|
||||
/// Size of the side of the view bounds square. Related to <see cref="CVars.NetMaxUpdateRange"/>
|
||||
/// </summary>
|
||||
private float _viewSize;
|
||||
|
||||
// see CVars.NetLowLodDistance
|
||||
private float _lowLodDistance;
|
||||
/// <summary>
|
||||
/// Size of the side of the priority view bounds square. Related to <see cref="CVars.NetPvsPriorityRange"/>
|
||||
/// </summary>
|
||||
private float _priorityViewSize;
|
||||
|
||||
/// <summary>
|
||||
/// Per-tick ack data to avoid re-allocating.
|
||||
@@ -132,14 +134,14 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
SubscribeLocalEvent<TransformComponent, TransformStartupEvent>(OnTransformStartup);
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
_transform.OnGlobalMoveEvent += OnEntityMove;
|
||||
_transform.OnBeforeMoveEvent += OnEntityMove;
|
||||
EntityManager.EntityAdded += OnEntityAdded;
|
||||
EntityManager.EntityDeleted += OnEntityDeleted;
|
||||
EntityManager.AfterEntityFlush += AfterEntityFlush;
|
||||
|
||||
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);
|
||||
@@ -157,7 +159,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
base.Shutdown();
|
||||
|
||||
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||
_transform.OnGlobalMoveEvent -= OnEntityMove;
|
||||
_transform.OnBeforeMoveEvent -= OnEntityMove;
|
||||
EntityManager.EntityAdded -= OnEntityAdded;
|
||||
EntityManager.EntityDeleted -= OnEntityDeleted;
|
||||
EntityManager.AfterEntityFlush -= AfterEntityFlush;
|
||||
@@ -276,12 +278,13 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
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)
|
||||
@@ -387,7 +390,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
private (Vector2 worldPos, float range, EntityUid? map) CalcViewBounds(Entity<TransformComponent, EyeComponent?> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -104,12 +104,16 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
|
||||
root.Add("version", new ValueDataNode("6"));
|
||||
|
||||
gridNode.Value = SerializeTiles(value);
|
||||
Dictionary<int, int>? 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<int, int>? tileWriteMap)
|
||||
{
|
||||
// number of bytes written per tile, because sizeof(Tile) is useless.
|
||||
const int structSize = 6;
|
||||
@@ -125,7 +129,11 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -53,5 +53,7 @@ namespace Robust.Server.Maps
|
||||
/// This should be set to false if you want to load a map file onto an existing map and do not wish to overwrite the existing entity.
|
||||
/// </remarks>
|
||||
public bool LoadMap { get; set; } = true;
|
||||
|
||||
public bool DoMapInit = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ namespace Robust.Server.ServerStatus
|
||||
_context = context;
|
||||
RequestMethod = new HttpMethod(context.Request.HttpMethod!);
|
||||
|
||||
var headers = new Dictionary<string, StringValues>();
|
||||
var headers = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (string? key in context.Request.Headers.Keys)
|
||||
{
|
||||
if (key == null)
|
||||
|
||||
@@ -157,11 +157,14 @@ namespace Robust.Server.ServerStatus
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -592,7 +592,11 @@ namespace Robust.Shared.Maths
|
||||
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 @@ namespace Robust.Shared.Maths
|
||||
|
||||
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)
|
||||
|
||||
@@ -59,6 +59,192 @@ public static class Vector2Helpers
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the lengths of two vectors.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avoids square root computations by using squared lengths.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// a positive value if <paramref name="a"/> is longer than <paramref name="b"/>,
|
||||
/// a negative value if <paramref name="b"/> is longer than <paramref name="a"/>,
|
||||
/// or 0 if <paramref name="a"/> and <paramref name="b"/> have equal lengths.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float CompareLength(Vector2 a, Vector2 b)
|
||||
{
|
||||
return a.LengthSquared() - b.LengthSquared();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the length of a vector with a scalar.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avoids a square root computation by using squared length.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// a positive value if <paramref name="vec"/> is longer than <paramref name="scalar"/>,
|
||||
/// a negative value if <paramref name="vec"/> is shorter than <paramref name="scalar"/>,
|
||||
/// or 0 if <paramref name="vec"/> has a length equal to <paramref name="scalar"/>.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float CompareLength(Vector2 vec, float scalar)
|
||||
{
|
||||
return vec.LengthSquared() - (scalar * scalar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the length of this vector with a scalar.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avoids a square root computation by using squared length.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// a positive value if this vector is longer than <paramref name="scalar"/>,
|
||||
/// a negative value if this vector is shorter than <paramref name="scalar"/>,
|
||||
/// or 0 if this vector has a length equal to <paramref name="scalar"/>.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float CompareLengthTo(this Vector2 vec, float scalar)
|
||||
{
|
||||
return CompareLength(vec, scalar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the length of this vector with another.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avoids square root computations by using squared lengths.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// a positive value if this vector is longer than <paramref name="otherVec"/>,
|
||||
/// a negative value if this vector is shorter than <paramref name="otherVec"/>,
|
||||
/// or 0 if this vector and <paramref name="otherVec"/> have equal lengths.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float CompareLengthTo(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector greater than <paramref name="scalar"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsLongerThan(this Vector2 vec, float scalar)
|
||||
{
|
||||
return CompareLength(vec, scalar) > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector greater than the length of <paramref name="otherVec"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsLongerThan(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec) > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector greater than or equal to <paramref name="scalar"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsLongerThanOrEqualTo(this Vector2 vec, float scalar)
|
||||
{
|
||||
return CompareLength(vec, scalar) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector greater than or equal to the length of <paramref name="otherVec"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsLongerThanOrEqualTo(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector less than <paramref name="scalar"/>?
|
||||
/// </summary>
|
||||
/// <param name="vec"></param>
|
||||
/// <param name="scalar"></param>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsShorterThan(this Vector2 vec, float scalar)
|
||||
{
|
||||
return CompareLength(vec, scalar) < 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector less than the length of <paramref name="otherVec"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsShorterThan(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec) < 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector less than or equal to <paramref name="scalar"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsShorterThanOrEqualTo(this Vector2 vec, float scalar)
|
||||
{
|
||||
return CompareLength(vec, scalar) <= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector less than or equal to the length of <paramref name="otherVec"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsShorterThanOrEqualTo(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec) <= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this vector's length is equal to <paramref name="scalar"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool LengthEquals(this Vector2 thisVec, float scalar)
|
||||
{
|
||||
return CompareLength(thisVec, scalar) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is this vector the same length as <paramref name="otherVec"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsEqualLengthTo(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the length of a vector with a scalar.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avoids a square root computation by using squared length.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// a positive value if <paramref name="vec"/> is shorter than <paramref name="scalar"/>,
|
||||
/// a negative value if <paramref name="vec"/> is longer than <paramref name="scalar"/>,
|
||||
/// or 0 if <paramref name="vec"/> has a length equal to <paramref name="scalar"/>.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float CompareLength(float scalar, Vector2 vec)
|
||||
{
|
||||
return (scalar * scalar) - vec.LengthSquared();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector zero?
|
||||
/// </summary>
|
||||
public static bool IsLengthZero(this Vector2 vec)
|
||||
{
|
||||
return vec.LengthSquared() == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the cross product on a scalar and a vector. In 2D this produces
|
||||
/// a vector.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
#if ROBUST_ANALYZERS_IMPL
|
||||
namespace Robust.Shared.Analyzers.Implementation;
|
||||
#else
|
||||
namespace Robust.Shared.Analyzers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
#if ROBUST_ANALYZERS_IMPL
|
||||
namespace Robust.Shared.Analyzers.Implementation;
|
||||
#else
|
||||
namespace Robust.Shared.Analyzers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
#if ROBUST_ANALYZERS_IMPL
|
||||
namespace Robust.Shared.Analyzers.Implementation;
|
||||
#else
|
||||
namespace Robust.Shared.Analyzers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
#if ROBUST_ANALYZERS_IMPL
|
||||
namespace Robust.Shared.Analyzers.Implementation;
|
||||
#else
|
||||
namespace Robust.Shared.Analyzers;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Robust.Shared.Audio;
|
||||
/// to allow the server to know audio lengths without shipping the large audio files themselves.
|
||||
/// </summary>
|
||||
[Prototype(ProtoName)]
|
||||
public sealed class AudioMetadataPrototype : IPrototype
|
||||
public sealed partial class AudioMetadataPrototype : IPrototype
|
||||
{
|
||||
public const string ProtoName = "audioMetadata";
|
||||
|
||||
|
||||
@@ -29,12 +29,6 @@ namespace Robust.Shared.Audio
|
||||
[DataDefinition]
|
||||
public partial struct AudioParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The DistanceModel to use for this specific source.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Attenuation Attenuation { get; set; } = Attenuation.LinearDistanceClamped;
|
||||
|
||||
/// <summary>
|
||||
/// Base volume to play the audio at, in dB.
|
||||
/// </summary>
|
||||
@@ -45,13 +39,13 @@ namespace Robust.Shared.Audio
|
||||
/// Scale for the audio pitch.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Pitch { get; set; } = Default.Pitch;
|
||||
public float Pitch
|
||||
{
|
||||
get => _pitch;
|
||||
set => _pitch = MathF.Max(0f, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audio bus to play on.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string BusName { get; set; } = Default.BusName;
|
||||
private float _pitch = Default.Pitch;
|
||||
|
||||
/// <summary>
|
||||
/// Only applies to positional audio.
|
||||
@@ -89,7 +83,7 @@ namespace Robust.Shared.Audio
|
||||
/// <summary>
|
||||
/// The "default" audio configuration.
|
||||
/// </summary>
|
||||
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 @@ namespace Robust.Shared.Audio
|
||||
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 @@ namespace Robust.Shared.Audio
|
||||
return me;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of this instance with a new bus name set, for easy chaining.
|
||||
/// </summary>
|
||||
/// <param name="bus">The new bus name.</param>
|
||||
[Pure]
|
||||
public readonly AudioParams WithBusName(string bus)
|
||||
{
|
||||
var me = this;
|
||||
me.BusName = bus;
|
||||
return me;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of this instance with a new max distance set, for easy chaining.
|
||||
/// </summary>
|
||||
@@ -227,18 +207,6 @@ namespace Robust.Shared.Audio
|
||||
return me;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of this instance with attenuation set, for easy chaining.
|
||||
/// </summary>
|
||||
/// <param name="attenuation">The new attenuation.</param>
|
||||
[Pure]
|
||||
public readonly AudioParams WithAttenuation(Attenuation attenuation)
|
||||
{
|
||||
var me = this;
|
||||
me.Attenuation = attenuation;
|
||||
return me;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public readonly AudioParams WithPlayOffset(float offset)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Robust.Shared.Audio;
|
||||
/// This can be used by <see cref="Content.Shared.Audio.SharedContentAudioSystem"/> to apply an audio preset.
|
||||
/// </summary>
|
||||
[Prototype("audioPreset")]
|
||||
public sealed class AudioPresetPrototype : IPrototype
|
||||
public sealed partial class AudioPresetPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
@@ -6,6 +6,7 @@ using Robust.Shared.Audio.Sources;
|
||||
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();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopPlaying() => Source.StopPlaying();
|
||||
public void StopPlaying()
|
||||
{
|
||||
PlaybackPosition = 0f;
|
||||
Source.StopPlaying();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Restart() => Source.Restart();
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public AudioState State = AudioState.Playing;
|
||||
|
||||
/// <summary>
|
||||
/// Time when the audio was paused so we can offset it later if relevant.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan? PauseTime;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IAudioSource.Playing"/>
|
||||
/// </summary>
|
||||
@@ -208,6 +222,7 @@ public sealed partial class AudioComponent : Component, IAudioSource
|
||||
/// <see cref="IAudioSource.PlaybackPosition"/>
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[Access(Other = AccessPermissions.ReadWriteExecute)]
|
||||
public float PlaybackPosition
|
||||
{
|
||||
get => Source.PlaybackPosition;
|
||||
@@ -240,6 +255,14 @@ public sealed partial class AudioComponent : Component, IAudioSource
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum AudioState : byte
|
||||
{
|
||||
Stopped,
|
||||
Playing,
|
||||
Paused,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum AudioFlags : byte
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -32,11 +33,12 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
|
||||
[Dependency] protected readonly IRobustRandom RandMan = default!;
|
||||
[Dependency] protected readonly MetaDataSystem MetadataSys = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Default max range at which the sound can be heard.
|
||||
/// </summary>
|
||||
public const float DefaultSoundRange = 20;
|
||||
public const float DefaultSoundRange = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Used in the PAS to designate the physics collision mask of occluders.
|
||||
@@ -55,6 +57,153 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
SubscribeLocalEvent<AudioComponent, EntityUnpausedEvent>(OnAudioUnpaused);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the playback position of audio to the specified spot.
|
||||
/// </summary>
|
||||
public void SetPlaybackPosition(Entity<AudioComponent?>? 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 <pause time - start time>, else it's <cur time - start time>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates playback position considering length paused.
|
||||
/// </summary>
|
||||
/// <param name="component"></param>
|
||||
/// <returns></returns>
|
||||
private float GetPlaybackPosition(AudioComponent component)
|
||||
{
|
||||
return (float) (Timing.CurTime - (component.PauseTime ?? TimeSpan.Zero) - component.AudioStart).TotalSeconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks this audio as being map-based.
|
||||
/// </summary>
|
||||
public virtual void SetMapAudio(Entity<AudioComponent>? audio)
|
||||
{
|
||||
if (audio == null)
|
||||
return;
|
||||
|
||||
audio.Value.Comp.Global = true;
|
||||
MetadataSys.AddFlag(audio.Value.Owner, MetaDataFlags.Undetachable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the shared state for an audio entity.
|
||||
/// </summary>
|
||||
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<TimedDespawnComponent>(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<TimedDespawnComponent>(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<TimedDespawnComponent>(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;
|
||||
@@ -505,4 +654,12 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Robust.Shared
|
||||
/// <seealso cref="NetMtuExpandFrequency"/>
|
||||
/// <seealso cref="NetMtuExpandFailAttempts"/>
|
||||
public static readonly CVarDef<bool> NetMtuExpand =
|
||||
CVarDef.Create("net.mtu_expand", true, CVar.ARCHIVE);
|
||||
CVarDef.Create("net.mtu_expand", false, CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Interval between MTU expansion attempts, in seconds.
|
||||
@@ -225,18 +225,24 @@ namespace Robust.Shared
|
||||
CVarDef.Create("net.pvs_async", true, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="NetPvsPriorityRange"/>.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> NetMaxUpdateRange =
|
||||
CVarDef.Create("net.pvs_range", 25f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="NetMaxUpdateRange"/>
|
||||
/// A variant of <see cref="NetMaxUpdateRange"/> that is used to limit the view-distance of entities with the
|
||||
/// <see cref="MetaDataFlags.PvsPriority"/> flag set. This can be used to extend the range at which certain
|
||||
/// entities become visible.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> NetLowLodRange =
|
||||
CVarDef.Create("net.low_lod_distance", 100f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
/// <remarks>
|
||||
/// 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 <see cref="NetMaxUpdateRange"/>, and that this
|
||||
/// only works for entities that are directly parented to a grid or map.
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<float> NetPvsPriorityRange =
|
||||
CVarDef.Create("net.pvs_priority_range", 32.5f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed delay between the current tick and a client's last acknowledged tick before we send the
|
||||
@@ -845,7 +851,7 @@ namespace Robust.Shared
|
||||
/// See the documentation of the <see cref="Network.AuthMode"/> enum for values.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> AuthMode =
|
||||
CVarDef.Create("auth.mode", (int) Network.AuthMode.Optional, CVar.SERVERONLY);
|
||||
CVarDef.Create("auth.mode", (int) Network.AuthMode.Required, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Allow unauthenticated localhost connections, even if the auth mode is set to required.
|
||||
@@ -880,6 +886,22 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<string> RenderFOVColor =
|
||||
CVarDef.Create("render.fov_color", Color.Black.ToHex(), CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* CONTROLS
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds to wait to consider double-click delays.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> DoubleClickDelay =
|
||||
CVarDef.Create("controls.double_click_delay", 250, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Range in pixels for double-clicks
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> DoubleClickRange =
|
||||
CVarDef.Create("controls.double_click_range", 10, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* DISPLAY
|
||||
*/
|
||||
@@ -1423,7 +1445,7 @@ namespace Robust.Shared
|
||||
/// Comma-separated list of URLs of hub servers to advertise to.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> HubUrls =
|
||||
CVarDef.Create("hub.hub_urls", "https://central.spacestation14.io/hub/", CVar.SERVERONLY);
|
||||
CVarDef.Create("hub.hub_urls", "https://hub.spacestation14.com/", CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// URL of this server to advertise.
|
||||
|
||||
@@ -71,6 +71,8 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO EXCEPTION TOLERANCE
|
||||
// Ensure lookup trees update before content code handles move events.
|
||||
SubscribeLocalEvent<TComp, MoveEvent>(HandleMove);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ internal sealed class RecursiveMoveSystem : EntitySystem
|
||||
public override void Shutdown()
|
||||
{
|
||||
if (_subscribed)
|
||||
_transform.OnGlobalMoveEvent -= AnythingMoved;
|
||||
_transform.OnBeforeMoveEvent -= AnythingMoved;
|
||||
|
||||
_subscribed = false;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ internal sealed class RecursiveMoveSystem : EntitySystem
|
||||
return;
|
||||
|
||||
_subscribed = true;
|
||||
_transform.OnGlobalMoveEvent += AnythingMoved;
|
||||
_transform.OnBeforeMoveEvent += AnythingMoved;
|
||||
}
|
||||
|
||||
private void AnythingMoved(ref MoveEvent args)
|
||||
|
||||
@@ -558,17 +558,21 @@ namespace Robust.Shared.Configuration
|
||||
OverrideDefault(def.Name, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T GetCVar<T>(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}'");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T GetCVar<T>(string name)
|
||||
{
|
||||
return (T)GetCVar(name);
|
||||
}
|
||||
|
||||
public T GetCVar<T>(CVarDef<T> def) where T : notnull
|
||||
{
|
||||
return GetCVar<T>(def.Name);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The environment variable for configuring CVar overrides. The value
|
||||
@@ -12,23 +13,38 @@ namespace Robust.Shared.Configuration
|
||||
/// </summary>
|
||||
public const string ConfigVarEnvironmentVariable = "ROBUST_CVARS";
|
||||
|
||||
public const string SingleVarPrefix = "ROBUST_CVAR_";
|
||||
|
||||
/// <summary>
|
||||
/// Get the CVar overrides defined in the relevant environment variable.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,13 @@ namespace Robust.Shared.Configuration
|
||||
/// <param name="value">The new default value of the CVar.</param>
|
||||
void OverrideDefault<T>(CVarDef<T> def, T value) where T : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Get the value of a CVar.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the CVar.</param>
|
||||
/// <returns></returns>
|
||||
object GetCVar(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Get the value of a CVar.
|
||||
/// </summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user