Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
… into swap-positions-method
EmoGarbage404 committed Apr 28, 2024
2 parents e16cfc9 + 40a9048 commit 5c7d4ad
Showing 299 changed files with 6,876 additions and 2,942 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion MSBuild/Robust.Engine.Version.props
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<PropertyGroup><Version>214.2.0</Version></PropertyGroup>
<PropertyGroup><Version>220.2.0</Version></PropertyGroup>
</Project>
314 changes: 313 additions & 1 deletion RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Release notes for RobustToolbox.
# Release notes for RobustToolbox.

<!--
NOTE: automatically updated sometimes by version.py.
@@ -54,6 +54,318 @@ END TEMPLATE-->
*None yet*


## 220.2.0

### New features

* RSIs can now specify load parameters, mimicking the ones from `.png.yml`. Currently only disabling sRGB is supported.
* Added a second UV channel to Clyde's vertex format. On regular batched sprite draws, this goes 0 -> 1 across the sprite quad.
* Added a new `CopyToShaderParameters` system for `SpriteComponent` layers.


## 220.1.0

### Bugfixes

* Fix client-side replay exceptions due to dropped states when recording.

### Other

* Remove IP + HWId from ViewVariables.
* Close BUIs upon disconnect.


## 220.0.0

### Breaking changes

* Refactor UserInterfaceSystem.
- The API has been significantly cleaned up and standardised, most noticeably callers don't need to worry about TryGetUi and can rely on either HasUi, SetUiState, CloseUi, or OpenUi to handle their code as appropriate.
- Interface data is now stored via key rather than as a flat list which is a breaking change for YAML.
- BoundUserInterfaces can now be completely handled via Shared code. Existing Server-side callers will behave similarly to before.
- BoundUserInterfaces now properly close in many more situations, additionally they are now attached to the entity so reconnecting can re-open them and they can be serialized properly.


## 219.2.0

### New features

* Add SetMapCoordinates to TransformSystem.
* Improve YAML Linter and validation of static fields.

### Bugfixes

* Fix DebugCoordsPanel freezing when hovering a control.

### Other

* Optimise physics networking to not dirty every tick of movement.


## 219.1.3

### Bugfixes

* Fix map-loader not pausing pre-init maps when not actively overwriting an existing map.


## 219.1.2

### Bugfixes

* Fix map-loader not map-initialising grids when loading into a post-init map.


## 219.1.1

### Bugfixes

* Fix map-loader not map-initialising maps when overwriting a post-init map.


## 219.1.0

### New features

* Added a new optional arguments to various entity spawning methods, including a new argument to set the entity's rotation.

### Bugfixes

* Fixes map initialisation not always initialising all entities on a map.

### Other

* The default value of the `auth.mode` cvar has changed


## 219.0.0

### Breaking changes

* Move most IMapManager functionality to SharedMapSystem.


## 218.2.0

### New features

* Control layout properties such as `Margin` can now be set via style sheets.
* Expose worldposition in SpriteComponent.Render
* Network audio entity Play/Pause/Stop states and playback position.
* Add `Disabled` functionality to `Slider` control.


## 218.1.0

### New features

* Add IEquatable.Equals to the sandbox.
* Enable roslyn extensions tests in CI.
* Add a VerticalTabContainer control to match the horizontal one.

### Bugfixes

* Fix divison remainder issue for Colors, fixing purples.

### Other

* Default hub address (`hub.hub_urls`) has been changed to `https://hub.spacestation14.com/`.


## 218.0.0

### Breaking changes

* `Robust.Shared.Configuration.EnvironmentVariables` is now internal and no longer usable by content.

### New features

* Add TryGetRandom to EntityManager to get a random entity with the specified component and TryGetRandom to IPrototypeManager to return a random prototype of the specified type.
* Add CopyData to AppearanceSystem.
* Update UI themes on prototype reloads.
* Allow scaling the line height of a RichTextLabel.
* You can now specify CVar overrides via environment variable with the `ROBUST_CVAR_*` prefix. For example `ROBUST_CVAR_game__hostname=foobar` would set the appropriate CVar. Double underscores in the environment variable name are replaced with ".".
* Added non-generic variant of `GetCVar` to `IConfigurationManager`.
* Add type tracking to FieldNotFoundErrorNode for serialization.
* Distance between lines of a `RichTextLabel` can now be modified with `LineHeightScale`.
* UI theme prototypes are now updated when reloaded.
* New `RA0025` analyzer diagnostic warns for manual assignment to `[Dependency]` fields.

### Bugfixes

* Request headers in `IStatusHandlerContext` are now case-insensitive.
* SetWorldPosition rotation now more closely aligns with prior behavior.
* Fix exception when inspecting elements in some cases.
* Fix HTTP errors on watchdog ping not being reported.

### Other

* Add an analyzer for redundantly assigning to dependency fields.

### Internal

* Remove redundant Exists checks in ContainerSystem.
* Improve logging on watchdog pings.


## 217.2.1

### Bugfixes

* Fix LineEdit tests on engine.

### Internal

* Make various ValueList enumerators access the span directly for performance.


## 217.2.0

### New features

* Added `AddComponents` and `RemoveComponents` methods to EntityManager that handle EntityPrototype / ComponentRegistry bulk component changes.
* Add double-clicking to LineEdit.

### Bugfixes

* Properly ignore non-hard fixtures for IntersectRayWithPredicate.
* Fix nullable TimeSpan addition on some platforms.


## 217.1.0

### New features

* Added `IRobustRandom.GetItems` extension methods for randomly picking multiple items from a collections.
* Added `SharedPhysicsSystem.EffectiveCurTime`. This is effectively a variation of `IGameTiming.CurTime` that takes into account the current physics sub-step.

### Bugfixes

* Fix `MapComponent.LightingEnabled` not leaving FOV rendering in a broken state.

### Internal

