Compare commits

..

4 Commits

Author SHA1 Message Date
PJB3005
e2cf359c7a Version: 252.0.2 2025-09-19 09:17:32 +02:00
Skye
30b07b7060 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:32 +02:00
PJB3005
977bb7385c Version: 252.0.1 2025-09-14 14:55:57 +02:00
PJB3005
8de8032904 Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

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

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

    Move CEF cache out of data directory

    Don't want content messing with this...

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

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

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

    Update SpaceWizards.NFluidSynth to 0.2.2

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

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
2025-09-14 14:55:56 +02:00
329 changed files with 3704 additions and 9601 deletions

View File

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

View File

@@ -71,6 +71,6 @@
</PropertyGroup>
<Exec
Condition="'$(_RobustUseExternalMSBuild)' == 'true'"
Command="&quot;$(DOTNET_HOST_PATH)&quot; msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false /p:IntermediateOutputPath=&quot;$(IntermediateOutputPath.TrimEnd('\'))/&quot;"/>
Command="&quot;$(DOTNET_HOST_PATH)&quot; msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
</Target>
</Project>

View File

@@ -1,4 +1,4 @@
# Release notes for RobustToolbox.
# Release notes for RobustToolbox.
<!--
NOTE: automatically updated sometimes by version.py.
@@ -54,325 +54,10 @@ END TEMPLATE-->
*None yet*
## 261.2.1
## 252.0.2
## 261.2.0
### New features
* Implement IEquatable for ResolvedPathSpecifier & ResolvedCollectionSpecifier.
* Add NearestChunkEnumerator.
### Bugfixes
* Fix static entities not having the center of mass updated.
* Fix TryQueueDelete.
* Fix tpto potentially parenting grids to non-map entities.
### Other
* TileChangedEvent is now raised once in clientside grid state handling rather than per tile.
* Removed ITileDefinition.ID as it was redundant.
* Change the lifestage checks on predicted entity deletion to check for terminating.
### Internal
* Update some `GetComponentName<T>` uses to generic.
## 261.1.0
### New features
* Automatically create logger sawmills for `UIController`s similar to `EntitySystem`s.
### Bugfixes
* Fix physics forces not auto-clearing / respecting the cvar.
### Internal
* Cleanup more compiler warnings in unit tests.
## 261.0.0
### Breaking changes
* Remove unused TryGetContainingContainer override.
* Stop recursive FrameUpdates for controls that are not visible.
* Initialize LocMgr earlier in the callstack for GameController.
* Fix FastNoiseLise fractal bounding and remove its DataField property as it should be derived on other properties updating.
* Make RaiseMoveEvent internal.
* MovedGridsComponent and PhysicsMapComponent are now purged and properties on `SharedPhysicsSystem`. Additionally the TransformComponent for Awake entities is stored alongside the PhysicsComponent for them.
* TransformComponent is now stored on physics contacts.
* Gravity2DComponent and Gravity2DController were moved to SharedPhysicsSystem.
### New features
* `IFileDialogManager` now allows specifying `FileAccess` and `FileShare` modes.
* Add Intersects and Enlarged to Box2i in line with Box2.
* Make `KeyFrame`s on `AnimationTrackProperty` public settable.
* Add the spawned entities to a returned array from `SpawnEntitiesAttachedTo`.
### Bugfixes
* Fixed SDL3 file dialog implementation having a memory leak and not opening files read-write.
* Fix GetMapLinearVelocity.
### Other
* `uploadfile` and `loadprototype` commands now only open files with read access.
* Optimize `ToMapCoordinates`.
### Internal
* Cleanup on internals of `IFileDialogManager`, removing duplicate code.
* Fix Contacts not correctly being marked as `Touching` while contact is ongoing.
## 260.2.0
### New features
* Add `StringBuilder.Insert(int, string)` to sandbox.
* Add the WorldNormal to the StartCollideEvent.
## 260.1.0
### New features
* `ComponentFactory` is now exposed to `EntitySystem` as `Factory`
### Other
* Cleanup warnings in PLacementManager
* Cleanup warnings in Clide.Sprite
## 260.0.0
### Breaking changes
* Fix / change `StartCollideEvent.WorldPoint` to return all points for the collision which may be up to 2 instead of 1.
### New features
* Add SpriteSystem dependency to VisualizerSystem.
* Add Vertical property to progress bars
* Add some `EntProtoId` overloads for group entity spawn methods.
## 259.0.0
### Breaking changes
* TileChangedEvent now has an array of tile changed entries rather than raising an individual event for every single tile changed.
### Other
* `Entity<T>` methods were marked as `readonly` as appropriate.
## 258.0.1
### Bugfixes
* Fix static physics bodies not generating contacts if they spawn onto sleeping bodies.
## 258.0.0
### Breaking changes
* `IMarkupTag` and related methods in `MarkupTagManager` have been obsoleted and should be replaced with the new `IMarkupTagHandler` interface. Various engine tags (e.g., `BoldTag`, `ColorTag`, etc) no longer implement the old interface.
### New features
* Add IsValidPath to ResPath and make some minor performance improvements.
### Bugfixes
* OutputPanel and RichTextLabel now remove controls associated with rich text tags when the text is updated.
* Fix `SpriteComponent.Visible` datafield not being read from yaml.
* Fix container state handling not forcing inserts.
### Other
* `SpriteSystem.LayerMapReserve()` no longer throws an exception if the specified layer already exists. This makes it behave like the obsoleted `SpriteComponent.LayerMapReserveBlank()`.
## 257.0.2
### Bugfixes
* Fix unshaded sprite layers not rendering correctly.
## 257.0.1
### Bugfixes
* Fix sprite layer bounding box calculations. This was causing various sprite rendering & render-tree lookup issues.
## 257.0.0
### Breaking changes
* The client will now automatically pause any entities that leave their PVS range.
* Contacts for terminating entities no longer raise wake events.
### New features
* Added `IPrototypeManager.IsIgnored()` for checking whether a given prototype kind has been marked as ignored via `RegisterIgnore()`.
* Added `PoolManager` & `TestPair` classes to `Robust.UnitTesting`. These classes make it easier to create & use pooled server/client instance pairs in integration tests.
* Catch NotYamlSerializable DataFields with an analyzer.
* Optimized RSI preloading and texture atlas creation.
### Bugfixes
* Fix clients unintentionally un-pausing paused entities that re-enter pvs range
### Other
* The yaml prototype id serialiser now provides better feedback when trying to validate an id for a prototype kind that has been ignored via `IPrototypeManager.RegisterIgnore()`
* Several SpriteComponent methods have been marked as obsolete, and should be replaced with new methods in SpriteSystem.
* Rotation events no longer check for grid traversal.
## 256.0.0
### Breaking changes
* `ITypeReaderWriter<TType, TNode>` has been removed due to being unused. Implement `ITypeSerializer<TType, TNode>` instead
* Moved AsNullable extension methods to the Entity struct.
### New features
* Add DevWindow tab to show all loaded textures.
* Add Vector2i / bitmask converfsion helpers.
* Allow texture preload to be skipped for some textures.
* Check audio file signatures instead of extensions.
* Add CancellationTokenRegistration to sandbox.
* Add the ability to serialize TimeSpan from text.
* Add support for rotated / mirrored tiles.
### Bugfixes
* Fix yaml hot reloading.
* Fix a linear dictionary lookup in PlacementManager.
### Other
* Make ItemList not run deselection callback on all items if they aren't selected.
* Cleanup warnings for CS0649 & CS0414.
### Internal
* Move PointLight component states to shared.
## 255.1.0
### New features
* The client localisation manager now supports hot-reloading ftl files.
* TransformSystem can now raise `GridUidChangedEvent` and `MapUidChangedEvent` when a entity's grid or map changes. This event is only raised if the `ExtraTransformEvents` metadata flag is enabled.
### Bugfixes
* Fixed a server crash due to a `NullReferenceException` in PVS system when a player's local entity is also one of their view subscriptions.
* Fix CompileRobustXamlTask for benchmarks.
* .ftl files will now hot reload.
* Fix placementmanager sometimes not clearing.
### Other
* Container events are now documented.
## 255.0.0
### Breaking changes
* `RobustIntegrationTest` now pools server/client instances by default. If a custom settings class is provided, it will still disable pooling unless explicitly enabled.
* Server/Client instances that are returned to the pool should be disconnected. This might require you to update some tests.
* Pooled instances also require you to use `RobustIntegrationTest` methods like `WaitPost()` to ensure the correct thread is used.
### Bugfixes
* Fix `EntityDeserializer` improperly setting entity lifestages when loading a post-mapinit map.
* Fix `EntityManager.PredictedDeleteEntity()` not deleting pure client-side entities.
* Fix grid fixtures using a locale dependent id. This could cause some clients to crash/freeze when connected to a server with a different locale.
### Other
* Add logic to block cycles in master MIDI renderers, which could otherwise cause client freezes.
## 254.1.0
### New features
* Add CC ND licences to the RGA validator.
* Add entity spawn prediction and entity deletion prediction. This is currently limited as you are unable to predict interactions with these entities. These are done via the new methods prefixed with "Predicted". You can also manually flag an entity as a predicted spawn with the `FlagPredicted` method which will clean it up when prediction is reset.
### Bugfixes
* Fix tile edge rendering for neighbor tiles being the same priority.
### Other
* Fix SpawnAttachedTo's system proxy method not the rotation arg like EntityManager.
## 254.0.0
### Breaking changes
* Yaml mappings/dictionaries now only support string keys instead of generic nodes
* Several MappingDataNode method arguments or return values now use strings instead of a DataNode object
* The MappingDataNode class has various helper methods that still accept a ValueDataNode, but these methods are marked as obsolete and may be removed in the future.
* yaml validators should use `MappingDataNode.GetKeyNode()` when validating mapping keys, so that errors can print node start & end information
* ValueTuple yaml serialization has changed
* Previously they would get serialized into a single mapping with one entry (i.e., `{foo : bar }`)
* Now they serialize into a sequence (i.e., `[foo, bar]`)
* The ValueTuple serializer will still try to read mappings, but due to the MappingDataNode this may fail if the previously serialized "key" can't be read as a simple string
### New features
* Add cvar to disable tile edges.
* Add GetContainingContainers method to ContainerSystem to recursively get containers upwards on an entity.
### Internal
* Make component lifecycle methods use generics.
## 253.0.0
### New features
* Add a new `SerializationManager.PushComposition()` overload that takes in a single parent instead of an array of parents.
* `BoundUserInterfaceMessageAttempt` once again gets raised as a broadcast event, in addition to being directed.
* This effectively reverts the breaking part of the changes made in v252.0.0
* Fix CreateDistanceJoint using an int instead of a float for minimum distance.
### Bugfixes
* Fix deferred component removal not setting the component's life stage to `ComponentLifeStage.Stopped` if the component has not yet been initialised.
* Fix some `EntitySystem.Resolve()` overloads not respecting the optional `logMissing` argument.
* Fix screen-space overlays not being useable without first initializing/starting entity manager & systems
* ItemList is now significantly optimized. VV's `AddComponent` window in particular should be much faster.
* Fix some more MapValidator fields.
* Fix popup text overflowing the sides of the screen.
* Improve location reporting for non-writeable datafields via analyzer.
### Other
* TestPoint now uses generics rather than IPhysShape directly.
## 252.0.1
## 252.0.0

View File

@@ -1,7 +1,5 @@
### Localization for engine console commands
cmd-hint-float = [float]
## generic command errors
cmd-invalid-arg-number-error = Invalid number of arguments.

View File

@@ -9,7 +9,6 @@ entity-spawn-window-override-menu-tooltip = Override placement
## TileSpawnWindow
tile-spawn-window-title = Place Tiles
tile-spawn-window-mirror-button-text = Mirror Tiles
## Console

View File

@@ -1,10 +0,0 @@
## "Textures" dev window tab
dev-window-tab-textures-title = Textures
dev-window-tab-textures-reload = Reload
dev-window-tab-textures-filter = Filter
dev-window-tab-textures-summary = Total (est): { $bytes }
dev-window-tab-textures-info = Width: { $width } Height: { $height }
PixelType: { $pixelType } sRGB: { $srgb }
Name: { $name }
Est. memory usage: { $bytes }

View File

