mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
03f8d4d3e0 | ||
|
|
728d541ca5 | ||
|
|
4cbce064b8 | ||
|
|
93bb7b1532 | ||
|
|
3ba91d2ed0 | ||
|
|
8f9e0f6bab | ||
|
|
3ed408de5d | ||
|
|
2f85408f8f | ||
|
|
f49b01b1b7 | ||
|
|
3ce764311d | ||
|
|
adf0b6ae78 | ||
|
|
04406311dc | ||
|
|
ee45a608b9 | ||
|
|
077c91a54b | ||
|
|
2ac17009ee | ||
|
|
a73d1b6666 | ||
|
|
e5d6f194be | ||
|
|
72d893dec5 | ||
|
|
191d7ab81c | ||
|
|
65d2f2dd2f | ||
|
|
02b451db2a | ||
|
|
d5d4584e11 | ||
|
|
b146b1b82c | ||
|
|
2f8f4f2f7a | ||
|
|
7365a59bd9 | ||
|
|
37560f663b | ||
|
|
bd489e9218 | ||
|
|
33166e8866 | ||
|
|
ed16032280 | ||
|
|
cf785c886b |
@@ -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>
|
||||
|
||||
178
RELEASE-NOTES.md
178
RELEASE-NOTES.md
@@ -1,4 +1,4 @@
|
||||
# Release notes for RobustToolbox.
|
||||
# Release notes for RobustToolbox.
|
||||
|
||||
<!--
|
||||
NOTE: automatically updated sometimes by version.py.
|
||||
@@ -54,6 +54,182 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 258.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix static physics bodies not generating contacts if they spawn onto sleeping bodies.
|
||||
|
||||
|
||||
## 258.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `IMarkupTag` and related methods in `MarkupTagManager` have been obsoleted and should be replaced with the new `IMarkupTagHandler` interface. Various engine tags (e.g., `BoldTag`, `ColorTag`, etc) no longer implement the old interface.
|
||||
|
||||
### New features
|
||||
|
||||
* Add IsValidPath to ResPath and make some minor performance improvements.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* OutputPanel and RichTextLabel now remove controls associated with rich text tags when the text is updated.
|
||||
* Fix `SpriteComponent.Visible` datafield not being read from yaml.
|
||||
* Fix container state handling not forcing inserts.
|
||||
|
||||
### Other
|
||||
|
||||
* `SpriteSystem.LayerMapReserve()` no longer throws an exception if the specified layer already exists. This makes it behave like the obsoleted `SpriteComponent.LayerMapReserveBlank()`.
|
||||
|
||||
|
||||
## 257.0.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix unshaded sprite layers not rendering correctly.
|
||||
|
||||
|
||||
## 257.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix sprite layer bounding box calculations. This was causing various sprite rendering & render-tree lookup issues.
|
||||
|
||||
|
||||
## 257.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The client will now automatically pause any entities that leave their PVS range.
|
||||
* Contacts for terminating entities no longer raise wake events.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `IPrototypeManager.IsIgnored()` for checking whether a given prototype kind has been marked as ignored via `RegisterIgnore()`.
|
||||
* Added `PoolManager` & `TestPair` classes to `Robust.UnitTesting`. These classes make it easier to create & use pooled server/client instance pairs in integration tests.
|
||||
* Catch NotYamlSerializable DataFields with an analyzer.
|
||||
* Optimized RSI preloading and texture atlas creation.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix clients unintentionally un-pausing paused entities that re-enter pvs range
|
||||
|
||||
### Other
|
||||
|
||||
* The yaml prototype id serialiser now provides better feedback when trying to validate an id for a prototype kind that has been ignored via `IPrototypeManager.RegisterIgnore()`
|
||||
* Several SpriteComponent methods have been marked as obsolete, and should be replaced with new methods in SpriteSystem.
|
||||
* Rotation events no longer check for grid traversal.
|
||||
|
||||
|
||||
## 256.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `ITypeReaderWriter<TType, TNode>` has been removed due to being unused. Implement `ITypeSerializer<TType, TNode>` instead
|
||||
* Moved AsNullable extension methods to the Entity struct.
|
||||
|
||||
### New features
|
||||
|
||||
* Add DevWindow tab to show all loaded textures.
|
||||
* Add Vector2i / bitmask converfsion helpers.
|
||||
* Allow texture preload to be skipped for some textures.
|
||||
* Check audio file signatures instead of extensions.
|
||||
* Add CancellationTokenRegistration to sandbox.
|
||||
* Add the ability to serialize TimeSpan from text.
|
||||
* Add support for rotated / mirrored tiles.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix yaml hot reloading.
|
||||
* Fix a linear dictionary lookup in PlacementManager.
|
||||
|
||||
### Other
|
||||
|
||||
* Make ItemList not run deselection callback on all items if they aren't selected.
|
||||
* Cleanup warnings for CS0649 & CS0414.
|
||||
|
||||
### Internal
|
||||
|
||||
* Move PointLight component states to shared.
|
||||
|
||||
|
||||
## 255.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* The client localisation manager now supports hot-reloading ftl files.
|
||||
* TransformSystem can now raise `GridUidChangedEvent` and `MapUidChangedEvent` when a entity's grid or map changes. This event is only raised if the `ExtraTransformEvents` metadata flag is enabled.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed a server crash due to a `NullReferenceException` in PVS system when a player's local entity is also one of their view subscriptions.
|
||||
* Fix CompileRobustXamlTask for benchmarks.
|
||||
* .ftl files will now hot reload.
|
||||
* Fix placementmanager sometimes not clearing.
|
||||
|
||||
### Other
|
||||
|
||||
* Container events are now documented.
|
||||
|
||||
|
||||
## 255.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `RobustIntegrationTest` now pools server/client instances by default. If a custom settings class is provided, it will still disable pooling unless explicitly enabled.
|
||||
* Server/Client instances that are returned to the pool should be disconnected. This might require you to update some tests.
|
||||
* Pooled instances also require you to use `RobustIntegrationTest` methods like `WaitPost()` to ensure the correct thread is used.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `EntityDeserializer` improperly setting entity lifestages when loading a post-mapinit map.
|
||||
* Fix `EntityManager.PredictedDeleteEntity()` not deleting pure client-side entities.
|
||||
* Fix grid fixtures using a locale dependent id. This could cause some clients to crash/freeze when connected to a server with a different locale.
|
||||
|
||||
### Other
|
||||
|
||||
* Add logic to block cycles in master MIDI renderers, which could otherwise cause client freezes.
|
||||
|
||||
|
||||
## 254.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add CC ND licences to the RGA validator.
|
||||
* Add entity spawn prediction and entity deletion prediction. This is currently limited as you are unable to predict interactions with these entities. These are done via the new methods prefixed with "Predicted". You can also manually flag an entity as a predicted spawn with the `FlagPredicted` method which will clean it up when prediction is reset.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix tile edge rendering for neighbor tiles being the same priority.
|
||||
|
||||
### Other
|
||||
|
||||
* Fix SpawnAttachedTo's system proxy method not the rotation arg like EntityManager.
|
||||
|
||||
|
||||
## 254.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Yaml mappings/dictionaries now only support string keys instead of generic nodes
|
||||
* Several MappingDataNode method arguments or return values now use strings instead of a DataNode object
|
||||
* The MappingDataNode class has various helper methods that still accept a ValueDataNode, but these methods are marked as obsolete and may be removed in the future.
|
||||
* yaml validators should use `MappingDataNode.GetKeyNode()` when validating mapping keys, so that errors can print node start & end information
|
||||
* ValueTuple yaml serialization has changed
|
||||
* Previously they would get serialized into a single mapping with one entry (i.e., `{foo : bar }`)
|
||||
* Now they serialize into a sequence (i.e., `[foo, bar]`)
|
||||
* The ValueTuple serializer will still try to read mappings, but due to the MappingDataNode this may fail if the previously serialized "key" can't be read as a simple string
|
||||
|
||||
### New features
|
||||
|
||||
* Add cvar to disable tile edges.
|
||||
* Add GetContainingContainers method to ContainerSystem to recursively get containers upwards on an entity.
|
||||
|
||||
### Internal
|
||||
|
||||
* Make component lifecycle methods use generics.
|
||||
|
||||
|
||||
## 253.0.0
|
||||
|
||||
### New features
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
### Localization for engine console commands
|
||||
|
||||
cmd-hint-float = [float]
|
||||
|
||||
## generic command errors
|
||||
|
||||
cmd-invalid-arg-number-error = Invalid number of arguments.
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
86
Robust.Analyzers.Tests/ObsoleteInheritanceAnalyzerTest.cs
Normal file
86
Robust.Analyzers.Tests/ObsoleteInheritanceAnalyzerTest.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ObsoleteInheritanceAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzer that implements <c>[ObsoleteInheritance]</c> checking, to give obsoletion warnings for inheriting types
|
||||
/// that should never have been virtual.
|
||||
/// </summary>
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
public sealed class ObsoleteInheritanceAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<ObsoleteInheritanceAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code },
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs"
|
||||
);
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestBasic()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
[ObsoleteInheritance]
|
||||
public class Base;
|
||||
|
||||
public class NotAllowed : Base;
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(6,14): warning RA0034: Type 'NotAllowed' inherits from 'Base', which has obsoleted inheriting from itself
|
||||
VerifyCS.Diagnostic(ObsoleteInheritanceAnalyzer.Rule).WithSpan(6, 14, 6, 24).WithArguments("NotAllowed", "Base")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestMessage()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
[ObsoleteInheritance("Sus")]
|
||||
public class Base;
|
||||
|
||||
public class NotAllowed : Base;
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(6,14): warning RA0034: Type 'NotAllowed' inherits from 'Base', which has obsoleted inheriting from itself: "Sus"
|
||||
VerifyCS.Diagnostic(ObsoleteInheritanceAnalyzer.RuleWithMessage).WithSpan(6, 14, 6, 24).WithArguments("NotAllowed", "Base", "Sus")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestNormal()
|
||||
{
|
||||
const string code = """
|
||||
public class Base;
|
||||
|
||||
public class AllowedAllowed : Base;
|
||||
""";
|
||||
|
||||
await Verifier(code);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferOtherTypeAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ForbidLiteralAttribute.cs" LogicalName="Robust.Shared.Analyzers.ForbidLiteralAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ObsoleteInheritanceAttribute.cs" LogicalName="Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\GameObjects\EventBusAttributes.cs" LogicalName="Robust.Shared.GameObjects.EventBusAttributes.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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))
|
||||
|
||||
75
Robust.Analyzers/ObsoleteInheritanceAnalyzer.cs
Normal file
75
Robust.Analyzers/ObsoleteInheritanceAnalyzer.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
#nullable enable
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class ObsoleteInheritanceAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string Attribute = "Robust.Shared.Analyzers.ObsoleteInheritanceAttribute";
|
||||
|
||||
public static readonly DiagnosticDescriptor Rule = new(
|
||||
Diagnostics.IdObsoleteInheritance,
|
||||
"Parent type has obsoleted inheritance",
|
||||
"Type '{0}' inherits from '{1}', which has obsoleted inheriting from itself",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public static readonly DiagnosticDescriptor RuleWithMessage = new(
|
||||
Diagnostics.IdObsoleteInheritanceWithMessage,
|
||||
"Parent type has obsoleted inheritance",
|
||||
"Type '{0}' inherits from '{1}', which has obsoleted inheriting from itself: \"{2}\"",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule, RuleWithMessage];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSymbolAction(CheckClass, SymbolKind.NamedType);
|
||||
}
|
||||
|
||||
private static void CheckClass(SymbolAnalysisContext context)
|
||||
{
|
||||
if (context.Symbol is not INamedTypeSymbol typeSymbol)
|
||||
return;
|
||||
|
||||
if (typeSymbol.IsValueType || typeSymbol.BaseType is not { } baseType)
|
||||
return;
|
||||
|
||||
if (!AttributeHelper.HasAttribute(baseType, Attribute, out var data))
|
||||
return;
|
||||
|
||||
var location = context.Symbol.Locations[0];
|
||||
|
||||
if (GetMessageFromAttributeData(data) is { } message)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
RuleWithMessage,
|
||||
location,
|
||||
[typeSymbol.Name, baseType.Name, message]));
|
||||
}
|
||||
else
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
Rule,
|
||||
location,
|
||||
[typeSymbol.Name, baseType.Name]));
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetMessageFromAttributeData(AttributeData data)
|
||||
{
|
||||
if (data.ConstructorArguments is not [var message, ..])
|
||||
return null;
|
||||
|
||||
return message.Value as string;
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,8 @@ public class RecursiveMoveBenchmark : RobustIntegrationTest
|
||||
public void GlobalSetup()
|
||||
{
|
||||
ProgramShared.PathOffset = "../../../../";
|
||||
var server = StartServer();
|
||||
var client = StartClient();
|
||||
var server = StartServer(new() {Pool = false});
|
||||
var client = StartClient(new() {Pool = false});
|
||||
|
||||
Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait();
|
||||
|
||||
|
||||
@@ -226,6 +226,9 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
if (value == _master)
|
||||
return;
|
||||
|
||||
if (CheckMasterCycle(value))
|
||||
throw new InvalidOperationException("Tried to set master to a child of this renderer!");
|
||||
|
||||
if (_master is { Disposed: false })
|
||||
{
|
||||
try
|
||||
@@ -729,4 +732,22 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
_synth?.Dispose();
|
||||
_player?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that a given renderer is not already a child of this renderer, i.e. it would introduce a cycle if set as master of this renderer.
|
||||
/// </summary>
|
||||
private bool CheckMasterCycle(IMidiRenderer? otherRenderer)
|
||||
{
|
||||
// Doesn't inside drift, cringe.
|
||||
|
||||
while (otherRenderer != null)
|
||||
{
|
||||
if (otherRenderer == this)
|
||||
return true;
|
||||
|
||||
otherRenderer = otherRenderer.Master;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
#if DEBUG
|
||||
using Robust.Client.Debugging;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
public sealed class DebugAnchoredCommand : LocalizedCommands
|
||||
public sealed class DebugAnchoredCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly DebugAnchoringSystem _system = default!;
|
||||
|
||||
public override string Command => "showanchored";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
EntitySystem.Get<DebugAnchoringSystem>().Enabled ^= true;
|
||||
_system.Enabled ^= true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
#if DEBUG
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
internal sealed class LightDebugCommand : LocalizedCommands
|
||||
internal sealed class LightDebugCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly DebugLightTreeSystem _system = default!;
|
||||
|
||||
public override string Command => "lightbb";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
EntitySystem.Get<DebugLightTreeSystem>().Enabled ^= true;
|
||||
_system.Enabled ^= true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
public sealed class ShowPlayerVelocityCommand : LocalizedCommands
|
||||
public sealed class ShowPlayerVelocityCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
|
||||
[Dependency] private readonly ShowPlayerVelocityDebugSystem _system = default!;
|
||||
|
||||
public override string Command => "showplayervelocity";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
_entitySystems.GetEntitySystem<ShowPlayerVelocityDebugSystem>().Enabled ^= true;
|
||||
_system.Enabled ^= true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -180,6 +182,7 @@ namespace Robust.Client
|
||||
_serializer.Initialize();
|
||||
_inputManager.Initialize();
|
||||
_console.Initialize();
|
||||
_loc.Initialize();
|
||||
|
||||
// Make sure this is done before we try to load prototypes,
|
||||
// avoid any possibility of race conditions causing the check to not finish
|
||||
|
||||
123
Robust.Client/GameObjects/ClientEntityManager.Spawn.cs
Normal file
123
Robust.Client/GameObjects/ClientEntityManager.Spawn.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed partial class ClientEntityManager
|
||||
{
|
||||
public override EntityUid PredictedSpawnAttachedTo(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
|
||||
{
|
||||
var ent = SpawnAttachedTo(protoName, coordinates, overrides, rotation);
|
||||
FlagPredicted(ent);
|
||||
return ent;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override EntityUid PredictedSpawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true)
|
||||
{
|
||||
var ent = Spawn(protoName, overrides, doMapInit);
|
||||
FlagPredicted(ent);
|
||||
return ent;
|
||||
}
|
||||
|
||||
public override EntityUid PredictedSpawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
|
||||
{
|
||||
var ent = Spawn(protoName, coordinates, overrides, rotation);
|
||||
FlagPredicted(ent);
|
||||
return ent;
|
||||
}
|
||||
|
||||
public override EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
|
||||
{
|
||||
var ent = SpawnAtPosition(protoName, coordinates, overrides);
|
||||
FlagPredicted(ent);
|
||||
return ent;
|
||||
}
|
||||
|
||||
public override bool PredictedTrySpawnNextTo(
|
||||
string? protoName,
|
||||
EntityUid target,
|
||||
[NotNullWhen(true)] out EntityUid? uid,
|
||||
TransformComponent? xform = null,
|
||||
ComponentRegistry? overrides = null)
|
||||
{
|
||||
if (!TrySpawnNextTo(protoName, target, out uid, xform, overrides))
|
||||
return false;
|
||||
|
||||
FlagPredicted(uid.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool PredictedTrySpawnInContainer(
|
||||
string? protoName,
|
||||
EntityUid containerUid,
|
||||
string containerId,
|
||||
[NotNullWhen(true)] out EntityUid? uid,
|
||||
ContainerManagerComponent? containerComp = null,
|
||||
ComponentRegistry? overrides = null)
|
||||
{
|
||||
if (!TrySpawnInContainer(protoName, containerUid, containerId, out uid, containerComp, overrides))
|
||||
return false;
|
||||
|
||||
FlagPredicted(uid.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override EntityUid PredictedSpawnNextToOrDrop(string? protoName, EntityUid target, TransformComponent? xform = null, ComponentRegistry? overrides = null)
|
||||
{
|
||||
var ent = SpawnNextToOrDrop(protoName, target, xform, overrides);
|
||||
FlagPredicted(ent);
|
||||
return ent;
|
||||
}
|
||||
|
||||
public override EntityUid PredictedSpawnInContainerOrDrop(
|
||||
string? protoName,
|
||||
EntityUid containerUid,
|
||||
string containerId,
|
||||
TransformComponent? xform = null,
|
||||
ContainerManagerComponent? containerComp = null,
|
||||
ComponentRegistry? overrides = null)
|
||||
{
|
||||
var ent = SpawnInContainerOrDrop(protoName, containerUid, containerId, xform, containerComp, overrides);
|
||||
FlagPredicted(ent);
|
||||
return ent;
|
||||
}
|
||||
|
||||
public override EntityUid PredictedSpawnInContainerOrDrop(
|
||||
string? protoName,
|
||||
EntityUid containerUid,
|
||||
string containerId,
|
||||
out bool inserted,
|
||||
TransformComponent? xform = null,
|
||||
ContainerManagerComponent? containerComp = null,
|
||||
ComponentRegistry? overrides = null)
|
||||
{
|
||||
var ent = SpawnInContainerOrDrop(protoName,
|
||||
containerUid,
|
||||
containerId,
|
||||
out inserted,
|
||||
xform,
|
||||
containerComp,
|
||||
overrides);
|
||||
|
||||
FlagPredicted(ent);
|
||||
return ent;
|
||||
}
|
||||
|
||||
public override void FlagPredicted(Entity<MetaDataComponent?> ent)
|
||||
{
|
||||
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp))
|
||||
return;
|
||||
|
||||
DebugTools.Assert(IsClientSide(ent.Owner, ent.Comp));
|
||||
EnsureComponent<PredictedSpawnComponent>(ent.Owner);
|
||||
|
||||
// TODO: Need to map call site or something, needs to be consistent between client and server.
|
||||
}
|
||||
}
|
||||
@@ -291,5 +291,54 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
|
||||
{
|
||||
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|
||||
|| ent.Comp1.EntityDeleted
|
||||
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// So there's 3 scenarios:
|
||||
// 1. Networked entity we just move to nullspace and rely on state handling.
|
||||
// 2. Clientside predicted entity we delete and rely on state handling.
|
||||
// 3. Clientside only entity that actually needs deleting here.
|
||||
|
||||
if (ent.Comp1.NetEntity.IsClientSide())
|
||||
{
|
||||
DeleteEntity(ent, ent.Comp1, ent.Comp2);
|
||||
}
|
||||
else
|
||||
{
|
||||
_xforms.DetachEntity(ent, ent.Comp2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
|
||||
{
|
||||
if (IsQueuedForDeletion(ent.Owner)
|
||||
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|
||||
|| ent.Comp1.EntityDeleted
|
||||
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ent.Comp1.NetEntity.IsClientSide())
|
||||
{
|
||||
// client-side QueueDeleteEntity re-fetches MetadataComp and checks IsClientSide().
|
||||
// base call to skip that.
|
||||
// TODO create override that takes in metadata comp
|
||||
base.QueueDeleteEntity(ent);
|
||||
}
|
||||
else
|
||||
{
|
||||
_xforms.DetachEntity(ent.Owner, ent.Comp2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -10,19 +10,21 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
public sealed class ShowSpriteBBCommand : LocalizedCommands
|
||||
public sealed class ShowSpriteBBCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly SpriteBoundsSystem _system = default!;
|
||||
|
||||
public override string Command => "showspritebb";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
EntitySystem.Get<SpriteBoundsSystem>().Enabled ^= true;
|
||||
_system.Enabled ^= true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SpriteBoundsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
|
||||
|
||||
@@ -40,7 +42,7 @@ namespace Robust.Client.GameObjects
|
||||
if (_enabled)
|
||||
{
|
||||
DebugTools.AssertNull(_overlay);
|
||||
_overlay = new SpriteBoundsOverlay(_spriteTree, _entityManager);
|
||||
_overlay = new SpriteBoundsOverlay(_spriteTree, _xformSystem);
|
||||
_overlayManager.AddOverlay(_overlay);
|
||||
}
|
||||
else
|
||||
@@ -59,13 +61,13 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly SharedTransformSystem _xformSystem;
|
||||
private SpriteTreeSystem _renderTree;
|
||||
|
||||
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, IEntityManager entityManager)
|
||||
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, SharedTransformSystem xformSystem)
|
||||
{
|
||||
_renderTree = renderTree;
|
||||
_entityManager = entityManager;
|
||||
_xformSystem = xformSystem;
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
@@ -76,7 +78,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
|
||||
{
|
||||
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
|
||||
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
|
||||
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
|
||||
|
||||
// Get scaled down bounds used to indicate the "south" of a sprite.
|
||||
|
||||
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)
|
||||
@@ -303,7 +302,7 @@ namespace Robust.Client.GameObjects
|
||||
while (parent.IsValid() && (!spriteOccluded || !lightOccluded))
|
||||
{
|
||||
var parentXform = TransformQuery.GetComponent(parent);
|
||||
if (TryComp<ContainerManagerComponent>(parent, out var manager) && manager.TryGetContainer(child, out var container))
|
||||
if (TryComp<ContainerManagerComponent>(parent, out var manager) && TryGetContainingContainer(parent, child, out var container, manager))
|
||||
{
|
||||
spriteOccluded = spriteOccluded || !container.ShowContents;
|
||||
lightOccluded = lightOccluded || container.OccludesLight;
|
||||
@@ -344,7 +343,7 @@ namespace Robust.Client.GameObjects
|
||||
var childLightOccluded = lightOccluded;
|
||||
|
||||
// We already know either sprite or light is not occluding so need to check container.
|
||||
if (manager.TryGetContainer(child, out var container))
|
||||
if (TryGetContainingContainer(entity, child, out var container, manager))
|
||||
{
|
||||
childSpriteOccluded = childSpriteOccluded || !container.ShowContents;
|
||||
childLightOccluded = childLightOccluded || container.OccludesLight;
|
||||
|
||||
@@ -2,24 +2,24 @@ using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Utility;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class DebugEntityLookupCommand : LocalizedCommands
|
||||
public sealed class DebugEntityLookupCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly DebugEntityLookupSystem _system = default!;
|
||||
|
||||
public override string Command => "togglelookup";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
EntitySystem.Get<DebugEntityLookupSystem>().Enabled ^= true;
|
||||
_system.Enabled ^= true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ namespace Robust.Client.GameObjects
|
||||
if (_enabled)
|
||||
{
|
||||
_lightOverlay = new DebugLightOverlay(
|
||||
EntitySystem.Get<EntityLookupSystem>(),
|
||||
EntityManager.System<EntityLookupSystem>(),
|
||||
IoCManager.Resolve<IEyeManager>(),
|
||||
IoCManager.Resolve<IMapManager>(),
|
||||
Get<LightTreeSystem>());
|
||||
EntityManager.System<LightTreeSystem>());
|
||||
|
||||
overlayManager.AddOverlay(_lightOverlay);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public sealed class EyeSystem : SharedEyeSystem
|
||||
eyeComponent.Target = null;
|
||||
}
|
||||
|
||||
eyeComponent.Eye.Position = xform.MapPosition;
|
||||
eyeComponent.Eye.Position = TransformSystem.GetMapCoordinates(xform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,6 +13,7 @@ using Robust.Client.Physics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Containers;
|
||||
@@ -563,6 +564,21 @@ namespace Robust.Client.GameStates
|
||||
var metaQuery = _entities.GetEntityQuery<MetaDataComponent>();
|
||||
RemQueue<IComponent> toRemove = new();
|
||||
|
||||
// Handle predicted entity spawns.
|
||||
var predicted = new ValueList<EntityUid>();
|
||||
var predictedQuery = _entities.AllEntityQueryEnumerator<PredictedSpawnComponent>();
|
||||
|
||||
while (predictedQuery.MoveNext(out var uid, out var _))
|
||||
{
|
||||
predicted.Add(uid);
|
||||
}
|
||||
|
||||
// Entity will get re-created as part of the tick.
|
||||
foreach (var ent in predicted)
|
||||
{
|
||||
_entities.DeleteEntity(ent);
|
||||
}
|
||||
|
||||
foreach (var entity in system.DirtyEntities)
|
||||
{
|
||||
DebugTools.Assert(toRemove.Count == 0);
|
||||
@@ -1290,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);
|
||||
@@ -1310,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);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,8 @@ namespace Robust.Client.Graphics
|
||||
/// <inheritdoc />
|
||||
public ScreenCoordinates CoordinatesToScreen(EntityCoordinates point)
|
||||
{
|
||||
return MapToScreen(point.ToMap(_entityManager, _entityManager.System<SharedTransformSystem>()));
|
||||
var transformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
return MapToScreen(transformSystem.ToMapCoordinates(point));
|
||||
}
|
||||
|
||||
public ScreenCoordinates MapToScreen(MapCoordinates point)
|
||||
|
||||
@@ -30,6 +30,23 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private int _indicesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
|
||||
|
||||
private List<Entity<MapGridComponent>> _grids = new();
|
||||
private bool _drawTileEdges;
|
||||
|
||||
private void RenderTileEdgesChanges(bool value)
|
||||
{
|
||||
_drawTileEdges = value;
|
||||
if (!value)
|
||||
return;
|
||||
|
||||
// Dirty all Edges
|
||||
foreach (var gridData in _mapChunkData.Values)
|
||||
{
|
||||
foreach (var chunk in gridData.Values)
|
||||
{
|
||||
chunk.EdgeDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBounds, IEye eye)
|
||||
{
|
||||
@@ -81,6 +98,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
_updateChunkMesh(mapGrid, chunk, datum);
|
||||
|
||||
if (!_drawTileEdges)
|
||||
continue;
|
||||
|
||||
// Dirty edge tiles for next step.
|
||||
datum.EdgeDirty = true;
|
||||
|
||||
@@ -99,17 +119,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
|
||||
|
||||
// Handle edge sprites.
|
||||
while (enumerator.MoveNext(out var chunk))
|
||||
if (_drawTileEdges)
|
||||
{
|
||||
var datum = data[chunk.Indices];
|
||||
|
||||
if (!datum.EdgeDirty)
|
||||
continue;
|
||||
|
||||
_updateChunkEdges(mapGrid, chunk, datum);
|
||||
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
|
||||
while (enumerator.MoveNext(out var chunk))
|
||||
{
|
||||
var datum = data[chunk.Indices];
|
||||
if (datum.EdgeDirty)
|
||||
_updateChunkEdges(mapGrid, chunk, datum);
|
||||
}
|
||||
}
|
||||
|
||||
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
|
||||
@@ -129,7 +148,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
if (datum.EdgeCount > 0)
|
||||
if (_drawTileEdges && datum.EdgeCount > 0)
|
||||
{
|
||||
BindVertexArray(datum.EdgeVAO);
|
||||
CheckGlError();
|
||||
@@ -138,7 +157,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GL.DrawElements(GetQuadGLPrimitiveType(), datum.EdgeCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
requiresFlush = false;
|
||||
@@ -236,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;
|
||||
}
|
||||
}
|
||||
@@ -274,7 +296,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var gridX = x + chunkOriginScaled.X;
|
||||
var gridY = y + chunkOriginScaled.Y;
|
||||
var tile = chunk.GetTile(x, y);
|
||||
var tileDef = _tileDefinitionManager[tile.TypeId];
|
||||
if (!_tileDefinitionManager.TryGetDefinition(tile.TypeId, out var tileDef))
|
||||
continue;
|
||||
|
||||
// Edge render
|
||||
for (var nx = -1; nx <= 1; nx++)
|
||||
@@ -288,14 +311,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (!maps.TryGetTile(grid.Comp, neighborIndices, out var neighborTile))
|
||||
continue;
|
||||
|
||||
var neighborDef = _tileDefinitionManager[neighborTile.TypeId];
|
||||
if (!_tileDefinitionManager.TryGetDefinition(neighborTile.TypeId, out var neighborDef))
|
||||
continue;
|
||||
|
||||
// If it's the same tile then no edge to be drawn.
|
||||
if (tile.TypeId == neighborTile.TypeId || neighborDef.EdgeSprites.Count == 0)
|
||||
continue;
|
||||
|
||||
// If neighbor is a lower priority then us then don't draw on our tile.
|
||||
if (neighborDef.EdgeSpritePriority < tileDef.EdgeSpritePriority)
|
||||
// If neighbor is a lower or same priority then us then don't draw on our tile.
|
||||
if (neighborDef.EdgeSpritePriority <= tileDef.EdgeSpritePriority)
|
||||
continue;
|
||||
|
||||
var direction = new Vector2i(nx, ny).AsDirection().GetOpposite();
|
||||
@@ -305,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;
|
||||
}
|
||||
}
|
||||
@@ -425,13 +449,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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_cfg.OnValueChanged(CVars.LightSoftShadows, SoftShadowsChanged, true);
|
||||
_cfg.OnValueChanged(CVars.MaxLightCount, MaxLightsChanged, true);
|
||||
_cfg.OnValueChanged(CVars.MaxOccluderCount, MaxOccludersChanged, true);
|
||||
_cfg.OnValueChanged(CVars.RenderTileEdges, RenderTileEdgesChanges, true);
|
||||
// I can't be bothered to tear down and set these threads up in a cvar change handler.
|
||||
|
||||
// Windows and Linux can be trusted to not explode with threaded windowing,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace Robust.Client.Input
|
||||
[Dependency] private readonly IUserInterfaceManagerInternal _uiMgr = default!;
|
||||
[Dependency] private readonly IConsoleHost _console = default!;
|
||||
[Dependency] private readonly ISerializationManager _serialization = default!;
|
||||
private ISawmill _logger = default!;
|
||||
|
||||
private bool _currentlyFindingViewport;
|
||||
|
||||
@@ -114,6 +115,8 @@ namespace Robust.Client.Input
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
{
|
||||
_logger = Logger.GetSawmill("input");
|
||||
|
||||
NetworkBindMap = new BoundKeyMap(_reflectionManager);
|
||||
NetworkBindMap.PopulateKeyFunctionsMap();
|
||||
|
||||
@@ -130,7 +133,7 @@ namespace Robust.Client.Input
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorS("input", "Failed to load user keybindings: " + e);
|
||||
_logger.Error("Failed to load user keybindings: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,8 +534,7 @@ namespace Robust.Client.Input
|
||||
|
||||
if (reg.Type != KeyBindingType.Command && !NetworkBindMap.FunctionExists(reg.Function.FunctionName))
|
||||
{
|
||||
Logger.DebugS("input", "Key function in {0} does not exist: '{1}'.", file,
|
||||
reg.Function);
|
||||
_logger.Debug("Key function in {0} does not exist: '{1}'.", file, reg.Function);
|
||||
invalid = true;
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -30,11 +30,13 @@ namespace Robust.Client.Placement.Modes
|
||||
return;
|
||||
}
|
||||
|
||||
var mapId = MouseCoords.GetMapId(pManager.EntityManager);
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
var mapId = transformSys.GetMapId(MouseCoords);
|
||||
|
||||
var snapToEntities = EntitySystem.Get<EntityLookupSystem>().GetEntitiesInRange(MouseCoords, SnapToRange)
|
||||
var snapToEntities = pManager.EntityManager.System<EntityLookupSystem>()
|
||||
.GetEntitiesInRange(MouseCoords, SnapToRange)
|
||||
.Where(entity => pManager.EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype == pManager.CurrentPrototype && pManager.EntityManager.GetComponent<TransformComponent>(entity).MapID == mapId)
|
||||
.OrderBy(entity => (pManager.EntityManager.GetComponent<TransformComponent>(entity).WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager, pManager.EntityManager.System<SharedTransformSystem>())).LengthSquared())
|
||||
.OrderBy(entity => (transformSys.GetWorldPosition(entity) - transformSys.ToMapCoordinates(MouseCoords).Position).LengthSquared())
|
||||
.ToList();
|
||||
|
||||
if (snapToEntities.Count == 0)
|
||||
@@ -51,10 +53,11 @@ namespace Robust.Client.Placement.Modes
|
||||
|
||||
var closestBounds = component.BaseRSI.Size;
|
||||
|
||||
var closestPos = transformSys.GetWorldPosition(closestTransform);
|
||||
var closestRect =
|
||||
Box2.FromDimensions(
|
||||
closestTransform.WorldPosition.X - closestBounds.X / 2f,
|
||||
closestTransform.WorldPosition.Y - closestBounds.Y / 2f,
|
||||
closestPos.X - closestBounds.X / 2f,
|
||||
closestPos.Y - closestBounds.Y / 2f,
|
||||
closestBounds.X, closestBounds.Y);
|
||||
|
||||
var sides = new[]
|
||||
|
||||
@@ -21,7 +21,8 @@ namespace Robust.Client.Placement.Modes
|
||||
{
|
||||
MouseCoords = ScreenToCursorGrid(mouseScreen);
|
||||
|
||||
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
var gridIdOpt = transformSys.GetGrid(MouseCoords);
|
||||
SnapSize = 1f;
|
||||
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
|
||||
{
|
||||
|
||||
@@ -16,8 +16,10 @@ namespace Robust.Client.Placement.Modes
|
||||
{
|
||||
MouseCoords = ScreenToCursorGrid(mouseScreen);
|
||||
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
|
||||
var tileSize = 1f;
|
||||
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
|
||||
var gridIdOpt = transformSys.GetGrid(MouseCoords);
|
||||
|
||||
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
|
||||
{
|
||||
|
||||
@@ -16,9 +16,11 @@ namespace Robust.Client.Placement.Modes
|
||||
{
|
||||
MouseCoords = ScreenToCursorGrid(mouseScreen);
|
||||
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
|
||||
var tileSize = 1f;
|
||||
|
||||
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
|
||||
var gridIdOpt = transformSys.GetGrid(MouseCoords);
|
||||
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
|
||||
{
|
||||
var mapGrid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
|
||||
@@ -174,6 +174,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 +200,9 @@ namespace Robust.Client.Placement
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? DirectionChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? MirroredChanged;
|
||||
|
||||
private PlacementOverlay _drawOverlay = default!;
|
||||
private bool _isActive;
|
||||
|
||||
@@ -356,6 +371,12 @@ namespace Robust.Client.Placement
|
||||
public event EventHandler? PlacementChanged;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearWithoutDeactivation();
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
private void ClearWithoutDeactivation()
|
||||
{
|
||||
PlacementChanged?.Invoke(this, EventArgs.Empty);
|
||||
Hijack = null;
|
||||
@@ -365,7 +386,6 @@ namespace Robust.Client.Placement
|
||||
CurrentMode = null;
|
||||
DeactivateSpecialPlacement();
|
||||
_placenextframe = false;
|
||||
IsActive = false;
|
||||
Eraser = false;
|
||||
EraserRect = null;
|
||||
PlacementOffset = Vector2i.Zero;
|
||||
@@ -480,18 +500,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)
|
||||
{
|
||||
@@ -772,7 +791,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 +817,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);
|
||||
|
||||
@@ -121,12 +121,12 @@ namespace Robust.Client.Placement
|
||||
{
|
||||
if (!coordinate.IsValid(pManager.EntityManager))
|
||||
return; // Just some paranoia just in case
|
||||
var worldPos = coordinate.ToMapPos(pManager.EntityManager, transformSys);
|
||||
var worldRot = pManager.EntityManager.GetComponent<TransformComponent>(coordinate.EntityId).WorldRotation + dirAng;
|
||||
var worldPos = transformSys.ToMapCoordinates(coordinate).Position;
|
||||
var worldRot = transformSys.GetWorldRotation(coordinate.EntityId) + dirAng;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ namespace Robust.Client.Placement
|
||||
|
||||
var range = pManager.CurrentPermission!.Range;
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
if (range > 0 && !pManager.EntityManager.GetComponent<TransformComponent>(controlled).Coordinates.InRange(pManager.EntityManager, transformSys, coordinates, range))
|
||||
if (range > 0 && !transformSys.InRange(pManager.EntityManager.GetComponent<TransformComponent>(controlled).Coordinates, coordinates, range))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
@@ -239,7 +239,7 @@ namespace Robust.Client.Placement
|
||||
{
|
||||
var bounds = pManager.ColliderAABB;
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
var mapCoords = coordinates.ToMap(pManager.EntityManager, transformSys);
|
||||
var mapCoords = transformSys.ToMapCoordinates(coordinates);
|
||||
var (x, y) = mapCoords.Position;
|
||||
|
||||
var collisionBox = Box2.FromDimensions(
|
||||
@@ -248,7 +248,9 @@ namespace Robust.Client.Placement
|
||||
bounds.Width,
|
||||
bounds.Height);
|
||||
|
||||
return EntitySystem.Get<SharedPhysicsSystem>().TryCollideRect(collisionBox, mapCoords.MapId);
|
||||
return pManager.EntityManager
|
||||
.System<SharedPhysicsSystem>()
|
||||
.TryCollideRect(collisionBox, mapCoords.MapId);
|
||||
}
|
||||
|
||||
protected Vector2 ScreenToWorld(Vector2 point)
|
||||
@@ -265,10 +267,8 @@ namespace Robust.Client.Placement
|
||||
{
|
||||
var mapCoords = pManager.EyeManager.PixelToMap(coords.Position);
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
|
||||
if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid))
|
||||
if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out _))
|
||||
{
|
||||
|
||||
return transformSys.ToCoordinates(mapCoords);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -15,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!;
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace Robust.Client.Prototypes
|
||||
using var _ = _timing.StartStateApplicationArea();
|
||||
ReloadPrototypes([file]);
|
||||
|
||||
Logger.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
|
||||
Sawmill.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
7
Robust.Client/UserInterface/UIConstants.cs
Normal file
7
Robust.Client/UserInterface/UIConstants.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
internal static class UIConstants
|
||||
{
|
||||
public const string ObsoleteInheritanceMessage =
|
||||
"Do not inherit from standard UI controls, compose via nesting instead";
|
||||
}
|
||||
@@ -103,7 +103,6 @@ internal partial class UserInterfaceManager
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
|
||||
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
|
||||
args.PointerLocation.Position - control.GlobalPixelPosition);
|
||||
@@ -115,10 +114,6 @@ internal partial class UserInterfaceManager
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
// Attempt to ensure that keybind-up events get raised after a keybind-down.
|
||||
DebugTools.Assert(!_focusedControls.TryGetValue(args.Function, out var existing)
|
||||
|| !existing.VisibleInTree
|
||||
|| args.IsRepeat && existing == control);
|
||||
_focusedControls[args.Function] = control;
|
||||
|
||||
OnKeyBindDown?.Invoke(control);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -37,6 +37,9 @@ public static class Diagnostics
|
||||
public const string IdPreferOtherType = "RA0031";
|
||||
public const string IdDuplicateDependency = "RA0032";
|
||||
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 +=
|
||||
|
||||
@@ -4,6 +4,7 @@ using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
@@ -16,6 +17,20 @@ public sealed class ScaleCommand : LocalizedCommands
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
public override string Command => "scale";
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 1:
|
||||
return CompletionResult.FromOptions(CompletionHelper.NetEntities(args[0], entManager: _entityManager));
|
||||
case 2:
|
||||
return CompletionResult.FromHint(Loc.GetString("cmd-hint-float"));
|
||||
default:
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -204,7 +204,7 @@ internal struct PvsMetadata
|
||||
{
|
||||
DebugTools.AssertEqual(NetEntity, comp.NetEntity);
|
||||
DebugTools.AssertEqual(VisMask, comp.VisibilityMask);
|
||||
DebugTools.Assert(LifeStage == comp.EntityLifeStage);
|
||||
DebugTools.AssertEqual(LifeStage, comp.EntityLifeStage);
|
||||
DebugTools.Assert(LastModifiedTick == comp.EntityLastModifiedTick || LastModifiedTick.Value == 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -268,7 +268,6 @@ namespace Robust.Server.Physics
|
||||
newGrids[i] = newGridUid;
|
||||
|
||||
// Keep same origin / velocity etc; this makes updating a lot faster and easier.
|
||||
_xformSystem.SetWorldPosition(newGridXform, gridPos);
|
||||
_xformSystem.SetWorldPositionRotation(newGridUid, gridPos, gridRot, newGridXform);
|
||||
var splitBody = _bodyQuery.GetComponent(newGridUid);
|
||||
_physics.SetLinearVelocity(newGridUid, mapBody.LinearVelocity, body: splitBody);
|
||||
@@ -290,7 +289,7 @@ namespace Robust.Server.Physics
|
||||
}
|
||||
|
||||
_maps.SetTiles(newGrid.Owner, newGrid.Comp, tileData);
|
||||
DebugTools.Assert(_mapManager.IsGrid(newGridUid), "A split grid had no tiles?");
|
||||
DebugTools.Assert(_gridQuery.HasComp(newGridUid), "A split grid had no tiles?");
|
||||
|
||||
// Set tiles on new grid + update anchored entities
|
||||
foreach (var node in group)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user