* `Shuffle<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

* Implement basic VV for SoundSpecifiers.

### Bugfixes

* Fix QueueDel during EndCollideEvents from throwing while removing contacts.


## 215.1.0

### New features

* Add a CompletionHelper for audio filepaths that handles server packaging.
* Add Random.NextAngle(min, max) method and Pick for `ValueList<T>`.
* Added an `ICommonSession` parser for toolshed commands.

### Bugfixes


## 215.0.0

### Breaking changes

* Update Lidgren to 0.3.0

### New features

* Made a new `IMetricsManager` interface with an `UpdateMetrics` event that can be used to update Prometheus metrics whenever they are scraped.
* Also added a `metrics.update_interval` CVar to go along with this, when metrics are scraped without usage of Prometheus directly.
* IoC now contains an `IMeterFactory` implementation that you can use to instantiate metric meters.
* `net.mtu_ipv6` CVar allows specifying a different MTU value for IPv6.
* Allows `player:entity` to take a parameter representing the player name.
* Add collection parsing to the dev window for UI.
* Add a debug assert to Dirty(uid, comp) to catch mismatches being passed in.

### Bugfixes

* Support transform states with unknown parents.
* Fix serialization error logging.
* Fix naming of ResizableMemoryRegion metrics.
* Fix uncaught overflow exception when parsing NetEntities.

### Other

* The replay system now allows loading a replay with a mismatching serializer type hash. This means replays should be more robust against future version updates (engine security patches or .NET updates).
* `CheckBox`'s interior texture is now vertically centered.
* Lidgren.Network has been updated to [`v0.3.0`](https://github.com/space-wizards/SpaceWizards.Lidgren.Network/blob/v0.3.0/RELEASE-NOTES.md).
* Lowered default IPv4 MTU to 900 (from 1000).
* Automatic MTU expansion (`net.mtu_expand`) is now enabled by default.

### Internal

* Cleanup some Dirty component calls internally.


## 214.2.0

### New features
1 change: 1 addition & 0 deletions Resources/Locale/en-US/commands.ftl
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ cmd-parse-failure-uid = {$arg} is not a valid entity UID.
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
cmd-parse-failure-grid = {$arg} is not a valid grid.
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
cmd-parse-failure-session = There is no session with username: {$username}
cmd-error-file-not-found = Could not find file: {$file}.
cmd-error-dir-not-found = Could not find directory: {$dir}.
15 changes: 15 additions & 0 deletions Resources/Locale/en-US/view-variables.ftl
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion Robust.Analyzers.Tests/AccessAnalyzer_Test.cs
Original file line number Diff line number Diff line change
@@ -20,11 +20,16 @@ public Task Verifier(string code, params DiagnosticResult[] expected)
{
TestState =
{
AdditionalReferences = { typeof(AccessAnalyzer).Assembly },
Sources = { code }
},
};

TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.AccessAttribute.cs",
"Robust.Shared.Analyzers.AccessPermissions.cs"
);

// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);

58 changes: 58 additions & 0 deletions Robust.Analyzers.Tests/DependencyAssignAnalyzerTest.cs
Original file line number Diff line number Diff line change
@@ -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"));
}
}
7 changes: 7 additions & 0 deletions Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj
Original file line number Diff line number Diff line change
@@ -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 changes: 22 additions & 0 deletions Robust.Analyzers.Tests/TestHelper.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 61 additions & 0 deletions Robust.Analyzers/DependencyAssignAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 9 additions & 4 deletions Robust.Analyzers/Robust.Analyzers.csproj
Original file line number Diff line number Diff line change
@@ -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>
83 changes: 83 additions & 0 deletions Robust.Benchmarks/Collections/ValueListEnumerationBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -26,9 +26,8 @@ public void GlobalSetup()
.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++)
{
4 changes: 2 additions & 2 deletions Robust.Benchmarks/EntityManager/ComponentIteratorBenchmark.cs
Original file line number Diff line number Diff line change
@@ -29,8 +29,8 @@ public void GlobalSetup()

Comps = new A[N+2];

var coords = new MapCoordinates(0, 0, new MapId(1));
_simulation.AddMap(coords.MapId);
var map = _simulation.CreateMap().MapId;
var coords = new MapCoordinates(default, map);

for (var i = 0; i < N; i++)
{
4 changes: 2 additions & 2 deletions Robust.Benchmarks/EntityManager/GetComponentBenchmark.cs
Original file line number Diff line number Diff line change
@@ -31,8 +31,8 @@ public void GlobalSetup()

Comps = new A[N+2];

var coords = new MapCoordinates(0, 0, new MapId(1));
_simulation.AddMap(coords.MapId);
var map = _simulation.CreateMap().Uid;
var coords = new EntityCoordinates(map, default);

for (var i = 0; i < N; i++)
{
7 changes: 3 additions & 4 deletions Robust.Benchmarks/EntityManager/SpawnDeleteEntityBenchmark.cs
Original file line number Diff line number Diff line change
@@ -29,10 +29,9 @@ public void GlobalSetup()
.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)]
3 changes: 1 addition & 2 deletions Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs
Original file line number Diff line number Diff line change
@@ -91,8 +91,7 @@ public void GlobalSetup()
// Set up map and spawn player
server.WaitPost(() =>
{
var mapId = mapMan.CreateMap();
var map = mapMan.GetMapEntityId(mapId);
var map = server.ResolveDependency<SharedMapSystem>().CreateMap(out var mapId);
var gridComp = mapMan.CreateGridEntity(mapId);
var grid = gridComp.Owner;
mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1));
6 changes: 4 additions & 2 deletions Robust.Client/Audio/AudioOverlay.cs
Original file line number Diff line number Diff line change
@@ -74,11 +74,13 @@ protected internal override void Draw(in OverlayDrawArgs args)
output.Clear();
output.AppendLine("Audio Source");
output.AppendLine("Runtime:");
output.AppendLine($"- Distance: {_audio.GetAudioDistance(distance.Length()):0.00}");
output.AppendLine($"- Occlusion: {posOcclusion:0.0000}");
output.AppendLine("Params:");
output.AppendLine($"- RolloffFactor: {comp.RolloffFactor:0.0000}");
output.AppendLine($"- Volume: {comp.Volume:0.0000}");
output.AppendLine($"- Reference distance: {comp.ReferenceDistance}");
output.AppendLine($"- Max distance: {comp.MaxDistance}");
output.AppendLine($"- Reference distance: {comp.ReferenceDistance:0.00}");
output.AppendLine($"- Max distance: {comp.MaxDistance:0.00}");
var outputText = output.ToString().Trim();
var dimensions = screenHandle.GetDimensions(_font, outputText, 1f);
var buffer = new Vector2(3f, 3f);
68 changes: 52 additions & 16 deletions Robust.Client/Audio/AudioSystem.cs
Original file line number Diff line number Diff line change
@@ -126,6 +126,33 @@ private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAuto
{
component.Source.SetAuxiliary(null);
}

switch (component.State)
{
case AudioState.Playing:
component.StartPlaying();
break;
case AudioState.Paused:
component.Pause();
break;
case AudioState.Stopped:
component.StopPlaying();
component.PlaybackPosition = 0f;
break;
}

// If playback position changed then update it.
if (!string.IsNullOrEmpty(component.FileName))
{
var position = (float) ((component.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
var currentPosition = component.Source.PlaybackPosition;
var diff = Math.Abs(position - currentPosition);

if (diff > 0.1f)
{
component.PlaybackPosition = position;
}
}
}

/// <summary>
@@ -173,7 +200,7 @@ private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentSt
private void SetupSource(Entity<AudioComponent> entity, AudioResource audioResource, TimeSpan? length = null)
{
var component = entity.Comp;

if (TryAudioLimit(component.FileName))
{
var newSource = _audio.CreateAudioSource(audioResource);
@@ -361,7 +388,7 @@ private void ProcessStream(EntityUid entity, AudioComponent component, Transform
var distance = delta.Length();

// Out of range so just clip it for us.
if (distance > component.MaxDistance)
if (GetAudioDistance(distance) > component.MaxDistance)
{
// Still keeps the source playing, just with no volume.
component.Gain = 0f;
@@ -427,13 +454,13 @@ private bool TryGetAudio(AudioStream stream, [NotNullWhen(true)] out AudioResour
return false;
}

public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
}

public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
}
@@ -460,8 +487,11 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(Soun
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
return null;

if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
@@ -493,8 +523,11 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(Soun
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
private (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
return null;

if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
@@ -534,8 +567,11 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(Soun
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
return null;

if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
@@ -569,25 +605,25 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(Soun
}

/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}

/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
{
return PlayEntity(filename, entity, audioParams);
}

/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}

/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}
@@ -603,31 +639,31 @@ public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
}

/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
}

/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
}

/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
}

/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}

/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
}
2 changes: 2 additions & 0 deletions Robust.Client/Audio/Sources/BaseAudioSource.cs
Original file line number Diff line number Diff line change
@@ -314,6 +314,8 @@ public float PlaybackPosition
set
{
_checkDisposed();

value = MathF.Max(value, 0f);
AL.Source(SourceHandle, ALSourcef.SecOffset, value);
Master._checkAlError($"Tried to set invalid playback position of {value:0.00}");
}
16 changes: 16 additions & 0 deletions Robust.Client/BaseClient.cs
Original file line number Diff line number Diff line change
@@ -285,6 +285,7 @@ void IPostInjectInit.PostInject()
/// <summary>
/// Enumeration of the run levels of the BaseClient.
/// </summary>
/// <seealso cref="ClientRunLevelExt"/>
public enum ClientRunLevel : byte
{
Error = 0,
@@ -315,6 +316,21 @@ public enum ClientRunLevel : byte
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>
30 changes: 25 additions & 5 deletions Robust.Client/Console/Commands/Debug.cs
Original file line number Diff line number Diff line change
@@ -294,7 +294,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
internal sealed class SnapGridGetCell : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _map = default!;

public override string Command => "sggcell";

@@ -320,7 +319,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
return;
}

if (_map.TryGetGrid(_entManager.GetEntity(gridNet), out var grid))
if (_entManager.TryGetComponent<MapGridComponent>(_entManager.GetEntity(gridNet), out var grid))
{
foreach (var entity in grid.GetAnchoredEntities(new Vector2i(
int.Parse(indices.Split(',')[0], CultureInfo.InvariantCulture),
@@ -429,7 +428,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
internal sealed class GridTileCount : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _map = default!;

public override string Command => "gridtc";

@@ -448,7 +446,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
return;
}

if (_map.TryGetGrid(gridUid, out var grid))
if (_entManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
{
shell.WriteLine(grid.GetAllTiles().Count().ToString());
}
@@ -555,7 +553,7 @@ internal static List<MemberInfo> GetAllMembers(Control control)
if (type != typeof(Control))
cname = $"Control > {cname}";

returnVal.GetOrNew(cname).Add((member.Name, member.GetValue(control)?.ToString() ?? "null"));
returnVal.GetOrNew(cname).Add((member.Name, GetMemberValue(member, control, ", ")));
}

foreach (var (attachedProperty, value) in control.AllAttachedProperties)
@@ -570,6 +568,28 @@ internal static List<MemberInfo> GetAllMembers(Control control)
}
return returnVal;
}

internal static string PropertyValuesString(Control control, string key)
{
var member = GetAllMembers(control).Find(m => m.Name == key);
return GetMemberValue(member, control, "\n", "\"{0}\"");
}

private static string GetMemberValue(MemberInfo? member, Control control, string separator, string
wrap = "{0}")
{
var value = member?.GetValue(control);
var o = value switch
{
ICollection<Control> controls => string.Join(separator,
controls.Select(ctrl => $"{ctrl.Name}({ctrl.GetType()})")),
ICollection<string> list => string.Join(separator, list),
null => null,
_ => value.ToString()
};
// Convert to quote surrounded string or null with no quotes
return o is not null ? string.Format(wrap, o) : "null";
}
}

internal sealed class SetClipboardCommand : LocalizedCommands
2 changes: 2 additions & 0 deletions Robust.Client/GameController/GameController.cs
Original file line number Diff line number Diff line change
@@ -612,6 +612,8 @@ private void Update(FrameEventArgs frameEventArgs)
{
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
}

_audio.FlushALDisposeQueues();
}

internal static void SetupLogging(
91 changes: 75 additions & 16 deletions Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs
Original file line number Diff line number Diff line change
@@ -6,13 +6,12 @@
using System.Numerics;
using System.Text;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Animations;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -30,6 +29,7 @@
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer;
using Direction = Robust.Shared.Maths.Direction;
using Vector4 = Robust.Shared.Maths.Vector4;

namespace Robust.Client.GameObjects
{
@@ -772,15 +772,7 @@ public void LayerSetData(int index, PrototypeLayerData layerDatum)
{
foreach (var keyString in layerDatum.MapKeys)
{
object key;
if (reflection.TryParseEnumReference(keyString, out var @enum))
{
key = @enum;
}
else
{
key = keyString;
}
var key = ParseKey(keyString);

if (LayerMap.TryGetValue(key, out var mappedIndex))
{
@@ -806,9 +798,30 @@ public void LayerSetData(int index, PrototypeLayerData layerDatum)
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = layerDatum.Visible ?? layer.Visible;

if (layerDatum.CopyToShaderParameters is { } copyParameters)
{
layer.CopyToShaderParameters = new CopyToShaderParameters(ParseKey(copyParameters.LayerKey))
{
ParameterTexture = copyParameters.ParameterTexture,
ParameterUV = copyParameters.ParameterUV
};
}
else
{
layer.CopyToShaderParameters = null;
}

RebuildBounds();
}

private object ParseKey(string keyString)
{
if (reflection.TryParseEnumReference(keyString, out var @enum))
return @enum;

return keyString;
}

public void LayerSetData(object layerKey, PrototypeLayerData data)
{
if (!LayerMapTryGet(layerKey, out var layer, true))
@@ -1237,9 +1250,9 @@ public RSI.StateId LayerGetState(int layer)
public IEnumerable<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 @@ public Vector2 Offset
[ViewVariables]
public LayerRenderingStrategy RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy;

[ViewVariables(VVAccess.ReadWrite)]
public CopyToShaderParameters? CopyToShaderParameters;

public Layer(SpriteComponent parent)
{
_parent = parent;
@@ -2009,8 +2025,6 @@ internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3 spriteMatrix,

// Set the drawing transform for this layer
GetLayerDrawMatrix(dir, out var layerMatrix);
Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
drawingHandle.SetTransform(in transformMatrix);

// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
// due to direction overrides or offsets.
@@ -2020,7 +2034,41 @@ internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3 spriteMatrix,

// Get the correct directional texture from the state, and draw it!
var texture = GetRenderTexture(_actualState, dir);
RenderTexture(drawingHandle, texture);

if (CopyToShaderParameters == null)
{
// Set the drawing transform for this layer
Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
drawingHandle.SetTransform(in transformMatrix);

RenderTexture(drawingHandle, texture);
}
else
{
// Multiple atrocities to god being committed right here.
var otherLayerIdx = _parent.LayerMap[CopyToShaderParameters.LayerKey!];
var otherLayer = _parent.Layers[otherLayerIdx];
if (otherLayer.Shader is not { } shader)
{
// No shader set apparently..?
return;
}

if (!shader.Mutable)
otherLayer.Shader = shader = shader.Duplicate();

var clydeTexture = Clyde.RenderHandle.ExtractTexture(texture, null, out var csr);
var sr = Clyde.RenderHandle.WorldTextureBoundsToUV(clydeTexture, csr);

if (CopyToShaderParameters.ParameterTexture is { } paramTexture)
shader.SetParameter(paramTexture, clydeTexture);

if (CopyToShaderParameters.ParameterUV is { } paramUV)
{
var uv = new Vector4(sr.Left, sr.Bottom, sr.Right, sr.Top);
shader.SetParameter(paramUV, uv);
}
}
}

private void RenderTexture(DrawingHandleWorld drawingHandle, Texture texture)
@@ -2098,6 +2146,17 @@ internal void AdvanceFrameAnimation(RSI.State state)
}
}

/// <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;
}

void IAnimationProperties.SetAnimatableProperty(string name, object value)
{
if (!name.StartsWith("layer/"))
6 changes: 3 additions & 3 deletions Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs
Original file line number Diff line number Diff line change
@@ -107,7 +107,7 @@ private void HandleComponentState(EntityUid uid, ContainerManagerComponent compo
toDelete.Add(id);
}

foreach (var dead in toDelete)
foreach (var dead in toDelete.Span)
{
component.Containers.Remove(dead);
}
@@ -142,7 +142,7 @@ private void HandleComponentState(EntityUid uid, ContainerManagerComponent compo
toRemove.Add(entity);
}

foreach (var entity in toRemove)
foreach (var entity in toRemove.Span)
{
Remove(
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity)),
@@ -162,7 +162,7 @@ private void HandleComponentState(EntityUid uid, ContainerManagerComponent compo
removedExpected.Add(netEntity);
}

foreach (var entityUid in removedExpected)
foreach (var entityUid in removedExpected.Span)
{
RemoveExpectedEntity(entityUid, out _);
}
3 changes: 2 additions & 1 deletion Robust.Client/GameObjects/EntitySystems/InputSystem.cs
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ public sealed class InputSystem : SharedInputSystem, IPostInjectInit
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;

private ISawmill _sawmillInputContext = default!;

@@ -151,7 +152,7 @@ private void GenerateInputCommand(IConsoleShell shell, string argstr, string[] a

var pxform = Transform(pent);
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
var coords = EntityCoordinates.FromMap(EntityManager, pent, new MapCoordinates(wPos, pxform.MapID));
var coords = EntityCoordinates.FromMap(pent, new MapCoordinates(wPos, pxform.MapID), _transform, EntityManager);

var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);

19 changes: 11 additions & 8 deletions Robust.Client/GameObjects/EntitySystems/MapSystem.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
using Robust.Client.Graphics;
using Robust.Client.Map;
using Robust.Client.Physics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Dynamics;

namespace Robust.Client.GameObjects;

@@ -16,6 +13,17 @@ public sealed class MapSystem : SharedMapSystem
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;

protected override MapId GetNextMapId()
{
// Client-side map entities use negative map Ids to avoid conflict with server-side maps.
var id = new MapId(--LastMapId);
while (MapManager.MapExists(id))
{
id = new MapId(--LastMapId);
}
return id;
}

public override void Initialize()
{
base.Initialize();
@@ -27,9 +35,4 @@ public override void Shutdown()
base.Shutdown();
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
}

protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
EnsureComp<PhysicsMapComponent>(uid);
}
}
82 changes: 3 additions & 79 deletions Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs
Original file line number Diff line number Diff line change
@@ -1,84 +1,8 @@
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using System;
using UserInterfaceComponent = Robust.Shared.GameObjects.UserInterfaceComponent;

namespace Robust.Client.GameObjects
{
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
[Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;

public override void Initialize()
{
base.Initialize();

SubscribeNetworkEvent<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;
namespace Robust.Client.GameObjects;

default:
if (cmp.OpenInterfaces.TryGetValue(uiKey, out var bui))
bui.InternalReceiveMessage(message);

break;
}
}

private bool TryOpenUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? uiComp = null)
{
if (!Resolve(uid, ref uiComp))
return false;

if (uiComp.OpenInterfaces.ContainsKey(uiKey))
return false;

var data = uiComp.MappedInterfaceData[uiKey];

// TODO: This type should be cached, but I'm too lazy.
var type = _reflectionManager.LooseGetType(data.ClientType);
var boundInterface =
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new object[] {uid, uiKey});

boundInterface.Open();
uiComp.OpenInterfaces[uiKey] = boundInterface;

if (_playerManager.LocalSession is { } playerSession)
{
uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);
}
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{

return true;
}
}
}
83 changes: 37 additions & 46 deletions Robust.Client/GameStates/ClientGameStateManager.cs
Original file line number Diff line number Diff line change
@@ -125,6 +125,8 @@ public sealed class ClientGameStateManager : IClientGameStateManager
#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 @@ private void MergeImplicitData(IEnumerable<NetEntity> createdEntities)

foreach (var netEntity in createdEntities)
{
#if EXCEPTION_TOLERANCE
if (!_entityManager.TryGetEntityData(netEntity, out _, out var meta))
{
_sawmill.Error($"Encountered deleted entity while merging implicit data! NetEntity: {netEntity}");
continue;
}
#else
var (_, meta) = _entityManager.GetEntityData(netEntity);
#endif

var compData = _compDataPool.Get();
_outputData.Add(netEntity, compData);

@@ -700,7 +711,7 @@ public IEnumerable<NetEntity> ApplyGameState(GameState curState, GameState? next
{
using var _ = _timing.StartStateApplicationArea();

// TODO repays optimize this.
// TODO replays optimize this.
// This currently just saves game states as they are applied.
// However this is inefficient and may have redundant data.
// E.g., we may record states: [10 to 15] [11 to 16] *error* [0 to 18] [18 to 19] [18 to 20] ...
@@ -1138,7 +1149,7 @@ private void Detach(GameTick maxTick,
if ((meta.Flags & MetaDataFlags.InContainer) != 0 &&
metas.TryGetComponent(xform.ParentUid, out var containerMeta) &&
(containerMeta.Flags & MetaDataFlags.Detached) == 0 &&
containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container, null, true))
containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container))
{
containerSys.Remove((ent.Value, xform, meta), container, false, true);
}
@@ -1157,63 +1168,58 @@ private void Detach(GameTick maxTick,

private void InitializeAndStart(Dictionary<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 +1335,8 @@ private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataCompo

foreach (var (comp, cur, next) in _compStateWork.Values)
{
try
{
var handleState = new ComponentHandleState(cur, next);
bus.RaiseComponentEvent(comp, ref handleState);
}
#pragma warning disable CS0168 // Variable is declared but never used
catch (Exception e)
#pragma warning restore CS0168 // Variable is declared but never used
{
#if EXCEPTION_TOLERANCE
_sawmill.Error($"Failed to apply comp state: entity={_entities.ToPrettyString(uid)}, comp={comp.GetType()}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(HandleEntityState)}");
#else
_sawmill.Error($"Failed to apply comp state: entity={_entities.ToPrettyString(uid)}, comp={comp.GetType()}");
throw;
#endif
}
var handleState = new ComponentHandleState(cur, next);
bus.RaiseComponentEvent(comp, ref handleState);
}
}

@@ -1414,7 +1405,7 @@ private void DetachEntCommand(IConsoleShell shell, string argStr, string[] args)
_entities.TryGetComponent(xform.ParentUid, out MetaDataComponent? containerMeta) &&
(containerMeta.Flags & MetaDataFlags.Detached) == 0)
{
containerSys.TryGetContainingContainer(xform.ParentUid, uid, out container, null, true);
containerSys.TryGetContainingContainer(xform.ParentUid, uid, out container);
}

_entities.EntitySysManager.GetEntitySystem<TransformSystem>().DetachParentToNull(uid, xform);
3 changes: 2 additions & 1 deletion Robust.Client/Graphics/Clyde/Clyde.Constants.cs
Original file line number Diff line number Diff line change
@@ -6,7 +6,8 @@ private static readonly (string, uint)[] BaseShaderAttribLocations =
{
("aPos", 0),
("tCoord", 1),
("modulate", 2)
("tCoord2", 2),
("modulate", 3)
};

private const int UniIModUV = 0;
2 changes: 1 addition & 1 deletion Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs
Original file line number Diff line number Diff line change
@@ -124,7 +124,7 @@ private void CullEmptyChunks()
{
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))
9 changes: 5 additions & 4 deletions Robust.Client/Graphics/Clyde/Clyde.HLR.cs
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Utility;
@@ -250,10 +251,8 @@ private List<Overlay> GetOverlaysForSpace(OverlaySpace space)
private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye)
{
var mapId = eye.Position.MapId;
if (mapId == MapId.Nullspace || !_mapManager.HasMapEntity(mapId))
{
if (mapId == MapId.Nullspace)
return;
}

RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldAABB, worldBounds);
var worldOverlays = GetOverlaysForSpace(OverlaySpace.WorldSpaceEntities);
@@ -514,7 +513,9 @@ private void RenderViewport(Viewport viewport)

if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawLight && eye.DrawFov)
{
ApplyFovToBuffer(viewport, eye);
var mapUid = _mapManager.GetMapEntityId(eye.Position.MapId);
if (_entityManager.GetComponent<MapComponent>(mapUid).LightingEnabled)
ApplyFovToBuffer(viewport, eye);
}
}

17 changes: 15 additions & 2 deletions Robust.Client/Graphics/Clyde/Clyde.Layout.cs
Original file line number Diff line number Diff line change
@@ -23,9 +23,12 @@ private static unsafe void SetupVAOLayout()
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
// Colour Modulation.
GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
// Texture Coords (2).
GL.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 4 * sizeof(float));
GL.EnableVertexAttribArray(2);
// Colour Modulation.
GL.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, sizeof(Vertex2D), 6 * sizeof(float));
GL.EnableVertexAttribArray(3);
}

// NOTE: This is:
@@ -37,6 +40,7 @@ private readonly struct Vertex2D
{
public readonly Vector2 Position;
public readonly Vector2 TextureCoordinates;
public readonly Vector2 TextureCoordinates2;
// Note that this color is in linear space.
public readonly Color Modulate;

@@ -48,6 +52,15 @@ public Vertex2D(Vector2 position, Vector2 textureCoordinates, Color modulate)
Modulate = modulate;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vertex2D(Vector2 position, Vector2 textureCoordinates, Vector2 textureCoordinates2, Color modulate)
{
Position = position;
TextureCoordinates = textureCoordinates;
TextureCoordinates2 = textureCoordinates2;
Modulate = modulate;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vertex2D(float x, float y, float u, float v, float r, float g, float b, float a)
: this(new Vector2(x, y), new Vector2(u, v), new Color(r, g, b, a))
13 changes: 9 additions & 4 deletions Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ internal partial class Clyde
{
private RenderHandle _renderHandle = default!;

private sealed class RenderHandle : IRenderHandle
internal sealed class RenderHandle : IRenderHandle
{
private readonly Clyde _clyde;
private readonly IEntityManager _entities;
@@ -88,16 +88,21 @@ public void DrawTextureWorld(Texture texture, Vector2 bl, Vector2 br, Vector2 tl
{
var clydeTexture = ExtractTexture(texture, in subRegion, out var csr);

var (w, h) = clydeTexture.Size;
var sr = new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
var sr = WorldTextureBoundsToUV(clydeTexture, csr);

_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
}

internal static Box2 WorldTextureBoundsToUV(ClydeTexture texture, UIBox2 csr)
{
var (w, h) = texture.Size;
return new Box2(csr.Left / w, (h - csr.Bottom) / h, csr.Right / w, (h - csr.Top) / h);
}

/// <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)
{
8 changes: 4 additions & 4 deletions Robust.Client/Graphics/Clyde/Clyde.Rendering.cs
Original file line number Diff line number Diff line change
@@ -578,10 +578,10 @@ private void DrawTexture(ClydeHandle texture, Vector2 bl, Vector2 br, Vector2 tl

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

28 changes: 17 additions & 11 deletions Robust.Client/Graphics/Clyde/Clyde.Textures.cs
Original file line number Diff line number Diff line change
@@ -601,7 +601,7 @@ private void FlushTextureDispose()
}
}

private sealed class ClydeTexture : OwnedTexture
internal sealed class ClydeTexture : OwnedTexture
{
private readonly Clyde _clyde;
public readonly bool IsSrgb;
@@ -649,24 +649,30 @@ public override string ToString()
return $"ClydeTexture: ({TextureId})";
}

public override Color GetPixel(int x, int y)
public override unsafe Color GetPixel(int x, int y)
{
if (!_clyde._loadedTextures.TryGetValue(TextureId, out var loaded))
{
throw new DataException("Texture not found");
}

Span<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.BindTexture(TextureTarget.Texture2D, loaded.OpenGLObject.Handle);

GL.GetTextureImage(loaded.OpenGLObject.Handle, 0, PF.Rgba, PT.UnsignedByte, 4*this.Size.X*this.Size.Y, (IntPtr) p);
}
fixed (byte* p = buffer)
{
GL.GetnTexImage(TextureTarget.Texture2D, 0, PF.Rgba, PT.UnsignedByte, bufSize, (IntPtr) p);
}
int pixelPos = (this.Size.X*(this.Size.Y-y) + x)*4;
return new Color(rgba[pixelPos+0], rgba[pixelPos+1], rgba[pixelPos+2], rgba[pixelPos+3]);

GL.BindTexture(TextureTarget.Texture2D, curTexture2D);

var pixelPos = (loaded.Size.X * (loaded.Size.Y - y - 1) + x) * 4;
var color = new Color(buffer[pixelPos+0], buffer[pixelPos+1], buffer[pixelPos+2], buffer[pixelPos+3]);
ArrayPool<byte>.Shared.Return(buffer);
return color;
}
}

1 change: 1 addition & 0 deletions Robust.Client/Graphics/Clyde/Shaders/base-default.frag
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
varying highp vec2 UV;
varying highp vec2 UV2;
varying highp vec2 Pos;
varying highp vec4 VtxModulate;

5 changes: 4 additions & 1 deletion Robust.Client/Graphics/Clyde/Shaders/base-default.vert
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions Robust.Client/Graphics/Clyde/Shaders/base-raw.frag
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
varying highp vec2 UV;
varying highp vec2 UV2;

uniform sampler2D lightMap;

5 changes: 4 additions & 1 deletion Robust.Client/Graphics/Clyde/Shaders/base-raw.vert
Original file line number Diff line number Diff line change
@@ -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]

41 changes: 6 additions & 35 deletions Robust.Client/Graphics/Drawing/DrawingHandleBase.cs
Original file line number Diff line number Diff line change
@@ -114,43 +114,12 @@ public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ReadOnlySpan
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)
{
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)
Color colorLinear = Color.FromSrgb(color);
for (var i = 0; i < output.Length; i++)
{
// Odd number of vertices. Handle the last manually.
output[lastPos] = new DrawVertexUV2DColor(input[lastPos], new Vector2(0.5f, 0.5f), colorLinear);
output[i] = new DrawVertexUV2DColor(input[i], new Vector2(0.5f, 0.5f), colorLinear);
}
}

@@ -268,6 +237,8 @@ public struct DrawVertexUV2DColor
{
public Vector2 Position;
public Vector2 UV;
public Vector2 UV2;

/// <summary>
/// Modulation colour for this vertex.
/// Note that this color is in linear space.
2 changes: 1 addition & 1 deletion Robust.Client/Graphics/Shaders/ShaderPrototype.cs
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@
namespace Robust.Client.Graphics
{
[Prototype("shader")]
public sealed class ShaderPrototype : IPrototype, ISerializationHooks
public sealed partial class ShaderPrototype : IPrototype, ISerializationHooks
{
[ViewVariables]
[IdDataField]
2 changes: 1 addition & 1 deletion Robust.Client/Placement/Modes/AlignSimilar.cs
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)

var snapToEntities = EntitySystem.Get<EntityLookupSystem>().GetEntitiesInRange(MouseCoords, SnapToRange)
.Where(entity => pManager.EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype == pManager.CurrentPrototype && pManager.EntityManager.GetComponent<TransformComponent>(entity).MapID == mapId)
.OrderBy(entity => (pManager.EntityManager.GetComponent<TransformComponent>(entity).WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager)).LengthSquared())
.OrderBy(entity => (pManager.EntityManager.GetComponent<TransformComponent>(entity).WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager, pManager.EntityManager.System<SharedTransformSystem>())).LengthSquared())
.ToList();

if (snapToEntities.Count == 0)
3 changes: 2 additions & 1 deletion Robust.Client/Placement/Modes/AlignSnapgridBorder.cs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;

namespace Robust.Client.Placement.Modes
@@ -24,7 +25,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
SnapSize = 1f;
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
Grid = pManager.MapManager.GetGrid(gridId);
Grid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
SnapSize = Grid.TileSize; //Find snap size for the grid.
}
else
2 changes: 1 addition & 1 deletion Robust.Client/Placement/Modes/AlignSnapgridCenter.cs
Original file line number Diff line number Diff line change
@@ -56,7 +56,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
SnapSize = 1f;
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
Grid = pManager.MapManager.GetGrid(gridId);
Grid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
SnapSize = Grid.TileSize; //Find snap size for the grid.
}
else
3 changes: 2 additions & 1 deletion Robust.Client/Placement/Modes/AlignTileAny.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Numerics;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;

namespace Robust.Client.Placement.Modes
{
@@ -19,7 +20,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)

var gridId = MouseCoords.GetGridUid(pManager.EntityManager);

if (!pManager.MapManager.TryGetGrid(gridId, out var mapGrid))
if (!pManager.EntityManager.TryGetComponent<MapGridComponent>(gridId, out var mapGrid))
return;

CurrentTile = mapGrid.GetTileRef(MouseCoords);
3 changes: 2 additions & 1 deletion Robust.Client/Placement/Modes/AlignTileDense.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;

namespace Robust.Client.Placement.Modes
{
@@ -20,7 +21,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)

if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
var mapGrid = pManager.MapManager.GetGrid(gridId);
var mapGrid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
tileSize = mapGrid.TileSize; //convert from ushort to float
}

3 changes: 2 additions & 1 deletion Robust.Client/Placement/Modes/AlignTileEmpty.cs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;

namespace Robust.Client.Placement.Modes
@@ -22,7 +23,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)

if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
var mapGrid = pManager.MapManager.GetGrid(gridId);
var mapGrid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
CurrentTile = mapGrid.GetTileRef(MouseCoords);
tileSize = mapGrid.TileSize; //convert from ushort to float
}
3 changes: 2 additions & 1 deletion Robust.Client/Placement/Modes/AlignTileNonDense.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;

namespace Robust.Client.Placement.Modes
{
@@ -20,7 +21,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
var mapGrid = pManager.MapManager.GetGrid(gridId);
var mapGrid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
tileSize = mapGrid.TileSize; //convert from ushort to float
}

9 changes: 7 additions & 2 deletions Robust.Client/Placement/PlacementManager.cs
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
using Robust.Shared.Utility;
using Robust.Shared.Log;
using Direction = Robust.Shared.Maths.Direction;
using Robust.Shared.Map.Components;

namespace Robust.Client.Placement
{
@@ -79,6 +80,10 @@ public bool IsActive
private set
{
_isActive = value;

if (CurrentPermission?.UseEditorContext is false)
return;

SwitchEditorContext(value);
}
}
@@ -332,7 +337,7 @@ private void HandlePlacementMessage(MsgPlacement msg)

private void HandleTileChanged(ref TileChangedEvent args)
{
var coords = MapManager.GetGrid(args.NewTile.GridUid).GridTileToLocal(args.NewTile.GridIndices);
var coords = EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid).GridTileToLocal(args.NewTile.GridIndices);
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
}

@@ -753,7 +758,7 @@ private void RequestPlacement(EntityCoordinates coordinates)
// If we have actually placed something on a valid grid...
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
var grid = MapManager.GetGrid(gridId);
var grid = EntityManager.GetComponent<MapGridComponent>(gridId);

// no point changing the tile to the same thing.
if (grid.GetTileRef(coordinates).Tile.TypeId == CurrentPermission.TileType)
23 changes: 15 additions & 8 deletions Robust.Client/Placement/PlacementMode.cs
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Utility;
@@ -115,11 +116,12 @@ public virtual void Render(DrawingHandleWorld handle)

var dirAng = pManager.Direction.ToAngle();
var spriteSys = pManager.EntityManager.System<SpriteSystem>();
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
foreach (var coordinate in locationcollection)
{
if (!coordinate.IsValid(pManager.EntityManager))
return; // Just some paranoia just in case
var worldPos = coordinate.ToMapPos(pManager.EntityManager);
var worldPos = coordinate.ToMapPos(pManager.EntityManager, transformSys);
var worldRot = pManager.EntityManager.GetComponent<TransformComponent>(coordinate.EntityId).WorldRotation + dirAng;

sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
@@ -136,11 +138,12 @@ public IEnumerable<EntityCoordinates> LineCoordinates()
{
var mouseScreen = pManager.InputManager.MouseScreenPosition;
var mousePos = pManager.EyeManager.PixelToMap(mouseScreen);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();

if (mousePos.MapId == MapId.Nullspace)
yield break;

var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, pManager.EntityManager) - pManager.StartPoint;
var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
float iterations;
Vector2 distance;
if (Math.Abs(x) > Math.Abs(y))
@@ -167,11 +170,12 @@ public IEnumerable<EntityCoordinates> GridCoordinates()
{
var mouseScreen = pManager.InputManager.MouseScreenPosition;
var mousePos = pManager.EyeManager.PixelToMap(mouseScreen);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();

if (mousePos.MapId == MapId.Nullspace)
yield break;

var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, pManager.EntityManager) - pManager.StartPoint;
var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;

var xSign = Math.Sign(placementdiff.X);
var ySign = Math.Sign(placementdiff.Y);
@@ -193,9 +197,9 @@ public IEnumerable<EntityCoordinates> GridCoordinates()
public TileRef GetTileRef(EntityCoordinates coordinates)
{
var gridUidOpt = coordinates.GetGridUid(pManager.EntityManager);
return gridUidOpt is EntityUid gridUid && gridUid.IsValid() ? pManager.MapManager.GetGrid(gridUid).GetTileRef(MouseCoords)
return gridUidOpt is EntityUid gridUid && gridUid.IsValid() ? pManager.EntityManager.GetComponent<MapGridComponent>(gridUid).GetTileRef(MouseCoords)
: new TileRef(gridUidOpt ?? EntityUid.Invalid,
MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager), Tile.Empty);
MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager, pManager.EntityManager.System<SharedTransformSystem>()), Tile.Empty);
}

public TextureResource GetSprite(string key)
@@ -223,15 +227,17 @@ public bool RangeCheck(EntityCoordinates coordinates)
}

var range = pManager.CurrentPermission!.Range;
if (range > 0 && !pManager.EntityManager.GetComponent<TransformComponent>(controlled).Coordinates.InRange(pManager.EntityManager, coordinates, range))
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
if (range > 0 && !pManager.EntityManager.GetComponent<TransformComponent>(controlled).Coordinates.InRange(pManager.EntityManager, transformSys, coordinates, range))
return false;
return true;
}

public bool IsColliding(EntityCoordinates coordinates)
{
var bounds = pManager.ColliderAABB;
var mapCoords = coordinates.ToMap(pManager.EntityManager);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var mapCoords = coordinates.ToMap(pManager.EntityManager, transformSys);
var (x, y) = mapCoords.Position;

var collisionBox = Box2.FromDimensions(
@@ -261,7 +267,8 @@ protected EntityCoordinates ScreenToCursorGrid(ScreenCoordinates coords)
return EntityCoordinates.FromMap(pManager.MapManager, mapCoords);
}

return EntityCoordinates.FromMap(pManager.EntityManager, gridUid, mapCoords);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
return EntityCoordinates.FromMap(gridUid, mapCoords, transformSys, pManager.EntityManager);
}
}
}
23 changes: 19 additions & 4 deletions Robust.Client/Replays/Loading/ReplayLoadManager.Checkpoints.cs
Original file line number Diff line number Diff line change
@@ -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 @@ TimeSpan GetTime(GameTick tick)
UpdatePlayerStates(curState.PlayerStates.Span, playerStates);
UpdateEntityStates(curState.EntityStates.Span, entStates, ref spawnedTracker, ref stateTracker, detached);
UpdateMessages(messages[i], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase);
ProcessQueue(curState.ToSequence, detachQueue, detached);
ProcessQueue(curState.ToSequence, detachQueue, detached, entStates);
UpdateDeletions(curState.EntityDeletions, entStates, detached);
serverTime[i] = GetTime(curState.ToSequence) - initialTime;
ticksSinceLastCheckpoint++;
@@ -176,14 +177,28 @@ TimeSpan GetTime(GameTick tick)
private void ProcessQueue(
GameTick curTick,
Dictionary<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?");
}
}
}
}

30 changes: 14 additions & 16 deletions Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs
Original file line number Diff line number Diff line change
@@ -129,39 +129,37 @@ public async Task<ReplayData> LoadReplayAsync(IReplayFileReader fileReader, Load
return parsed.FirstOrDefault()?.Root as MappingDataNode;
}

private (MappingDataNode YamlData, HashSet<string> CVars, TimeSpan Duration, TimeSpan StartTime, bool ClientSide)
private (MappingDataNode YamlData, HashSet<string> CVars, TimeSpan? Duration, TimeSpan StartTime, bool ClientSide)
LoadMetadata(IReplayFileReader fileReader)
{
_sawmill.Info($"Reading replay metadata");
var data = LoadYamlMetadata(fileReader);
if (data == null)
throw new Exception("Failed to load yaml metadata");

TimeSpan duration;
var finalData = LoadYamlFinalMetadata(fileReader);
if (finalData == null)
{
var msg = "Failed to load final yaml metadata";
if (!_confMan.GetCVar(CVars.ReplayIgnoreErrors))
throw new Exception(msg);
TimeSpan? duration = finalData == null
? null
: TimeSpan.Parse(((ValueDataNode) finalData[MetaFinalKeyDuration]).Value);

_sawmill.Error(msg);
duration = TimeSpan.FromDays(1);
}
else
{
duration = TimeSpan.Parse(((ValueDataNode) finalData[MetaFinalKeyDuration]).Value);
}
if (finalData == null)
_sawmill.Warning("Failed to load final yaml metadata. Partial/incomplete replay?");

var typeHash = Convert.FromHexString(((ValueDataNode) data[MetaKeyTypeHash]).Value);
var typeHashString = ((ValueDataNode) data[MetaKeyTypeHash]).Value;
var typeHash = Convert.FromHexString(typeHashString);
var stringHash = Convert.FromHexString(((ValueDataNode) data[MetaKeyStringHash]).Value);
var startTick = ((ValueDataNode) data[MetaKeyStartTick]).Value;
var timeBaseTick = ((ValueDataNode) data[MetaKeyBaseTick]).Value;
var timeBaseTimespan = ((ValueDataNode) data[MetaKeyBaseTime]).Value;
var clientSide = bool.Parse(((ValueDataNode) data[MetaKeyIsClientRecording]).Value);

if (!typeHash.SequenceEqual(_serializer.GetSerializableTypesHash()))
throw new Exception($"{nameof(IRobustSerializer)} hashes do not match. Loading replays using a bad replay-client version?");
{
if (!_confMan.GetCVar(CVars.ReplayIgnoreErrors))
throw new Exception($"RobustSerializer hash mismatch. do not match. Client hash: {_serializer.GetSerializableTypesHashString()}, replay hash: {typeHashString}.");

_sawmill.Warning($"RobustSerializer hash mismatch. Replay may fail to load!");
}

using var stringFile = fileReader.Open(FileStrings);
var stringData = new byte[stringFile.Length];
19 changes: 9 additions & 10 deletions Robust.Client/Replays/Playback/ReplayPlaybackManager.Checkpoint.cs
Original file line number Diff line number Diff line change
@@ -79,32 +79,31 @@ private void EnsureDetachedExist(CheckpointState checkpoint)
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;

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;
}
}
5 changes: 3 additions & 2 deletions Robust.Client/Replays/UI/ReplayControlWidget.cs
Original file line number Diff line number Diff line change
@@ -90,6 +90,7 @@ protected override void FrameUpdate(FrameEventArgs args)
var maxIndex = Math.Max(1, replay.States.Count - 1);
var state = replay.States[index];
var replayTime = TimeSpan.FromSeconds(TickSlider.Value);
var end = replay.Duration == null ? "N/A" : replay.Duration.Value.ToString(TimeFormat);

IndexLabel.Text = Loc.GetString("replay-time-box-index-label",
("current", index), ("total", maxIndex), ("percentage", percentage));
@@ -98,10 +99,10 @@ protected override void FrameUpdate(FrameEventArgs args)
("current", state.ToSequence), ("total", replay.States[^1].ToSequence), ("percentage", percentage));

TimeLabel.Text = Loc.GetString("replay-time-box-replay-time-label",
("current", replayTime.ToString(TimeFormat)), ("end", replay.Duration.ToString(TimeFormat)), ("percentage", percentage));
("current", replayTime.ToString(TimeFormat)), ("end", end), ("percentage", percentage));

var serverTime = (replayTime + replay.StartTime).ToString(TimeFormat);
var duration = (replay.Duration + replay.StartTime).ToString(TimeFormat);
string duration = replay.Duration == null ? "N/A" : (replay.Duration + replay.StartTime).Value.ToString(TimeFormat);
ServerTimeLabel.Text = Loc.GetString("replay-time-box-server-time-label",
("current", serverTime), ("end", duration), ("percentage", percentage));

34 changes: 28 additions & 6 deletions Robust.Client/ResourceManagement/ResourceCache.Preload.cs
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
@@ -142,6 +143,26 @@ private void PreloadRsis(ISawmill sawmill)
}
});

// Do not meta-atlas RSIs with custom load parameters.
var atlasList = rsiList.Where(x => x.LoadParameters == TextureLoadParameters.Default).ToArray();
var nonAtlasList = rsiList.Where(x => x.LoadParameters != TextureLoadParameters.Default).ToArray();

foreach (var data in nonAtlasList)
{
if (data.Bad)
continue;

try
{
RSIResource.LoadTexture(Clyde, data);
}
catch (Exception e)
{
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
}
}

// This combines individual RSI atlases into larger atlases to reduce draw batches. currently this is a VERY
// lazy bundling and is not at all compact, its basically an atlas of RSI atlases. Really what this should
// try to do is to have each RSI write directly to the atlas, rather than having each RSI write to its own
@@ -155,7 +176,7 @@ private void PreloadRsis(ISawmill sawmill)
// TODO allow RSIs to opt out (useful for very big & rare RSIs)
// TODO combine with (non-rsi) texture atlas?

Array.Sort(rsiList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
Array.Sort(atlasList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));

// Each RSI sub atlas has a different size.
// Even if we iterate through them once to estimate total area, I have NFI how to sanely estimate an optimal square-texture size.
@@ -167,9 +188,9 @@ private void PreloadRsis(ISawmill sawmill)
Vector2i offset = default;
int finalized = -1;
int atlasCount = 0;
for (int i = 0; i < rsiList.Length; i++)
for (int i = 0; i < atlasList.Length; i++)
{
var rsi = rsiList[i];
var rsi = atlasList[i];
if (rsi.Bad)
continue;

@@ -200,14 +221,14 @@ private void PreloadRsis(ISawmill sawmill)
var height = offset.Y + deltaY;
var croppedSheet = new Image<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 @@ void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
}

sawmill.Debug(
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountErrored} errored) in {LoadTime}",
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountNotAtlas} not atlassed, {CountErrored} errored) in {LoadTime}",
rsiList.Length,
atlasCount,
nonAtlasList.Length,
errors,
sw.Elapsed);

16 changes: 11 additions & 5 deletions Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
Original file line number Diff line number Diff line change
@@ -40,17 +40,21 @@ public override void Load(IDependencyCollection dependencies, ResPath path)
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 @@ internal static void LoadPreTexture(IResourceManager manager, LoadStepData data)
data.FrameSize = frameSize;
data.DimX = dimensionX;
data.CallbackOffsets = callbackOffsets;
data.LoadParameters = metadata.LoadParameters;
}

internal static void LoadPostTexture(LoadStepData data)
@@ -380,6 +385,7 @@ internal sealed class LoadStepData
public Texture AtlasTexture = default!;
public Vector2i AtlasOffset;
public RSI Rsi = default!;
public TextureLoadParameters LoadParameters;
}

internal struct StateReg
2 changes: 1 addition & 1 deletion Robust.Client/Robust.Client.csproj
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@
<PackageReference Include="Robust.Natives" />
<PackageReference Include="System.Numerics.Vectors" />
<PackageReference Include="TerraFX.Interop.Windows" PrivateAssets="compile" />
<PackageReference Condition="'$(FullRelease)' != 'True'" Include="JetBrains.Profiler.Api" PrivateAssets="compile" />
<PackageReference Condition="'$(RobustToolsBuild)' == 'True'" Include="JetBrains.Profiler.Api" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.Sodium" PrivateAssets="compile" />
<PackageReference Include="Microsoft.NET.ILLink.Tasks" />
<PackageReference Include="TerraFX.Interop.Xlib" />
2 changes: 1 addition & 1 deletion Robust.Client/UserInterface/Control.Animations.cs
Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@ private void ProcessAnimations(FrameEventArgs args)
toRemove.Add(key);
}

foreach (var key in toRemove)
foreach (var key in toRemove.Span)
{
_playingAnimations.Remove(key);
AnimationCompleted?.Invoke(key);
132 changes: 132 additions & 0 deletions Robust.Client/UserInterface/Control.Layout.Styling.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
36 changes: 29 additions & 7 deletions Robust.Client/UserInterface/Control.Layout.cs
Original file line number Diff line number Diff line change
@@ -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,13 +52,18 @@ public partial class Control
[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
{
get => _margin;
set
{
_margin = value;
SetLayoutStyleProp(LayoutStyleProperties.Margin);
InvalidateMeasure();
}
}
@@ -242,6 +253,7 @@ public HAlignment HorizontalAlignment
set
{
_horizontalAlignment = value;
SetLayoutStyleProp(LayoutStyleProperties.HorizontalAlignment);
InvalidateArrange();
}
}
@@ -258,6 +270,7 @@ public VAlignment VerticalAlignment
set
{
_verticalAlignment = value;
SetLayoutStyleProp(LayoutStyleProperties.VerticalAlignment);
InvalidateArrange();
}
}
@@ -276,6 +289,7 @@ public bool HorizontalExpand
set
{
_horizontalExpand = value;
SetLayoutStyleProp(LayoutStyleProperties.HorizontalExpand);
Parent?.InvalidateMeasure();
}
}
@@ -294,6 +308,7 @@ public bool VerticalExpand
set
{
_verticalExpand = value;
SetLayoutStyleProp(LayoutStyleProperties.VerticalExpand);
Parent?.InvalidateArrange();
}
}
@@ -318,6 +333,7 @@ public float SizeFlagsStretchRatio

_sizeFlagsStretchRatio = value;

SetLayoutStyleProp(LayoutStyleProperties.StretchRatio);
Parent?.InvalidateArrange();
}
}
@@ -394,6 +410,7 @@ public float MinWidth
set
{
_minWidth = value;
SetLayoutStyleProp(LayoutStyleProperties.MinWidth);
InvalidateMeasure();
}
}
@@ -408,6 +425,7 @@ public float MinHeight
set
{
_minHeight = value;
SetLayoutStyleProp(LayoutStyleProperties.MinHeight);
InvalidateMeasure();
}
}
@@ -422,6 +440,7 @@ public float SetWidth
set
{
_setWidth = value;
SetLayoutStyleProp(LayoutStyleProperties.SetWidth);
InvalidateMeasure();
}
}
@@ -436,6 +455,7 @@ public float SetHeight
set
{
_setHeight = value;
SetLayoutStyleProp(LayoutStyleProperties.SetHeight);
InvalidateMeasure();
}
}
@@ -450,6 +470,7 @@ public float MaxWidth
set
{
_maxWidth = value;
SetLayoutStyleProp(LayoutStyleProperties.MaxWidth);
InvalidateMeasure();
}
}
@@ -464,6 +485,7 @@ public float MaxHeight
set
{
_maxHeight = value;
SetLayoutStyleProp(LayoutStyleProperties.MaxHeight);
InvalidateMeasure();
}
}
1 change: 1 addition & 0 deletions Robust.Client/UserInterface/Control.Styling.cs
Original file line number Diff line number Diff line change
@@ -239,6 +239,7 @@ internal void DoStyleUpdate()

protected virtual void StylePropertiesChanged()
{
UpdateLayoutStyleProperties();
InvalidateMeasure();
}

6 changes: 5 additions & 1 deletion Robust.Client/UserInterface/Control.cs
Original file line number Diff line number Diff line change
@@ -641,7 +641,11 @@ public void RemoveAllChildren()

foreach (var child in Children.ToArray())
{
RemoveChild(child);
// This checks fails in some obscure cases like using the element inspector in the dev window.
// Why? Well I could probably spend 15 minutes in a debugger to find out,
// but I'd probably still end up with this fix.
if (child.Parent == this)
RemoveChild(child);
}
}

1 change: 0 additions & 1 deletion Robust.Client/UserInterface/Controllers/UIController.cs
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@

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
1 change: 1 addition & 0 deletions Robust.Client/UserInterface/Controls/CheckBox.cs
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ public CheckBox()
TextureRect = new TextureRect
{
StyleClasses = { StyleClassCheckBox },
VerticalAlignment = VAlignment.Center,
};
hBox.AddChild(TextureRect);

Loading

0 comments on commit 5c7d4ad

Please sign in to comment.