@@ -21,53 +21,47 @@ public sealed class DataDefinitionAnalyzerTest
},
};
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
private const string TestTypeDefs = """
using System;
namespace Robust.Shared.ViewVariables
{
public sealed class ViewVariablesAttribute : Attribute
{
public readonly VVAccess Access = VVAccess.ReadOnly;
public ViewVariablesAttribute() { }
public ViewVariablesAttribute(VVAccess access)
{
Access = access;
}
}
public enum VVAccess : byte
{
ReadOnly = 0,
ReadWrite = 1,
}
}
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
public sealed class NotYamlSerializableAttribute : Attribute;
}
""";
[Test]
public async Task NoVVReadOnlyTest()
public async Task Test()
{
const string code = """
using System;
using Robust.Shared.ViewVariables;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.ViewVariables
{
public sealed class ViewVariablesAttribute : Attribute
{
public readonly VVAccess Access = VVAccess.ReadOnly;
public ViewVariablesAttribute() { }
public ViewVariablesAttribute(VVAccess access)
{
Access = access;
}
}
public enum VVAccess : byte
{
ReadOnly = 0,
ReadWrite = 1,
}
}
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
}
[DataDefinition]
public sealed partial class Foo
{
@@ -89,86 +83,8 @@ public sealed class DataDefinitionAnalyzerTest
""";
await Verifier(code,
// /0/Test0.cs(7,17): info RA0028: Data field Bad in data definition Foo has ViewVariables attribute with ReadWrite access, which is redundant
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(7, 17, 7, 50).WithArguments("Bad", "Foo")
);
}
[Test]
public async Task ReadOnlyFieldTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[DataDefinition]
public sealed partial class Foo
{
[DataField]
public readonly int Bad;
[DataField]
public int Good;
}
""";
await Verifier(code,
// /0/Test0.cs(7,12): error RA0019: Data field Bad in data definition Foo is readonly
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(7, 12, 7, 20).WithArguments("Bad", "Foo")
);
}
[Test]
public async Task ReadOnlyPropertyTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[DataDefinition]
public sealed partial class Foo
{
[DataField]
public int Bad { get; }
[DataField]
public int Good { get; private set; }
}
""";
await Verifier(code,
// /0/Test0.cs(7,20): error RA0020: Data field property Bad in data definition Foo does not have a setter
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldPropertyWritableRule).WithSpan(7, 20, 7, 28).WithArguments("Bad", "Foo")
);
}
[Test]
public async Task NotYamlSerializableTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[NotYamlSerializable]
public sealed class NotSerializableClass { }
[DataDefinition]
public sealed partial class Foo
{
[DataField]
public NotSerializableClass BadField;
[DataField]
public NotSerializableClass BadProperty { get; set; }
public NotSerializableClass GoodField; // Not a DataField, not a problem
public NotSerializableClass GoodProperty { get; set; } // Not a DataField, not a problem
}
""";
await Verifier(code,
// /0/Test0.cs(10,12): error RA0033: Data field BadField in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(10, 12, 10, 32).WithArguments("BadField", "Foo", "NotSerializableClass"),
// /0/Test0.cs(13,12): error RA0033: Data field BadProperty in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(13, 12, 13, 32).WithArguments("BadProperty", "Foo", "NotSerializableClass")
// /0/Test0.cs(35,17): info RA0028: Data field Bad in data definition Foo has ViewVariables attribute with ReadWrite access, which is redundant
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(35, 17, 35, 50).WithArguments("Bad", "Foo")
);
}
}

View File

@@ -1,86 +0,0 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ObsoleteInheritanceAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
/// <summary>
/// Analyzer that implements <c>[ObsoleteInheritance]</c> checking, to give obsoletion warnings for inheriting types
/// that should never have been virtual.
/// </summary>
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class ObsoleteInheritanceAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<ObsoleteInheritanceAnalyzer, DefaultVerifier>()
{
TestState =
{
Sources = { code },
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task TestBasic()
{
const string code = """
using Robust.Shared.Analyzers;
[ObsoleteInheritance]
public class Base;
public class NotAllowed : Base;
""";
await Verifier(code,
// /0/Test0.cs(6,14): warning RA0034: Type 'NotAllowed' inherits from 'Base', which has obsoleted inheriting from itself
VerifyCS.Diagnostic(ObsoleteInheritanceAnalyzer.Rule).WithSpan(6, 14, 6, 24).WithArguments("NotAllowed", "Base")
);
}
[Test]
public async Task TestMessage()
{
const string code = """
using Robust.Shared.Analyzers;
[ObsoleteInheritance("Sus")]
public class Base;
public class NotAllowed : Base;
""";
await Verifier(code,
// /0/Test0.cs(6,14): warning RA0034: Type 'NotAllowed' inherits from 'Base', which has obsoleted inheriting from itself: "Sus"
VerifyCS.Diagnostic(ObsoleteInheritanceAnalyzer.RuleWithMessage).WithSpan(6, 14, 6, 24).WithArguments("NotAllowed", "Base", "Sus")
);
}
[Test]
public async Task TestNormal()
{
const string code = """
public class Base;
public class AllowedAllowed : Base;
""";
await Verifier(code);
}
}

View File

@@ -14,7 +14,6 @@
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferOtherTypeAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ForbidLiteralAttribute.cs" LogicalName="Robust.Shared.Analyzers.ForbidLiteralAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ObsoleteInheritanceAttribute.cs" LogicalName="Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\GameObjects\EventBusAttributes.cs" LogicalName="Robust.Shared.GameObjects.EventBusAttributes.cs" LinkBase="Implementations" />
</ItemGroup>

View File

@@ -18,7 +18,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute";
private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute";
private const string ViewVariablesNamespace = "Robust.Shared.ViewVariables.ViewVariablesAttribute";
private const string NotYamlSerializableName = "Robust.Shared.Serialization.Manager.Attributes.NotYamlSerializableAttribute";
private const string DataFieldAttributeName = "DataField";
private const string ViewVariablesAttributeName = "ViewVariables";
@@ -42,7 +41,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to mark any type containing a nested data definition as partial."
);
public static readonly DiagnosticDescriptor DataFieldWritableRule = new(
private static readonly DiagnosticDescriptor DataFieldWritableRule = new(
Diagnostics.IdDataFieldWritable,
"Data field must not be readonly",
"Data field {0} in data definition {1} is readonly",
@@ -52,7 +51,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to remove the readonly modifier."
);
public static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
private static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
Diagnostics.IdDataFieldPropertyWritable,
"Data field property must have a setter",
"Data field property {0} in data definition {1} does not have a setter",
@@ -82,19 +81,9 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to remove the ViewVariables attribute."
);
public static readonly DiagnosticDescriptor DataFieldYamlSerializableRule = new(
Diagnostics.IdDataFieldYamlSerializable,
"Data field type is not YAML serializable",
"Data field {0} in data definition {1} is type {2}, which is not YAML serializable",
"Usage",
DiagnosticSeverity.Error,
true,
"Make sure to use a type that is YAML serializable."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule,
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule, DataFieldYamlSerializableRule
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule
);
public override void Initialize(AnalysisContext context)
@@ -160,8 +149,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (IsReadOnlyDataField(type, fieldSymbol))
{
TryGetModifierLocation(field, SyntaxKind.ReadOnlyKeyword, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, location, fieldSymbol.Name, type.Name));
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, context.Node.GetLocation(), fieldSymbol.Name, type.Name));
}
if (HasRedundantTag(fieldSymbol))
@@ -175,19 +163,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
TryGetAttributeLocation(field, ViewVariablesAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, fieldSymbol.Name, type.Name));
}
if (context.SemanticModel.GetSymbolInfo(field.Declaration.Type).Symbol is not ITypeSymbol fieldTypeSymbol)
continue;
if (IsNotYamlSerializable(fieldSymbol, fieldTypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
(context.Node as FieldDeclarationSyntax)?.Declaration.Type.GetLocation(),
fieldSymbol.Name,
type.Name,
fieldTypeSymbol.MetadataName
));
}
}
}
@@ -210,8 +185,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (IsReadOnlyDataField(type, propertySymbol))
{
var location = property.AccessorList != null ? property.AccessorList.GetLocation() : property.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, location, propertySymbol.Name, type.Name));
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, context.Node.GetLocation(), propertySymbol.Name, type.Name));
}
if (HasRedundantTag(propertySymbol))
@@ -225,19 +199,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
TryGetAttributeLocation(property, ViewVariablesAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, propertySymbol.Name, type.Name));
}
if (context.SemanticModel.GetSymbolInfo(property.Type).Symbol is not ITypeSymbol propertyTypeSymbol)
return;
if (IsNotYamlSerializable(propertySymbol, propertyTypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
(context.Node as PropertyDeclarationSyntax)?.Type.GetLocation(),
propertySymbol.Name,
type.Name,
propertyTypeSymbol.Name
));
}
}
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
@@ -324,20 +285,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return false;
}
private static bool TryGetModifierLocation(MemberDeclarationSyntax syntax, SyntaxKind modifierKind, out Location location)
{
foreach (var modifier in syntax.Modifiers)
{
if (modifier.IsKind(modifierKind))
{
location = modifier.GetLocation();
return true;
}
}
location = syntax.GetLocation();
return false;
}
private static bool IsReadOnlyMember(ITypeSymbol type, ISymbol member)
{
if (member is IFieldSymbol field)
@@ -420,14 +367,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return (VVAccess)accessByte == VVAccess.ReadWrite;
}
private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type)
{
if (!IsDataField(field, out _, out _))
return false;
return HasAttribute(type, NotYamlSerializableName);
}
private static bool IsImplicitDataDefinition(ITypeSymbol type)
{
if (HasAttribute(type, ImplicitDataDefinitionNamespace))

View File

@@ -1,75 +0,0 @@
#nullable enable
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ObsoleteInheritanceAnalyzer : DiagnosticAnalyzer
{
private const string Attribute = "Robust.Shared.Analyzers.ObsoleteInheritanceAttribute";
public static readonly DiagnosticDescriptor Rule = new(
Diagnostics.IdObsoleteInheritance,
"Parent type has obsoleted inheritance",
"Type '{0}' inherits from '{1}', which has obsoleted inheriting from itself",
"Usage",
DiagnosticSeverity.Warning,
true);
public static readonly DiagnosticDescriptor RuleWithMessage = new(
Diagnostics.IdObsoleteInheritanceWithMessage,
"Parent type has obsoleted inheritance",
"Type '{0}' inherits from '{1}', which has obsoleted inheriting from itself: \"{2}\"",
"Usage",
DiagnosticSeverity.Warning,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule, RuleWithMessage];
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolAction(CheckClass, SymbolKind.NamedType);
}
private static void CheckClass(SymbolAnalysisContext context)
{
if (context.Symbol is not INamedTypeSymbol typeSymbol)
return;
if (typeSymbol.IsValueType || typeSymbol.BaseType is not { } baseType)
return;
if (!AttributeHelper.HasAttribute(baseType, Attribute, out var data))
return;
var location = context.Symbol.Locations[0];
if (GetMessageFromAttributeData(data) is { } message)
{
context.ReportDiagnostic(Diagnostic.Create(
RuleWithMessage,
location,
[typeSymbol.Name, baseType.Name, message]));
}
else
{
context.ReportDiagnostic(Diagnostic.Create(
Rule,
location,
[typeSymbol.Name, baseType.Name]));
}
}
private static string? GetMessageFromAttributeData(AttributeData data)
{
if (data.ConstructorArguments is not [var message, ..])
return null;
return message.Value as string;
}
}

View File

@@ -42,8 +42,8 @@ public class RecursiveMoveBenchmark : RobustIntegrationTest
public void GlobalSetup()
{
ProgramShared.PathOffset = "../../../../";
var server = StartServer(new() {Pool = false});
var client = StartClient(new() {Pool = false});
var server = StartServer();
var client = StartClient();
Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait();

View File

@@ -13,7 +13,7 @@ namespace Robust.Client.Animations
/// </summary>
public abstract class AnimationTrackProperty : AnimationTrack
{
public List<KeyFrame> KeyFrames { get; set; } = new();
public List<KeyFrame> KeyFrames { get; protected set; } = new();
/// <summary>
/// How to interpolate values when between two keyframes.

View File

@@ -226,9 +226,6 @@ internal sealed class MidiRenderer : IMidiRenderer
if (value == _master)
return;
if (CheckMasterCycle(value))
throw new InvalidOperationException("Tried to set master to a child of this renderer!");
if (_master is { Disposed: false })
{
try
@@ -732,22 +729,4 @@ internal sealed class MidiRenderer : IMidiRenderer
_synth?.Dispose();
_player?.Dispose();
}
/// <summary>
/// Check that a given renderer is not already a child of this renderer, i.e. it would introduce a cycle if set as master of this renderer.
/// </summary>
private bool CheckMasterCycle(IMidiRenderer? otherRenderer)
{
// Doesn't inside drift, cringe.
while (otherRenderer != null)
{
if (otherRenderer == this)
return true;
otherRenderer = otherRenderer.Master;
}
return false;
}
}

View File

@@ -10,7 +10,6 @@ using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.HWId;
using Robust.Client.Input;
using Robust.Client.Localization;
using Robust.Client.Map;
using Robust.Client.Placement;
using Robust.Client.Player;
@@ -37,7 +36,6 @@ using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics;
@@ -106,8 +104,6 @@ namespace Robust.Client
deps.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
deps.Register<NetworkResourceManager>();
deps.Register<IReloadManager, ReloadManager>();
deps.Register<ILocalizationManager, ClientLocalizationManager>();
deps.Register<ILocalizationManagerInternal, ClientLocalizationManager>();
switch (mode)
{

View File

@@ -2,7 +2,6 @@ using System.Numerics;
using Robust.Client.GameObjects;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
@@ -10,7 +9,25 @@ namespace Robust.Client.ComponentTrees;
public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent, SpriteComponent>
{
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpriteComponent, QueueSpriteTreeUpdateEvent>(OnQueueUpdate);
}
private void OnQueueUpdate(EntityUid uid, SpriteComponent component, ref QueueSpriteTreeUpdateEvent args)
=> QueueTreeUpdate(uid, component, args.Xform);
// TODO remove this when finally ECSing sprite components
[ByRefEvent]
internal readonly struct QueueSpriteTreeUpdateEvent
{
public readonly TransformComponent Xform;
public QueueSpriteTreeUpdateEvent(TransformComponent xform)
{
Xform = xform;
}
}
#region Component Tree Overrides
protected override bool DoFrameUpdate => true;
@@ -19,11 +36,6 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
protected override int InitialCapacity => 1024;
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
{
// TODO SPRITE optimize this
// Because the just take the BB of the rotated BB, I'mt pretty sure we do a lot of unnecessary maths.
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
}
=> entry.Component.CalculateRotatedBoundingBox(pos, rot, default).CalcBoundingBox();
#endregion
}

View File

@@ -1,19 +1,17 @@
#if DEBUG
using Robust.Client.Debugging;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.GameObjects;
namespace Robust.Client.Console.Commands
{
public sealed class DebugAnchoredCommand : LocalizedEntityCommands
public sealed class DebugAnchoredCommand : LocalizedCommands
{
[Dependency] private readonly DebugAnchoringSystem _system = default!;
public override string Command => "showanchored";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_system.Enabled ^= true;
EntitySystem.Get<DebugAnchoringSystem>().Enabled ^= true;
}
}
}

View File

@@ -1,19 +1,17 @@
#if DEBUG
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.GameObjects;
namespace Robust.Client.Console.Commands
{
internal sealed class LightDebugCommand : LocalizedEntityCommands
internal sealed class LightDebugCommand : LocalizedCommands
{
[Dependency] private readonly DebugLightTreeSystem _system = default!;
public override string Command => "lightbb";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_system.Enabled ^= true;
EntitySystem.Get<DebugLightTreeSystem>().Enabled ^= true;
}
}
}

View File

@@ -1,18 +1,19 @@
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
public sealed class ShowPlayerVelocityCommand : LocalizedEntityCommands
public sealed class ShowPlayerVelocityCommand : LocalizedCommands
{
[Dependency] private readonly ShowPlayerVelocityDebugSystem _system = default!;
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
public override string Command => "showplayervelocity";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_system.Enabled ^= true;
_entitySystems.GetEntitySystem<ShowPlayerVelocityDebugSystem>().Enabled ^= true;
}
}
}

View File

@@ -31,7 +31,6 @@ using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
@@ -95,7 +94,6 @@ namespace Robust.Client
[Dependency] private readonly IReplayRecordingManagerInternal _replayRecording = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IReloadManager _reload = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private IWebViewManagerHook? _webViewHook;
@@ -160,7 +158,6 @@ namespace Robust.Client
}
_serializationManager.Initialize();
_loc.Initialize();
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);

View File

@@ -1,123 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed partial class ClientEntityManager
{
public override EntityUid PredictedSpawnAttachedTo(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
{
var ent = SpawnAttachedTo(protoName, coordinates, overrides, rotation);
FlagPredicted(ent);
return ent;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override EntityUid PredictedSpawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true)
{
var ent = Spawn(protoName, overrides, doMapInit);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
{
var ent = Spawn(protoName, coordinates, overrides, rotation);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
{
var ent = SpawnAtPosition(protoName, coordinates, overrides);
FlagPredicted(ent);
return ent;
}
public override bool PredictedTrySpawnNextTo(
string? protoName,
EntityUid target,
[NotNullWhen(true)] out EntityUid? uid,
TransformComponent? xform = null,
ComponentRegistry? overrides = null)
{
if (!TrySpawnNextTo(protoName, target, out uid, xform, overrides))
return false;
FlagPredicted(uid.Value);
return true;
}
public override bool PredictedTrySpawnInContainer(
string? protoName,
EntityUid containerUid,
string containerId,
[NotNullWhen(true)] out EntityUid? uid,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
if (!TrySpawnInContainer(protoName, containerUid, containerId, out uid, containerComp, overrides))
return false;
FlagPredicted(uid.Value);
return true;
}
public override EntityUid PredictedSpawnNextToOrDrop(string? protoName, EntityUid target, TransformComponent? xform = null, ComponentRegistry? overrides = null)
{
var ent = SpawnNextToOrDrop(protoName, target, xform, overrides);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
var ent = SpawnInContainerOrDrop(protoName, containerUid, containerId, xform, containerComp, overrides);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
out bool inserted,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
var ent = SpawnInContainerOrDrop(protoName,
containerUid,
containerId,
out inserted,
xform,
containerComp,
overrides);
FlagPredicted(ent);
return ent;
}
public override void FlagPredicted(Entity<MetaDataComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp))
return;
DebugTools.Assert(IsClientSide(ent.Owner, ent.Comp));
EnsureComponent<PredictedSpawnComponent>(ent.Owner);
// TODO: Need to map call site or something, needs to be consistent between client and server.
}
}

View File

@@ -291,54 +291,5 @@ namespace Robust.Client.GameObjects
}
}
#endregion
/// <inheritdoc />
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
}
// So there's 3 scenarios:
// 1. Networked entity we just move to nullspace and rely on state handling.
// 2. Clientside predicted entity we delete and rely on state handling.
// 3. Clientside only entity that actually needs deleting here.
if (ent.Comp1.NetEntity.IsClientSide())
{
DeleteEntity(ent, ent.Comp1, ent.Comp2);
}
else
{
_xforms.DetachEntity(ent, ent.Comp2);
}
}
/// <inheritdoc />
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (IsQueuedForDeletion(ent.Owner)
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
}
if (ent.Comp1.NetEntity.IsClientSide())
{
// client-side QueueDeleteEntity re-fetches MetadataComp and checks IsClientSide().
// base call to skip that.
// TODO create override that takes in metadata comp
base.QueueDeleteEntity(ent);
}
else
{
_xforms.DetachEntity(ent.Owner, ent.Comp2);
}
}
}
}

View File

@@ -1,11 +1,9 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects
{
[Obsolete]
public partial interface IRenderableComponent : IComponent
{
int DrawDepth { get; set; }

View File

@@ -1,5 +1,6 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;

View File

@@ -10,21 +10,21 @@ using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
public sealed class ShowSpriteBBCommand : LocalizedEntityCommands
public sealed class ShowSpriteBBCommand : LocalizedCommands
{
[Dependency] private readonly SpriteBoundsSystem _system = default!;
public override string Command => "showspritebb";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_system.Enabled ^= true;
EntitySystem.Get<SpriteBoundsSystem>().Enabled ^= true;
}
}
public sealed class SpriteBoundsSystem : EntitySystem
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
private SpriteBoundsOverlay? _overlay;
@@ -40,7 +40,7 @@ namespace Robust.Client.GameObjects
if (_enabled)
{
DebugTools.AssertNull(_overlay);
_overlay = new SpriteBoundsOverlay(EntityManager);
_overlay = new SpriteBoundsOverlay(_spriteTree, _entityManager);
_overlayManager.AddOverlay(_overlay);
}
else
@@ -55,13 +55,18 @@ namespace Robust.Client.GameObjects
private bool _enabled;
}
public sealed class SpriteBoundsOverlay(IEntityManager entMan) : Overlay
public sealed class SpriteBoundsOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly SharedTransformSystem _xformSystem = entMan.System<SharedTransformSystem>();
private readonly SpriteSystem _spriteSystem = entMan.System<SpriteSystem>();
private readonly SpriteTreeSystem _renderTree = entMan.System<SpriteTreeSystem>();
private readonly IEntityManager _entityManager;
private SpriteTreeSystem _renderTree;
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, IEntityManager entityManager)
{
_renderTree = renderTree;
_entityManager = entityManager;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
@@ -69,11 +74,10 @@ namespace Robust.Client.GameObjects
var currentMap = args.MapId;
var viewport = args.WorldBounds;
foreach (var entry in _renderTree.QueryAabb(currentMap, viewport))
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
{
var (sprite, xform) = entry;
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
var bounds = _spriteSystem.CalculateBounds((entry.Uid, sprite), worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
// Get scaled down bounds used to indicate the "south" of a sprite.
var localBound = bounds.Box;

View File

@@ -14,9 +14,7 @@ namespace Robust.Client.GameObjects
private EntityQuery<AnimationPlayerComponent> _playerQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
#pragma warning disable CS0414
[Dependency] private readonly IComponentFactory _compFact = default!;
#pragma warning restore CS0414
public override void Initialize()
{

View File

@@ -1,14 +1,15 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Serialization;
using static Robust.Shared.Containers.ContainerManagerComponent;
namespace Robust.Client.GameObjects
@@ -57,7 +58,7 @@ namespace Robust.Client.GameObjects
if (!RemoveExpectedEntity(meta.NetEntity, out var container))
return;
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container, force: true);
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container);
}
public override void ShutdownContainer(BaseContainer container)
@@ -231,7 +232,7 @@ namespace Robust.Client.GameObjects
return;
}
Insert(message.Entity, container, force: true);
Insert(message.Entity, container);
}
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)
@@ -302,7 +303,7 @@ namespace Robust.Client.GameObjects
while (parent.IsValid() && (!spriteOccluded || !lightOccluded))
{
var parentXform = TransformQuery.GetComponent(parent);
if (TryComp<ContainerManagerComponent>(parent, out var manager) && TryGetContainingContainer(parent, child, out var container, manager))
if (TryComp<ContainerManagerComponent>(parent, out var manager) && manager.TryGetContainer(child, out var container))
{
spriteOccluded = spriteOccluded || !container.ShowContents;
lightOccluded = lightOccluded || container.OccludesLight;
@@ -343,7 +344,7 @@ namespace Robust.Client.GameObjects
var childLightOccluded = lightOccluded;
// We already know either sprite or light is not occluding so need to check container.
if (TryGetContainingContainer(entity, child, out var container, manager))
if (manager.TryGetContainer(child, out var container))
{
childSpriteOccluded = childSpriteOccluded || !container.ShowContents;
childLightOccluded = childLightOccluded || container.OccludesLight;

View File

@@ -2,24 +2,24 @@ using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.GameObjects;
public sealed class DebugEntityLookupCommand : LocalizedEntityCommands
public sealed class DebugEntityLookupCommand : LocalizedCommands
{
[Dependency] private readonly DebugEntityLookupSystem _system = default!;
public override string Command => "togglelookup";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_system.Enabled ^= true;
EntitySystem.Get<DebugEntityLookupSystem>().Enabled ^= true;
}
}

View File

@@ -26,10 +26,10 @@ namespace Robust.Client.GameObjects
if (_enabled)
{
_lightOverlay = new DebugLightOverlay(
EntityManager.System<EntityLookupSystem>(),
EntitySystem.Get<EntityLookupSystem>(),
IoCManager.Resolve<IEyeManager>(),
IoCManager.Resolve<IMapManager>(),
EntityManager.System<LightTreeSystem>());
Get<LightTreeSystem>());
overlayManager.AddOverlay(_lightOverlay);
}

View File

@@ -62,7 +62,7 @@ public sealed class EyeSystem : SharedEyeSystem
eyeComponent.Target = null;
}
eyeComponent.Eye.Position = TransformSystem.GetMapCoordinates(xform);
eyeComponent.Eye.Position = xform.MapPosition;
}
}
}

View File

@@ -16,7 +16,6 @@ namespace Robust.Client.GameObjects
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PointLightComponent, ComponentGetState>(OnLightGetState);
SubscribeLocalEvent<PointLightComponent, ComponentInit>(HandleInit);
SubscribeLocalEvent<PointLightComponent, ComponentHandleState>(OnLightHandleState);
}

View File

@@ -1,149 +0,0 @@
using System;
using System.Linq;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects;
// This partial class contains code related to updating a sprites bounding boxes and its position in the sprite tree.
public sealed partial class SpriteSystem
{
/// <summary>
/// Get a sprite's local bounding box. The returned bounds do factor in the sprite's scale but not the rotation or
/// offset.
/// </summary>
public Box2 GetLocalBounds(Entity<SpriteComponent> sprite)
{
if (!sprite.Comp.BoundsDirty)
{
DebugTools.Assert(sprite.Comp.Layers.All(x => !x.BoundsDirty || !x.Drawn));
return sprite.Comp._bounds;
}
var bounds = new Box2();
foreach (var layer in sprite.Comp.Layers)
{
if (layer.Drawn)
bounds = bounds.Union(GetLocalBounds(layer));
}
sprite.Comp._bounds = bounds;
sprite.Comp.BoundsDirty = false;
return sprite.Comp._bounds;
}
/// <summary>
/// Get a layer's local bounding box relative to its owning sprite. Unlike the sprite variant of this method, this
/// does account for the layer's rotation and offset.
/// </summary>
public Box2 GetLocalBounds(Layer layer)
{
if (!layer.BoundsDirty)
{
DebugTools.Assert(layer.Bounds.EqualsApprox(CalculateLocalBounds(layer)));
return layer.Bounds;
}
layer.Bounds = CalculateLocalBounds(layer);
layer.BoundsDirty = false;
return layer.Bounds;
}
internal Box2 CalculateLocalBounds(Layer layer)
{
var textureSize = (Vector2) layer.PixelSize / EyeManager.PixelsPerMeter;
var longestSide = MathF.Max(textureSize.X, textureSize.Y);
var longestRotatedSide = Math.Max(longestSide, (textureSize.X + textureSize.Y) / MathF.Sqrt(2));
Vector2 size;
var sprite = layer.Owner.Comp;
// If this layer has any form of arbitrary rotation, return a bounding box big enough to cover
// any possible rotation.
if (layer._rotation != 0)
{
size = new Vector2(longestRotatedSide, longestRotatedSide);
return Box2.CenteredAround(layer.Offset, size * layer._scale);
}
var snapToCardinals = sprite.SnapCardinals;
if (sprite.GranularLayersRendering && layer.RenderingStrategy != LayerRenderingStrategy.UseSpriteStrategy)
{
snapToCardinals = layer.RenderingStrategy == LayerRenderingStrategy.SnapToCardinals;
}
if (snapToCardinals)
{
// Snapping to cardinals only makes sense for 1-directional layers/sprites
DebugTools.Assert(layer._actualState == null || layer._actualState.RsiDirections == RsiDirectionType.Dir1);
// We won't know the actual direction it snaps to, so we ahve to assume the box is given by the longest side.
size = new Vector2(longestSide, longestSide);
return Box2.CenteredAround(layer.Offset, size * layer._scale);
}
// Build the bounding box based on how many directions the sprite has
size = (layer._actualState?.RsiDirections) switch
{
RsiDirectionType.Dir4 => new Vector2(longestSide, longestSide),
RsiDirectionType.Dir8 => new Vector2(longestRotatedSide, longestRotatedSide),
_ => textureSize
};
return Box2.CenteredAround(layer.Offset, size * layer._scale);
}
/// <summary>
/// Gets a sprite's bounding box in world coordinates.
/// </summary>
public Box2Rotated CalculateBounds(Entity<SpriteComponent> sprite, Vector2 worldPos, Angle worldRot, Angle eyeRot)
{
// fast check for invisible sprites
if (!sprite.Comp.Visible || sprite.Comp.Layers.Count == 0)
return new Box2Rotated(new Box2(worldPos, worldPos), Angle.Zero, worldPos);
// We need to modify world rotation so that it lies between 0 and 2pi.
// This matters for 4 or 8 directional sprites deciding which quadrant (octant?) they lie in.
// the 0->2pi convention is set by the sprite-rendering code that selects the layers.
// See RenderInternal().
worldRot = worldRot.Reduced();
if (worldRot.Theta < 0)
worldRot = new Angle(worldRot.Theta + Math.Tau);
// Next, what we do is take the box2 and apply the sprite's transform, and then the entity's transform. We
// could do this via Matrix3.TransformBox, but that only yields bounding boxes. So instead we manually
// transform our box by the combination of these matrices:
var finalRotation = sprite.Comp.NoRotation
? sprite.Comp.Rotation - eyeRot
: sprite.Comp.Rotation + worldRot;
var bounds = GetLocalBounds(sprite);
// slightly faster path if offset == 0 (true for 99.9% of sprites)
if (sprite.Comp.Offset == Vector2.Zero)
return new Box2Rotated(bounds.Translated(worldPos), finalRotation, worldPos);
var adjustedOffset = sprite.Comp.NoRotation
? (-eyeRot).RotateVec(sprite.Comp.Offset)
: worldRot.RotateVec(sprite.Comp.Offset);
var position = adjustedOffset + worldPos;
return new Box2Rotated(bounds.Translated(position), finalRotation, position);
}
private void DirtyBounds(Entity<SpriteComponent> sprite)
{
sprite.Comp.BoundsDirty = true;
foreach (var layer in sprite.Comp.Layers)
{
layer.BoundsDirty = true;
}
}
}

View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using System.Linq;
namespace Robust.Client.GameObjects;
@@ -45,66 +41,4 @@ public sealed partial class SpriteSystem
layer.AnimationTimeLeft = (float) -(time % state.TotalDelay);
layer.AnimationFrame = 0;
}
public void CopySprite(Entity<SpriteComponent?> source, Entity<SpriteComponent?> target)
{
if (!Resolve(source.Owner, ref source.Comp))
return;
if (!Resolve(target.Owner, ref target.Comp))
return;
target.Comp._baseRsi = source.Comp._baseRsi;
target.Comp._bounds = source.Comp._bounds;
target.Comp._visible = source.Comp._visible;
target.Comp.color = source.Comp.color;
target.Comp.offset = source.Comp.offset;
target.Comp.rotation = source.Comp.rotation;
target.Comp.scale = source.Comp.scale;
target.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
in target.Comp.offset,
in target.Comp.rotation,
in target
.Comp.scale);
target.Comp.drawDepth = source.Comp.drawDepth;
target.Comp.NoRotation = source.Comp.NoRotation;
target.Comp.DirectionOverride = source.Comp.DirectionOverride;
target.Comp.EnableDirectionOverride = source.Comp.EnableDirectionOverride;
target.Comp.Layers = new List<SpriteComponent.Layer>(source.Comp.Layers.Count);
foreach (var otherLayer in source.Comp.Layers)
{
var layer = new SpriteComponent.Layer(otherLayer, target.Comp);
layer.Index = target.Comp.Layers.Count;
layer.Owner = target!;
target.Comp.Layers.Add(layer);
}
target.Comp.IsInert = source.Comp.IsInert;
target.Comp.LayerMap = source.Comp.LayerMap.ShallowClone();
target.Comp.PostShader = source.Comp.PostShader is {Mutable: true}
? source.Comp.PostShader.Duplicate()
: source.Comp.PostShader;
target.Comp.RenderOrder = source.Comp.RenderOrder;
target.Comp.GranularLayersRendering = source.Comp.GranularLayersRendering;
DirtyBounds(target!);
_tree.QueueTreeUpdate(target!);
}
/// <summary>
/// Adds a sprite to a queue that will update <see cref="SpriteComponent.IsInert"/> next frame.
/// </summary>
public void QueueUpdateIsInert(Entity<SpriteComponent> sprite)
{
if (sprite.Comp._inertUpdateQueued)
return;
sprite.Comp._inertUpdateQueued = true;
_inertUpdateQueue.Enqueue(sprite);
}
[Obsolete("Use QueueUpdateIsInert")]
public void QueueUpdateInert(EntityUid uid, SpriteComponent sprite) => QueueUpdateIsInert(new (uid, sprite));
}

View File

@@ -1,153 +1,11 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
// This partial class contains various public helper methods, including methods for extracting textures/icons from
// sprite specifiers and entity prototypes.
public sealed partial class SpriteSystem
{
private readonly Dictionary<string, IRsiStateLike> _cachedPrototypeIcons = new();
public Texture Frame0(EntityPrototype prototype)
{
return GetPrototypeIcon(prototype).Default;
}
public Texture Frame0(SpriteSpecifier specifier)
{
return RsiStateLike(specifier).Default;
}
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
{
switch (specifier)
{
case SpriteSpecifier.Texture tex:
return GetTexture(tex);
case SpriteSpecifier.Rsi rsi:
return GetState(rsi);
case SpriteSpecifier.EntityPrototype prototypeIcon:
return GetPrototypeIcon(prototypeIcon.EntityPrototypeId);
default:
throw new NotSupportedException();
}
}
public Texture GetIcon(IconComponent icon)
{
return GetState(icon.Icon).Frame0;
}
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method caches the result based on the prototype identifier.
/// </summary>
public IRsiStateLike GetPrototypeIcon(string prototype)
{
// Check if this prototype has been cached before, and if so return the result.
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
return cachedResult;
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
{
// The specified prototype doesn't exist, return the fallback "error" sprite.
_sawmill.Error("Failed to load PrototypeIcon {0}", prototype);
return GetFallbackState();
}
// Generate the icon and cache it in case it's ever needed again.
var result = GetPrototypeIcon(entityPrototype);
_cachedPrototypeIcons[prototype] = result;
return result;
}
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method does NOT cache the result.
/// </summary>
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
{
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
if (prototype.Components.TryGetValue("Icon", out var compData)
&& compData.Component is IconComponent icon)
{
return GetIcon(icon);
}
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
if (!prototype.Components.ContainsKey("Sprite"))
{
return GetFallbackState();
}
// Finally, we use spawn a dummy entity to get its icon.
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? GetFallbackState();
Del(dummy);
return result;
}
[Pure]
public RSI.State GetFallbackState()
{
return _resourceCache.GetFallback<RSIResource>().RSI["error"];
}
public Texture GetFallbackTexture()
{
return _resourceCache.GetFallback<TextureResource>().Texture;
}
[Pure]
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
{
if (_resourceCache.TryGetResource<RSIResource>(
TextureRoot / rsiSpecifier.RsiPath,
out var theRsi) &&
theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
{
return state;
}
_sawmill.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
return GetFallbackState();
}
public Texture GetTexture(SpriteSpecifier.Texture texSpecifier)
{
return _resourceCache
.GetResource<TextureResource>(TextureRoot / texSpecifier.TexturePath)
.Texture;
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.TryGetModified<EntityPrototype>(out var modified))
return;
// Remove all changed prototypes from the cache, if they're there.
foreach (var prototype in modified)
{
// Let's be lazy and not regenerate them until something needs them again.
_cachedPrototypeIcons.Remove(prototype);
}
}
/// <summary>
/// Gets an entity's sprite position in world terms.
/// </summary>

View File

@@ -1,257 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects;
// This partial class contains various public methods for managing a sprite's layers.
// This setter methods for modifying a layer's properties are in a separate file.
public sealed partial class SpriteSystem
{
public bool LayerExists(Entity<SpriteComponent?> sprite, int index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return false;
return index > 0 && index < sprite.Comp.Layers.Count;
}
public bool TryGetLayer(
Entity<SpriteComponent?> sprite,
int index,
[NotNullWhen(true)] out Layer? layer,
bool logMissing)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (index >= 0 && index < sprite.Comp.Layers.Count)
{
layer = sprite.Comp.Layers[index];
DebugTools.AssertEqual(layer.Owner, sprite!);
DebugTools.AssertEqual(layer.Index, index);
return true;
}
if (logMissing)
Log.Error($"Layer index '{index}' on entity {ToPrettyString(sprite)} does not exist! Trace:\n{Environment.StackTrace}");
return false;
}
public bool RemoveLayer(Entity<SpriteComponent?> sprite, int index, bool logMissing = true)
{
return RemoveLayer(sprite.Owner, index, out _, logMissing);
}
public bool RemoveLayer(
Entity<SpriteComponent?> sprite,
int index,
[NotNullWhen(true)] out Layer? layer,
bool logMissing = true)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (!TryGetLayer(sprite, index, out layer, logMissing))
return false;
sprite.Comp.Layers.RemoveAt(index);
foreach (var otherLayer in sprite.Comp.Layers[index..])
{
otherLayer.Index--;
}
// TODO SPRITE track inverse-mapping?
foreach (var (key, value) in sprite.Comp.LayerMap)
{
if (value == index)
sprite.Comp.LayerMap.Remove(key);
else if (value > index)
{
sprite.Comp.LayerMap[key]--;
}
}
layer.Owner = default;
layer.Index = -1;
#if DEBUG
foreach (var otherLayer in sprite.Comp.Layers)
{
DebugTools.AssertEqual(otherLayer, sprite.Comp.Layers[otherLayer.Index]);
}
#endif
sprite.Comp.BoundsDirty = true;
_tree.QueueTreeUpdate(sprite!);
QueueUpdateIsInert(sprite!);
return true;
}
#region AddLayer
/// <summary>
/// Add the given sprite layer. If an index is specified, this will insert the layer with the given index, resulting
/// in all other layers being reshuffled.
/// </summary>
public int AddLayer(Entity<SpriteComponent?> sprite, Layer layer, int? index = null)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
{
layer.Index = -1;
layer.Owner = default;
return -1;
}
layer.Owner = sprite!;
if (index is { } i && i != sprite.Comp.Layers.Count)
{
foreach (var otherLayer in sprite.Comp.Layers[i..])
{
otherLayer.Index++;
}
// TODO SPRITE track inverse-mapping?
sprite.Comp.Layers.Insert(i, layer);
layer.Index = i;
foreach (var (key, value) in sprite.Comp.LayerMap)
{
if (value >= i)
sprite.Comp.LayerMap[key]++;
}
}
else
{
layer.Index = sprite.Comp.Layers.Count;
sprite.Comp.Layers.Add(layer);
}
#if DEBUG
foreach (var otherLayer in sprite.Comp.Layers)
{
DebugTools.AssertEqual(otherLayer, sprite.Comp.Layers[otherLayer.Index]);
}
#endif
layer.BoundsDirty = true;
if (!layer.Blank)
{
sprite.Comp.BoundsDirty = true;
_tree.QueueTreeUpdate(sprite!);
QueueUpdateIsInert(sprite!);
}
return layer.Index;
}
/// <summary>
/// Add a layer corresponding to the given RSI state.
/// </summary>
/// <param name="sprite">The sprite</param>
/// <param name="stateId">The RSI state</param>
/// <param name="rsi">The RSI to use. If not specified, it will default to using <see cref="SpriteComponent.BaseRSI"/></param>
/// <param name="index">The layer index to use for the new sprite.</param>
/// <returns></returns>
public int AddRsiLayer(Entity<SpriteComponent?> sprite, RSI.StateId stateId, RSI? rsi = null, int? index = null)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
var layer = AddBlankLayer(sprite!, index);
if (rsi != null)
LayerSetRsi(layer, rsi, stateId);
else
LayerSetRsiState(layer, stateId);
return layer.Index;
}
/// <summary>
/// Add a layer corresponding to the given RSI state.
/// </summary>
/// <param name="sprite">The sprite</param>
/// <param name="state">The RSI state</param>
/// <param name="path">The path to the RSI.</param>
/// <param name="index">The layer index to use for the new sprite.</param>
/// <returns></returns>
public int AddRsiLayer(Entity<SpriteComponent?> sprite, RSI.StateId state, ResPath path, int? index = null)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
if (!_resourceCache.TryGetResource<RSIResource>(TextureRoot / path, out var res))
Log.Error($"Unable to load RSI '{path}'. Trace:\n{Environment.StackTrace}");
if (path.Extension != "rsi")
Log.Error($"Expected rsi path but got '{path}'?");
return AddRsiLayer(sprite, state, res?.RSI, index);
}
public int AddTextureLayer(Entity<SpriteComponent?> sprite, ResPath path, int? index = null)
{
if (_resourceCache.TryGetResource<TextureResource>(TextureRoot / path, out var texture))
return AddTextureLayer(sprite, texture?.Texture, index);
if (path.Extension == "rsi")
Log.Error($"Expected texture but got rsi '{path}', did you mean 'sprite:' instead of 'texture:'?");
Log.Error($"Unable to load texture '{path}'. Trace:\n{Environment.StackTrace}");
return AddTextureLayer(sprite, texture?.Texture, index);
}
public int AddTextureLayer(Entity<SpriteComponent?> sprite, Texture? texture, int? index = null)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
var layer = new Layer {Texture = texture};
return AddLayer(sprite, layer, index);
}
public int AddLayer(Entity<SpriteComponent?> sprite, SpriteSpecifier specifier, int? newIndex = null)
{
return specifier switch
{
SpriteSpecifier.Texture tex => AddTextureLayer(sprite, tex.TexturePath, newIndex),
SpriteSpecifier.Rsi rsi => AddRsiLayer(sprite, rsi.RsiState, rsi.RsiPath, newIndex),
_ => throw new NotImplementedException()
};
}
/// <summary>
/// Add a new sprite layer and populate it using the provided layer data.
/// </summary>
public int AddLayer(Entity<SpriteComponent?> sprite, PrototypeLayerData layerDatum, int? index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
var layer = AddBlankLayer(sprite!, index);
LayerSetData(layer, layerDatum);
return layer.Index;
}
/// <summary>
/// Add a blank sprite layer.
/// </summary>
public Layer AddBlankLayer(Entity<SpriteComponent> sprite, int? index = null)
{
var layer = new Layer();
AddLayer(sprite!, layer, index);
return layer;
}
#endregion
}

View File

@@ -1,150 +0,0 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using static Robust.Client.GameObjects.SpriteComponent;
using static Robust.Client.Graphics.RSI;
namespace Robust.Client.GameObjects;
// This partial class contains various public methods for reading a layer's properties
public sealed partial class SpriteSystem
{
#region RsiState
/// <summary>
/// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g.,
/// this might be a texture layer that does not use RSIs.
/// </summary>
public StateId LayerGetRsiState(Entity<SpriteComponent?> sprite, int index)
{
if (TryGetLayer(sprite, index, out var layer, true))
return layer.StateId;
return StateId.Invalid;
}
/// <summary>
/// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g.,
/// this might be a texture layer that does not use RSIs.
/// </summary>
public StateId LayerGetRsiState(Entity<SpriteComponent?> sprite, string key, StateId state)
{
if (TryGetLayer(sprite, key, out var layer, true))
return layer.StateId;
return StateId.Invalid;
}
/// <summary>
/// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g.,
/// this might be a texture layer that does not use RSIs.
/// </summary>
public StateId LayerGetRsiState(Entity<SpriteComponent?> sprite, Enum key, StateId state)
{
if (TryGetLayer(sprite, key, out var layer, true))
return layer.StateId;
return StateId.Invalid;
}
#endregion
#region RsiState
/// <summary>
/// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this
/// will just be the base RSI of the owning sprite (<see cref="SpriteComponent.BaseRSI"/>).
/// </summary>
public RSI? LayerGetEffectiveRsi(Entity<SpriteComponent?> sprite, int index)
{
TryGetLayer(sprite, index, out var layer, true);
return layer?.ActualRsi;
}
/// <summary>
/// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this
/// will just be the base RSI of the owning sprite (<see cref="SpriteComponent.BaseRSI"/>).
/// </summary>
public RSI? LayerGetEffectiveRsi(Entity<SpriteComponent?> sprite, string key, StateId state)
{
TryGetLayer(sprite, key, out var layer, true);
return layer?.ActualRsi;
}
/// <summary>
/// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this
/// will just be the base RSI of the owning sprite (<see cref="SpriteComponent.BaseRSI"/>).
/// </summary>
public RSI? LayerGetEffectiveRsi(Entity<SpriteComponent?> sprite, Enum key, StateId state)
{
TryGetLayer(sprite, key, out var layer, true);
return layer?.ActualRsi;
}
#endregion
#region Directions
public RsiDirectionType LayerGetDirections(Entity<SpriteComponent?> sprite, int index)
{
return TryGetLayer(sprite, index, out var layer, true)
? LayerGetDirections(layer)
: RsiDirectionType.Dir1;
}
public RsiDirectionType LayerGetDirections(Entity<SpriteComponent?> sprite, Enum key)
{
return TryGetLayer(sprite, key, out var layer, true)
? LayerGetDirections(layer)
: RsiDirectionType.Dir1;
}
public RsiDirectionType LayerGetDirections(Entity<SpriteComponent?> sprite, string key)
{
return TryGetLayer(sprite, key, out var layer, true)
? LayerGetDirections(layer)
: RsiDirectionType.Dir1;
}
public RsiDirectionType LayerGetDirections(Layer layer)
{
if (!layer.StateId.IsValid)
return RsiDirectionType.Dir1;
// Pull texture from RSI state instead.
if (layer.ActualRsi is not {} rsi || !rsi.TryGetState(layer.StateId, out var state))
return RsiDirectionType.Dir1;
return state.RsiDirections;
}
public int LayerGetDirectionCount(Entity<SpriteComponent?> sprite, int index)
{
return TryGetLayer(sprite, index, out var layer, true) ? LayerGetDirectionCount(layer) : 1;
}
public int LayerGetDirectionCount(Entity<SpriteComponent?> sprite, Enum key)
{
return TryGetLayer(sprite, key, out var layer, true) ? LayerGetDirectionCount(layer) : 1;
}
public int LayerGetDirectionCount(Entity<SpriteComponent?> sprite, string key)
{
return TryGetLayer(sprite, key, out var layer, true) ? LayerGetDirectionCount(layer) : 1;
}
public int LayerGetDirectionCount(Layer layer)
{
return LayerGetDirections(layer) switch
{
RsiDirectionType.Dir1 => 1,
RsiDirectionType.Dir4 => 4,
RsiDirectionType.Dir8 => 8,
_ => throw new ArgumentOutOfRangeException()
};
}
#endregion
}

View File

@@ -1,299 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects;
// This partial class contains various public methods for manipulating layer mappings.
public sealed partial class SpriteSystem
{
/// <summary>
/// Map an enum to a layer index.
/// </summary>
public void LayerMapSet(Entity<SpriteComponent?> sprite, Enum key, int index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (index < 0 || index >= sprite.Comp.Layers.Count)
throw new ArgumentOutOfRangeException(nameof(index));
sprite.Comp.LayerMap[key] = index;
}
/// <summary>
/// Map string to a layer index. If possible, it is preferred to use an enum key.
/// string keys mainly exist to make it easier to define custom layer keys in yaml.
/// </summary>
public void LayerMapSet(Entity<SpriteComponent?> sprite, string key, int index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (index < 0 || index >= sprite.Comp.Layers.Count)
throw new ArgumentOutOfRangeException(nameof(index));
sprite.Comp.LayerMap[key] = index;
}
/// <summary>
/// Map an enum to a layer index.
/// </summary>
public void LayerMapAdd(Entity<SpriteComponent?> sprite, Enum key, int index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (index < 0 || index >= sprite.Comp.Layers.Count)
throw new ArgumentOutOfRangeException(nameof(index));
sprite.Comp.LayerMap.Add(key, index);
}
/// <summary>
/// Map a string to a layer index. If possible, it is preferred to use an enum key.
/// string keys mainly exist to make it easier to define custom layer keys in yaml.
/// </summary>
public void LayerMapAdd(Entity<SpriteComponent?> sprite, string key, int index)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (index < 0 || index >= sprite.Comp.Layers.Count)
throw new ArgumentOutOfRangeException(nameof(index));
sprite.Comp.LayerMap.Add(key, index);
}
/// <summary>
/// Remove an enum mapping.
/// </summary>
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, Enum key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return false;
return sprite.Comp.LayerMap.Remove(key);
}
/// <summary>
/// Remove a string mapping.
/// </summary>
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, string key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return false;
return sprite.Comp.LayerMap.Remove(key);
}
/// <summary>
/// Remove an enum mapping.
/// </summary>
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, Enum key, out int index)
{
if (_query.Resolve(sprite.Owner, ref sprite.Comp))
return sprite.Comp.LayerMap.Remove(key, out index);
index = 0;
return false;
}
/// <summary>
/// Remove a string mapping.
/// </summary>
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, string key, out int index)
{
if (_query.Resolve(sprite.Owner, ref sprite.Comp))
return sprite.Comp.LayerMap.Remove(key, out index);
index = 0;
return false;
}
/// <summary>
/// Attempt to resolve an enum mapping.
/// </summary>
public bool LayerMapTryGet(Entity<SpriteComponent?> sprite, Enum key, out int index, bool logMissing)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
{
index = 0;
return false;
}
if (sprite.Comp.LayerMap.TryGetValue(key, out index))
return true;
if (logMissing)
Log.Error($"Layer with key '{key}' does not exist on entity {ToPrettyString(sprite)}! Trace:\n{Environment.StackTrace}");
return false;
}
/// <summary>
/// Attempt to resolve a string mapping.
/// </summary>
public bool LayerMapTryGet(Entity<SpriteComponent?> sprite, string key, out int index, bool logMissing)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
{
index = 0;
return false;
}
if (sprite.Comp.LayerMap.TryGetValue(key, out index))
return true;
if (logMissing)
Log.Error($"Layer with key '{key}' does not exist on entity {ToPrettyString(sprite)}! Trace:\n{Environment.StackTrace}");
return false;
}
/// <summary>
/// Attempt to resolve an enum mapping.
/// </summary>
public bool TryGetLayer(Entity<SpriteComponent?> sprite, Enum key, [NotNullWhen(true)] out Layer? layer, bool logMissing)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
return LayerMapTryGet(sprite, key, out var index, logMissing)
&& TryGetLayer(sprite, index, out layer, logMissing);
}
/// <summary>
/// Attempt to resolve a string mapping.
/// </summary>
public bool TryGetLayer(Entity<SpriteComponent?> sprite, string key, [NotNullWhen(true)] out Layer? layer, bool logMissing)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
return LayerMapTryGet(sprite, key, out var index, logMissing)
&& TryGetLayer(sprite, index, out layer, logMissing);
}
public int LayerMapGet(Entity<SpriteComponent?> sprite, Enum key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
return sprite.Comp.LayerMap[key];
}
public int LayerMapGet(Entity<SpriteComponent?> sprite, string key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
return sprite.Comp.LayerMap[key];
}
public bool LayerExists(Entity<SpriteComponent?> sprite, string key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return false;
return sprite.Comp.LayerMap.TryGetValue(key, out var index)
&& LayerExists(sprite, index);
}
public bool LayerExists(Entity<SpriteComponent?> sprite, Enum key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return false;
return sprite.Comp.LayerMap.TryGetValue(key, out var index)
&& LayerExists(sprite, index);
}
/// <summary>
/// Ensures that a layer with the given key exists and return the layer's index.
/// If the layer does not yet exist, this will create and add a blank layer.
/// </summary>
public int LayerMapReserve(Entity<SpriteComponent?> sprite, Enum key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
if (LayerMapTryGet(sprite, key, out var layerIndex, false))
return layerIndex;
var layer = AddBlankLayer(sprite!);
LayerMapSet(sprite, key, layer.Index);
return layer.Index;
}
/// <inheritdoc cref="LayerMapReserve(Entity{SpriteComponent?},System.Enum)"/>
public int LayerMapReserve(Entity<SpriteComponent?> sprite, string key)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return -1;
if (LayerMapTryGet(sprite, key, out var layerIndex, false))
return layerIndex;
var layer = AddBlankLayer(sprite!);
LayerMapSet(sprite, key, layer.Index);
return layer.Index;
}
public bool RemoveLayer(Entity<SpriteComponent?> sprite, string key, bool logMissing = true)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
return false;
return RemoveLayer(sprite, index, logMissing);
}
public bool RemoveLayer(Entity<SpriteComponent?> sprite, Enum key, bool logMissing = true)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
return false;
return RemoveLayer(sprite, index, logMissing);
}
public bool RemoveLayer(
Entity<SpriteComponent?> sprite,
string key,
[NotNullWhen(true)] out Layer? layer,
bool logMissing = true)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
return false;
return RemoveLayer(sprite, index, out layer, logMissing);
}
public bool RemoveLayer(
Entity<SpriteComponent?> sprite,
Enum key,
[NotNullWhen(true)] out Layer? layer,
bool logMissing = true)
{
layer = null;
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
return false;
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
return false;
return RemoveLayer(sprite, index, out layer, logMissing);
}
}

View File

@@ -1,605 +0,0 @@
using System;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
using static Robust.Client.Graphics.RSI;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Robust.Client.GameObjects;
// This partial class contains various public methods for modifying a layer's properties.
public sealed partial class SpriteSystem
{
#region SetData
public void LayerSetData(Entity<SpriteComponent?> sprite, int index, PrototypeLayerData data)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetData(layer, data);
}
public void LayerSetData(Entity<SpriteComponent?> sprite, string key, PrototypeLayerData data)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetData(layer, data);
}
public void LayerSetData(Entity<SpriteComponent?> sprite, Enum key, PrototypeLayerData data)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetData(layer, data);
}
public void LayerSetData(Layer layer, PrototypeLayerData data)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
// TODO SPRITE ECS
layer._parent.LayerSetData(layer, data);
}
#endregion
#region SpriteSpecifier
public void LayerSetSprite(Entity<SpriteComponent?> sprite, int index, SpriteSpecifier specifier)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetSprite(layer, specifier);
}
public void LayerSetSprite(Entity<SpriteComponent?> sprite, string key, SpriteSpecifier specifier)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetSprite(layer, specifier);
}
public void LayerSetSprite(Entity<SpriteComponent?> sprite, Enum key, SpriteSpecifier specifier)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetSprite(layer, specifier);
}
public void LayerSetSprite(Layer layer, SpriteSpecifier specifier)
{
switch (specifier)
{
case SpriteSpecifier.Texture tex:
LayerSetTexture(layer, tex.TexturePath);
break;
case SpriteSpecifier.Rsi rsi:
LayerSetRsi(layer, rsi.RsiPath, rsi.RsiState);
break;
default:
throw new NotImplementedException();
}
}
#endregion
#region Texture
public void LayerSetTexture(Entity<SpriteComponent?> sprite, int index, Texture? texture)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetTexture(layer, texture);
}
public void LayerSetTexture(Entity<SpriteComponent?> sprite, string key, Texture? texture)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetTexture(layer, texture);
}
public void LayerSetTexture(Entity<SpriteComponent?> sprite, Enum key, Texture? texture)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetTexture(layer, texture);
}
public void LayerSetTexture(Layer layer, Texture? texture)
{
LayerSetRsiState(layer, StateId.Invalid, refresh: true);
layer.Texture = texture;
}
public void LayerSetTexture(Entity<SpriteComponent?> sprite, int index, ResPath path)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetTexture(layer, path);
}
public void LayerSetTexture(Entity<SpriteComponent?> sprite, string key, ResPath path)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetTexture(layer, path);
}
public void LayerSetTexture(Entity<SpriteComponent?> sprite, Enum key, ResPath path)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetTexture(layer, path);
}
private void LayerSetTexture(Layer layer, ResPath path)
{
if (!_resourceCache.TryGetResource<TextureResource>(TextureRoot / path, out var texture))
{
if (path.Extension == "rsi")
Log.Error($"Expected texture but got rsi '{path}', did you mean 'sprite:' instead of 'texture:'?");
Log.Error($"Unable to load texture '{path}'. Trace:\n{Environment.StackTrace}");
}
LayerSetTexture(layer, texture?.Texture);
}
#endregion
#region RsiState
public void LayerSetRsiState(Entity<SpriteComponent?> sprite, int index, StateId state)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetRsiState(layer, state);
}
public void LayerSetRsiState(Entity<SpriteComponent?> sprite, string key, StateId state)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsiState(layer, state);
}
public void LayerSetRsiState(Entity<SpriteComponent?> sprite, Enum key, StateId state)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsiState(layer, state);
}
public void LayerSetRsiState(Layer layer, StateId state, bool refresh = false)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer.StateId == state && !refresh)
return;
layer.StateId = state;
RefreshCachedState(layer, true, null);
_tree.QueueTreeUpdate(layer.Owner);
QueueUpdateIsInert(layer.Owner);
layer.BoundsDirty = true;
layer.Owner.Comp.BoundsDirty = true;
}
#endregion
#region Rsi
public void LayerSetRsi(Entity<SpriteComponent?> sprite, int index, RSI? rsi, StateId? state = null)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Entity<SpriteComponent?> sprite, string key, RSI? rsi, StateId? state = null)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Entity<SpriteComponent?> sprite, Enum key, RSI? rsi, StateId? state = null)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Layer layer, RSI? rsi, StateId? state = null)
{
layer._rsi = rsi;
LayerSetRsiState(layer, state ?? layer.StateId, refresh: true);
}
public void LayerSetRsi(Entity<SpriteComponent?> sprite, int index, ResPath rsi, StateId? state = null)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Entity<SpriteComponent?> sprite, string key, ResPath rsi, StateId? state = null)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Entity<SpriteComponent?> sprite, Enum key, ResPath rsi, StateId? state = null)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRsi(layer, rsi, state);
}
public void LayerSetRsi(Layer layer, ResPath rsi, StateId? state = null)
{
if (!_resourceCache.TryGetResource<RSIResource>(TextureRoot / rsi, out var res))
Log.Error($"Unable to load RSI '{rsi}' for entity {ToPrettyString(layer.Owner)}. Trace:\n{Environment.StackTrace}");
LayerSetRsi(layer, res?.RSI, state);
}
#endregion
#region Scale
public void LayerSetScale(Entity<SpriteComponent?> sprite, int index, Vector2 value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetScale(layer, value);
}
public void LayerSetScale(Entity<SpriteComponent?> sprite, string key, Vector2 value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetScale(layer, value);
}
public void LayerSetScale(Entity<SpriteComponent?> sprite, Enum key, Vector2 value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetScale(layer, value);
}
public void LayerSetScale(Layer layer, Vector2 value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer._scale.EqualsApprox(value))
return;
if (!ValidateScale(layer.Owner, value))
return;
layer._scale = value;
layer.UpdateLocalMatrix();
_tree.QueueTreeUpdate(layer.Owner);
layer.BoundsDirty = true;
layer.Owner.Comp.BoundsDirty = true;
}
#endregion
#region Rotation
public void LayerSetRotation(Entity<SpriteComponent?> sprite, int index, Angle value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetRotation(layer, value);
}
public void LayerSetRotation(Entity<SpriteComponent?> sprite, string key, Angle value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRotation(layer, value);
}
public void LayerSetRotation(Entity<SpriteComponent?> sprite, Enum key, Angle value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRotation(layer, value);
}
public void LayerSetRotation(Layer layer, Angle value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer._rotation.EqualsApprox(value))
return;
layer._rotation = value;
layer.UpdateLocalMatrix();
_tree.QueueTreeUpdate(layer.Owner);
layer.BoundsDirty = true;
layer.Owner.Comp.BoundsDirty = true;
}
#endregion
#region Offset
public void LayerSetOffset(Entity<SpriteComponent?> sprite, int index, Vector2 value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetOffset(layer, value);
}
public void LayerSetOffset(Entity<SpriteComponent?> sprite, string key, Vector2 value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetOffset(layer, value);
}
public void LayerSetOffset(Entity<SpriteComponent?> sprite, Enum key, Vector2 value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetOffset(layer, value);
}
public void LayerSetOffset(Layer layer, Vector2 value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer._offset.EqualsApprox(value))
return;
layer._offset = value;
layer.UpdateLocalMatrix();
_tree.QueueTreeUpdate(layer.Owner);
layer.BoundsDirty = true;
layer.Owner.Comp.BoundsDirty = true;
}
#endregion
#region Visible
public void LayerSetVisible(Entity<SpriteComponent?> sprite, int index, bool value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetVisible(layer, value);
}
public void LayerSetVisible(Entity<SpriteComponent?> sprite, string key, bool value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetVisible(layer, value);
}
public void LayerSetVisible(Entity<SpriteComponent?> sprite, Enum key, bool value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetVisible(layer, value);
}
public void LayerSetVisible(Layer layer, bool value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer._visible == value)
return;
layer._visible = value;
QueueUpdateIsInert(layer.Owner);
_tree.QueueTreeUpdate(layer.Owner);
layer.Owner.Comp.BoundsDirty = true;
}
#endregion
#region Color
public void LayerSetColor(Entity<SpriteComponent?> sprite, int index, Color value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetColor(layer, value);
}
public void LayerSetColor(Entity<SpriteComponent?> sprite, string key, Color value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetColor(layer, value);
}
public void LayerSetColor(Entity<SpriteComponent?> sprite, Enum key, Color value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetColor(layer, value);
}
public void LayerSetColor(Layer layer, Color value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
layer.Color = value;
}
#endregion
#region DirOffset
public void LayerSetDirOffset(Entity<SpriteComponent?> sprite, int index, DirectionOffset value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetDirOffset(layer, value);
}
public void LayerSetDirOffset(Entity<SpriteComponent?> sprite, string key, DirectionOffset value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetDirOffset(layer, value);
}
public void LayerSetDirOffset(Entity<SpriteComponent?> sprite, Enum key, DirectionOffset value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetDirOffset(layer, value);
}
public void LayerSetDirOffset(Layer layer, DirectionOffset value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
layer.DirOffset = value;
}
#endregion
#region AnimationTime
public void LayerSetAnimationTime(Entity<SpriteComponent?> sprite, int index, float value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetAnimationTime(layer, value);
}
public void LayerSetAnimationTime(Entity<SpriteComponent?> sprite, string key, float value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetAnimationTime(layer, value);
}
public void LayerSetAnimationTime(Entity<SpriteComponent?> sprite, Enum key, float value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetAnimationTime(layer, value);
}
public void LayerSetAnimationTime(Layer layer, float value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (!layer.StateId.IsValid)
return;
if (layer.ActualRsi is not { } rsi)
return;
var state = rsi[layer.StateId];
if (value > layer.AnimationTime)
{
// Handle advancing differently from going backwards.
layer.AnimationTimeLeft -= (value - layer.AnimationTime);
}
else
{
// Going backwards we re-calculate from zero.
// Definitely possible to optimize this for going backwards but I'm too lazy to figure that out.
layer.AnimationTimeLeft = -value + state.GetDelay(0);
layer.AnimationFrame = 0;
}
layer.AnimationTime = value;
layer.AdvanceFrameAnimation(state);
layer.SetAnimationTime(value);
}
#endregion
#region AutoAnimated
public void LayerSetAutoAnimated(Entity<SpriteComponent?> sprite, int index, bool value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetAutoAnimated(layer, value);
}
public void LayerSetAutoAnimated(Entity<SpriteComponent?> sprite, string key, bool value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetAutoAnimated(layer, value);
}
public void LayerSetAutoAnimated(Entity<SpriteComponent?> sprite, Enum key, bool value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetAutoAnimated(layer, value);
}
public void LayerSetAutoAnimated(Layer layer, bool value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
if (layer._autoAnimated == value)
return;
layer._autoAnimated = value;
QueueUpdateIsInert(layer.Owner);
}
#endregion
#region LayerSetRenderingStrategy
public void LayerSetRenderingStrategy(Entity<SpriteComponent?> sprite, int index, LayerRenderingStrategy value)
{
if (TryGetLayer(sprite, index, out var layer, true))
LayerSetRenderingStrategy(layer, value);
}
public void LayerSetRenderingStrategy(Entity<SpriteComponent?> sprite, string key, LayerRenderingStrategy value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRenderingStrategy(layer, value);
}
public void LayerSetRenderingStrategy(Entity<SpriteComponent?> sprite, Enum key, LayerRenderingStrategy value)
{
if (TryGetLayer(sprite, key, out var layer, true))
LayerSetRenderingStrategy(layer, value);
}
public void LayerSetRenderingStrategy(Layer layer, LayerRenderingStrategy value)
{
DebugTools.Assert(layer.Owner != default);
DebugTools.AssertNotNull(layer.Owner.Comp);
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
layer.RenderingStrategy = value;
layer.BoundsDirty = true;
layer.Owner.Comp.BoundsDirty = true;
_tree.QueueTreeUpdate(layer.Owner);
}
#endregion
/// <summary>
/// Refreshes an RSI layer's cached RSI state.
/// </summary>
private void RefreshCachedState(Layer layer, bool logErrors, RSI.State? fallback)
{
if (!layer.StateId.IsValid)
{
layer._actualState = null;
}
else if (layer.ActualRsi is not { } rsi)
{
layer._actualState = fallback ?? GetFallbackState();
if (logErrors)
Log.Error(
$"{ToPrettyString(layer.Owner)} has no RSI to pull new state from! Trace:\n{Environment.StackTrace}");
}
else if (!rsi.TryGetState(layer.StateId, out layer._actualState))
{
layer._actualState = fallback ?? GetFallbackState();
if (logErrors)
Log.Error(
$"{ToPrettyString(layer.Owner)}'s state '{layer.StateId}' does not exist in RSI {rsi.Path}. Trace:\n{Environment.StackTrace}");
}
layer.AnimationFrame = 0;
layer.AnimationTime = 0;
layer.AnimationTimeLeft = layer._actualState?.GetDelay(0) ?? 0f;
}
}

View File

@@ -1,196 +0,0 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
using Vector4 = Robust.Shared.Maths.Vector4;
using SysVec4 = System.Numerics.Vector4;
namespace Robust.Client.GameObjects;
// This partial class contains code related to actually rendering sprites.
public sealed partial class SpriteSystem
{
public void RenderSprite(
Entity<SpriteComponent> sprite,
DrawingHandleWorld drawingHandle,
Angle eyeRotation,
Angle worldRotation,
Vector2 worldPosition)
{
RenderSprite(sprite,
drawingHandle,
eyeRotation,
worldRotation,
worldPosition,
sprite.Comp.EnableDirectionOverride ? sprite.Comp.DirectionOverride : null);
}
public void RenderSprite(
Entity<SpriteComponent> sprite,
DrawingHandleWorld drawingHandle,
Angle eyeRotation,
Angle worldRotation,
Vector2 worldPosition,
Direction? overrideDirection)
{
// TODO SPRITE RENDERING
// Add fast path for simple sprites.
// I.e., when a sprite is modified, check if it is "simple". If it is. cache texture information in a struct
// and use a fast path here.
// E.g., simple 1-directional, 1-layer sprites can basically become a direct texture draw call. (most in game items).
// Similarly, 1-directional multi-layer sprites can become a sequence of direct draw calls (most in game walls).
if (!sprite.Comp.IsInert)
_queuedFrameUpdate.Add(sprite);
var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs
angle = angle.Reduced().FlipPositive(); // Reduce the angles to fix math shenanigans
var cardinal = Angle.Zero;
// If we have a 1-directional sprite then snap it to try and always face it south if applicable.
if (sprite.Comp is {NoRotation: false, SnapCardinals: true})
cardinal = angle.RoundToCardinalAngle();
// worldRotation + eyeRotation should be the angle of the entity on-screen. If no-rot is enabled this is just set to zero.
// However, at some point later the eye-matrix is applied separately, so we subtract -eye rotation for now:
var entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, sprite.Comp.NoRotation ? -eyeRotation : worldRotation - cardinal);
var spriteMatrix = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
// Fast path for when all sprites use the same transform matrix
if (!sprite.Comp.GranularLayersRendering)
{
foreach (var layer in sprite.Comp.Layers)
{
RenderLayer(layer, drawingHandle, ref spriteMatrix, angle, overrideDirection);
}
return;
}
//Default rendering (NoRotation = false)
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation);
var transformDefault = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
//Snap to cardinals
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation - angle.RoundToCardinalAngle());
var transformSnap = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
//No rotation
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, -eyeRotation);
var transformNoRot = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
foreach (var layer in sprite.Comp.Layers)
{
switch (layer.RenderingStrategy)
{
case LayerRenderingStrategy.UseSpriteStrategy:
RenderLayer(layer, drawingHandle, ref spriteMatrix, angle, overrideDirection);
break;
case LayerRenderingStrategy.Default:
RenderLayer(layer, drawingHandle, ref transformDefault, angle, overrideDirection);
break;
case LayerRenderingStrategy.NoRotation:
RenderLayer(layer, drawingHandle, ref transformNoRot, angle, overrideDirection);
break;
case LayerRenderingStrategy.SnapToCardinals:
RenderLayer(layer, drawingHandle, ref transformSnap, angle, overrideDirection);
break;
default:
Log.Error($"Tried to render a layer with unknown rendering stragegy: {layer.RenderingStrategy}");
break;
}
}
}
/// <summary>
/// Render a layer. This assumes that the input angle is between 0 and 2pi.
/// </summary>
private void RenderLayer(Layer layer, DrawingHandleWorld drawingHandle, ref Matrix3x2 spriteMatrix, Angle angle, Direction? overrideDirection)
{
if (!layer.Visible || layer.Blank)
return;
var state = layer._actualState;
var dir = state == null ? RsiDirection.South : Layer.GetDirection(state.RsiDirections, angle);
// Set the drawing transform for this layer
layer.GetLayerDrawMatrix(dir, out var layerMatrix, layer.Owner.Comp.NoRotation);
// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
// due to direction overrides or offsets.
if (overrideDirection != null && state != null)
dir = overrideDirection.Value.Convert(state.RsiDirections);
dir = dir.OffsetRsiDir(layer.DirOffset);
var texture = state?.GetFrame(dir, layer.AnimationFrame) ?? layer.Texture ?? GetFallbackTexture();
// TODO SPRITE
// Refactor shader-param-layers to a separate layer type after layers are split into types & collections.
// I.e., separate Layer -> RsiLayer, TextureLayer, LayerCollection, SpriteLayer, and ShaderLayer
if (layer.CopyToShaderParameters != null)
{
HandleShaderLayer(layer, texture, layer.CopyToShaderParameters);
return;
}
// Set the drawing transform for this layer
var transformMatrix = Matrix3x2.Multiply(layerMatrix, spriteMatrix);
drawingHandle.SetTransform(in transformMatrix);
if (layer.Shader != null)
drawingHandle.UseShader(layer.Shader);
var layerColor = layer.Owner.Comp.color * layer.Color;
var textureSize = texture.Size / (float) EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(textureSize / -2, textureSize);
if (layer.UnShaded)
{
DebugTools.AssertNull(layer.Shader);
DebugTools.Assert(layerColor is {R: >= 0, G: >= 0, B: >= 0, A: >= 0}, "Default shader should not be used with negative color modulation.");
// Negative color modulation values are by the default shader to disable light shading.
// Specifically we set colour = - 1 - colour
// This is good enough to ensure that non-negative values become negative & is trivially invertible.
layerColor = new(new SysVec4(-1) - layerColor.RGBA);
}
drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
if (layer.Shader != null)
drawingHandle.UseShader(null);
}
/// <summary>
/// Handle a a "fake layer" that just exists to modify the parameters of a shader being used by some other
/// layer.
/// </summary>
private void HandleShaderLayer(Layer layer, Texture texture, CopyToShaderParameters @params)
{
// Multiple atrocities to god being committed right here.
var otherLayerIdx = layer._parent.LayerMap[@params.LayerKey!];
var otherLayer = layer._parent.Layers[otherLayerIdx];
if (otherLayer.Shader is not { } shader)
return;
if (!shader.Mutable)
otherLayer.Shader = shader = shader.Duplicate();
var clydeTexture = Clyde.RenderHandle.ExtractTexture(texture, null, out var csr);
if (@params.ParameterTexture is { } paramTexture)
shader.SetParameter(paramTexture, clydeTexture);
if (@params.ParameterUV is not { } paramUV)
return;
var sr = Clyde.RenderHandle.WorldTextureBoundsToUV(clydeTexture, csr);
var uv = new Vector4(sr.Left, sr.Bottom, sr.Right, sr.Top);
shader.SetParameter(paramUV, uv);
}
}

View File

@@ -1,166 +0,0 @@
using System;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects;
// This partial class contains various public methods for setting sprite component data.
public sealed partial class SpriteSystem
{
private bool ValidateScale(Entity<SpriteComponent> sprite, Vector2 scale)
{
if (!(MathF.Abs(scale.X) < 0.005f) && !(MathF.Abs(scale.Y) < 0.005f))
return true;
// Scales of ~0.0025 or lower can lead to singular matrices due to rounding errors.
Log.Error(
$"Attempted to set layer sprite scale to very small values. Entity: {ToPrettyString(sprite)}. Scale: {scale}");
return false;
}
#region Transform
public void SetScale(Entity<SpriteComponent?> sprite, Vector2 value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (!ValidateScale(sprite!, value))
return;
sprite.Comp._bounds = sprite.Comp._bounds.Scale(value / sprite.Comp.scale);
sprite.Comp.scale = value;
sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
in sprite.Comp.offset,
in sprite.Comp.rotation,
in sprite.Comp.scale);
}
public void SetRotation(Entity<SpriteComponent?> sprite, Angle value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
sprite.Comp.rotation = value;
sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
in sprite.Comp.offset,
in sprite.Comp.rotation,
in sprite.Comp.scale);
}
public void SetOffset(Entity<SpriteComponent?> sprite, Vector2 value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
sprite.Comp.offset = value;
sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
in sprite.Comp.offset,
in sprite.Comp.rotation,
in sprite.Comp.scale);
}
#endregion
public void SetVisible(Entity<SpriteComponent?> sprite, bool value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (sprite.Comp.Visible == value)
return;
sprite.Comp._visible = value;
_tree.QueueTreeUpdate(sprite!);
}
public void SetDrawDepth(Entity<SpriteComponent?> sprite, int value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
sprite.Comp.drawDepth = value;
}
public void SetColor(Entity<SpriteComponent?> sprite, Color value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
sprite.Comp.color = value;
}
/// <summary>
/// Modify a sprites base RSI. This is the RSI that is used by any RSI layers that do not specify their own.
/// Note that changing the base RSI may result in existing layers having an invalid state. This will not log errors
/// under the assumption that the states of each layers will be updated after the base RSI has changed.
/// </summary>
public void SetBaseRsi(Entity<SpriteComponent?> sprite, RSI? value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (value == sprite.Comp._baseRsi)
return;
sprite.Comp._baseRsi = value;
if (value == null)
return;
var fallback = GetFallbackState();
for (var i = 0; i < sprite.Comp.Layers.Count; i++)
{
var layer = sprite.Comp.Layers[i];
if (!layer.State.IsValid || layer.RSI != null)
continue;
RefreshCachedState(layer, logErrors: false, fallback);
if (value.TryGetState(layer.State, out var state))
{
layer.AnimationTimeLeft = state.GetDelay(0);
}
else
{
Log.Error($"Layer {i} no longer has state '{layer.State}' due to base RSI change. Trace:\n{Environment.StackTrace}");
layer.Texture = null;
}
}
}
public void SetContainerOccluded(Entity<SpriteComponent?> sprite, bool value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
sprite.Comp._containerOccluded = value;
_tree.QueueTreeUpdate(sprite!);
}
public void SetSnapCardinals(Entity<SpriteComponent?> sprite, bool value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (value == sprite.Comp._snapCardinals)
return;
sprite.Comp._snapCardinals = value;
_tree.QueueTreeUpdate(sprite!);
DirtyBounds(sprite!);
}
public void SetGranularLayersRendering(Entity<SpriteComponent?> sprite, bool value)
{
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
return;
if (value == sprite.Comp.GranularLayersRendering)
return;
sprite.Comp.GranularLayersRendering = value;
_tree.QueueTreeUpdate(sprite!);
DirtyBounds(sprite!);
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed partial class SpriteSystem
{
private readonly Dictionary<string, IRsiStateLike> _cachedPrototypeIcons = new();
public Texture Frame0(EntityPrototype prototype)
{
return GetPrototypeIcon(prototype).Default;
}
public Texture Frame0(SpriteSpecifier specifier)
{
return RsiStateLike(specifier).Default;
}
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
{
switch (specifier)
{
case SpriteSpecifier.Texture tex:
return tex.GetTexture(_resourceCache);
case SpriteSpecifier.Rsi rsi:
return GetState(rsi);
case SpriteSpecifier.EntityPrototype prototypeIcon:
return GetPrototypeIcon(prototypeIcon.EntityPrototypeId);
default:
throw new NotSupportedException();
}
}
public Texture GetIcon(IconComponent icon)
{
return GetState(icon.Icon).Frame0;
}
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method caches the result based on the prototype identifier.
/// </summary>
public IRsiStateLike GetPrototypeIcon(string prototype)
{
// Check if this prototype has been cached before, and if so return the result.
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
return cachedResult;
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
{
// The specified prototype doesn't exist, return the fallback "error" sprite.
_sawmill.Error("Failed to load PrototypeIcon {0}", prototype);
return GetFallbackState();
}
// Generate the icon and cache it in case it's ever needed again.
var result = GetPrototypeIcon(entityPrototype);
_cachedPrototypeIcons[prototype] = result;
return result;
}
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method does NOT cache the result.
/// </summary>
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
{
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
if (prototype.Components.TryGetValue("Icon", out var compData)
&& compData.Component is IconComponent icon)
{
return GetIcon(icon);
}
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
if (!prototype.Components.ContainsKey("Sprite"))
{
return GetFallbackState();
}
// Finally, we use spawn a dummy entity to get its icon.
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? GetFallbackState();
Del(dummy);
return result;
}
[Pure]
public RSI.State GetFallbackState()
{
return _resourceCache.GetFallback<RSIResource>().RSI["error"];
}
[Pure]
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
{
if (_resourceCache.TryGetResource<RSIResource>(
SpriteSpecifierSerializer.TextureRoot / rsiSpecifier.RsiPath,
out var theRsi) &&
theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
{
return state;
}
_sawmill.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
return GetFallbackState();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.TryGetModified<EntityPrototype>(out var modified))
return;
// Remove all changed prototypes from the cache, if they're there.
foreach (var prototype in modified)
{
// Let's be lazy and not regenerate them until something needs them again.
_cachedPrototypeIcons.Remove(prototype);
}
}
}

View File

@@ -13,9 +13,10 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
@@ -35,20 +36,24 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly SharedTransformSystem _xforms = default!;
[Dependency] private readonly SpriteTreeSystem _tree = default!;
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
public static readonly ResPath TextureRoot = SpriteSpecifierSerializer.TextureRoot;
/// <summary>
/// Entities that require a sprite frame update.
/// </summary>
private readonly HashSet<EntityUid> _queuedFrameUpdate = new();
private ISawmill _sawmill = default!;
private EntityQuery<SpriteComponent> _query;
internal void Render(EntityUid uid, SpriteComponent sprite, DrawingHandleWorld drawingHandle, Angle eyeRotation, in Angle worldRotation, in Vector2 worldPosition)
{
if (!sprite.IsInert)
_queuedFrameUpdate.Add(uid);
sprite.RenderInternal(drawingHandle, eyeRotation, worldRotation, worldPosition, sprite.EnableDirectionOverride ? sprite.DirectionOverride : null);
}
public override void Initialize()
{
@@ -57,11 +62,11 @@ namespace Robust.Client.GameObjects
UpdatesAfter.Add(typeof(SpriteTreeSystem));
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
SubscribeLocalEvent<SpriteComponent, SpriteUpdateInertEvent>(QueueUpdateInert);
SubscribeLocalEvent<SpriteComponent, ComponentInit>(OnInit);
Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
_sawmill = _logManager.GetSawmill("sprite");
_query = GetEntityQuery<SpriteComponent>();
}
public bool IsVisible(Layer layer)
@@ -80,6 +85,18 @@ namespace Robust.Client.GameObjects
SpriteComponent.DirectionBias = value;
}
private void QueueUpdateInert(EntityUid uid, SpriteComponent sprite, ref SpriteUpdateInertEvent ev)
=> QueueUpdateInert(uid, sprite);
public void QueueUpdateInert(EntityUid uid, SpriteComponent sprite)
{
if (sprite._inertUpdateQueued)
return;
sprite._inertUpdateQueued = true;
_inertUpdateQueue.Enqueue(sprite);
}
private void DoUpdateIsInert(SpriteComponent component)
{
component._inertUpdateQueued = false;

View File

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

View File

@@ -13,7 +13,6 @@ using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Shared;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Containers;
@@ -564,21 +563,6 @@ namespace Robust.Client.GameStates
var metaQuery = _entities.GetEntityQuery<MetaDataComponent>();
RemQueue<IComponent> toRemove = new();
// Handle predicted entity spawns.
var predicted = new ValueList<EntityUid>();
var predictedQuery = _entities.AllEntityQueryEnumerator<PredictedSpawnComponent>();
while (predictedQuery.MoveNext(out var uid, out var _))
{
predicted.Add(uid);
}
// Entity will get re-created as part of the tick.
foreach (var ent in predicted)
{
_entities.DeleteEntity(ent);
}
foreach (var entity in system.DirtyEntities)
{
DebugTools.Assert(toRemove.Count == 0);
@@ -1306,11 +1290,6 @@ namespace Robust.Client.GameStates
meta.LastStateApplied = lastStateApplied.Value;
var xform = xforms.GetComponent(ent.Value);
// TODO PVS DETACH
// Why is this if block here again? If a null-space entity gets sent to a player via some PVS override,
// and then later on it gets removed, you would assume that the client marks it as detached?
// I.e., modifying the metadata flag & pausing the entity should probably happen outside of this block.
if (xform.ParentUid.IsValid())
{
lookupSys.RemoveFromEntityTree(ent.Value, xform);
@@ -1331,13 +1310,6 @@ namespace Robust.Client.GameStates
xformSys.DetachEntity(ent.Value, xform);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
// We mark the entity as paused, without raising a pause-event.
// The entity gets un-paused when the metadata's comp-state is reapplied (which also does not raise
// an un-pause event). The assumption is that game logic that has to handle the pausing should be
// getting networked anyway. And if its some client-side timer on a networked entity, the timer
// shouldn't actually be getting paused just because the entity has left the players view.
meta.PauseTime = TimeSpan.Zero;
if (container != null)
containerSys.AddExpectedEntity(netEntity, container);
}

View File

@@ -123,8 +123,7 @@ namespace Robust.Client.Graphics
/// <inheritdoc />
public ScreenCoordinates CoordinatesToScreen(EntityCoordinates point)
{
var transformSystem = _entityManager.System<SharedTransformSystem>();
return MapToScreen(transformSystem.ToMapCoordinates(point));
return MapToScreen(point.ToMap(_entityManager, _entityManager.System<SharedTransformSystem>()));
}
public ScreenCoordinates MapToScreen(MapCoordinates point)

View File

@@ -30,23 +30,6 @@ namespace Robust.Client.Graphics.Clyde
private int _indicesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
private List<Entity<MapGridComponent>> _grids = new();
private bool _drawTileEdges;
private void RenderTileEdgesChanges(bool value)
{
_drawTileEdges = value;
if (!value)
return;
// Dirty all Edges
foreach (var gridData in _mapChunkData.Values)
{
foreach (var chunk in gridData.Values)
{
chunk.EdgeDirty = true;
}
}
}
private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBounds, IEye eye)
{
@@ -98,9 +81,6 @@ namespace Robust.Client.Graphics.Clyde
_updateChunkMesh(mapGrid, chunk, datum);
if (!_drawTileEdges)
continue;
// Dirty edge tiles for next step.
datum.EdgeDirty = true;
@@ -119,16 +99,17 @@ namespace Robust.Client.Graphics.Clyde
}
}
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
// Handle edge sprites.
if (_drawTileEdges)
while (enumerator.MoveNext(out var chunk))
{
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
while (enumerator.MoveNext(out var chunk))
{
var datum = data[chunk.Indices];
if (datum.EdgeDirty)
_updateChunkEdges(mapGrid, chunk, datum);
}
var datum = data[chunk.Indices];
if (!datum.EdgeDirty)
continue;
_updateChunkEdges(mapGrid, chunk, datum);
}
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
@@ -148,7 +129,7 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
}
if (_drawTileEdges && datum.EdgeCount > 0)
if (datum.EdgeCount > 0)
{
BindVertexArray(datum.EdgeVAO);
CheckGlError();
@@ -157,6 +138,7 @@ namespace Robust.Client.Graphics.Clyde
GL.DrawElements(GetQuadGLPrimitiveType(), datum.EdgeCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
}
}
requiresFlush = false;
@@ -254,11 +236,7 @@ namespace Robust.Client.Graphics.Clyde
region = regionMaybe[tile.Variant];
}
var rotationMirroring = _tileDefinitionManager[tile.TypeId].AllowRotationMirror
? tile.RotationMirroring
: 0;
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, rotationMirroring);
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
i += 1;
}
}
@@ -296,8 +274,7 @@ namespace Robust.Client.Graphics.Clyde
var gridX = x + chunkOriginScaled.X;
var gridY = y + chunkOriginScaled.Y;
var tile = chunk.GetTile(x, y);
if (!_tileDefinitionManager.TryGetDefinition(tile.TypeId, out var tileDef))
continue;
var tileDef = _tileDefinitionManager[tile.TypeId];
// Edge render
for (var nx = -1; nx <= 1; nx++)
@@ -311,15 +288,14 @@ namespace Robust.Client.Graphics.Clyde
if (!maps.TryGetTile(grid.Comp, neighborIndices, out var neighborTile))
continue;
if (!_tileDefinitionManager.TryGetDefinition(neighborTile.TypeId, out var neighborDef))
continue;
var neighborDef = _tileDefinitionManager[neighborTile.TypeId];
// If it's the same tile then no edge to be drawn.
if (tile.TypeId == neighborTile.TypeId || neighborDef.EdgeSprites.Count == 0)
continue;
// If neighbor is a lower or same priority then us then don't draw on our tile.
if (neighborDef.EdgeSpritePriority <= tileDef.EdgeSpritePriority)
// If neighbor is a lower priority then us then don't draw on our tile.
if (neighborDef.EdgeSpritePriority < tileDef.EdgeSpritePriority)
continue;
var direction = new Vector2i(nx, ny).AsDirection().GetOpposite();
@@ -329,7 +305,7 @@ namespace Robust.Client.Graphics.Clyde
continue;
var region = regionMaybe[0];
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, 0);
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
i += 1;
}
}
@@ -412,11 +388,8 @@ namespace Robust.Client.Graphics.Clyde
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
{
var gridData = _mapChunkData.GetOrNew(args.Entity);
foreach (var change in args.Changes)
{
if (gridData.TryGetValue(change.ChunkIndex, out var data))
data.Dirty = true;
}
if (gridData.TryGetValue(args.ChunkIndex, out var data))
data.Dirty = true;
}
private void _updateOnGridCreated(GridStartupEvent ev)
@@ -452,57 +425,13 @@ namespace Robust.Client.Graphics.Clyde
int gridY,
Span<Vertex2D> vertexBuffer,
Span<ushort> indexBuffer,
Box2 region,
int rotationMirroring)
Box2 region)
{
var rLeftBottom = (region.Left, region.Bottom);
var rRightBottom = (region.Right, region.Bottom);
var rRightTop = (region.Right, region.Top);
var rLeftTop = (region.Left, region.Top);
// The vertices must be changed if there's any rotation or mirroring to the tile
if (rotationMirroring != 0)
{
// Rotate the tile
for (int r = 0; r < rotationMirroring % 4; r++)
{
(rLeftBottom, rRightBottom, rRightTop, rLeftTop) =
(rLeftTop, rLeftBottom, rRightBottom, rRightTop);
}
// Mirror on the x-axis
if (rotationMirroring >= 4)
{
if (rotationMirroring % 2 == 0)
{
rLeftBottom = (rLeftBottom.Item1.Equals(region.Left) ? region.Right : region.Left,
rLeftBottom.Item2);
rRightBottom = (rRightBottom.Item1.Equals(region.Left) ? region.Right : region.Left,
rRightBottom.Item2);
rRightTop = (rRightTop.Item1.Equals(region.Left) ? region.Right : region.Left,
rRightTop.Item2);
rLeftTop = (rLeftTop.Item1.Equals(region.Left) ? region.Right : region.Left,
rLeftTop.Item2);
}
else
{
rLeftBottom = (rLeftBottom.Item1,
rLeftBottom.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
rRightBottom = (rRightBottom.Item1,
rRightBottom.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
rRightTop = (rRightTop.Item1,
rRightTop.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
rLeftTop = (rLeftTop.Item1,
rLeftTop.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
}
}
}
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(gridX, gridY, rLeftBottom.Left, rLeftBottom.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gridX + 1, gridY, rRightBottom.Right, rRightBottom.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gridX + 1, gridY + 1, rRightTop.Right, rRightTop.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gridX, gridY + 1, rLeftTop.Left, rLeftTop.Top, Color.White);
vertexBuffer[vIdx + 0] = new Vertex2D(gridX, gridY, region.Left, region.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gridX + 1, gridY, region.Right, region.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gridX + 1, gridY + 1, region.Right, region.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gridX, gridY + 1, region.Left, region.Top, Color.White);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort)(i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);

View File

@@ -123,13 +123,9 @@ namespace Robust.Client.Graphics.Clyde
private void RenderSingleWorldOverlay(Overlay overlay, Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
{
// Check that entity manager has started.
// This is required for us to be able to use MapSystem.
DebugTools.Assert(_entityManager.Started, "Entity manager should be started/initialized before rendering world-space overlays");
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
var mapId = vp.Eye?.Position.MapId ?? MapId.Nullspace;
var mapId = vp.Eye!.Position.MapId;
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapSystem.GetMapOrInvalid(mapId), mapId, worldBox, worldBounds);
if (!overlay.BeforeDraw(args))
@@ -156,7 +152,6 @@ namespace Robust.Client.Graphics.Clyde
private void RenderOverlays(Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
{
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
using (DebugGroup($"Overlays: {space}"))
{
foreach (var overlay in GetOverlaysForSpace(space))
@@ -181,18 +176,9 @@ namespace Robust.Client.Graphics.Clyde
var worldBounds = CalcWorldBounds(vp);
var worldAABB = worldBounds.CalcBoundingBox();
var mapId = vp.Eye?.Position.MapId ?? MapId.Nullspace;
var mapUid = EntityUid.Invalid;
var mapId = vp.Eye!.Position.MapId;
// Screen space overlays may be getting used before entity manager & entity systems have been initialized.
// This might mean that _mapSystem is currently null.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (_entityManager.Started && _mapSystem != null)
mapUid = _mapSystem.GetMapOrInvalid(mapId);
DebugTools.Assert(_mapSystem != null || !_entityManager.Started);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, mapUid, mapId, worldAABB, worldBounds);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, _mapSystem.GetMapOrInvalid(mapId), mapId, worldAABB, worldBounds);
foreach (var overlay in list)
{
@@ -318,7 +304,7 @@ namespace Robust.Client.Graphics.Clyde
screenSpriteSize.Y++;
bool exit = false;
if (entry.Sprite.GetScreenTexture && entry.Sprite.PostShader != null)
if (entry.Sprite.GetScreenTexture)
{
FlushRenderQueue();
var tex = CopyScreenTexture(viewport.RenderTarget);
@@ -369,7 +355,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite), _renderHandle.DrawingHandleWorld, eye.Rotation, entry.WorldRot, entry.WorldPos);
spriteSystem.Render(entry.Uid, entry.Sprite, _renderHandle.DrawingHandleWorld, eye.Rotation, in entry.WorldRot, in entry.WorldPos);
if (entry.Sprite.PostShader != null && entityPostRenderTarget != null)
{

View File

@@ -613,8 +613,6 @@ namespace Robust.Client.Graphics.Clyde
EnsureBatchSpaceAvailable(4, GetQuadBatchIndexCount());
EnsureBatchState(texture, true, GetQuadBatchPrimitiveType(), _queuedShader);
// TODO RENDERING
// It's probably better to do this on the GPU.
bl = Vector2.Transform(bl, _currentMatrixModel);
br = Vector2.Transform(br, _currentMatrixModel);
tr = Vector2.Transform(tr, _currentMatrixModel);

View File

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

View File

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

View File

@@ -305,8 +305,8 @@ namespace Robust.Client.Graphics.Clyde
IsSrgb = srgb,
Name = name,
MemoryPressure = memoryPressure,
TexturePixelType = pixType,
TextureInstance = new WeakReference<ClydeTexture>(instance)
TexturePixelType = pixType
// TextureInstance = new WeakReference<ClydeTexture>(instance)
};
_loadedTextures.Add(id, loaded);
@@ -466,15 +466,15 @@ namespace Robust.Client.Graphics.Clyde
{
var white = new Image<Rgba32>(1, 1);
white[0, 0] = new Rgba32(255, 255, 255, 255);
_stockTextureWhite = (ClydeTexture) Texture.LoadFromImage(white, name: "StockTextureWhite");
_stockTextureWhite = (ClydeTexture) Texture.LoadFromImage(white);
var black = new Image<Rgba32>(1, 1);
black[0, 0] = new Rgba32(0, 0, 0, 255);
_stockTextureBlack = (ClydeTexture) Texture.LoadFromImage(black, name: "StockTextureBlack");
_stockTextureBlack = (ClydeTexture) Texture.LoadFromImage(black);
var blank = new Image<Rgba32>(1, 1);
blank[0, 0] = new Rgba32(0, 0, 0, 0);
_stockTextureTransparent = (ClydeTexture) Texture.LoadFromImage(blank, name: "StockTextureTransparent");
_stockTextureTransparent = (ClydeTexture) Texture.LoadFromImage(blank);
}
/// <summary>
@@ -571,7 +571,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
internal sealed class LoadedTexture
private sealed class LoadedTexture
{
public GLHandle OpenGLObject;
public int Width;
@@ -582,10 +582,10 @@ namespace Robust.Client.Graphics.Clyde
public TexturePixelType TexturePixelType;
public Vector2i Size => (Width, Height);
public required WeakReference<ClydeTexture> TextureInstance;
// public WeakReference<ClydeTexture> TextureInstance;
}
internal enum TexturePixelType : byte
private enum TexturePixelType : byte
{
RenderTarget = 0,
Rgba32,
@@ -686,16 +686,5 @@ namespace Robust.Client.Graphics.Clyde
_ => throw new ArgumentException(nameof(stockTexture))
};
}
public IEnumerable<(ClydeTexture, LoadedTexture)> GetLoadedTextures()
{
foreach (var loaded in _loadedTextures.Values)
{
if (!loaded.TextureInstance.TryGetTarget(out var textureInstance))
continue;
yield return (textureInstance, loaded);
}
}
}
}

View File

@@ -467,7 +467,7 @@ namespace Robust.Client.Graphics.Clyde
_windowing!.RunOnWindowThread(a);
}
public IFileDialogManagerImplementation? FileDialogImpl => _windowing as IFileDialogManagerImplementation;
public IFileDialogManager? FileDialogImpl => _windowing as IFileDialogManager;
private abstract class WindowReg
{

View File

@@ -121,7 +121,6 @@ namespace Robust.Client.Graphics.Clyde
_cfg.OnValueChanged(CVars.LightSoftShadows, SoftShadowsChanged, true);
_cfg.OnValueChanged(CVars.MaxLightCount, MaxLightsChanged, true);
_cfg.OnValueChanged(CVars.MaxOccluderCount, MaxOccludersChanged, true);
_cfg.OnValueChanged(CVars.RenderTileEdges, RenderTileEdgesChanges, true);
// I can't be bothered to tear down and set these threads up in a cvar change handler.
// Windows and Linux can be trusted to not explode with threaded windowing,

View File

@@ -72,11 +72,6 @@ namespace Robust.Client.Graphics.Clyde
return new DummyTexture((1, 1));
}
public IEnumerable<(Clyde.ClydeTexture, Clyde.LoadedTexture)> GetLoadedTextures()
{
return [];
}
public ClydeDebugLayers DebugLayers { get; set; }
public string GetKeyName(Keyboard.Key key) => string.Empty;
@@ -307,7 +302,7 @@ namespace Robust.Client.Graphics.Clyde
action();
}
public IFileDialogManagerImplementation? FileDialogImpl => null;
public IFileDialogManager? FileDialogImpl => null;
private sealed class DummyCursor : ICursor
{

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.Graphics.Clyde
/// Basically just a handle around the integer object handles returned by OpenGL.
/// </summary>
[PublicAPI]
internal struct GLHandle : IEquatable<GLHandle>
private struct GLHandle : IEquatable<GLHandle>
{
public readonly uint Handle;

View File

@@ -87,13 +87,11 @@ namespace Robust.Client.Graphics.Clyde
if (cmd.Cursor != default)
ptr = _winThreadCursors[cmd.Cursor].Ptr;
#if DEBUG
if (_win32Experience)
{
// Based on a true story.
Thread.Sleep(15);
}
#endif
GLFW.SetCursor(window, ptr);
}

View File

@@ -23,9 +23,7 @@ namespace Robust.Client.Graphics.Clyde
private readonly ISawmill _sawmillGlfw;
private bool _glfwInitialized;
#if DEBUG
private bool _win32Experience;
#endif
public GlfwWindowingImpl(Clyde clyde, IDependencyCollection deps)
{

View File

@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -11,16 +12,31 @@ namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl : IFileDialogManagerImplementation
private sealed partial class Sdl3WindowingImpl : IFileDialogManager
{
public async Task<string?> OpenFile(FileDialogFilters? filters)
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
{
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
if (fileName == null)
return null;
return File.OpenRead(fileName);
}
public async Task<string?> SaveFile(FileDialogFilters? filters)
public async Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
{
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
if (fileName == null)
return null;
try
{
return (File.Open(fileName, truncate ? FileMode.Truncate : FileMode.Open), true);
}
catch (FileNotFoundException)
{
return (File.Open(fileName, FileMode.Create), false);
}
}
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
@@ -58,8 +74,6 @@ internal partial class Clyde
NativeMemory.Free(filter.name);
NativeMemory.Free(filter.pattern);
}
NativeMemory.Free(filtersAlloc);
}
return task;

View File

@@ -54,7 +54,6 @@ namespace Robust.Client.Graphics
IClydeDebugStats DebugStats { get; }
Texture GetStockTexture(ClydeStockTexture stockTexture);
IEnumerable<(Clyde.Clyde.ClydeTexture, Clyde.Clyde.LoadedTexture)> GetLoadedTextures();
ClydeDebugLayers DebugLayers { get; set; }
@@ -71,6 +70,6 @@ namespace Robust.Client.Graphics
void RunOnWindowThread(Action action);
IFileDialogManagerImplementation? FileDialogImpl { get; }
IFileDialogManager? FileDialogImpl { get; }
}
}

View File

@@ -48,7 +48,6 @@ namespace Robust.Client.Input
[Dependency] private readonly IUserInterfaceManagerInternal _uiMgr = default!;
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
private ISawmill _logger = default!;
private bool _currentlyFindingViewport;
@@ -115,8 +114,6 @@ namespace Robust.Client.Input
/// <inheritdoc />
public void Initialize()
{
_logger = Logger.GetSawmill("input");
NetworkBindMap = new BoundKeyMap(_reflectionManager);
NetworkBindMap.PopulateKeyFunctionsMap();
@@ -133,7 +130,7 @@ namespace Robust.Client.Input
}
catch (Exception e)
{
_logger.Error("Failed to load user keybindings: " + e);
Logger.ErrorS("input", "Failed to load user keybindings: " + e);
}
}
@@ -534,7 +531,8 @@ namespace Robust.Client.Input
if (reg.Type != KeyBindingType.Command && !NetworkBindMap.FunctionExists(reg.Function.FunctionName))
{
_logger.Debug("Key function in {0} does not exist: '{1}'.", file, reg.Function);
Logger.DebugS("input", "Key function in {0} does not exist: '{1}'.", file,
reg.Function);
invalid = true;
}

View File

@@ -1,33 +0,0 @@
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
namespace Robust.Client.Localization;
internal sealed class ClientLocalizationManager : LocalizationManager, ILocalizationManagerInternal
{
[Dependency] private readonly IReloadManager _reload = default!;
void ILocalizationManager.Initialize() => Initialize();
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
_reload.Register(LocaleDirPath, "*.ftl");
_reload.OnChanged += OnReload;
}
/// <summary>
/// Handles Fluent hot reloading via LocalizationManager.ReloadLocalizations()
/// </summary>
private void OnReload(ResPath args)
{
if (args.Extension != "ftl")
return;
ReloadLocalizations();
}
}

View File

@@ -47,7 +47,7 @@ namespace Robust.Client.Physics
* This will draw above every body involved in a particular island solve.
*/
public readonly Queue<(TimeSpan Time, List<Entity<PhysicsComponent, TransformComponent>> Bodies)> IslandSolve = new();
public readonly Queue<(TimeSpan Time, List<PhysicsComponent> Bodies)> IslandSolve = new();
public const float SolveDuration = 0.1f;
public override void Initialize()

View File

@@ -90,10 +90,9 @@ public sealed partial class PhysicsSystem
// existing contacts for predicted entities before performing any actual prediction.
var contacts = new List<Contact>();
var maps = new HashSet<EntityUid>();
var enumerator = AllEntityQuery<PredictedPhysicsComponent, PhysicsComponent, TransformComponent>();
_broadphase.FindNewContacts();
while (enumerator.MoveNext(out _, out var physics, out var xform))
{
DebugTools.Assert(physics.Predict);
@@ -101,6 +100,10 @@ public sealed partial class PhysicsSystem
if (xform.MapUid is not { } map)
continue;
if (maps.Add(map) && PhysMapQuery.TryGetComponent(map, out var physMap) &&
MapQuery.TryGetComponent(map, out var mapComp))
_broadphase.FindNewContacts(physMap, mapComp.MapId);
contacts.AddRange(physics.Contacts);
}

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics;
@@ -24,23 +23,21 @@ namespace Robust.Client.Physics
SimulateWorld(frameTime, _gameTiming.InPrediction);
}
protected override void Cleanup(float frameTime)
protected override void Cleanup(PhysicsMapComponent component, float frameTime)
{
var toRemove = new ValueList<Entity<PhysicsComponent, TransformComponent>>();
var toRemove = new List<Entity<PhysicsComponent>>();
// Because we're not predicting 99% of bodies its sleep timer never gets incremented so we'll just do it ourselves.
// (and serializing it over the network isn't necessary?)
// This is a client-only problem.
// Also need to suss out having the client build the island anyway and just... not solving it?
foreach (var ent in AwakeBodies)
foreach (var body in component.AwakeBodies)
{
var body = ent.Comp1;
if (!body.SleepingAllowed || body.LinearVelocity.Length() > LinearToleranceSqr / 2f || body.AngularVelocity * body.AngularVelocity > AngularToleranceSqr / 2f) continue;
body.SleepTime += frameTime;
if (body.SleepTime > TimeToSleep)
{
toRemove.Add(ent);
toRemove.Add(new Entity<PhysicsComponent>(body.Owner, body));
}
}
@@ -49,38 +46,37 @@ namespace Robust.Client.Physics
SetAwake(body, false);
}
base.Cleanup(frameTime);
base.Cleanup(component, frameTime);
}
protected override void UpdateLerpData(List<Entity<PhysicsComponent, TransformComponent>> bodies)
protected override void UpdateLerpData(PhysicsMapComponent component, List<PhysicsComponent> bodies, EntityQuery<TransformComponent> xformQuery)
{
foreach (var bodyEnt in bodies)
foreach (var body in bodies)
{
var body = bodyEnt.Comp1;
var xform = bodyEnt.Comp2;
if (body.BodyType == BodyType.Static ||
LerpData.TryGetValue(bodyEnt, out var lerpData) ||
lerpData == xform.ParentUid)
component.LerpData.TryGetValue(body.Owner, out var lerpData) ||
!xformQuery.TryGetComponent(body.Owner, out var xform) ||
lerpData.ParentUid == xform.ParentUid)
{
continue;
}
LerpData[bodyEnt.Owner] = xform.ParentUid;
component.LerpData[xform.Owner] = (xform.ParentUid, xform.LocalPosition, xform.LocalRotation);
}
}
/// <summary>
/// Flush all of our lerping data.
/// </summary>
protected override void FinalStep()
protected override void FinalStep(PhysicsMapComponent component)
{
base.FinalStep();
base.FinalStep(component);
var xformQuery = GetEntityQuery<TransformComponent>();
foreach (var (uid, parentUid) in LerpData)
foreach (var (uid, (parentUid, position, rotation)) in component.LerpData)
{
// Can't just re-use xform from before as movement events may cause event subs to fire.
if (!XformQuery.TryGetComponent(uid, out var xform) || !parentUid.IsValid())
if (!xformQuery.TryGetComponent(uid, out var xform) ||
!parentUid.IsValid())
{
continue;
}
@@ -89,7 +85,7 @@ namespace Robust.Client.Physics
_transform.SetLocalPositionRotation(uid, xform.LocalPosition, xform.LocalRotation, xform);
}
LerpData.Clear();
component.LerpData.Clear();
}
}
}

View File

@@ -24,20 +24,10 @@ namespace Robust.Client.Placement
Direction Direction { get; set; }
/// <summary>
/// Whether a tile placement should be mirrored or not.
/// </summary>
bool Mirrored { get; set; }
/// <summary>
/// Gets called when Direction changed (presently for EntitySpawnWindow/TileSpawnWindow UI)
/// Gets called when Direction changed (presently for EntitySpawnWindow UI)
/// </summary>
event EventHandler DirectionChanged;
/// <summary>
/// Gets called when Mirrored changed (presently for TileSpawnWindow UI)
/// </summary>
event EventHandler MirroredChanged;
/// <summary>
/// Gets called when the PlacementManager changed its build/erase mode or when the hijacks changed
/// </summary>

View File

@@ -30,13 +30,11 @@ namespace Robust.Client.Placement.Modes
return;
}
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var mapId = transformSys.GetMapId(MouseCoords);
var mapId = MouseCoords.GetMapId(pManager.EntityManager);
var snapToEntities = pManager.EntityManager.System<EntityLookupSystem>()
.GetEntitiesInRange(MouseCoords, SnapToRange)
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 => (transformSys.GetWorldPosition(entity) - transformSys.ToMapCoordinates(MouseCoords).Position).LengthSquared())
.OrderBy(entity => (pManager.EntityManager.GetComponent<TransformComponent>(entity).WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager, pManager.EntityManager.System<SharedTransformSystem>())).LengthSquared())
.ToList();
if (snapToEntities.Count == 0)
@@ -53,11 +51,10 @@ namespace Robust.Client.Placement.Modes
var closestBounds = component.BaseRSI.Size;
var closestPos = transformSys.GetWorldPosition(closestTransform);
var closestRect =
Box2.FromDimensions(
closestPos.X - closestBounds.X / 2f,
closestPos.Y - closestBounds.Y / 2f,
closestTransform.WorldPosition.X - closestBounds.X / 2f,
closestTransform.WorldPosition.Y - closestBounds.Y / 2f,
closestBounds.X, closestBounds.Y);
var sides = new[]

View File

@@ -21,8 +21,7 @@ namespace Robust.Client.Placement.Modes
{
MouseCoords = ScreenToCursorGrid(mouseScreen);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var gridIdOpt = transformSys.GetGrid(MouseCoords);
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
SnapSize = 1f;
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{

View File

@@ -16,10 +16,8 @@ namespace Robust.Client.Placement.Modes
{
MouseCoords = ScreenToCursorGrid(mouseScreen);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var tileSize = 1f;
var gridIdOpt = transformSys.GetGrid(MouseCoords);
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{

View File

@@ -16,11 +16,9 @@ namespace Robust.Client.Placement.Modes
{
MouseCoords = ScreenToCursorGrid(mouseScreen);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var tileSize = 1f;
var gridIdOpt = transformSys.GetGrid(MouseCoords);
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
var mapGrid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);

View File

@@ -47,7 +47,6 @@ namespace Robust.Client.Placement
private SharedMapSystem Maps => EntityManager.System<SharedMapSystem>();
private SharedTransformSystem XformSystem => EntityManager.System<SharedTransformSystem>();
private SpriteSystem Sprite => EntityManager.System<SpriteSystem>();
/// <summary>
/// How long before a pending tile change is dropped.
@@ -175,18 +174,6 @@ namespace Robust.Client.Placement
private Direction _direction = Direction.South;
private bool _mirrored;
public bool Mirrored
{
get => _mirrored;
set
{
_mirrored = value;
MirroredChanged?.Invoke(this, EventArgs.Empty);
}
}
/// <inheritdoc />
public Direction Direction
{
@@ -201,9 +188,6 @@ namespace Robust.Client.Placement
/// <inheritdoc />
public event EventHandler? DirectionChanged;
/// <inheritdoc />
public event EventHandler? MirroredChanged;
private PlacementOverlay _drawOverlay = default!;
private bool _isActive;
@@ -360,27 +344,18 @@ namespace Robust.Client.Placement
private void HandleTileChanged(ref TileChangedEvent args)
{
foreach (var change in args.Changes)
{
var coords = Maps.GridTileToLocal(
args.Entity,
args.Entity.Comp,
change.GridIndices);
var coords = Maps.GridTileToLocal(
args.NewTile.GridUid,
EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid),
args.NewTile.GridIndices);
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
}
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
}
/// <inheritdoc />
public event EventHandler? PlacementChanged;
public void Clear()
{
ClearWithoutDeactivation();
IsActive = false;
}
private void ClearWithoutDeactivation()
{
PlacementChanged?.Invoke(this, EventArgs.Empty);
Hijack = null;
@@ -390,6 +365,7 @@ namespace Robust.Client.Placement
CurrentMode = null;
DeactivateSpecialPlacement();
_placenextframe = false;
IsActive = false;
Eraser = false;
EraserRect = null;
PlacementOffset = Vector2i.Zero;
@@ -504,17 +480,18 @@ namespace Robust.Client.Placement
public void BeginHijackedPlacing(PlacementInformation info, PlacementHijack? hijack = null)
{
ClearWithoutDeactivation();
Clear();
if (info.PlacementOption is not { } option || !_modeDictionary.TryGetValue(option, out var placeMode))
CurrentPermission = info;
if (!_modeDictionary.TryFirstOrNull(pair => pair.Key.Equals(CurrentPermission.PlacementOption), out KeyValuePair<string, Type>? placeMode))
{
_sawmill.Log(LogLevel.Warning, $"Invalid placement mode `{info.PlacementOption}`");
_sawmill.Log(LogLevel.Warning, $"Invalid placement mode `{CurrentPermission.PlacementOption}`");
Clear();
return;
}
CurrentPermission = info;
CurrentMode = (PlacementMode) Activator.CreateInstance(placeMode, this)!;
CurrentMode = (PlacementMode) Activator.CreateInstance(placeMode.Value.Value, this)!;
if (hijack != null)
{
@@ -712,11 +689,11 @@ namespace Robust.Client.Placement
CurrentPlacementOverlayEntity = null;
}
private Entity<SpriteComponent> SetupPlacementOverlayEntity()
private SpriteComponent SetupPlacementOverlayEntity()
{
EnsureNoPlacementOverlayEntity();
CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace);
return (CurrentPlacementOverlayEntity.Value, EntityManager.EnsureComponent<SpriteComponent>(CurrentPlacementOverlayEntity.Value));
return EntityManager.EnsureComponent<SpriteComponent>(CurrentPlacementOverlayEntity.Value);
}
private void PreparePlacement(string templateName)
@@ -733,16 +710,10 @@ namespace Robust.Client.Placement
EntityManager.GetComponent<MetaDataComponent>(CurrentPlacementOverlayEntity.Value));
}
public void PreparePlacementSprite(Entity<SpriteComponent> sprite)
{
var sc = SetupPlacementOverlayEntity();
Sprite.CopySprite(sprite.AsNullable(), sc.AsNullable());
}
[Obsolete("Use the Entity<SpriteComponent> overload.")]
public void PreparePlacementSprite(SpriteComponent sprite)
{
PreparePlacementSprite((sprite.Owner, sprite));
var sc = SetupPlacementOverlayEntity();
sc.CopyFrom(sprite);
}
public void PreparePlacementTexList(List<IDirectionalTextureProvider>? texs, bool noRot, EntityPrototype? prototype)
@@ -753,27 +724,27 @@ namespace Robust.Client.Placement
// This one covers most cases (including Construction)
foreach (var v in texs)
{
if (v is RSI.State st)
if (v is RSI.State)
{
Sprite.AddRsiLayer(sc.AsNullable(), st.StateId, st.RSI);
var st = (RSI.State) v;
sc.AddLayer(st.StateId, st.RSI);
}
else
{
// Fallback
Sprite.AddTextureLayer(sc.AsNullable(), v.Default);
sc.AddLayer(v.Default);
}
}
}
else
{
Sprite.AddTextureLayer(sc.AsNullable(), new ResPath("/Textures/Interface/tilebuildoverlay.png"));
sc.AddLayer(new ResPath("/Textures/Interface/tilebuildoverlay.png"));
}
sc.Comp.NoRotation = noRot;
sc.NoRotation = noRot;
if (prototype != null && prototype.TryGetComponent<SpriteComponent>("Sprite", out var spriteComp))
{
Sprite.SetScale(sc.AsNullable(), spriteComp.Scale);
sc.Scale = spriteComp.Scale;
}
}
@@ -781,7 +752,7 @@ namespace Robust.Client.Placement
private void PreparePlacementTile()
{
var sc = SetupPlacementOverlayEntity();
Sprite.AddTextureLayer(sc.AsNullable(), new ResPath("/Textures/Interface/tilebuildoverlay.png"));
sc.AddLayer(new ResPath("/Textures/Interface/tilebuildoverlay.png"));
IsActive = true;
}
@@ -801,9 +772,7 @@ namespace Robust.Client.Placement
var grid = EntityManager.GetComponent<MapGridComponent>(gridId);
// no point changing the tile to the same thing.
var tileRef = Maps.GetTileRef(gridId, grid, coordinates).Tile;
if (tileRef.TypeId == CurrentPermission.TileType &&
tileRef.RotationMirroring == Tile.DirectionToByte(Direction) + (Mirrored ? 4 : 0))
if (Maps.GetTileRef(gridId, grid, coordinates).Tile.TypeId == CurrentPermission.TileType)
return;
}
@@ -827,14 +796,9 @@ namespace Robust.Client.Placement
};
if (CurrentPermission.IsTile)
{
message.TileType = CurrentPermission.TileType;
message.Mirrored = Mirrored;
}
else
{
message.EntityTemplateName = CurrentPermission.EntityType;
}
// world x and y
message.NetCoordinates = EntityManager.GetNetCoordinates(coordinates);

View File

@@ -121,12 +121,12 @@ namespace Robust.Client.Placement
{
if (!coordinate.IsValid(pManager.EntityManager))
return; // Just some paranoia just in case
var worldPos = transformSys.ToMapCoordinates(coordinate).Position;
var worldRot = transformSys.GetWorldRotation(coordinate.EntityId) + dirAng;
var worldPos = coordinate.ToMapPos(pManager.EntityManager, transformSys);
var worldRot = pManager.EntityManager.GetComponent<TransformComponent>(coordinate.EntityId).WorldRotation + dirAng;
sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
var rot = args.Viewport.Eye?.Rotation ?? default;
spriteSys.RenderSprite((uid.Value, sprite), args.WorldHandle, rot, worldRot, worldPos);
spriteSys.Render(uid.Value, sprite, args.WorldHandle, rot, worldRot, worldPos);
}
}
@@ -230,7 +230,7 @@ namespace Robust.Client.Placement
var range = pManager.CurrentPermission!.Range;
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
if (range > 0 && !transformSys.InRange(pManager.EntityManager.GetComponent<TransformComponent>(controlled).Coordinates, coordinates, range))
if (range > 0 && !pManager.EntityManager.GetComponent<TransformComponent>(controlled).Coordinates.InRange(pManager.EntityManager, transformSys, coordinates, range))
return false;
return true;
}
@@ -239,7 +239,7 @@ namespace Robust.Client.Placement
{
var bounds = pManager.ColliderAABB;
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var mapCoords = transformSys.ToMapCoordinates(coordinates);
var mapCoords = coordinates.ToMap(pManager.EntityManager, transformSys);
var (x, y) = mapCoords.Position;
var collisionBox = Box2.FromDimensions(
@@ -248,9 +248,7 @@ namespace Robust.Client.Placement
bounds.Width,
bounds.Height);
return pManager.EntityManager
.System<SharedPhysicsSystem>()
.TryCollideRect(collisionBox, mapCoords.MapId);
return EntitySystem.Get<SharedPhysicsSystem>().TryCollideRect(collisionBox, mapCoords.MapId);
}
protected Vector2 ScreenToWorld(Vector2 point)
@@ -267,8 +265,10 @@ namespace Robust.Client.Placement
{
var mapCoords = pManager.EyeManager.PixelToMap(coords.Position);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out _))
if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid))
{
return transformSys.ToCoordinates(mapCoords);
}

View File

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

View File

@@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.Diagnostics;
using Robust.Client.Timing;
using Robust.Client.Utility;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Prototypes;
@@ -13,9 +15,7 @@ namespace Robust.Client.Prototypes
public sealed class ClientPrototypeManager : PrototypeManager
{
[Dependency] private readonly INetManager _netManager = default!;
#pragma warning disable CS0414
[Dependency] private readonly IClientGameTiming _timing = default!;
#pragma warning restore CS0414
[Dependency] private readonly IGameControllerInternal _controller = default!;
[Dependency] private readonly IReloadManager _reload = default!;
@@ -56,7 +56,7 @@ namespace Robust.Client.Prototypes
using var _ = _timing.StartStateApplicationArea();
ReloadPrototypes([file]);
Sawmill.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
Logger.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
#endif
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
@@ -9,7 +8,6 @@ using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Graphics;
@@ -61,14 +59,7 @@ namespace Robust.Client.ResourceManagement
{
try
{
TextureResource.LoadTextureParameters(_manager, data);
if (!data.LoadParameters.Preload)
{
data.Skip = true;
return;
}
TextureResource.LoadPreTextureData(_manager, data);
TextureResource.LoadPreTexture(_manager, data);
}
catch (Exception e)
{
@@ -81,7 +72,7 @@ namespace Robust.Client.ResourceManagement
foreach (var data in texList)
{
if (data.Bad || data.Skip)
if (data.Bad)
continue;
try
@@ -96,7 +87,6 @@ namespace Robust.Client.ResourceManagement
}
var errors = 0;
var skipped = 0;
foreach (var data in texList)
{
if (data.Bad)
@@ -105,12 +95,6 @@ namespace Robust.Client.ResourceManagement
continue;
}
if (data.Skip)
{
skipped += 1;
continue;
}
try
{
var texResource = new TextureResource();
@@ -126,10 +110,9 @@ namespace Robust.Client.ResourceManagement
}
sawmill.Debug(
"Preloaded {CountLoaded} textures ({CountErrored} errored, {CountSkipped} skipped) in {LoadTime}",
texList.Length - skipped - errors,
"Preloaded {CountLoaded} textures ({CountErrored} errored) in {LoadTime}",
texList.Length,
errors,
skipped,
sw.Elapsed);
}
@@ -193,143 +176,65 @@ namespace Robust.Client.ResourceManagement
// TODO allow RSIs to opt out (useful for very big & rare RSIs)
// TODO combine with (non-rsi) texture atlas?
// We now need to insert the RSIs into the atlas. This specific problem is 2BP|O|F - the items are oriented
// and cutting is free. The sorting is done by a slightly modified FFDH algorithm. The algorithm is exactly
// the same as the standard FFDH algorithm with one main difference: We create new "levels" above placed
// blocks. For example if the first block was 10x20, then the second was 10x10 units, we would create a
// 10x10 level above the second block that would be treated as a normal level. This increases the packing
// efficiency from ~85% to ~95% with very little extra computational effort. The algorithm appears to be
// ~97% effective for storing SS14s RSIs.
//
// Here are some more resources about the strip packing problem!
// - https://en.wikipedia.org/w/index.php?title=Strip_packing_problem&oldid=1263496949#First-fit_decreasing-height_(FFDH)
// - https://www.csc.liv.ac.uk/~epa/surveyhtml.html
// - https://www.dei.unipd.it/~fisch/ricop/tesi/tesi_dottorato_Lodi_1999.pdf
// The array must be sorted from biggest to smallest first.
Array.Sort(atlasList, (b, a) => a.AtlasSheet.Height.CompareTo(b.AtlasSheet.Height));
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.
// So fuck it, just default to letting it be as large as it needs to and crop it as needed?
var maxSize = Math.Min(GL.GetInteger(GetPName.MaxTextureSize), _configurationManager.GetCVar(CVars.ResRSIAtlasSize));
var sheet = new Image<Rgba32>(maxSize, maxSize);
// THIS IS NOT GUARANTEED TO HAVE ANY PARTICULARLY LOGICAL ORDERING.
// E.G you could have atlas 1 RSIs appear *before* you're done seeing atlas 2 RSIs.
var levels = new ValueList<Level>();
// List of all the image atlases.
var imageAtlases = new ValueList<Image<Rgba32>>();
// List of all the actual atlases.
var finalAtlases = new ValueList<OwnedTexture>();
// Number of total pixels in each atlas.
var finalPixels = new ValueList<int>();
// First we just find the location of all the RSIs in the atlas before actually placing them.
// This allows us to effectively determine how much space we need to allocate for the images.
var currentHeight = 0;
var currentAtlasIndex = 0;
foreach (var rsi in atlasList)
var deltaY = 0;
Vector2i offset = default;
int finalized = -1;
int atlasCount = 0;
for (int i = 0; i < atlasList.Length; i++)
{
var insertHeight = rsi.AtlasSheet.Height;
var insertWidth = rsi.AtlasSheet.Width;
var found = false;
for (var i = 0; i < levels.Count && !found; i++)
{
var levelPosition = levels[i].Position;
var levelWidth = levels[i].Width;
var levelHeight = levels[i].Height;
// Check if it can fit in this level.
if (levelHeight < insertHeight || levelWidth + insertWidth > levels[i].MaxWidth)
continue;
found = true;
levels[i].Width += insertWidth;
rsi.AtlasOffset = levelPosition + new Vector2i(levelWidth, 0);
levels[i].RSIList.Add(rsi);
// Creating the extra "free" space above blocks that can be used for inserting more items.
// This differs from the FFDH spec which just ignores this space.
Debug.Assert(levelHeight >= insertHeight); // Must be true because the array needs to be sorted
if (levelHeight - insertHeight == 0)
continue;
var freeLevel = new Level
{
AtlasId = levels[i].AtlasId,
Position = levelPosition + new Vector2i(levelWidth, insertHeight),
Height = levelHeight - insertHeight,
Width = 0,
MaxWidth = insertWidth,
RSIList = [ ]
};
levels.Add(freeLevel);
}
if (found)
var rsi = atlasList[i];
if (rsi.Bad)
continue;
// Ran out of space, we need to move on to the next atlas.
// This also isn't in the normal FFDH algorithm (obviously) but its close enough.
if (currentHeight + insertHeight > maxSize)
DebugTools.Assert(rsi.AtlasSheet.Width < sheet.Width);
DebugTools.Assert(rsi.AtlasSheet.Height < sheet.Height);
if (offset.X + rsi.AtlasSheet.Width > sheet.Width)
{
imageAtlases.Add(new Image<Rgba32>(maxSize, currentHeight));
finalPixels.Add(0);
currentHeight = 0;
currentAtlasIndex++;
offset.X = 0;
offset.Y += deltaY;
}
rsi.AtlasOffset = new Vector2i(0, currentHeight);
var newLevel = new Level
if (offset.Y + rsi.AtlasSheet.Height > sheet.Height)
{
AtlasId = currentAtlasIndex,
Position = new Vector2i(0, currentHeight),
Height = insertHeight,
Width = insertWidth,
MaxWidth = maxSize,
RSIList = [ rsi ]
};
levels.Add(newLevel);
currentHeight += insertHeight;
}
// This allocation takes a long time.
imageAtlases.Add(new Image<Rgba32>(maxSize, currentHeight));
finalPixels.Add(0);
// Put all textures on the atlases
foreach (var level in levels)
{
foreach (var rsi in level.RSIList)
{
var box = new UIBox2i(0, 0, rsi.AtlasSheet.Width, rsi.AtlasSheet.Height);
rsi.AtlasSheet.Blit(box, imageAtlases[level.AtlasId], rsi.AtlasOffset);
finalPixels[level.AtlasId] += rsi.AtlasSheet.Width * rsi.AtlasSheet.Height;
FinalizeMetaAtlas(i-1, sheet);
sheet = new Image<Rgba32>(maxSize, maxSize);
deltaY = 0;
offset = default;
}
deltaY = Math.Max(deltaY, rsi.AtlasSheet.Height);
var box = new UIBox2i(0, 0, rsi.AtlasSheet.Width, rsi.AtlasSheet.Height);
rsi.AtlasSheet.Blit(box, sheet, offset);
rsi.AtlasOffset = offset;
offset.X += rsi.AtlasSheet.Width;
}
// Finalize the atlases.
for (var i = 0; i < imageAtlases.Count; i++)
{
var atlasTexture = Clyde.LoadTextureFromImage(imageAtlases[i], $"Meta atlas {i}");
finalAtlases.Add(atlasTexture);
var height = offset.Y + deltaY;
var croppedSheet = new Image<Rgba32>(maxSize, height);
sheet.Blit(new UIBox2i(0, 0, maxSize, height), croppedSheet, default);
FinalizeMetaAtlas(atlasList.Length - 1, croppedSheet);
sawmill.Debug($"(Meta atlas {i}) - cropped utilization: {(float)finalPixels[i] / (maxSize * imageAtlases[i].Height):P2}, fill percentage: {(float)imageAtlases[i].Height / maxSize:P2}");
}
// Finally, reference the actual atlas from the RSIs.
foreach (var level in levels)
void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
{
foreach (var rsi in level.RSIList)
var fromIndex = finalized + 1;
var atlas = Clyde.LoadTextureFromImage(sheet, $"Meta atlas {fromIndex}-{toIndex}");
for (int i = fromIndex; i <= toIndex; i++)
{
rsi.AtlasTexture = finalAtlases[level.AtlasId];
var rsi = atlasList[i];
rsi.AtlasTexture = atlas;
}
finalized = toIndex;
atlasCount++;
}
Parallel.ForEach(rsiList, data =>
@@ -374,7 +279,7 @@ namespace Robust.Client.ResourceManagement
sawmill.Debug(
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountNotAtlas} not atlassed, {CountErrored} errored) in {LoadTime}",
rsiList.Length,
finalAtlases.Count,
atlasCount,
nonAtlasList.Length,
errors,
sw.Elapsed);
@@ -385,38 +290,4 @@ namespace Robust.Client.ResourceManagement
return rsi.MetaAtlas && rsi.LoadParameters == TextureLoadParameters.Default;
}
}
/// <summary>
/// A "Level" to place boxes. Similar to FFDH levels, but with more parameters so we can fit in "free" levels
/// above placed boxes.
/// </summary>
internal sealed class Level
{
/// <summary>
/// Index of the atlas this is located.
/// </summary>
public required int AtlasId;
/// <summary>
/// Bottom left of the location for the RSIs.
/// </summary>
public required Vector2i Position;
/// <summary>
/// The current width of the level.
/// </summary>
/// <remarks>This can (and will) be 0. Will change.</remarks>
public required int Width;
/// <summary>
/// The current height of the level.
/// </summary>
/// <remarks>This value should never change.</remarks>
public required int Height;
/// <summary>
/// Maximum width of the level.
/// </summary>
public required int MaxWidth;
/// <summary>
/// List of all the RSIs stored in this level. RSIs are ordered from tallest to smallest per level.
/// </summary>
public required List<RSIResource.LoadStepData> RSIList;
}
}

View File

@@ -1,8 +1,8 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using Robust.Client.Audio;
using Robust.Shared.Audio;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
@@ -11,13 +11,6 @@ namespace Robust.Client.ResourceManagement;
public sealed class AudioResource : BaseResource
{
// from: https://en.wikipedia.org/wiki/List_of_file_signatures
private static readonly byte[] OggSignature = "OggS"u8.ToArray();
private static readonly byte[] RiffSignature = "RIFF"u8.ToArray();
private const int WavSignatureStart = 8; // RIFF????
private static readonly byte[] WavSignature = "WAVE"u8.ToArray();
private const int MaxSignatureLength = 12; // RIFF????WAVE
public AudioStream AudioStream { get; private set; } = default!;
public void Load(AudioStream stream)
@@ -35,19 +28,14 @@ public sealed class AudioResource : BaseResource
}
using var fileStream = cache.ContentFileRead(path);
var seekableStream = fileStream.CanSeek ? fileStream : fileStream.CopyToMemoryStream();
byte[] signature = seekableStream.ReadExact(MaxSignatureLength);
seekableStream.Seek(0, SeekOrigin.Begin);
var audioManager = dependencies.Resolve<IAudioInternal>();
if (signature[..OggSignature.Length].SequenceEqual(OggSignature))
if (path.Extension == "ogg")
{
AudioStream = audioManager.LoadAudioOggVorbis(seekableStream, path.ToString());
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
}
else if (signature[..RiffSignature.Length].SequenceEqual(RiffSignature)
&& signature[WavSignatureStart..MaxSignatureLength].SequenceEqual(WavSignature))
else if (path.Extension == "wav")
{
AudioStream = audioManager.LoadAudioWav(seekableStream, path.ToString());
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
}
else
{

View File

@@ -32,22 +32,18 @@ namespace Robust.Client.ResourceManagement
var data = new LoadStepData {Path = path};
LoadTextureParameters(dependencies.Resolve<IResourceManager>(), data);
LoadPreTextureData(dependencies.Resolve<IResourceManager>(), data);
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
LoadTexture(dependencies.Resolve<IClyde>(), data);
LoadFinish(dependencies.Resolve<IResourceCache>(), data);
}
internal static void LoadPreTextureData(IResourceManager cache, LoadStepData data)
internal static void LoadPreTexture(IResourceManager cache, LoadStepData data)
{
using (var stream = cache.ContentFileRead(data.Path))
{
data.Image = Image.Load<Rgba32>(stream);
}
}
internal static void LoadTextureParameters(IResourceManager cache, LoadStepData data)
{
data.LoadParameters = TryLoadTextureParameters(cache, data.Path) ?? TextureLoadParameters.Default;
}
@@ -99,8 +95,7 @@ namespace Robust.Client.ResourceManagement
{
var data = new LoadStepData {Path = path};
LoadTextureParameters(dependencies.Resolve<IResourceManager>(), data);
LoadPreTextureData(dependencies.Resolve<IResourceManager>(), data);
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
if (data.Image.Width == Texture.Width && data.Image.Height == Texture.Height)
{
@@ -124,7 +119,6 @@ namespace Robust.Client.ResourceManagement
public Image<Rgba32> Image = default!;
public TextureLoadParameters LoadParameters;
public OwnedTexture Texture = default!;
public bool Skip;
public bool Bad;
}

View File

@@ -22,7 +22,7 @@ public sealed class LoadPrototypeCommand : IConsoleCommand
var dialogManager = IoCManager.Resolve<IFileDialogManager>();
var loadManager = IoCManager.Resolve<IGamePrototypeLoadManager>();
var stream = await dialogManager.OpenFile(access: FileAccess.Read);
var stream = await dialogManager.OpenFile();
if (stream is null)
return;

View File

@@ -1,4 +1,3 @@
using System.IO;
using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Configuration;
@@ -37,7 +36,7 @@ public sealed class UploadFileCommand : IConsoleCommand
var path = new ResPath(args[0]).ToRelativePath();
var filters = new FileDialogFilters(new FileDialogFilters.Group(path.Extension));
await using var file = await _dialog.OpenFile(filters, FileAccess.Read);
await using var file = await _dialog.OpenFile(filters);
if (file == null)
{

View File

@@ -994,9 +994,6 @@ namespace Robust.Client.UserInterface
internal int DoFrameUpdateRecursive(FrameEventArgs args)
{
if (!Visible)
return 0;
var total = 1;
FrameUpdate(args);

View File

@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.Placement;
using Robust.Client.Placement.Modes;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
@@ -30,23 +28,19 @@ public sealed class TileSpawningUIController : UIController
private readonly List<ITileDefinition> _shownTiles = new();
private bool _clearingTileSelections;
private bool _eraseTile;
private bool _mirrorableTile; // Tracks if the chosen tile even can be mirrored.
private bool _mirroredTile;
public override void Initialize()
{
DebugTools.Assert(_init == false);
_init = true;
_placement.PlacementChanged += ClearTileSelection;
_placement.DirectionChanged += OnDirectionChanged;
_placement.MirroredChanged += OnMirroredChanged;
}
private void StartTilePlacement(int tileType)
{
var newObjInfo = new PlacementInformation
{
PlacementOption = nameof(AlignTileAny),
PlacementOption = "AlignTileAny",
TileType = tileType,
Range = 400,
IsTile = true
@@ -73,17 +67,6 @@ public sealed class TileSpawningUIController : UIController
args.Button.Pressed = args.Pressed;
}
private void OnTileMirroredToggled(ButtonToggledEventArgs args)
{
if (_window == null || _window.Disposed)
return;
_placement.Mirrored = args.Pressed;
_mirroredTile = _placement.Mirrored;
args.Button.Pressed = args.Pressed;
}
public void ToggleWindow()
{
EnsureWindow();
@@ -95,9 +78,6 @@ public sealed class TileSpawningUIController : UIController
else
{
_window.Open();
UpdateEntityDirectionLabel();
UpdateMirroredButton();
_window.SearchBar.GrabKeyboardFocus();
}
}
@@ -113,10 +93,6 @@ public sealed class TileSpawningUIController : UIController
_window.TileList.OnItemDeselected += OnTileItemDeselected;
_window.EraseButton.Pressed = _eraseTile;
_window.EraseButton.OnToggled += OnTileEraseToggled;
_window.MirroredButton.Disabled = !_mirrorableTile;
_window.RotationLabel.FontColorOverride = _mirrorableTile ? Color.White : Color.Gray;
_window.MirroredButton.Pressed = _mirroredTile;
_window.MirroredButton.OnToggled += OnTileMirroredToggled;
BuildTileList();
}
@@ -134,7 +110,6 @@ public sealed class TileSpawningUIController : UIController
_window.TileList.ClearSelected();
_clearingTileSelections = false;
_window.EraseButton.Pressed = false;
_window.MirroredButton.Pressed = _placement.Mirrored;
}
private void OnTileClearPressed(ButtonEventArgs args)
@@ -162,7 +137,6 @@ public sealed class TileSpawningUIController : UIController
{
var definition = _shownTiles[args.ItemIndex];
StartTilePlacement(definition.TileId);
UpdateMirroredButton();
}
private void OnTileItemDeselected(ItemList.ItemListDeselectedEventArgs args)
@@ -175,41 +149,6 @@ public sealed class TileSpawningUIController : UIController
_placement.Clear();
}
private void OnDirectionChanged(object? sender, EventArgs e)
{
UpdateEntityDirectionLabel();
}
private void UpdateEntityDirectionLabel()
{
if (_window == null || _window.Disposed)
return;
_window.RotationLabel.Text = _placement.Direction.ToString();
}
private void OnMirroredChanged(object? sender, EventArgs e)
{
UpdateMirroredButton();
}
private void UpdateMirroredButton()
{
if (_window == null || _window.Disposed)
return;
if (_placement.CurrentPermission != null && _placement.CurrentPermission.IsTile)
{
var allowed = _tiles[_placement.CurrentPermission.TileType].AllowRotationMirror;
_mirrorableTile = allowed;
_window.MirroredButton.Disabled = !_mirrorableTile;
_window.RotationLabel.FontColorOverride = _mirrorableTile ? Color.White : Color.Gray;
}
_mirroredTile = _placement.Mirrored;
_window.MirroredButton.Pressed = _mirroredTile;
}
private void BuildTileList(string? searchStr = null)
{
if (_window == null || _window.Disposed) return;

View File

@@ -1,9 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.UserInterface.Controllers;
@@ -14,14 +11,11 @@ namespace Robust.Client.UserInterface.Controllers;
/// and <see cref="UISystemDependencyAttribute"/> to depend on <see cref="EntitySystem"/>s, which will be automatically
/// injected once they are created.
/// </summary>
public abstract partial class UIController : IPostInjectInit
public abstract partial class UIController
{
[Dependency] protected readonly IUserInterfaceManager UIManager = default!;
[Dependency] protected readonly IEntitySystemManager EntitySystemManager = default!;
[Dependency] protected readonly IEntityManager EntityManager = default!;
[Dependency] protected readonly ILogManager LogManager = default!;
public ISawmill Log { get; protected set; } = default!;
public virtual void Initialize()
{
@@ -30,39 +24,4 @@ public abstract partial class UIController : IPostInjectInit
public virtual void FrameUpdate(FrameEventArgs args)
{
}
protected virtual string SawmillName
{
get
{
var name = GetType().Name;
// Strip trailing "UIController"
if (name.EndsWith("UIController"))
name = name.Substring(0, name.Length - "UIController".Length);
// Convert CamelCase to snake_case
// Ignore if all uppercase, assume acronym (e.g. NPC or HTN)
if (name.All(char.IsUpper))
{
name = name.ToLower(CultureInfo.InvariantCulture);
}
else
{
name = string.Concat(name.Select(x => char.IsUpper(x) ? $"_{char.ToLower(x)}" : x.ToString()));
name = name.Trim('_');
}
return $"ui.{name}";
}
}
public void PostInject()
{
Log = LogManager.GetSawmill(SawmillName);
#if !DEBUG
Log.Level = LogLevel.Info;
#endif
}
}

View File

@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Collections;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Maths;
@@ -84,23 +83,9 @@ namespace Robust.Client.UserInterface.Controls
_updateScrollbarVisibility();
}
public void Add(IEnumerable<Item> items)
{
foreach (var item in items)
{
if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");
_itemList.Add(item);
item.OnSelected += Select;
item.OnDeselected += Deselect;
}
Recalculate();
}
public void Add(Item item)
{
if (item == null) return;
if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");
_itemList.Add(item);
@@ -108,19 +93,9 @@ namespace Robust.Client.UserInterface.Controls
item.OnSelected += Select;
item.OnDeselected += Deselect;
Recalculate();
}
public void AddItems(IEnumerable<string> texts, Texture? icon = null, bool selectable = true, object? metadata = null)
{
var items = new ValueList<Item>();
foreach (var text in texts)
{
items.Add(new Item(this) {Text = text, Icon = icon, Selectable = selectable, Metadata = metadata});
}
Add(items);
RecalculateContentHeight();
if (_isAtBottom && ScrollFollowing)
_scrollBar.MoveToEnd();
}
public Item AddItem(string text, Texture? icon = null, bool selectable = true, object? metadata = null)
@@ -132,15 +107,11 @@ namespace Robust.Client.UserInterface.Controls
public void Clear()
{
// Handle this manually so we can just clear all at once.
foreach (var item in _itemList)
foreach (var item in _itemList.ToArray())
{
item.OnSelected -= Select;
item.OnDeselected -= Deselect;
Remove(item);
}
_itemList.Clear();
Recalculate();
_totalContentHeight = 0;
}
@@ -154,35 +125,25 @@ namespace Robust.Client.UserInterface.Controls
_itemList.CopyTo(array, arrayIndex);
}
private void InternalRemoveAt(int index)
{
if (_itemList.Count <= index)
return;
// If you modify this then also make sure to update Clear!
var item = _itemList[index];
_itemList.RemoveAt(index);
item.OnSelected -= Select;
item.OnDeselected -= Deselect;
}
public bool Remove(Item item)
{
if (item == null) return false;
var value = _itemList.Remove(item);
item.OnSelected -= Select;
item.OnDeselected -= Deselect;
Recalculate();
RecalculateContentHeight();
if (_isAtBottom && ScrollFollowing)
_scrollBar.MoveToEnd();
return value;
}
public void RemoveAt(int index)
{
InternalRemoveAt(index);
Recalculate();
Remove(this[index]);
}
public IEnumerator<Item> GetEnumerator()
@@ -200,24 +161,16 @@ namespace Robust.Client.UserInterface.Controls
return _itemList.IndexOf(item);
}
private void InternalInsert(int index, Item item)
public void Insert(int index, Item item)
{
if (item == null) return;
if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");
_itemList.Insert(index, item);
item.OnSelected += Select;
item.OnDeselected += Deselect;
}
public void Insert(int index, Item item)
{
InternalInsert(index, item);
Recalculate();
}
private void Recalculate()
{
RecalculateContentHeight();
if (_isAtBottom && ScrollFollowing)
_scrollBar.MoveToEnd();
@@ -238,6 +191,7 @@ namespace Robust.Client.UserInterface.Controls
SetItems(newItems, (a,b) => string.Compare(a.Text, b.Text));
}
/// <inheritdoc />
/// <summary>
/// This variant allows for a custom equality operator to compare items, when
/// comparing the Item text is not desired.
@@ -261,13 +215,13 @@ namespace Robust.Client.UserInterface.Controls
else if (cmpResult > 0)
{
// Item exists in our list, but not in `newItems`. Remove it.
InternalRemoveAt(i);
RemoveAt(i);
i--;
}
else if (cmpResult < 0)
{
// A new entry which doesn't exist in our list. Insert it.
InternalInsert(i + 1, newItems[j]);
Insert(i + 1, newItems[j]);
j--;
}
}
@@ -275,18 +229,16 @@ namespace Robust.Client.UserInterface.Controls
// Any remaining items in our list don't exist in `newItems` so remove them
while (i >= 0)
{
InternalRemoveAt(i);
RemoveAt(i);
i--;
}
// And finally, any remaining items in `newItems` don't exist in our list. Create them.
while (j >= 0)
{
InternalInsert(0, newItems[j]);
Insert(0, newItems[j]);
j--;
}
Recalculate();
}
// Without this attribute, this would compile into a property called "Item", causing problems with the Item class.
@@ -338,12 +290,9 @@ namespace Robust.Client.UserInterface.Controls
public void ClearSelected(int? except = null)
{
for (var i = 0; i < _itemList.Count; i++)
foreach (var item in GetSelected())
{
if (i == except)
continue;
var item = _itemList[i];
if(IndexOf(item) == except) continue;
item.Selected = false;
}
}
@@ -741,7 +690,7 @@ namespace Robust.Client.UserInterface.Controls
get => _selected;
set
{
if (!Selectable || _selected == value) return;
if (!Selectable) return;
_selected = value;
if(_selected) OnSelected?.Invoke(this);
else OnDeselected?.Invoke(this);

View File

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

View File

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

View File

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

View File

@@ -10,9 +10,7 @@
</BoxContainer>
<ItemList Name="TileList" Access="Public" VerticalExpand="True"/>
<BoxContainer Orientation="Horizontal">
<Button Name="MirroredButton" Access="Public" ToggleMode="True" Text="{Loc tile-spawn-window-mirror-button-text}"/>
<Button Name="EraseButton" Access="Public" ToggleMode="True" Text="{Loc window-erase-button-text}"/>
</BoxContainer>
<Label Name="RotationLabel" Access="Public"/>
</BoxContainer>
</TileSpawnWindow>

View File

@@ -5,6 +5,5 @@
<DebugConsole Name="DebugConsole" />
<DevWindowTabUI Name="UI" />
<DevWindowTabPerf Name="Perf" />
<DevWindowTabTextures Name="Textures" />
</TabContainer>
</Control>

View File

@@ -8,7 +8,6 @@ using Robust.Client.UserInterface.Stylesheets;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Robust.Client.UserInterface
{
@@ -27,7 +26,6 @@ namespace Robust.Client.UserInterface
TabContainer.SetTabTitle(DebugConsole, "Debug Console");
TabContainer.SetTabTitle(UI, "User Interface");
TabContainer.SetTabTitle(Perf, "Profiling");
TabContainer.SetTabTitle(Textures, Loc.GetString("dev-window-tab-textures-title"));
Stylesheet =
new DefaultStylesheet(IoCManager.Resolve<IResourceCache>(), IoCManager.Resolve<IUserInterfaceManager>()).Stylesheet;

View File

@@ -1,26 +0,0 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Robust.Client.UserInterface.DevWindowTabTextures">
<SplitContainer Orientation="Horizontal">
<!-- Left pane: list of textures -->
<BoxContainer Orientation="Vertical" MinWidth="200">
<Button Name="ReloadButton" Text="{Loc 'dev-window-tab-textures-reload'}" />
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'dev-window-tab-textures-filter'}" />
<ScrollContainer HScrollEnabled="False" VerticalExpand="True">
<BoxContainer Name="TextureList" Orientation="Vertical" />
</ScrollContainer>
<Label Name="SummaryLabel" Margin="4" />
</BoxContainer>
<!-- Right pane: show the selected texture info -->
<Control MinWidth="400">
<BoxContainer Orientation="Vertical">
<TextureRect Name="SelectedTextureDisplay" VerticalExpand="True" CanShrink="True"
Stretch="KeepAspectCentered" />
<Label Name="SelectedTextureInfo" />
</BoxContainer>
</Control>
</SplitContainer>
</Control>

View File

@@ -1,136 +0,0 @@
using System;
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface;
/// <summary>
/// Shows all loaded textures in the game.
/// </summary>
[GenerateTypedNameReferences]
internal sealed partial class DevWindowTabTextures : Control
{
[Dependency] private readonly IClydeInternal _clyde = null!;
[Dependency] private readonly ILocalizationManager _loc = null!;
public DevWindowTabTextures()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
ReloadButton.OnPressed += _ => Reload();
SearchBar.OnTextChanged += _ => Reload();
}
protected override void VisibilityChanged(bool newVisible)
{
if (newVisible)
{
Reload();
}
else
{
// Clear to release memory when tab not visible.
Clear();
}
}
private void Clear()
{
TextureList.RemoveAllChildren();
}
private void Reload()
{
Clear();
var total = 0L;
foreach (var (clydeTexture, loadedTexture) in _clyde.GetLoadedTextures()
.OrderByDescending(tup => tup.Item2.MemoryPressure))
{
if (!string.IsNullOrWhiteSpace(SearchBar.Text))
{
if (loadedTexture.Name is not { } name)
continue;
if (!name.Contains(SearchBar.Text, StringComparison.CurrentCultureIgnoreCase))
continue;
}
if (loadedTexture.MemoryPressure == 0)
{
// Bad hack to avoid showing render targets lol.
continue;
}
var button = new ContainerButton
{
Children = { new TextureEntry(loadedTexture, clydeTexture) }
};
button.OnPressed += _ => SelectTexture(loadedTexture, clydeTexture);
TextureList.AddChild(button);
total += loadedTexture.MemoryPressure;
}
SummaryLabel.Text =
_loc.GetString("dev-window-tab-textures-summary", ("bytes", ByteHelpers.FormatBytes(total)));
}
private void SelectTexture(Clyde.LoadedTexture loaded, Clyde.ClydeTexture texture)
{
SelectedTextureDisplay.Texture = texture;
SelectedTextureInfo.Text = _loc.GetString("dev-window-tab-textures-info",
("width", loaded.Width),
("height", loaded.Height),
("pixelType", loaded.TexturePixelType),
("srgb", loaded.IsSrgb),
("name", loaded.Name ?? ""),
("bytes", ByteHelpers.FormatBytes(loaded.MemoryPressure)));
}
private sealed class TextureEntry : Control
{
public TextureEntry(Clyde.LoadedTexture loaded, Clyde.ClydeTexture texture)
{
SetHeight = 64;
var bytes = ByteHelpers.FormatBytes(loaded.MemoryPressure);
var label = loaded.Name == null
? $"{texture.TextureId} ({bytes})"
: $"{loaded.Name}\n{texture.TextureId} ({bytes})";
AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
new TextureRect
{
SetWidth = 64,
Texture = texture,
CanShrink = true,
RectClipContent = true,
Stretch = TextureRect.StretchMode.Scale
},
new Label
{
Text = label,
ClipText = true,
HorizontalExpand = true
}
}
});
}
}
}

View File

@@ -8,19 +8,12 @@ namespace Robust.Client.UserInterface
/// </summary>
internal sealed class DummyFileDialogManager : IFileDialogManager
{
public Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null)
public Task<Stream?> OpenFile(FileDialogFilters? filters = null)
{
return Task.FromResult<Stream?>(null);
}
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
FileDialogFilters? filters = null,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None)
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
{
return Task.FromResult<(Stream fileStream, bool alreadyExisted)?>(null);
}

View File

@@ -7,7 +7,9 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Robust.Client.Graphics;
using Robust.Shared;
using Robust.Shared.Console;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
@@ -28,28 +30,18 @@ namespace Robust.Client.UserInterface
private bool _kDialogAvailable;
private bool _checkedKDialogAvailable;
public async Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null)
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
{
if ((access & FileAccess.ReadWrite) != access)
throw new ArgumentException("Invalid file access specified");
var realShare = share ?? (access == FileAccess.Read ? FileShare.Read : FileShare.None);
if ((realShare & (FileShare.ReadWrite | FileShare.Delete)) != realShare)
throw new ArgumentException("Invalid file share specified");
string? name;
if (_clyde.FileDialogImpl is { } clydeImpl)
name = await clydeImpl.OpenFile(filters);
else
name = await GetOpenFileName(filters);
return await clydeImpl.OpenFile(filters);
var name = await GetOpenFileName(filters);
if (name == null)
{
return null;
}
return File.Open(name, FileMode.Open, access, realShare);
return File.Open(name, FileMode.Open);
}
private async Task<string?> GetOpenFileName(FileDialogFilters? filters)
@@ -62,34 +54,24 @@ namespace Robust.Client.UserInterface
return await OpenFileNfd(filters);
}
public async Task<(Stream, bool)?> SaveFile(
FileDialogFilters? filters,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None)
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
{
if ((access & FileAccess.ReadWrite) != access)
throw new ArgumentException("Invalid file access specified");
if ((share & (FileShare.ReadWrite | FileShare.Delete)) != share)
throw new ArgumentException("Invalid file share specified");
string? name;
if (_clyde.FileDialogImpl is { } clydeImpl)
name = await clydeImpl.SaveFile(filters);
else
name = await GetSaveFileName(filters);
return await clydeImpl.SaveFile(filters);
var name = await GetSaveFileName(filters);
if (name == null)
{
return null;
}
try
{
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open, access, share), true);
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open), true);
}
catch (FileNotFoundException)
{
return (File.Open(name, FileMode.Create, access, share), false);
return (File.Open(name, FileMode.Create), false);
}
}

View File

@@ -1,6 +1,5 @@
using System.IO;
using System.Threading.Tasks;
using Robust.Client.Graphics;
namespace Robust.Client.UserInterface
{
@@ -20,17 +19,7 @@ namespace Robust.Client.UserInterface
/// The file stream for the file the user opened.
/// <see langword="null" /> if the user cancelled the action.
/// </returns>
/// <param name="filters">Filters for file types that the user can select.</param>
/// <param name="access">What access is desired from the file operation.</param>
/// <param name="share">
/// What sharing mode is desired from the file operation.
/// If null is provided and <paramref name="access"/> is <see cref="FileAccess.Read"/>,
/// <see cref="FileShare.Read"/> is selected, otherwise <see cref="FileShare.None"/>.
/// </param>
Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null);
Task<Stream?> OpenFile(FileDialogFilters? filters = null);
/// <summary>
/// Open a file dialog used for saving a single file.
@@ -40,21 +29,6 @@ namespace Robust.Client.UserInterface
/// Null if the user cancelled the action.
/// </returns>
/// <param name="truncate">Should we truncate an existing file to 0-size then write or append.</param>
/// <param name="access">What access is desired from the file operation.</param>
/// <param name="share">Sharing mode for the opened file.</param>
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
FileDialogFilters? filters = null,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None);
}
/// <summary>
/// Internal implementation interface used to connect <see cref="FileDialogManager"/> and <see cref="IClydeInternal"/>.
/// </summary>
internal interface IFileDialogManagerImplementation
{
Task<string?> OpenFile(FileDialogFilters? filters);
Task<string?> SaveFile(FileDialogFilters? filters);
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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