mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e19a77995 | ||
|
|
0b219299c6 | ||
|
|
93c887eec5 | ||
|
|
45a2895e4b | ||
|
|
7d6def6adf | ||
|
|
f733a9efa5 | ||
|
|
8a68dce987 | ||
|
|
b8d840437e | ||
|
|
2b8057acf0 | ||
|
|
bec3caa5da | ||
|
|
ea6126563b | ||
|
|
00494ad9eb | ||
|
|
6672b7b1bd | ||
|
|
8dc55e8748 | ||
|
|
44ea2cd396 | ||
|
|
2c5604432b | ||
|
|
c696466522 | ||
|
|
01bb98e400 | ||
|
|
af08e747de | ||
|
|
8c35c2c380 | ||
|
|
6d46d3f4a5 | ||
|
|
50e06e43fa | ||
|
|
986b0f979d | ||
|
|
a51d786dee | ||
|
|
5f5fed5d6c | ||
|
|
e475cc7898 | ||
|
|
ee8ea4ec3b | ||
|
|
7482451ec4 | ||
|
|
dddf5cd2fb | ||
|
|
01979c451d | ||
|
|
181a5ef0b4 | ||
|
|
e7c7011cc0 | ||
|
|
dc97615fd4 | ||
|
|
3b4944376b | ||
|
|
fa6bd8f7ba | ||
|
|
2398cbcf26 | ||
|
|
38ce48a83f | ||
|
|
4e7de2f272 | ||
|
|
b61075c660 | ||
|
|
7b571dc80e | ||
|
|
f1c76ca899 | ||
|
|
84dcd658aa | ||
|
|
a634d6bd04 | ||
|
|
36f9df3079 | ||
|
|
824c018a69 | ||
|
|
4b6b688c72 | ||
|
|
71df25b251 | ||
|
|
be14a3c249 | ||
|
|
3c2a4d5c79 | ||
|
|
44180b3ee0 | ||
|
|
bb0e77e937 | ||
|
|
684b9bc852 | ||
|
|
9f3db6693e | ||
|
|
40d869948d | ||
|
|
5c97b15849 | ||
|
|
3d8a9a41fa | ||
|
|
92fc8722da | ||
|
|
73f6555624 | ||
|
|
2ac7bc3ce4 | ||
|
|
05cb4bb1c9 | ||
|
|
a393efc87a | ||
|
|
4d47cfa1a6 | ||
|
|
2b1d755d9f | ||
|
|
db7de0a99f | ||
|
|
47f18703af | ||
|
|
97c1548301 | ||
|
|
cd97f1583f | ||
|
|
5fbe25ec9d | ||
|
|
516ee47b51 | ||
|
|
89be682e24 | ||
|
|
6086076559 | ||
|
|
5bd90c908a | ||
|
|
a3d0921cc9 | ||
|
|
15d5b9aa02 | ||
|
|
d24854d94f | ||
|
|
b3cf427013 | ||
|
|
c458abdc69 | ||
|
|
c76444a33f | ||
|
|
4754661467 | ||
|
|
2a8b776ee9 | ||
|
|
7d8e5a5841 | ||
|
|
8e416e4519 | ||
|
|
65f74943d3 | ||
|
|
eb5ed12270 | ||
|
|
c43b7b16c0 | ||
|
|
aee03f0805 | ||
|
|
cfd2b03248 | ||
|
|
8905a3fe14 | ||
|
|
a878da5b80 | ||
|
|
806c23e034 | ||
|
|
e80f5d13a1 | ||
|
|
a6905151b6 | ||
|
|
e742f021fa | ||
|
|
62b4714f1f | ||
|
|
1d0404953f | ||
|
|
d0da13f895 | ||
|
|
ff23f98b26 | ||
|
|
ccfef2a786 | ||
|
|
62ce9724fc | ||
|
|
3bbe0e7f44 | ||
|
|
addd8b5bdd |
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -71,6 +71,6 @@
|
||||
</PropertyGroup>
|
||||
<Exec
|
||||
Condition="'$(_RobustUseExternalMSBuild)' == 'true'"
|
||||
Command=""$(DOTNET_HOST_PATH)" msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
|
||||
Command=""$(DOTNET_HOST_PATH)" msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false /p:IntermediateOutputPath="$(IntermediateOutputPath.TrimEnd('\'))/""/>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
245
RELEASE-NOTES.md
245
RELEASE-NOTES.md
@@ -54,13 +54,252 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 255.0.3
|
||||
## 261.2.4
|
||||
|
||||
|
||||
## 255.0.2
|
||||
## 261.2.3
|
||||
|
||||
|
||||
## 255.0.1
|
||||
## 261.2.2
|
||||
|
||||
|
||||
## 261.2.1
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@@ -9,6 +9,7 @@ entity-spawn-window-override-menu-tooltip = Override placement
|
||||
## TileSpawnWindow
|
||||
|
||||
tile-spawn-window-title = Place Tiles
|
||||
tile-spawn-window-mirror-button-text = Mirror Tiles
|
||||
|
||||
## Console
|
||||
|
||||
|
||||
10
Resources/Locale/en-US/dev-window.ftl
Normal file
10
Resources/Locale/en-US/dev-window.ftl
Normal file
@@ -0,0 +1,10 @@
|
||||
## "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 }
|
||||
@@ -21,47 +21,53 @@ 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 Test()
|
||||
public async Task NoVVReadOnlyTest()
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -83,8 +89,8 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /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")
|
||||
// /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")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,16 +98,8 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
public async Task ReadOnlyFieldTest()
|
||||
{
|
||||
const string code = """
|
||||
using System;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
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
|
||||
{
|
||||
@@ -114,8 +112,8 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(15,12): error RA0019: Data field Bad in data definition Foo is readonly
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(15, 12, 15, 20).WithArguments("Bad", "Foo")
|
||||
// /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")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,16 +121,8 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
public async Task ReadOnlyPropertyTest()
|
||||
{
|
||||
const string code = """
|
||||
using System;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
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
|
||||
{
|
||||
@@ -145,8 +135,40 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(15,20): error RA0020: Data field property Bad in data definition Foo does not have a setter
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldPropertyWritableRule).WithSpan(15, 20, 15, 28).WithArguments("Bad", "Foo")
|
||||
// /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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ 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";
|
||||
|
||||
@@ -81,9 +82,19 @@ 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
|
||||
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule, DataFieldYamlSerializableRule
|
||||
);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
@@ -164,6 +175,19 @@ 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +225,19 @@ 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)
|
||||
@@ -383,6 +420,14 @@ 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))
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Robust.Client.Animations
|
||||
/// </summary>
|
||||
public abstract class AnimationTrackProperty : AnimationTrack
|
||||
{
|
||||
public List<KeyFrame> KeyFrames { get; protected set; } = new();
|
||||
public List<KeyFrame> KeyFrames { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// How to interpolate values when between two keyframes.
|
||||
|
||||
@@ -10,6 +10,7 @@ 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;
|
||||
@@ -36,6 +37,7 @@ 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;
|
||||
@@ -104,6 +106,8 @@ namespace Robust.Client
|
||||
deps.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
|
||||
deps.Register<NetworkResourceManager>();
|
||||
deps.Register<IReloadManager, ReloadManager>();
|
||||
deps.Register<ILocalizationManager, ClientLocalizationManager>();
|
||||
deps.Register<ILocalizationManagerInternal, ClientLocalizationManager>();
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
|
||||
@@ -9,25 +10,7 @@ namespace Robust.Client.ComponentTrees;
|
||||
|
||||
public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent, SpriteComponent>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
#region Component Tree Overrides
|
||||
protected override bool DoFrameUpdate => true;
|
||||
@@ -36,6 +19,11 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
|
||||
protected override int InitialCapacity => 1024;
|
||||
|
||||
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
|
||||
=> entry.Component.CalculateRotatedBoundingBox(pos, rot, default).CalcBoundingBox();
|
||||
{
|
||||
// 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();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ 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;
|
||||
@@ -94,6 +95,7 @@ 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;
|
||||
|
||||
@@ -158,6 +160,7 @@ namespace Robust.Client
|
||||
}
|
||||
|
||||
_serializationManager.Initialize();
|
||||
_loc.Initialize();
|
||||
|
||||
// Call Init in game assemblies.
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
|
||||
|
||||
@@ -296,7 +296,7 @@ namespace Robust.Client.GameObjects
|
||||
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
|
||||
{
|
||||
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|
||||
|| ent.Comp1.EntityDeleted
|
||||
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
|
||||
{
|
||||
return;
|
||||
@@ -322,7 +322,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
if (IsQueuedForDeletion(ent.Owner)
|
||||
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|
||||
|| ent.Comp1.EntityDeleted
|
||||
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using System;
|
||||
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; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Graphics.RSI;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
|
||||
@@ -24,9 +24,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public sealed class SpriteBoundsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
|
||||
|
||||
private SpriteBoundsOverlay? _overlay;
|
||||
|
||||
@@ -42,7 +40,7 @@ namespace Robust.Client.GameObjects
|
||||
if (_enabled)
|
||||
{
|
||||
DebugTools.AssertNull(_overlay);
|
||||
_overlay = new SpriteBoundsOverlay(_spriteTree, _xformSystem);
|
||||
_overlay = new SpriteBoundsOverlay(EntityManager);
|
||||
_overlayManager.AddOverlay(_overlay);
|
||||
}
|
||||
else
|
||||
@@ -57,18 +55,13 @@ namespace Robust.Client.GameObjects
|
||||
private bool _enabled;
|
||||
}
|
||||
|
||||
public sealed class SpriteBoundsOverlay : Overlay
|
||||
public sealed class SpriteBoundsOverlay(IEntityManager entMan) : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
private readonly SharedTransformSystem _xformSystem;
|
||||
private SpriteTreeSystem _renderTree;
|
||||
|
||||
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, SharedTransformSystem xformSystem)
|
||||
{
|
||||
_renderTree = renderTree;
|
||||
_xformSystem = xformSystem;
|
||||
}
|
||||
private readonly SharedTransformSystem _xformSystem = entMan.System<SharedTransformSystem>();
|
||||
private readonly SpriteSystem _spriteSystem = entMan.System<SpriteSystem>();
|
||||
private readonly SpriteTreeSystem _renderTree = entMan.System<SpriteTreeSystem>();
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
@@ -76,10 +69,11 @@ namespace Robust.Client.GameObjects
|
||||
var currentMap = args.MapId;
|
||||
var viewport = args.WorldBounds;
|
||||
|
||||
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
|
||||
foreach (var entry in _renderTree.QueryAabb(currentMap, viewport))
|
||||
{
|
||||
var (sprite, xform) = entry;
|
||||
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
|
||||
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
|
||||
var bounds = _spriteSystem.CalculateBounds((entry.Uid, sprite), worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
|
||||
|
||||
// Get scaled down bounds used to indicate the "south" of a sprite.
|
||||
var localBound = bounds.Box;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,9 @@ 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()
|
||||
{
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Shared.Containers.ContainerManagerComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
@@ -58,7 +57,7 @@ namespace Robust.Client.GameObjects
|
||||
if (!RemoveExpectedEntity(meta.NetEntity, out var container))
|
||||
return;
|
||||
|
||||
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container);
|
||||
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container, force: true);
|
||||
}
|
||||
|
||||
public override void ShutdownContainer(BaseContainer container)
|
||||
@@ -232,7 +231,7 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
Insert(message.Entity, container);
|
||||
Insert(message.Entity, container, force: true);
|
||||
}
|
||||
|
||||
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Robust.Client.GameObjects
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentGetState>(OnLightGetState);
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentInit>(HandleInit);
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentHandleState>(OnLightHandleState);
|
||||
}
|
||||
|
||||
149
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Bounds.cs
Normal file
149
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Bounds.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
@@ -41,4 +45,66 @@ 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));
|
||||
}
|
||||
|
||||
@@ -1,11 +1,153 @@
|
||||
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>
|
||||
|
||||
257
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs
Normal file
257
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
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
|
||||
}
|
||||
299
Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerMap.cs
Normal file
299
Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerMap.cs
Normal file
@@ -0,0 +1,299 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
196
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs
Normal file
196
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
166
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs
Normal file
166
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
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!);
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,9 @@ 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;
|
||||
@@ -36,24 +35,20 @@ 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!;
|
||||
|
||||
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);
|
||||
}
|
||||
private EntityQuery<SpriteComponent> _query;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -62,11 +57,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)
|
||||
@@ -85,18 +80,6 @@ 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;
|
||||
|
||||
@@ -11,6 +11,7 @@ public abstract class VisualizerSystem<T> : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly AppearanceSystem AppearanceSystem = default!;
|
||||
[Dependency] protected readonly AnimationPlayerSystem AnimationSystem = default!;
|
||||
[Dependency] protected readonly SpriteSystem SpriteSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -1306,6 +1306,11 @@ 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);
|
||||
@@ -1326,6 +1331,13 @@ 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);
|
||||
}
|
||||
|
||||
@@ -254,7 +254,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
region = regionMaybe[tile.Variant];
|
||||
}
|
||||
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
|
||||
var rotationMirroring = _tileDefinitionManager[tile.TypeId].AllowRotationMirror
|
||||
? tile.RotationMirroring
|
||||
: 0;
|
||||
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, rotationMirroring);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
@@ -325,7 +329,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
continue;
|
||||
|
||||
var region = regionMaybe[0];
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, 0);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
@@ -408,8 +412,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
|
||||
{
|
||||
var gridData = _mapChunkData.GetOrNew(args.Entity);
|
||||
if (gridData.TryGetValue(args.ChunkIndex, out var data))
|
||||
data.Dirty = true;
|
||||
foreach (var change in args.Changes)
|
||||
{
|
||||
if (gridData.TryGetValue(change.ChunkIndex, out var data))
|
||||
data.Dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void _updateOnGridCreated(GridStartupEvent ev)
|
||||
@@ -445,13 +452,57 @@ namespace Robust.Client.Graphics.Clyde
|
||||
int gridY,
|
||||
Span<Vertex2D> vertexBuffer,
|
||||
Span<ushort> indexBuffer,
|
||||
Box2 region)
|
||||
Box2 region,
|
||||
int rotationMirroring)
|
||||
{
|
||||
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, 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);
|
||||
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);
|
||||
var nIdx = i * GetQuadBatchIndexCount();
|
||||
var tIdx = (ushort)(i * 4);
|
||||
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
|
||||
|
||||
@@ -318,7 +318,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
screenSpriteSize.Y++;
|
||||
|
||||
bool exit = false;
|
||||
if (entry.Sprite.GetScreenTexture)
|
||||
if (entry.Sprite.GetScreenTexture && entry.Sprite.PostShader != null)
|
||||
{
|
||||
FlushRenderQueue();
|
||||
var tex = CopyScreenTexture(viewport.RenderTarget);
|
||||
@@ -369,7 +369,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
spriteSystem.Render(entry.Uid, entry.Sprite, _renderHandle.DrawingHandleWorld, eye.Rotation, in entry.WorldRot, in entry.WorldPos);
|
||||
spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite), _renderHandle.DrawingHandleWorld, eye.Rotation, entry.WorldRot, entry.WorldPos);
|
||||
|
||||
if (entry.Sprite.PostShader != null && entityPostRenderTarget != null)
|
||||
{
|
||||
|
||||
@@ -613,6 +613,8 @@ 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);
|
||||
|
||||
@@ -153,7 +153,7 @@ internal partial class Clyde
|
||||
|
||||
// special casing angle = n*pi/2 to avoid box rotation & bounding calculations doesn't seem to give significant speedups.
|
||||
data.SpriteScreenBB = TransformCenteredBox(
|
||||
data.Sprite.Bounds,
|
||||
_spriteSystem.GetLocalBounds((data.Uid, data.Sprite)),
|
||||
finalRotation,
|
||||
pos + batch.PreScaleViewOffset,
|
||||
batch.ViewScale);
|
||||
|
||||
@@ -10,6 +10,7 @@ internal sealed partial class Clyde
|
||||
private MapSystem _mapSystem = default!;
|
||||
private LightTreeSystem _lightTreeSystem = default!;
|
||||
private TransformSystem _transformSystem = default!;
|
||||
private SpriteSystem _spriteSystem = default!;
|
||||
private SpriteTreeSystem _spriteTreeSystem = default!;
|
||||
private ClientOccluderSystem _occluderSystem = default!;
|
||||
|
||||
@@ -24,6 +25,7 @@ internal sealed partial class Clyde
|
||||
_mapSystem = _entitySystemManager.GetEntitySystem<MapSystem>();
|
||||
_lightTreeSystem = _entitySystemManager.GetEntitySystem<LightTreeSystem>();
|
||||
_transformSystem = _entitySystemManager.GetEntitySystem<TransformSystem>();
|
||||
_spriteSystem = _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||
_spriteTreeSystem = _entitySystemManager.GetEntitySystem<SpriteTreeSystem>();
|
||||
_occluderSystem = _entitySystemManager.GetEntitySystem<ClientOccluderSystem>();
|
||||
}
|
||||
@@ -33,6 +35,7 @@ internal sealed partial class Clyde
|
||||
_mapSystem = null!;
|
||||
_lightTreeSystem = null!;
|
||||
_transformSystem = null!;
|
||||
_spriteSystem = null!;
|
||||
_spriteTreeSystem = null!;
|
||||
_occluderSystem = null!;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
_stockTextureWhite = (ClydeTexture) Texture.LoadFromImage(white, name: "StockTextureWhite");
|
||||
|
||||
var black = new Image<Rgba32>(1, 1);
|
||||
black[0, 0] = new Rgba32(0, 0, 0, 255);
|
||||
_stockTextureBlack = (ClydeTexture) Texture.LoadFromImage(black);
|
||||
_stockTextureBlack = (ClydeTexture) Texture.LoadFromImage(black, name: "StockTextureBlack");
|
||||
|
||||
var blank = new Image<Rgba32>(1, 1);
|
||||
blank[0, 0] = new Rgba32(0, 0, 0, 0);
|
||||
_stockTextureTransparent = (ClydeTexture) Texture.LoadFromImage(blank);
|
||||
_stockTextureTransparent = (ClydeTexture) Texture.LoadFromImage(blank, name: "StockTextureTransparent");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -571,7 +571,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoadedTexture
|
||||
internal 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 WeakReference<ClydeTexture> TextureInstance;
|
||||
public required WeakReference<ClydeTexture> TextureInstance;
|
||||
}
|
||||
|
||||
private enum TexturePixelType : byte
|
||||
internal enum TexturePixelType : byte
|
||||
{
|
||||
RenderTarget = 0,
|
||||
Rgba32,
|
||||
@@ -686,5 +686,16 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_windowing!.RunOnWindowThread(a);
|
||||
}
|
||||
|
||||
public IFileDialogManager? FileDialogImpl => _windowing as IFileDialogManager;
|
||||
public IFileDialogManagerImplementation? FileDialogImpl => _windowing as IFileDialogManagerImplementation;
|
||||
|
||||
private abstract class WindowReg
|
||||
{
|
||||
|
||||
@@ -72,6 +72,11 @@ 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;
|
||||
@@ -302,7 +307,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
action();
|
||||
}
|
||||
|
||||
public IFileDialogManager? FileDialogImpl => null;
|
||||
public IFileDialogManagerImplementation? FileDialogImpl => null;
|
||||
|
||||
private sealed class DummyCursor : ICursor
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
/// Basically just a handle around the integer object handles returned by OpenGL.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
private struct GLHandle : IEquatable<GLHandle>
|
||||
internal struct GLHandle : IEquatable<GLHandle>
|
||||
{
|
||||
public readonly uint Handle;
|
||||
|
||||
|
||||
@@ -87,11 +87,13 @@ 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);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private readonly ISawmill _sawmillGlfw;
|
||||
|
||||
private bool _glfwInitialized;
|
||||
#if DEBUG
|
||||
private bool _win32Experience;
|
||||
#endif
|
||||
|
||||
public GlfwWindowingImpl(Clyde clyde, IDependencyCollection deps)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -12,31 +11,16 @@ namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl : IFileDialogManager
|
||||
private sealed partial class Sdl3WindowingImpl : IFileDialogManagerImplementation
|
||||
{
|
||||
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
public async Task<string?> OpenFile(FileDialogFilters? filters)
|
||||
{
|
||||
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
|
||||
if (fileName == null)
|
||||
return null;
|
||||
|
||||
return File.OpenRead(fileName);
|
||||
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
|
||||
}
|
||||
|
||||
public async Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
|
||||
public async Task<string?> SaveFile(FileDialogFilters? 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);
|
||||
}
|
||||
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
|
||||
}
|
||||
|
||||
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
|
||||
@@ -74,6 +58,8 @@ internal partial class Clyde
|
||||
NativeMemory.Free(filter.name);
|
||||
NativeMemory.Free(filter.pattern);
|
||||
}
|
||||
|
||||
NativeMemory.Free(filtersAlloc);
|
||||
}
|
||||
|
||||
return task;
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace Robust.Client.Graphics
|
||||
IClydeDebugStats DebugStats { get; }
|
||||
|
||||
Texture GetStockTexture(ClydeStockTexture stockTexture);
|
||||
IEnumerable<(Clyde.Clyde.ClydeTexture, Clyde.Clyde.LoadedTexture)> GetLoadedTextures();
|
||||
|
||||
ClydeDebugLayers DebugLayers { get; set; }
|
||||
|
||||
@@ -70,6 +71,6 @@ namespace Robust.Client.Graphics
|
||||
|
||||
void RunOnWindowThread(Action action);
|
||||
|
||||
IFileDialogManager? FileDialogImpl { get; }
|
||||
IFileDialogManagerImplementation? FileDialogImpl { get; }
|
||||
}
|
||||
}
|
||||
|
||||
33
Robust.Client/Localization/ClientLocalizationManager.cs
Normal file
33
Robust.Client/Localization/ClientLocalizationManager.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<PhysicsComponent> Bodies)> IslandSolve = new();
|
||||
public readonly Queue<(TimeSpan Time, List<Entity<PhysicsComponent, TransformComponent>> Bodies)> IslandSolve = new();
|
||||
public const float SolveDuration = 0.1f;
|
||||
|
||||
public override void Initialize()
|
||||
|
||||
@@ -90,9 +90,10 @@ 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);
|
||||
@@ -100,10 +101,6 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Physics;
|
||||
@@ -23,21 +24,23 @@ namespace Robust.Client.Physics
|
||||
SimulateWorld(frameTime, _gameTiming.InPrediction);
|
||||
}
|
||||
|
||||
protected override void Cleanup(PhysicsMapComponent component, float frameTime)
|
||||
protected override void Cleanup(float frameTime)
|
||||
{
|
||||
var toRemove = new List<Entity<PhysicsComponent>>();
|
||||
var toRemove = new ValueList<Entity<PhysicsComponent, TransformComponent>>();
|
||||
|
||||
// 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 body in component.AwakeBodies)
|
||||
foreach (var ent in 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(new Entity<PhysicsComponent>(body.Owner, body));
|
||||
toRemove.Add(ent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,37 +49,38 @@ namespace Robust.Client.Physics
|
||||
SetAwake(body, false);
|
||||
}
|
||||
|
||||
base.Cleanup(component, frameTime);
|
||||
base.Cleanup(frameTime);
|
||||
}
|
||||
|
||||
protected override void UpdateLerpData(PhysicsMapComponent component, List<PhysicsComponent> bodies, EntityQuery<TransformComponent> xformQuery)
|
||||
protected override void UpdateLerpData(List<Entity<PhysicsComponent, TransformComponent>> bodies)
|
||||
{
|
||||
foreach (var body in bodies)
|
||||
foreach (var bodyEnt in bodies)
|
||||
{
|
||||
var body = bodyEnt.Comp1;
|
||||
var xform = bodyEnt.Comp2;
|
||||
|
||||
if (body.BodyType == BodyType.Static ||
|
||||
component.LerpData.TryGetValue(body.Owner, out var lerpData) ||
|
||||
!xformQuery.TryGetComponent(body.Owner, out var xform) ||
|
||||
lerpData.ParentUid == xform.ParentUid)
|
||||
LerpData.TryGetValue(bodyEnt, out var lerpData) ||
|
||||
lerpData == xform.ParentUid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
component.LerpData[xform.Owner] = (xform.ParentUid, xform.LocalPosition, xform.LocalRotation);
|
||||
LerpData[bodyEnt.Owner] = xform.ParentUid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush all of our lerping data.
|
||||
/// </summary>
|
||||
protected override void FinalStep(PhysicsMapComponent component)
|
||||
protected override void FinalStep()
|
||||
{
|
||||
base.FinalStep(component);
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
base.FinalStep();
|
||||
|
||||
foreach (var (uid, (parentUid, position, rotation)) in component.LerpData)
|
||||
foreach (var (uid, parentUid) in LerpData)
|
||||
{
|
||||
if (!xformQuery.TryGetComponent(uid, out var xform) ||
|
||||
!parentUid.IsValid())
|
||||
// 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())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -85,7 +89,7 @@ namespace Robust.Client.Physics
|
||||
_transform.SetLocalPositionRotation(uid, xform.LocalPosition, xform.LocalRotation, xform);
|
||||
}
|
||||
|
||||
component.LerpData.Clear();
|
||||
LerpData.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,20 @@ namespace Robust.Client.Placement
|
||||
Direction Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets called when Direction changed (presently for EntitySpawnWindow UI)
|
||||
/// 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)
|
||||
/// </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>
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace Robust.Client.Placement
|
||||
|
||||
private SharedMapSystem Maps => EntityManager.System<SharedMapSystem>();
|
||||
private SharedTransformSystem XformSystem => EntityManager.System<SharedTransformSystem>();
|
||||
private SpriteSystem Sprite => EntityManager.System<SpriteSystem>();
|
||||
|
||||
/// <summary>
|
||||
/// How long before a pending tile change is dropped.
|
||||
@@ -174,6 +175,18 @@ 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
|
||||
{
|
||||
@@ -188,6 +201,9 @@ namespace Robust.Client.Placement
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? DirectionChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? MirroredChanged;
|
||||
|
||||
private PlacementOverlay _drawOverlay = default!;
|
||||
private bool _isActive;
|
||||
|
||||
@@ -344,18 +360,27 @@ namespace Robust.Client.Placement
|
||||
|
||||
private void HandleTileChanged(ref TileChangedEvent args)
|
||||
{
|
||||
var coords = Maps.GridTileToLocal(
|
||||
args.NewTile.GridUid,
|
||||
EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid),
|
||||
args.NewTile.GridIndices);
|
||||
foreach (var change in args.Changes)
|
||||
{
|
||||
var coords = Maps.GridTileToLocal(
|
||||
args.Entity,
|
||||
args.Entity.Comp,
|
||||
change.GridIndices);
|
||||
|
||||
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
|
||||
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? PlacementChanged;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearWithoutDeactivation();
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
private void ClearWithoutDeactivation()
|
||||
{
|
||||
PlacementChanged?.Invoke(this, EventArgs.Empty);
|
||||
Hijack = null;
|
||||
@@ -365,7 +390,6 @@ namespace Robust.Client.Placement
|
||||
CurrentMode = null;
|
||||
DeactivateSpecialPlacement();
|
||||
_placenextframe = false;
|
||||
IsActive = false;
|
||||
Eraser = false;
|
||||
EraserRect = null;
|
||||
PlacementOffset = Vector2i.Zero;
|
||||
@@ -480,18 +504,17 @@ namespace Robust.Client.Placement
|
||||
|
||||
public void BeginHijackedPlacing(PlacementInformation info, PlacementHijack? hijack = null)
|
||||
{
|
||||
Clear();
|
||||
ClearWithoutDeactivation();
|
||||
|
||||
CurrentPermission = info;
|
||||
|
||||
if (!_modeDictionary.TryFirstOrNull(pair => pair.Key.Equals(CurrentPermission.PlacementOption), out KeyValuePair<string, Type>? placeMode))
|
||||
if (info.PlacementOption is not { } option || !_modeDictionary.TryGetValue(option, out var placeMode))
|
||||
{
|
||||
_sawmill.Log(LogLevel.Warning, $"Invalid placement mode `{CurrentPermission.PlacementOption}`");
|
||||
_sawmill.Log(LogLevel.Warning, $"Invalid placement mode `{info.PlacementOption}`");
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentMode = (PlacementMode) Activator.CreateInstance(placeMode.Value.Value, this)!;
|
||||
CurrentPermission = info;
|
||||
CurrentMode = (PlacementMode) Activator.CreateInstance(placeMode, this)!;
|
||||
|
||||
if (hijack != null)
|
||||
{
|
||||
@@ -689,11 +712,11 @@ namespace Robust.Client.Placement
|
||||
CurrentPlacementOverlayEntity = null;
|
||||
}
|
||||
|
||||
private SpriteComponent SetupPlacementOverlayEntity()
|
||||
private Entity<SpriteComponent> SetupPlacementOverlayEntity()
|
||||
{
|
||||
EnsureNoPlacementOverlayEntity();
|
||||
CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
return EntityManager.EnsureComponent<SpriteComponent>(CurrentPlacementOverlayEntity.Value);
|
||||
return (CurrentPlacementOverlayEntity.Value, EntityManager.EnsureComponent<SpriteComponent>(CurrentPlacementOverlayEntity.Value));
|
||||
}
|
||||
|
||||
private void PreparePlacement(string templateName)
|
||||
@@ -710,10 +733,16 @@ namespace Robust.Client.Placement
|
||||
EntityManager.GetComponent<MetaDataComponent>(CurrentPlacementOverlayEntity.Value));
|
||||
}
|
||||
|
||||
public void PreparePlacementSprite(SpriteComponent sprite)
|
||||
public void PreparePlacementSprite(Entity<SpriteComponent> sprite)
|
||||
{
|
||||
var sc = SetupPlacementOverlayEntity();
|
||||
sc.CopyFrom(sprite);
|
||||
Sprite.CopySprite(sprite.AsNullable(), sc.AsNullable());
|
||||
}
|
||||
|
||||
[Obsolete("Use the Entity<SpriteComponent> overload.")]
|
||||
public void PreparePlacementSprite(SpriteComponent sprite)
|
||||
{
|
||||
PreparePlacementSprite((sprite.Owner, sprite));
|
||||
}
|
||||
|
||||
public void PreparePlacementTexList(List<IDirectionalTextureProvider>? texs, bool noRot, EntityPrototype? prototype)
|
||||
@@ -724,27 +753,27 @@ namespace Robust.Client.Placement
|
||||
// This one covers most cases (including Construction)
|
||||
foreach (var v in texs)
|
||||
{
|
||||
if (v is RSI.State)
|
||||
if (v is RSI.State st)
|
||||
{
|
||||
var st = (RSI.State) v;
|
||||
sc.AddLayer(st.StateId, st.RSI);
|
||||
Sprite.AddRsiLayer(sc.AsNullable(), st.StateId, st.RSI);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
sc.AddLayer(v.Default);
|
||||
Sprite.AddTextureLayer(sc.AsNullable(), v.Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sc.AddLayer(new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
Sprite.AddTextureLayer(sc.AsNullable(), new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
}
|
||||
sc.NoRotation = noRot;
|
||||
|
||||
sc.Comp.NoRotation = noRot;
|
||||
|
||||
if (prototype != null && prototype.TryGetComponent<SpriteComponent>("Sprite", out var spriteComp))
|
||||
{
|
||||
sc.Scale = spriteComp.Scale;
|
||||
Sprite.SetScale(sc.AsNullable(), spriteComp.Scale);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -752,7 +781,7 @@ namespace Robust.Client.Placement
|
||||
private void PreparePlacementTile()
|
||||
{
|
||||
var sc = SetupPlacementOverlayEntity();
|
||||
sc.AddLayer(new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
Sprite.AddTextureLayer(sc.AsNullable(), new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
|
||||
IsActive = true;
|
||||
}
|
||||
@@ -772,7 +801,9 @@ namespace Robust.Client.Placement
|
||||
var grid = EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
|
||||
// no point changing the tile to the same thing.
|
||||
if (Maps.GetTileRef(gridId, grid, coordinates).Tile.TypeId == CurrentPermission.TileType)
|
||||
var tileRef = Maps.GetTileRef(gridId, grid, coordinates).Tile;
|
||||
if (tileRef.TypeId == CurrentPermission.TileType &&
|
||||
tileRef.RotationMirroring == Tile.DirectionToByte(Direction) + (Mirrored ? 4 : 0))
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -796,9 +827,14 @@ 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);
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace Robust.Client.Placement
|
||||
|
||||
sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
|
||||
var rot = args.Viewport.Eye?.Rotation ?? default;
|
||||
spriteSys.Render(uid.Value, sprite, args.WorldHandle, rot, worldRot, worldPos);
|
||||
spriteSys.RenderSprite((uid.Value, sprite), args.WorldHandle, rot, worldRot, worldPos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ namespace Robust.Client.Player
|
||||
{
|
||||
if (_client.RunLevel != ClientRunLevel.SinglePlayerGame)
|
||||
Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues.");
|
||||
var eye = (EyeComponent) Factory.GetComponent(typeof(EyeComponent));
|
||||
var eye = Factory.GetComponent<EyeComponent>();
|
||||
eye.NetSyncEnabled = false;
|
||||
EntManager.AddComponent(uid.Value, eye);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ 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!;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,6 +9,7 @@ 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;
|
||||
@@ -59,7 +61,14 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
try
|
||||
{
|
||||
TextureResource.LoadPreTexture(_manager, data);
|
||||
TextureResource.LoadTextureParameters(_manager, data);
|
||||
if (!data.LoadParameters.Preload)
|
||||
{
|
||||
data.Skip = true;
|
||||
return;
|
||||
}
|
||||
|
||||
TextureResource.LoadPreTextureData(_manager, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -72,7 +81,7 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
foreach (var data in texList)
|
||||
{
|
||||
if (data.Bad)
|
||||
if (data.Bad || data.Skip)
|
||||
continue;
|
||||
|
||||
try
|
||||
@@ -87,6 +96,7 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
|
||||
var errors = 0;
|
||||
var skipped = 0;
|
||||
foreach (var data in texList)
|
||||
{
|
||||
if (data.Bad)
|
||||
@@ -95,6 +105,12 @@ namespace Robust.Client.ResourceManagement
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data.Skip)
|
||||
{
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var texResource = new TextureResource();
|
||||
@@ -110,9 +126,10 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
|
||||
sawmill.Debug(
|
||||
"Preloaded {CountLoaded} textures ({CountErrored} errored) in {LoadTime}",
|
||||
texList.Length,
|
||||
"Preloaded {CountLoaded} textures ({CountErrored} errored, {CountSkipped} skipped) in {LoadTime}",
|
||||
texList.Length - skipped - errors,
|
||||
errors,
|
||||
skipped,
|
||||
sw.Elapsed);
|
||||
}
|
||||
|
||||
@@ -176,65 +193,143 @@ namespace Robust.Client.ResourceManagement
|
||||
// TODO allow RSIs to opt out (useful for very big & rare RSIs)
|
||||
// TODO combine with (non-rsi) texture atlas?
|
||||
|
||||
Array.Sort(atlasList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
|
||||
// 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));
|
||||
|
||||
// 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);
|
||||
|
||||
var deltaY = 0;
|
||||
Vector2i offset = default;
|
||||
int finalized = -1;
|
||||
int atlasCount = 0;
|
||||
for (int i = 0; i < atlasList.Length; i++)
|
||||
// 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 rsi = atlasList[i];
|
||||
if (rsi.Bad)
|
||||
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)
|
||||
continue;
|
||||
|
||||
DebugTools.Assert(rsi.AtlasSheet.Width < sheet.Width);
|
||||
DebugTools.Assert(rsi.AtlasSheet.Height < sheet.Height);
|
||||
|
||||
if (offset.X + rsi.AtlasSheet.Width > sheet.Width)
|
||||
// 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)
|
||||
{
|
||||
offset.X = 0;
|
||||
offset.Y += deltaY;
|
||||
imageAtlases.Add(new Image<Rgba32>(maxSize, currentHeight));
|
||||
finalPixels.Add(0);
|
||||
currentHeight = 0;
|
||||
currentAtlasIndex++;
|
||||
}
|
||||
|
||||
if (offset.Y + rsi.AtlasSheet.Height > sheet.Height)
|
||||
{
|
||||
FinalizeMetaAtlas(i-1, sheet);
|
||||
sheet = new Image<Rgba32>(maxSize, maxSize);
|
||||
deltaY = 0;
|
||||
offset = default;
|
||||
}
|
||||
rsi.AtlasOffset = new Vector2i(0, currentHeight);
|
||||
|
||||
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;
|
||||
var newLevel = new Level
|
||||
{
|
||||
AtlasId = currentAtlasIndex,
|
||||
Position = new Vector2i(0, currentHeight),
|
||||
Height = insertHeight,
|
||||
Width = insertWidth,
|
||||
MaxWidth = maxSize,
|
||||
RSIList = [ rsi ]
|
||||
};
|
||||
levels.Add(newLevel);
|
||||
|
||||
currentHeight += insertHeight;
|
||||
}
|
||||
|
||||
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);
|
||||
// This allocation takes a long time.
|
||||
imageAtlases.Add(new Image<Rgba32>(maxSize, currentHeight));
|
||||
finalPixels.Add(0);
|
||||
|
||||
void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
|
||||
// Put all textures on the atlases
|
||||
foreach (var level in levels)
|
||||
{
|
||||
var fromIndex = finalized + 1;
|
||||
var atlas = Clyde.LoadTextureFromImage(sheet, $"Meta atlas {fromIndex}-{toIndex}");
|
||||
for (int i = fromIndex; i <= toIndex; i++)
|
||||
foreach (var rsi in level.RSIList)
|
||||
{
|
||||
var rsi = atlasList[i];
|
||||
rsi.AtlasTexture = atlas;
|
||||
}
|
||||
var box = new UIBox2i(0, 0, rsi.AtlasSheet.Width, rsi.AtlasSheet.Height);
|
||||
|
||||
finalized = toIndex;
|
||||
atlasCount++;
|
||||
rsi.AtlasSheet.Blit(box, imageAtlases[level.AtlasId], rsi.AtlasOffset);
|
||||
finalPixels[level.AtlasId] += rsi.AtlasSheet.Width * rsi.AtlasSheet.Height;
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize the atlases.
|
||||
for (var i = 0; i < imageAtlases.Count; i++)
|
||||
{
|
||||
var atlasTexture = Clyde.LoadTextureFromImage(imageAtlases[i], $"Meta atlas {i}");
|
||||
finalAtlases.Add(atlasTexture);
|
||||
|
||||
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)
|
||||
{
|
||||
foreach (var rsi in level.RSIList)
|
||||
{
|
||||
rsi.AtlasTexture = finalAtlases[level.AtlasId];
|
||||
}
|
||||
}
|
||||
|
||||
Parallel.ForEach(rsiList, data =>
|
||||
@@ -279,7 +374,7 @@ namespace Robust.Client.ResourceManagement
|
||||
sawmill.Debug(
|
||||
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountNotAtlas} not atlassed, {CountErrored} errored) in {LoadTime}",
|
||||
rsiList.Length,
|
||||
atlasCount,
|
||||
finalAtlases.Count,
|
||||
nonAtlasList.Length,
|
||||
errors,
|
||||
sw.Elapsed);
|
||||
@@ -290,4 +385,38 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +11,13 @@ 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)
|
||||
@@ -28,14 +35,19 @@ 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 (path.Extension == "ogg")
|
||||
if (signature[..OggSignature.Length].SequenceEqual(OggSignature))
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(seekableStream, path.ToString());
|
||||
}
|
||||
else if (path.Extension == "wav")
|
||||
else if (signature[..RiffSignature.Length].SequenceEqual(RiffSignature)
|
||||
&& signature[WavSignatureStart..MaxSignatureLength].SequenceEqual(WavSignature))
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
|
||||
AudioStream = audioManager.LoadAudioWav(seekableStream, path.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -32,18 +32,22 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
var data = new LoadStepData {Path = path};
|
||||
|
||||
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadTextureParameters(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadPreTextureData(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadTexture(dependencies.Resolve<IClyde>(), data);
|
||||
LoadFinish(dependencies.Resolve<IResourceCache>(), data);
|
||||
}
|
||||
|
||||
internal static void LoadPreTexture(IResourceManager cache, LoadStepData data)
|
||||
internal static void LoadPreTextureData(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;
|
||||
}
|
||||
|
||||
@@ -95,7 +99,8 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
var data = new LoadStepData {Path = path};
|
||||
|
||||
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadTextureParameters(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadPreTextureData(dependencies.Resolve<IResourceManager>(), data);
|
||||
|
||||
if (data.Image.Width == Texture.Width && data.Image.Height == Texture.Height)
|
||||
{
|
||||
@@ -119,6 +124,7 @@ namespace Robust.Client.ResourceManagement
|
||||
public Image<Rgba32> Image = default!;
|
||||
public TextureLoadParameters LoadParameters;
|
||||
public OwnedTexture Texture = default!;
|
||||
public bool Skip;
|
||||
public bool Bad;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class LoadPrototypeCommand : IConsoleCommand
|
||||
var dialogManager = IoCManager.Resolve<IFileDialogManager>();
|
||||
var loadManager = IoCManager.Resolve<IGamePrototypeLoadManager>();
|
||||
|
||||
var stream = await dialogManager.OpenFile();
|
||||
var stream = await dialogManager.OpenFile(access: FileAccess.Read);
|
||||
if (stream is null)
|
||||
return;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -36,7 +37,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);
|
||||
await using var file = await _dialog.OpenFile(filters, FileAccess.Read);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
|
||||
@@ -994,6 +994,9 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
internal int DoFrameUpdateRecursive(FrameEventArgs args)
|
||||
{
|
||||
if (!Visible)
|
||||
return 0;
|
||||
|
||||
var total = 1;
|
||||
FrameUpdate(args);
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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;
|
||||
@@ -28,19 +30,23 @@ 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 = "AlignTileAny",
|
||||
PlacementOption = nameof(AlignTileAny),
|
||||
TileType = tileType,
|
||||
Range = 400,
|
||||
IsTile = true
|
||||
@@ -67,6 +73,17 @@ 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();
|
||||
@@ -78,6 +95,9 @@ public sealed class TileSpawningUIController : UIController
|
||||
else
|
||||
{
|
||||
_window.Open();
|
||||
UpdateEntityDirectionLabel();
|
||||
UpdateMirroredButton();
|
||||
_window.SearchBar.GrabKeyboardFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +113,10 @@ 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();
|
||||
}
|
||||
|
||||
@@ -110,6 +134,7 @@ public sealed class TileSpawningUIController : UIController
|
||||
_window.TileList.ClearSelected();
|
||||
_clearingTileSelections = false;
|
||||
_window.EraseButton.Pressed = false;
|
||||
_window.MirroredButton.Pressed = _placement.Mirrored;
|
||||
}
|
||||
|
||||
private void OnTileClearPressed(ButtonEventArgs args)
|
||||
@@ -137,6 +162,7 @@ public sealed class TileSpawningUIController : UIController
|
||||
{
|
||||
var definition = _shownTiles[args.ItemIndex];
|
||||
StartTilePlacement(definition.TileId);
|
||||
UpdateMirroredButton();
|
||||
}
|
||||
|
||||
private void OnTileItemDeselected(ItemList.ItemListDeselectedEventArgs args)
|
||||
@@ -149,6 +175,41 @@ 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;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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;
|
||||
@@ -11,11 +14,14 @@ 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
|
||||
public abstract partial class UIController : IPostInjectInit
|
||||
{
|
||||
[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()
|
||||
{
|
||||
@@ -24,4 +30,39 @@ public abstract partial class UIController
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,7 +741,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
get => _selected;
|
||||
set
|
||||
{
|
||||
if (!Selectable) return;
|
||||
if (!Selectable || _selected == value) return;
|
||||
_selected = value;
|
||||
if(_selected) OnSelected?.Invoke(this);
|
||||
else OnDeselected?.Invoke(this);
|
||||
|
||||
@@ -95,6 +95,12 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public void Clear()
|
||||
{
|
||||
_firstLine = true;
|
||||
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
entry.RemoveControls();
|
||||
}
|
||||
|
||||
_entries.Clear();
|
||||
_totalContentHeight = 0;
|
||||
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
|
||||
@@ -104,6 +110,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public void RemoveEntry(Index index)
|
||||
{
|
||||
var entry = _entries[index];
|
||||
entry.RemoveControls();
|
||||
_entries.RemoveAt(index.GetOffset(_entries.Count));
|
||||
|
||||
var font = _getFont();
|
||||
@@ -189,6 +196,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (entryOffset > contentBox.Height)
|
||||
{
|
||||
entry.HideControls();
|
||||
|
||||
// We know that every subsequent entry will also fail the test, but we also need to
|
||||
// hide all the controls, so we cannot simply break out of the loop
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,27 @@ namespace Robust.Client.UserInterface.Controls
|
||||
private StyleBox? _backgroundStyleBoxOverride;
|
||||
private StyleBox? _foregroundStyleBoxOverride;
|
||||
|
||||
private bool _vertical;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the progress bar is oriented vertically.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A vertical progress bar fills from bottom to top.
|
||||
/// </remarks>
|
||||
public bool Vertical
|
||||
{
|
||||
get => _vertical;
|
||||
set
|
||||
{
|
||||
if (_vertical != value)
|
||||
{
|
||||
_vertical = value;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public StyleBox? BackgroundStyleBoxOverride
|
||||
{
|
||||
get => _backgroundStyleBoxOverride;
|
||||
@@ -70,11 +91,23 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
return;
|
||||
}
|
||||
var minSize = fg.MinimumSize;
|
||||
var size = PixelWidth * GetAsRatio() - minSize.X;
|
||||
if (size > 0)
|
||||
|
||||
if (_vertical)
|
||||
{
|
||||
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, PixelHeight), UIScale);
|
||||
var size = PixelHeight * GetAsRatio();
|
||||
if (size > 0)
|
||||
{
|
||||
fg.Draw(handle, UIBox2.FromDimensions(0, PixelHeight - size, PixelWidth, size), UIScale);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var minSize = fg.MinimumSize;
|
||||
var size = PixelWidth * GetAsRatio() - minSize.X;
|
||||
if (size > 0)
|
||||
{
|
||||
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, PixelHeight), UIScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -15,8 +17,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
[Dependency] private readonly MarkupTagManager _tagManager = default!;
|
||||
|
||||
private FormattedMessage? _message;
|
||||
private RichTextEntry _entry;
|
||||
private RichTextEntry? _entry;
|
||||
private float _lineHeightScale = 1;
|
||||
private bool _lineHeightOverride;
|
||||
|
||||
@@ -40,19 +41,26 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public string? Text
|
||||
{
|
||||
get => _message?.ToMarkup();
|
||||
get => _entry?.Message.ToMarkup();
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
_message?.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
SetMessage(FormattedMessage.FromMarkupPermissive(value));
|
||||
Clear();
|
||||
else
|
||||
SetMessage(FormattedMessage.FromMarkupPermissive(value));
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_entry?.RemoveControls();
|
||||
_entry = null;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
|
||||
public IEnumerable<Control> Controls => _entry?.Controls?.Values ?? Enumerable.Empty<Control>();
|
||||
public IReadOnlyList<MarkupNode> Nodes => _entry?.Message.Nodes ?? Array.Empty<MarkupNode>();
|
||||
|
||||
public RichTextLabel()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
@@ -61,8 +69,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public void SetMessage(FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
|
||||
{
|
||||
_message = message;
|
||||
_entry = new RichTextEntry(_message, this, _tagManager, tagsAllowed, defaultColor);
|
||||
_entry?.RemoveControls();
|
||||
_entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
|
||||
@@ -73,31 +81,31 @@ namespace Robust.Client.UserInterface.Controls
|
||||
SetMessage(msg, tagsAllowed, defaultColor);
|
||||
}
|
||||
|
||||
public string? GetMessage() => _message?.ToMarkup();
|
||||
public string? GetMessage() => _entry?.Message.ToMarkup();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the currently used formatted message.
|
||||
/// </summary>
|
||||
public FormattedMessage? GetFormattedMessage() => _entry == null ? null : new FormattedMessage(_entry.Value.Message);
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
if (_message == null)
|
||||
{
|
||||
if (_entry == null)
|
||||
return Vector2.Zero;
|
||||
}
|
||||
|
||||
var font = _getFont();
|
||||
_entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
|
||||
|
||||
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
|
||||
// _entry is nullable struct.
|
||||
// cannot just call _entry.Value.Update() as that doesn't actually update _entry.
|
||||
_entry = _entry.Value.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
|
||||
|
||||
return new Vector2(_entry.Value.Width / UIScale, _entry.Value.Height / UIScale);
|
||||
}
|
||||
|
||||
protected internal override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (_message == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entry.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
|
||||
_entry?.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
</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>
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
<DebugConsole Name="DebugConsole" />
|
||||
<DevWindowTabUI Name="UI" />
|
||||
<DevWindowTabPerf Name="Perf" />
|
||||
<DevWindowTabTextures Name="Textures" />
|
||||
</TabContainer>
|
||||
</Control>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
{
|
||||
@@ -26,6 +27,7 @@ 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;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<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>
|
||||
@@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,19 @@ namespace Robust.Client.UserInterface
|
||||
/// </summary>
|
||||
internal sealed class DummyFileDialogManager : IFileDialogManager
|
||||
{
|
||||
public Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
public Task<Stream?> OpenFile(
|
||||
FileDialogFilters? filters = null,
|
||||
FileAccess access = FileAccess.ReadWrite,
|
||||
FileShare? share = null)
|
||||
{
|
||||
return Task.FromResult<Stream?>(null);
|
||||
}
|
||||
|
||||
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
|
||||
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
|
||||
FileDialogFilters? filters = null,
|
||||
bool truncate = true,
|
||||
FileAccess access = FileAccess.ReadWrite,
|
||||
FileShare share = FileShare.None)
|
||||
{
|
||||
return Task.FromResult<(Stream fileStream, bool alreadyExisted)?>(null);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ 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;
|
||||
@@ -30,18 +28,28 @@ namespace Robust.Client.UserInterface
|
||||
private bool _kDialogAvailable;
|
||||
private bool _checkedKDialogAvailable;
|
||||
|
||||
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
public async Task<Stream?> OpenFile(
|
||||
FileDialogFilters? filters = null,
|
||||
FileAccess access = FileAccess.ReadWrite,
|
||||
FileShare? share = 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)
|
||||
return await clydeImpl.OpenFile(filters);
|
||||
name = await clydeImpl.OpenFile(filters);
|
||||
else
|
||||
name = await GetOpenFileName(filters);
|
||||
|
||||
var name = await GetOpenFileName(filters);
|
||||
if (name == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return File.Open(name, FileMode.Open);
|
||||
return File.Open(name, FileMode.Open, access, realShare);
|
||||
}
|
||||
|
||||
private async Task<string?> GetOpenFileName(FileDialogFilters? filters)
|
||||
@@ -54,24 +62,34 @@ namespace Robust.Client.UserInterface
|
||||
return await OpenFileNfd(filters);
|
||||
}
|
||||
|
||||
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
|
||||
public async Task<(Stream, bool)?> SaveFile(
|
||||
FileDialogFilters? filters,
|
||||
bool truncate = true,
|
||||
FileAccess access = FileAccess.ReadWrite,
|
||||
FileShare share = FileShare.None)
|
||||
{
|
||||
if (_clyde.FileDialogImpl is { } clydeImpl)
|
||||
return await clydeImpl.SaveFile(filters);
|
||||
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);
|
||||
|
||||
var name = await GetSaveFileName(filters);
|
||||
if (name == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open), true);
|
||||
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open, access, share), true);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return (File.Open(name, FileMode.Create), false);
|
||||
return (File.Open(name, FileMode.Create, access, share), false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
@@ -19,7 +20,17 @@ namespace Robust.Client.UserInterface
|
||||
/// The file stream for the file the user opened.
|
||||
/// <see langword="null" /> if the user cancelled the action.
|
||||
/// </returns>
|
||||
Task<Stream?> OpenFile(FileDialogFilters? filters = null);
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Open a file dialog used for saving a single file.
|
||||
@@ -29,6 +40,21 @@ 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>
|
||||
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true);
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class BoldItalicTag : IMarkupTag
|
||||
public sealed class BoldItalicTag : IMarkupTagHandler
|
||||
{
|
||||
public const string BoldItalicFont = "DefaultBoldItalic";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class BoldTag : IMarkupTag
|
||||
public sealed class BoldTag : IMarkupTagHandler
|
||||
{
|
||||
public const string BoldFont = "DefaultBold";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class BulletTag : IMarkupTag
|
||||
public sealed class BulletTag : IMarkupTagHandler
|
||||
{
|
||||
public string Name => "bullet";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Robust.Client.UserInterface.RichText;
|
||||
/// <summary>
|
||||
/// Colors the text inside its opening and closing nodes
|
||||
/// </summary>
|
||||
public sealed class ColorTag : IMarkupTag
|
||||
public sealed class ColorTag : IMarkupTagHandler
|
||||
{
|
||||
public static readonly Color DefaultColor = new(200, 200, 200);
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class CommandLinkTag : IMarkupTag
|
||||
public sealed class CommandLinkTag : IMarkupTagHandler
|
||||
{
|
||||
[Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!;
|
||||
|
||||
public string Name => "cmdlink";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
if (!node.Value.TryGetString(out var text)
|
||||
|| !node.Attributes.TryGetValue("command", out var commandParameter)
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Robust.Client.UserInterface.RichText;
|
||||
/// Applies the font provided as the tags parameter to the markup drawing context.
|
||||
/// Definitely not save for user supplied markup
|
||||
/// </summary>
|
||||
public sealed class FontTag : IMarkupTag
|
||||
public sealed class FontTag : IMarkupTagHandler
|
||||
{
|
||||
public const string DefaultFont = "Default";
|
||||
public const int DefaultSize = 12;
|
||||
|
||||
@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class HeadingTag : IMarkupTag
|
||||
public sealed class HeadingTag : IMarkupTagHandler
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public interface IMarkupTag
|
||||
/// <summary>
|
||||
/// Classes that implement this interface will be instantiated by <see cref="MarkupTagManager"/> and used to handle
|
||||
/// the parsing and behaviour of markup tags. Note that each class is only ever instantiated once by the tag manager,
|
||||
/// and wil be used to handle all tags of that kind, and thus should not contain state information relevant to a
|
||||
/// specific tag.
|
||||
/// </summary>
|
||||
public interface IMarkupTagHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The string used as the tags name when writing rich text
|
||||
@@ -54,17 +61,32 @@ public interface IMarkupTag
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called inside the constructor of <see cref="RichTextEntry"/> to
|
||||
/// supply a control that gets rendered inline before this tags children<br/>
|
||||
/// Text continues to the right of the control until the next line and then continues bellow it
|
||||
/// Called inside the constructor of <see cref="RichTextEntry"/> to supply a control that gets rendered inline
|
||||
/// before this tags children. The returned control must be new instance to avoid issues with shallow cloning
|
||||
/// <see cref="FormattedMessage"/> nodes. Text continues to the right of the control until the next line and
|
||||
/// then continues bellow it.
|
||||
/// </summary>
|
||||
/// <param name="node">The markup node containing the parameter and attributes</param>
|
||||
/// <param name="control">A UI control for placing in line with this tags children</param>
|
||||
/// <returns>true if this tag supplies a control</returns>
|
||||
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use IMarkupTagHandler")]
|
||||
public interface IMarkupTag : IMarkupTagHandler
|
||||
{
|
||||
bool IMarkupTagHandler.TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
return TryGetControl(node, out control);
|
||||
}
|
||||
|
||||
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class ItalicTag : IMarkupTag
|
||||
public sealed class ItalicTag : IMarkupTagHandler
|
||||
{
|
||||
public const string ItalicFont = "DefaultItalic";
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class MarkupTagManager
|
||||
/// <summary>
|
||||
/// Tags defined in engine need to be instantiated here because of sandboxing
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, IMarkupTag> _markupTagTypes = new IMarkupTag[] {
|
||||
private readonly Dictionary<string, IMarkupTagHandler> _markupTagTypes = new IMarkupTagHandler[] {
|
||||
new BoldItalicTag(),
|
||||
new BoldTag(),
|
||||
new BulletTag(),
|
||||
@@ -44,13 +44,13 @@ public sealed class MarkupTagManager
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTag>())
|
||||
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTagHandler>())
|
||||
{
|
||||
//Prevent tags defined inside engine from being instantiated
|
||||
if (_engineTypes.Contains(type))
|
||||
continue;
|
||||
|
||||
var instance = (IMarkupTag)_sandboxHelper.CreateInstance(type);
|
||||
var instance = (IMarkupTagHandler)_sandboxHelper.CreateInstance(type);
|
||||
_markupTagTypes[instance.Name.ToLower()] = instance;
|
||||
}
|
||||
|
||||
@@ -60,22 +60,48 @@ public sealed class MarkupTagManager
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use GetMarkupTagHandler")]
|
||||
public IMarkupTag? GetMarkupTag(string name)
|
||||
{
|
||||
return _markupTagTypes.GetValueOrDefault(name) as IMarkupTag;
|
||||
}
|
||||
|
||||
public IMarkupTagHandler? GetMarkupTagHandler(string name)
|
||||
{
|
||||
return _markupTagTypes.GetValueOrDefault(name);
|
||||
}
|
||||
|
||||
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
|
||||
/// <summary>
|
||||
/// Attempt to get the tag handler with the corresponding name.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the tag, as specified by <see cref="IMarkupTag.Name"/></param>
|
||||
/// <param name="tagsAllowed">List of allowed tag types. If null, all types are allowed.</param>
|
||||
/// <param name="handler">The instance responsible for handling tags of this type.</param>
|
||||
/// <returns></returns>
|
||||
public bool TryGetMarkupTagHandler(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTagHandler? handler)
|
||||
{
|
||||
if (_markupTagTypes.TryGetValue(name, out var markupTag)
|
||||
// Using a whitelist prevents new tags from sneaking in.
|
||||
&& (tagsAllowed == null || Array.IndexOf(tagsAllowed, markupTag.GetType()) != -1))
|
||||
{
|
||||
tag = markupTag;
|
||||
handler = markupTag;
|
||||
return true;
|
||||
}
|
||||
|
||||
tag = null;
|
||||
handler = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
[Obsolete("Use TryGetMarkupTagHandler")]
|
||||
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
|
||||
{
|
||||
if (!TryGetMarkupTagHandler(name, tagsAllowed, out var handler) || handler is not IMarkupTag cast)
|
||||
{
|
||||
tag = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
tag = cast;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Used by <see cref="OutputPanel"/> and <see cref="RichTextLabel"/> to handle rich text layout.
|
||||
/// Note that if this text is ever removed or modified without removing the owning control,
|
||||
/// then <see cref="RemoveControls"/> should be called to ensure that any controls that were added by this
|
||||
/// entry are also removed.
|
||||
/// </summary>
|
||||
internal struct RichTextEntry
|
||||
{
|
||||
@@ -36,7 +39,7 @@ namespace Robust.Client.UserInterface
|
||||
/// </summary>
|
||||
public ValueList<int> LineBreaks;
|
||||
|
||||
private readonly Dictionary<int, Control>? _tagControls;
|
||||
public readonly Dictionary<int, Control>? Controls;
|
||||
|
||||
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
|
||||
{
|
||||
@@ -56,15 +59,35 @@ namespace Robust.Client.UserInterface
|
||||
if (node.Name == null)
|
||||
continue;
|
||||
|
||||
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
|
||||
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var handler) || !handler.TryCreateControl(node, out var control))
|
||||
continue;
|
||||
|
||||
// Markup tag handler instances are shared across controls. We need to ensure that the hanlder doesn't
|
||||
// store state information and return the same control for each rich text entry.
|
||||
DebugTools.Assert(handler.TryCreateControl(node, out var other) && other != control);
|
||||
|
||||
parent.Children.Add(control);
|
||||
tagControls ??= new Dictionary<int, Control>();
|
||||
tagControls.Add(nodeIndex, control);
|
||||
}
|
||||
|
||||
_tagControls = tagControls;
|
||||
Controls = tagControls;
|
||||
}
|
||||
|
||||
// TODO RICH TEXT
|
||||
// Somehow ensure that this **has** to be called when removing rich text from some control.
|
||||
/// <summary>
|
||||
/// Remove all owned controls from their parents.
|
||||
/// </summary>
|
||||
public readonly void RemoveControls()
|
||||
{
|
||||
if (Controls == null)
|
||||
return;
|
||||
|
||||
foreach (var ctrl in Controls.Values)
|
||||
{
|
||||
ctrl.Orphan();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,7 +97,7 @@ namespace Robust.Client.UserInterface
|
||||
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
|
||||
/// <param name="uiScale"></param>
|
||||
/// <param name="lineHeightScale"></param>
|
||||
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
|
||||
public RichTextEntry Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
|
||||
{
|
||||
// This method is gonna suck due to complexity.
|
||||
// Bear with me here.
|
||||
@@ -112,10 +135,10 @@ namespace Robust.Client.UserInterface
|
||||
continue;
|
||||
|
||||
if (ProcessMetric(ref this, metrics, out breakLine))
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
|
||||
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
|
||||
continue;
|
||||
|
||||
control.Measure(new Vector2(Width, Height));
|
||||
@@ -128,12 +151,14 @@ namespace Robust.Client.UserInterface
|
||||
desiredSize.Y);
|
||||
|
||||
if (ProcessMetric(ref this, controlMetrics, out breakLine))
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
|
||||
Width = wordWrap.FinalizeText(out breakLine);
|
||||
CheckLineBreak(ref this, breakLine);
|
||||
|
||||
return this;
|
||||
|
||||
bool ProcessRune(ref RichTextEntry src, Rune rune, out int? outBreakLine)
|
||||
{
|
||||
wordWrap.NextRune(rune, out breakLine, out var breakNewLine, out var skip);
|
||||
@@ -166,9 +191,10 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
internal readonly void HideControls()
|
||||
{
|
||||
if (_tagControls == null)
|
||||
if (Controls == null)
|
||||
return;
|
||||
foreach (var control in _tagControls.Values)
|
||||
|
||||
foreach (var control in Controls.Values)
|
||||
{
|
||||
control.Visible = false;
|
||||
}
|
||||
@@ -220,7 +246,7 @@ namespace Robust.Client.UserInterface
|
||||
globalBreakCounter += 1;
|
||||
}
|
||||
|
||||
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
|
||||
continue;
|
||||
|
||||
// Controls may have been previously hidden via HideControls due to being "out-of frame".
|
||||
@@ -243,7 +269,7 @@ namespace Robust.Client.UserInterface
|
||||
return node.Value.StringValue ?? "";
|
||||
|
||||
//Skip the node if there is no markup tag for it.
|
||||
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
|
||||
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var tag))
|
||||
return "";
|
||||
|
||||
if (!node.Closing)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
@@ -21,12 +20,14 @@ internal sealed class ReloadManager : IReloadManager
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
#pragma warning disable CS0414
|
||||
[Dependency] private readonly ITaskManager _tasks = default!;
|
||||
#pragma warning restore CS0414
|
||||
|
||||
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
|
||||
private CancellationTokenSource _reloadToken = new();
|
||||
private readonly HashSet<ResPath> _reloadQueue = new();
|
||||
private List<FileSystemWatcher> _watchers = new();
|
||||
private List<FileSystemWatcher> _watchers = new(); // this list is never used but needed to prevent them from being garbage collected
|
||||
|
||||
public event Action<ResPath>? OnChanged;
|
||||
|
||||
@@ -69,6 +70,11 @@ internal sealed class ReloadManager : IReloadManager
|
||||
_reloadQueue.Clear();
|
||||
}
|
||||
|
||||
public void Register(ResPath directory, string filter)
|
||||
{
|
||||
Register(directory.ToString(), filter);
|
||||
}
|
||||
|
||||
public void Register(string directory, string filter)
|
||||
{
|
||||
if (!_cfg.GetCVar(CVars.ResPrototypeReloadWatch))
|
||||
@@ -90,7 +96,7 @@ internal sealed class ReloadManager : IReloadManager
|
||||
NotifyFilter = NotifyFilters.LastWrite
|
||||
};
|
||||
|
||||
_watchers.Add(watcher);
|
||||
_watchers.Add(watcher); // prevent garbage collection
|
||||
|
||||
watcher.Changed += OnWatch;
|
||||
|
||||
@@ -100,7 +106,7 @@ internal sealed class ReloadManager : IReloadManager
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.Error($"Watching resources in path {path} threw an exception:\n{ex}");
|
||||
_sawmill.Error($"Watching resources in path {path} threw an exception:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +142,6 @@ internal sealed class ReloadManager : IReloadManager
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Robust.Client.Utility
|
||||
/// </summary>
|
||||
public static class SpriteSpecifierExt
|
||||
{
|
||||
[Obsolete("Use SpriteSystem.GetTexture() instead")]
|
||||
public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, IResourceCache cache)
|
||||
{
|
||||
return cache
|
||||
@@ -24,13 +25,14 @@ namespace Robust.Client.Utility
|
||||
.Texture;
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem")]
|
||||
[Obsolete("Use SpriteSystem.GetState() instead")]
|
||||
public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourceCache cache)
|
||||
{
|
||||
if (!cache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / rsiSpecifier.RsiPath, out var theRsi))
|
||||
{
|
||||
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
Logger.Error("SpriteSpecifier failed to load RSI {0}", rsiSpecifier.RsiPath);
|
||||
return SpriteComponent.GetFallbackState(cache);
|
||||
return sys.GetFallbackState();
|
||||
}
|
||||
|
||||
if (theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
|
||||
@@ -39,21 +41,22 @@ namespace Robust.Client.Utility
|
||||
}
|
||||
|
||||
Logger.Error($"SpriteSpecifier has invalid RSI state '{rsiSpecifier.RsiState}' for RSI: {rsiSpecifier.RsiPath}");
|
||||
return SpriteComponent.GetFallbackState(cache);
|
||||
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>().GetFallbackState();
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem")]
|
||||
[Obsolete("Use SpriteSystem.Frame0() instead")]
|
||||
public static Texture Frame0(this SpriteSpecifier specifier)
|
||||
{
|
||||
return specifier.RsiStateLike().Default;
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem.RsiStateLike() instead")]
|
||||
public static IDirectionalTextureProvider DirFrame0(this SpriteSpecifier specifier)
|
||||
{
|
||||
return specifier.RsiStateLike();
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem")]
|
||||
[Obsolete("Use SpriteSystem.RsiStateLike() instead")]
|
||||
public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier)
|
||||
{
|
||||
var resC = IoCManager.Resolve<IResourceCache>();
|
||||
@@ -67,10 +70,11 @@ namespace Robust.Client.Utility
|
||||
|
||||
case SpriteSpecifier.EntityPrototype prototypeIcon:
|
||||
var protMgr = IoCManager.Resolve<IPrototypeManager>();
|
||||
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
if (!protMgr.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
|
||||
{
|
||||
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
|
||||
return SpriteComponent.GetFallbackState(resC);
|
||||
return sys.GetFallbackState();
|
||||
}
|
||||
|
||||
return SpriteComponent.GetPrototypeIcon(prototype, resC);
|
||||
|
||||
@@ -39,6 +39,7 @@ public static class Diagnostics
|
||||
public const string IdForbidLiteral = "RA0033";
|
||||
public const string IdObsoleteInheritance = "RA0034";
|
||||
public const string IdObsoleteInheritanceWithMessage = "RA0035";
|
||||
public const string IdDataFieldYamlSerializable = "RA0036";
|
||||
|
||||
public static SuppressionDescriptor MeansImplicitAssignment =>
|
||||
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");
|
||||
|
||||
@@ -331,6 +331,7 @@ namespace Robust.Server
|
||||
// TODO: solve this properly.
|
||||
_serializer.Initialize();
|
||||
|
||||
_loc.Initialize();
|
||||
_loc.AddLoadedToStringSerializer(_stringSerializer);
|
||||
|
||||
//IoCManager.Resolve<IMapLoader>().LoadedMapData +=
|
||||
|
||||
@@ -14,6 +14,19 @@ public sealed class PointLightSystem : SharedPointLightSystem
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentGetState>(OnLightGetState);
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentStartup>(OnLightStartup);
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentShutdown>(OnLightShutdown);
|
||||
SubscribeLocalEvent<PointLightComponent, MetaFlagRemoveAttemptEvent>(OnFlagRemoveAttempt);
|
||||
}
|
||||
|
||||
private void OnLightShutdown(Entity<PointLightComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
UpdatePriority(ent.Owner, ent.Comp, MetaData(ent.Owner));
|
||||
}
|
||||
|
||||
private void OnFlagRemoveAttempt(Entity<PointLightComponent> ent, ref MetaFlagRemoveAttemptEvent args)
|
||||
{
|
||||
if (IsHighPriority(ent.Comp))
|
||||
args.ToRemove &= ~MetaDataFlags.PvsPriority;
|
||||
}
|
||||
|
||||
private void OnLightStartup(EntityUid uid, PointLightComponent component, ComponentStartup args)
|
||||
@@ -21,24 +34,14 @@ public sealed class PointLightSystem : SharedPointLightSystem
|
||||
UpdatePriority(uid, component, MetaData(uid));
|
||||
}
|
||||
|
||||
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
|
||||
private bool IsHighPriority(SharedPointLightComponent comp)
|
||||
{
|
||||
var isHighPriority = comp.Enabled && comp.CastShadows && (comp.Radius > 7);
|
||||
_metadata.SetFlag((uid, meta), MetaDataFlags.PvsPriority, isHighPriority);
|
||||
return comp is {Enabled: true, CastShadows: true, Radius: > 7, LifeStage: <= ComponentLifeStage.Running};
|
||||
}
|
||||
|
||||
private void OnLightGetState(EntityUid uid, PointLightComponent component, ref ComponentGetState args)
|
||||
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
|
||||
{
|
||||
args.State = new PointLightComponentState()
|
||||
{
|
||||
Color = component.Color,
|
||||
Enabled = component.Enabled,
|
||||
Energy = component.Energy,
|
||||
Offset = component.Offset,
|
||||
Radius = component.Radius,
|
||||
Softness = component.Softness,
|
||||
CastShadows = component.CastShadows,
|
||||
};
|
||||
_metadata.SetFlag((uid, meta), MetaDataFlags.PvsPriority, IsHighPriority(comp));
|
||||
}
|
||||
|
||||
public override SharedPointLightComponent EnsureLight(EntityUid uid)
|
||||
|
||||
@@ -9,12 +9,30 @@ public sealed class ServerOccluderSystem : OccluderSystem
|
||||
{
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<OccluderComponent, MetaFlagRemoveAttemptEvent>(OnFlagRemoveAttempt);
|
||||
}
|
||||
|
||||
private void OnFlagRemoveAttempt(Entity<OccluderComponent> ent, ref MetaFlagRemoveAttemptEvent args)
|
||||
{
|
||||
if (ent.Comp is {Enabled: true, LifeStage: <= ComponentLifeStage.Running})
|
||||
args.ToRemove &= ~MetaDataFlags.PvsPriority;
|
||||
}
|
||||
|
||||
protected override void OnCompStartup(EntityUid uid, OccluderComponent component, ComponentStartup args)
|
||||
{
|
||||
base.OnCompStartup(uid, component, args);
|
||||
_metadata.SetFlag(uid, MetaDataFlags.PvsPriority, component.Enabled);
|
||||
}
|
||||
|
||||
protected override void OnCompRemoved(EntityUid uid, OccluderComponent component, ComponentRemove args)
|
||||
{
|
||||
base.OnCompRemoved(uid, component, args);
|
||||
_metadata.SetFlag(uid, MetaDataFlags.PvsPriority, false);
|
||||
}
|
||||
|
||||
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
|
||||
@@ -166,11 +166,11 @@ internal sealed partial class PvsSystem
|
||||
var session = pvsSession.Session;
|
||||
if (session.Status != SessionStatus.InGame)
|
||||
{
|
||||
pvsSession.Viewers = Array.Empty<Entity<TransformComponent, EyeComponent?>>();
|
||||
pvsSession.Viewers = Array.Empty<Entity<TransformComponent, EyeComponent?>>();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fast path
|
||||
// The majority of players will have no view subscriptions
|
||||
if (session.ViewSubscriptions.Count == 0)
|
||||
{
|
||||
if (session.AttachedEntity is not {} attached)
|
||||
@@ -184,15 +184,21 @@ internal sealed partial class PvsSystem
|
||||
return;
|
||||
}
|
||||
|
||||
var count = session.ViewSubscriptions.Count;
|
||||
var i = 0;
|
||||
if (session.AttachedEntity is { } local)
|
||||
{
|
||||
Array.Resize(ref pvsSession.Viewers, session.ViewSubscriptions.Count + 1);
|
||||
if (!session.ViewSubscriptions.Contains(local))
|
||||
count += 1;
|
||||
|
||||
Array.Resize(ref pvsSession.Viewers, count);
|
||||
|
||||
// Attached entity is always the first viewer, to prioritize it and help reduce pop-in for the "main" eye.
|
||||
pvsSession.Viewers[i++] = (local, Transform(local), _eyeQuery.CompOrNull(local));
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Resize(ref pvsSession.Viewers, session.ViewSubscriptions.Count);
|
||||
Array.Resize(ref pvsSession.Viewers, count);
|
||||
}
|
||||
|
||||
foreach (var ent in session.ViewSubscriptions)
|
||||
@@ -200,6 +206,8 @@ internal sealed partial class PvsSystem
|
||||
if (ent != session.AttachedEntity)
|
||||
pvsSession.Viewers[i++] = (ent, Transform(ent), _eyeQuery.CompOrNull(ent));
|
||||
}
|
||||
|
||||
DebugTools.AssertEqual(i, pvsSession.Viewers.Length);
|
||||
}
|
||||
|
||||
private void ProcessVisibleChunks()
|
||||
|
||||
8
Robust.Server/Localization/ServerLocalizationManager.cs
Normal file
8
Robust.Server/Localization/ServerLocalizationManager.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Server.Localization;
|
||||
|
||||
internal sealed class ServerLocalizationManager : LocalizationManager, ILocalizationManager
|
||||
{
|
||||
void ILocalizationManager.Initialize() => Initialize();
|
||||
}
|
||||
82
Robust.Server/Physics/Commands/BoxStackCommand.cs
Normal file
82
Robust.Server/Physics/Commands/BoxStackCommand.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Controllers;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
|
||||
namespace Robust.Server.Physics.Commands;
|
||||
|
||||
public sealed class BoxStackCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public string Command => "boxstack";
|
||||
public string Description => string.Empty;
|
||||
public string Help => "boxstack [mapid] [columns] [rows]";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 3 ||
|
||||
!int.TryParse(args[0], out var mapInt) ||
|
||||
!int.TryParse(args[1], out var columns) ||
|
||||
!int.TryParse(args[2], out var rows))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mapId = new MapId(mapInt);
|
||||
|
||||
var fixtureSystem = _entManager.System<FixtureSystem>();
|
||||
var physSystem = _entManager.System<SharedPhysicsSystem>();
|
||||
physSystem.SetGravity(new Vector2(0f, -9.8f));
|
||||
|
||||
var groundUid = _entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
|
||||
var ground = _entManager.AddComponent<PhysicsComponent>(groundUid);
|
||||
var groundManager = _entManager.EnsureComponent<FixturesComponent>(groundUid);
|
||||
|
||||
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
|
||||
fixtureSystem.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 1, 1, true), manager: groundManager, body: ground);
|
||||
|
||||
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
|
||||
fixtureSystem.CreateFixture(groundUid, "fix2", new Fixture(vertical, 1, 1, true), manager: groundManager, body: ground);
|
||||
|
||||
physSystem.WakeBody(groundUid, manager: groundManager, body: ground);
|
||||
|
||||
var xs = new[]
|
||||
{
|
||||
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
|
||||
};
|
||||
|
||||
for (var j = 0; j < columns; j++)
|
||||
{
|
||||
for (var i = 0; i < rows; i++)
|
||||
{
|
||||
var x = 0.0f;
|
||||
|
||||
var boxUid = _entManager.SpawnEntity(null,
|
||||
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 2.1f * i), mapId));
|
||||
var box = _entManager.AddComponent<PhysicsComponent>(boxUid);
|
||||
var manager = _entManager.EnsureComponent<FixturesComponent>(boxUid);
|
||||
|
||||
physSystem.SetBodyType(boxUid, BodyType.Dynamic, manager: manager, body: box);
|
||||
var poly = new PolygonShape(0.001f);
|
||||
poly.Set(new List<Vector2>()
|
||||
{
|
||||
new(0.5f, -0.5f),
|
||||
new(0.5f, 0.5f),
|
||||
new(-0.5f, 0.5f),
|
||||
new(-0.5f, -0.5f),
|
||||
});
|
||||
|
||||
fixtureSystem.CreateFixture(boxUid, "fix1", new Fixture(poly, 1, 1, true), manager: manager, body: box);
|
||||
physSystem.WakeBody(boxUid, manager: manager, body: box);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ namespace Robust.Server.Placement
|
||||
{
|
||||
// Replace existing entities if relevant.
|
||||
if (msg.Replacement && _prototype.Index<EntityPrototype>(entityTemplateName).Components.TryGetValue(
|
||||
_factory.GetComponentName(typeof(PlacementReplacementComponent)), out var compRegistry))
|
||||
_factory.GetComponentName<PlacementReplacementComponent>(), out var compRegistry))
|
||||
{
|
||||
var key = ((PlacementReplacementComponent)compRegistry.Component).Key;
|
||||
var gridUid = _xformSystem.GetGrid(coordinates);
|
||||
@@ -179,11 +179,14 @@ namespace Robust.Server.Placement
|
||||
}
|
||||
else
|
||||
{
|
||||
PlaceNewTile(tileType, coordinates, msg.MsgChannel.UserId);
|
||||
if (_tileDefinitionManager[tileType].AllowRotationMirror)
|
||||
PlaceNewTile(tileType, coordinates, msg.MsgChannel.UserId, Tile.DirectionToByte(dirRcv), msg.Mirrored);
|
||||
else
|
||||
PlaceNewTile(tileType, coordinates, msg.MsgChannel.UserId, Tile.DirectionToByte(Direction.South), false);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId placingUserId)
|
||||
private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId placingUserId, byte direction, bool mirrored)
|
||||
{
|
||||
if (!coordinates.IsValid(_entityManager)) return;
|
||||
|
||||
@@ -193,7 +196,7 @@ namespace Robust.Server.Placement
|
||||
if (_entityManager.TryGetComponent(coordinates.EntityId, out grid)
|
||||
|| _mapManager.TryFindGridAt(_xformSystem.ToMapCoordinates(coordinates), out gridId, out grid))
|
||||
{
|
||||
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType));
|
||||
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0))));
|
||||
|
||||
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
|
||||
@@ -207,7 +210,7 @@ namespace Robust.Server.Placement
|
||||
|
||||
_xformSystem.SetWorldPosition(newGridXform, coordinates.Position - newGrid.Comp.TileSizeHalfVector); // assume bottom left tile origin
|
||||
var tilePos = _maps.WorldToTile(newGrid.Owner, newGrid.Comp, coordinates.Position);
|
||||
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType));
|
||||
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0))));
|
||||
|
||||
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
|
||||
|
||||
@@ -13,8 +13,10 @@ namespace Robust.Server.Prototypes
|
||||
{
|
||||
public sealed class ServerPrototypeManager : PrototypeManager
|
||||
{
|
||||
#pragma warning disable CS0414
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IConGroupController _conGroups = default!;
|
||||
#pragma warning restore CS0414
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly IBaseServerInternal _server = default!;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Robust.Server.Console;
|
||||
using Robust.Server.DataMetrics;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Server.Localization;
|
||||
using Robust.Server.Placement;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Server.Prototypes;
|
||||
@@ -21,6 +22,7 @@ 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.Player;
|
||||
@@ -97,7 +99,9 @@ namespace Robust.Server
|
||||
deps.Register<NetworkResourceManager>();
|
||||
deps.Register<IHttpClientHolder, HttpClientHolder>();
|
||||
deps.Register<UploadedContentManager>();
|
||||
deps.Register<IHWId, DummyHWId>();
|
||||
deps.Register<IHWId, DummyHWId>();
|
||||
deps.Register<ILocalizationManager, ServerLocalizationManager>();
|
||||
deps.Register<ILocalizationManagerInternal, ServerLocalizationManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,16 @@ namespace Robust.Shared.Maths
|
||||
return (Direction) (Math.Floor((ang + CardinalOffset) / CardinalSegment) * 2 % 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the angle to the nearest cardinal direction. This behaves similarly to a combination of
|
||||
/// <see cref="GetCardinalDir"/> and Direction.ToAngle(), however this may return an angle outside of the range
|
||||
/// returned by those methods (-pi to pi).
|
||||
/// </summary>
|
||||
public Angle RoundToCardinalAngle()
|
||||
{
|
||||
return new Angle(CardinalSegment * Math.Floor((Theta + CardinalOffset) / CardinalSegment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates the vector counter-clockwise around its origin by the value of Theta.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -101,6 +102,7 @@ namespace Robust.Shared.Maths
|
||||
/// <summary>
|
||||
/// Returns the smallest rectangle that contains both of the rectangles.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public readonly Box2i Union(in Box2i other)
|
||||
{
|
||||
var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft);
|
||||
@@ -207,6 +209,7 @@ namespace Robust.Shared.Maths
|
||||
/// <summary>
|
||||
/// Multiplies each side of the box by the scalar.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public Box2i Scale(int scalar)
|
||||
{
|
||||
return new Box2i(
|
||||
@@ -215,6 +218,21 @@ namespace Robust.Shared.Maths
|
||||
Right * scalar,
|
||||
Top * scalar);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public bool Intersects(in Box2i other)
|
||||
{
|
||||
return other.Bottom <= this.Top && other.Top >= this.Bottom && other.Right >= this.Left &&
|
||||
other.Left <= this.Right;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public readonly Box2i Enlarged(int size)
|
||||
{
|
||||
return new(Left - size, Bottom - size, Right + size, Top + size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace Robust.Shared.Audio;
|
||||
/// <seealso cref="ResolvedPathSpecifier"/>
|
||||
/// <seealso cref="ResolvedCollectionSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public abstract partial class ResolvedSoundSpecifier {
|
||||
public abstract partial class ResolvedSoundSpecifier
|
||||
{
|
||||
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
|
||||
public static implicit operator ResolvedSoundSpecifier(string s) => new ResolvedPathSpecifier(s);
|
||||
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
|
||||
@@ -22,8 +23,10 @@ public abstract partial class ResolvedSoundSpecifier {
|
||||
/// <summary>
|
||||
/// Returns whether <c>s</c> is null, or if it contains an empty path/collection ID.
|
||||
/// </summary>
|
||||
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s) {
|
||||
return s switch {
|
||||
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s)
|
||||
{
|
||||
return s switch
|
||||
{
|
||||
null => true,
|
||||
ResolvedPathSpecifier path => path.Path.ToString() == "",
|
||||
ResolvedCollectionSpecifier collection => string.IsNullOrEmpty(collection.Collection),
|
||||
@@ -37,7 +40,8 @@ public abstract partial class ResolvedSoundSpecifier {
|
||||
/// </summary>
|
||||
/// <seealso cref="ResolvedCollectionSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
|
||||
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedPathSpecifier>
|
||||
{
|
||||
/// <summary>
|
||||
/// The resource path of the sound.
|
||||
/// </summary>
|
||||
@@ -57,6 +61,21 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
|
||||
public ResolvedPathSpecifier(string path) : this(new ResPath(path))
|
||||
{
|
||||
}
|
||||
|
||||
public bool Equals(ResolvedPathSpecifier? other)
|
||||
{
|
||||
return Path.Equals(other?.Path);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return Equals(obj as ResolvedPathSpecifier);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Path.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,7 +83,9 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
|
||||
/// </summary>
|
||||
/// <seealso cref="ResolvedPathSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier {
|
||||
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedCollectionSpecifier>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the <see cref="SoundCollectionPrototype">sound collection</see> to look up.
|
||||
/// </summary>
|
||||
@@ -87,4 +108,19 @@ public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier
|
||||
Collection = collection;
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public bool Equals(ResolvedCollectionSpecifier? other)
|
||||
{
|
||||
return Collection.Equals(other?.Collection) && Index.Equals(other?.Index);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return Equals(obj as ResolvedCollectionSpecifier);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Collection, Index);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user