mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
516ee47b51 | ||
|
|
89be682e24 | ||
|
|
6086076559 | ||
|
|
5bd90c908a | ||
|
|
a3d0921cc9 | ||
|
|
15d5b9aa02 | ||
|
|
d24854d94f | ||
|
|
b3cf427013 | ||
|
|
c458abdc69 | ||
|
|
c76444a33f | ||
|
|
4754661467 | ||
|
|
2a8b776ee9 | ||
|
|
7d8e5a5841 | ||
|
|
8e416e4519 | ||
|
|
65f74943d3 | ||
|
|
eb5ed12270 | ||
|
|
c43b7b16c0 | ||
|
|
aee03f0805 | ||
|
|
cfd2b03248 | ||
|
|
8905a3fe14 | ||
|
|
a878da5b80 | ||
|
|
806c23e034 | ||
|
|
e80f5d13a1 | ||
|
|
a6905151b6 | ||
|
|
e742f021fa | ||
|
|
62b4714f1f | ||
|
|
1d0404953f | ||
|
|
d0da13f895 | ||
|
|
ff23f98b26 | ||
|
|
ccfef2a786 | ||
|
|
62ce9724fc | ||
|
|
3bbe0e7f44 | ||
|
|
addd8b5bdd |
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -71,6 +71,6 @@
|
||||
</PropertyGroup>
|
||||
<Exec
|
||||
Condition="'$(_RobustUseExternalMSBuild)' == 'true'"
|
||||
Command=""$(DOTNET_HOST_PATH)" msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
|
||||
Command=""$(DOTNET_HOST_PATH)" msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false /p:IntermediateOutputPath="$(IntermediateOutputPath.TrimEnd('\'))/""/>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -54,6 +54,82 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@@ -9,6 +9,7 @@ entity-spawn-window-override-menu-tooltip = Override placement
|
||||
## TileSpawnWindow
|
||||
|
||||
tile-spawn-window-title = Place Tiles
|
||||
tile-spawn-window-mirror-button-text = Mirror Tiles
|
||||
|
||||
## Console
|
||||
|
||||
|
||||
10
Resources/Locale/en-US/dev-window.ftl
Normal file
10
Resources/Locale/en-US/dev-window.ftl
Normal file
@@ -0,0 +1,10 @@
|
||||
## "Textures" dev window tab
|
||||
|
||||
dev-window-tab-textures-title = Textures
|
||||
dev-window-tab-textures-reload = Reload
|
||||
dev-window-tab-textures-filter = Filter
|
||||
dev-window-tab-textures-summary = Total (est): { $bytes }
|
||||
dev-window-tab-textures-info = Width: { $width } Height: { $height }
|
||||
PixelType: { $pixelType } sRGB: { $srgb }
|
||||
Name: { $name }
|
||||
Est. memory usage: { $bytes }
|
||||
@@ -21,47 +21,53 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
},
|
||||
};
|
||||
|
||||
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
private const string TestTypeDefs = """
|
||||
using System;
|
||||
|
||||
namespace Robust.Shared.ViewVariables
|
||||
{
|
||||
public sealed class ViewVariablesAttribute : Attribute
|
||||
{
|
||||
public readonly VVAccess Access = VVAccess.ReadOnly;
|
||||
|
||||
public ViewVariablesAttribute() { }
|
||||
|
||||
public ViewVariablesAttribute(VVAccess access)
|
||||
{
|
||||
Access = access;
|
||||
}
|
||||
}
|
||||
public enum VVAccess : byte
|
||||
{
|
||||
ReadOnly = 0,
|
||||
ReadWrite = 1,
|
||||
}
|
||||
}
|
||||
|
||||
namespace Robust.Shared.Serialization.Manager.Attributes
|
||||
{
|
||||
public class DataFieldBaseAttribute : Attribute;
|
||||
public class DataFieldAttribute : DataFieldBaseAttribute;
|
||||
public sealed class DataDefinitionAttribute : Attribute;
|
||||
public sealed class NotYamlSerializableAttribute : Attribute;
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
public async Task NoVVReadOnlyTest()
|
||||
{
|
||||
const string code = """
|
||||
using System;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Shared.ViewVariables
|
||||
{
|
||||
public sealed class ViewVariablesAttribute : Attribute
|
||||
{
|
||||
public readonly VVAccess Access = VVAccess.ReadOnly;
|
||||
|
||||
public ViewVariablesAttribute() { }
|
||||
|
||||
public ViewVariablesAttribute(VVAccess access)
|
||||
{
|
||||
Access = access;
|
||||
}
|
||||
}
|
||||
public enum VVAccess : byte
|
||||
{
|
||||
ReadOnly = 0,
|
||||
ReadWrite = 1,
|
||||
}
|
||||
}
|
||||
|
||||
namespace Robust.Shared.Serialization.Manager.Attributes
|
||||
{
|
||||
public class DataFieldBaseAttribute : Attribute;
|
||||
public class DataFieldAttribute : DataFieldBaseAttribute;
|
||||
public sealed class DataDefinitionAttribute : Attribute;
|
||||
}
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
@@ -83,8 +89,8 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(35,17): info RA0028: Data field Bad in data definition Foo has ViewVariables attribute with ReadWrite access, which is redundant
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(35, 17, 35, 50).WithArguments("Bad", "Foo")
|
||||
// /0/Test0.cs(7,17): info RA0028: Data field Bad in data definition Foo has ViewVariables attribute with ReadWrite access, which is redundant
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(7, 17, 7, 50).WithArguments("Bad", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,16 +98,8 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
public async Task ReadOnlyFieldTest()
|
||||
{
|
||||
const string code = """
|
||||
using System;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Shared.Serialization.Manager.Attributes
|
||||
{
|
||||
public class DataFieldBaseAttribute : Attribute;
|
||||
public class DataFieldAttribute : DataFieldBaseAttribute;
|
||||
public sealed class DataDefinitionAttribute : Attribute;
|
||||
}
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
@@ -114,8 +112,8 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(15,12): error RA0019: Data field Bad in data definition Foo is readonly
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(15, 12, 15, 20).WithArguments("Bad", "Foo")
|
||||
// /0/Test0.cs(7,12): error RA0019: Data field Bad in data definition Foo is readonly
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(7, 12, 7, 20).WithArguments("Bad", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,16 +121,8 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
public async Task ReadOnlyPropertyTest()
|
||||
{
|
||||
const string code = """
|
||||
using System;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Shared.Serialization.Manager.Attributes
|
||||
{
|
||||
public class DataFieldBaseAttribute : Attribute;
|
||||
public class DataFieldAttribute : DataFieldBaseAttribute;
|
||||
public sealed class DataDefinitionAttribute : Attribute;
|
||||
}
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
@@ -145,8 +135,40 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(15,20): error RA0020: Data field property Bad in data definition Foo does not have a setter
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldPropertyWritableRule).WithSpan(15, 20, 15, 28).WithArguments("Bad", "Foo")
|
||||
// /0/Test0.cs(7,20): error RA0020: Data field property Bad in data definition Foo does not have a setter
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldPropertyWritableRule).WithSpan(7, 20, 7, 28).WithArguments("Bad", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NotYamlSerializableTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[NotYamlSerializable]
|
||||
public sealed class NotSerializableClass { }
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[DataField]
|
||||
public NotSerializableClass BadField;
|
||||
|
||||
[DataField]
|
||||
public NotSerializableClass BadProperty { get; set; }
|
||||
|
||||
public NotSerializableClass GoodField; // Not a DataField, not a problem
|
||||
|
||||
public NotSerializableClass GoodProperty { get; set; } // Not a DataField, not a problem
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(10,12): error RA0033: Data field BadField in data definition Foo is type NotSerializableClass, which is not YAML serializable
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(10, 12, 10, 32).WithArguments("BadField", "Foo", "NotSerializableClass"),
|
||||
// /0/Test0.cs(13,12): error RA0033: Data field BadProperty in data definition Foo is type NotSerializableClass, which is not YAML serializable
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(13, 12, 13, 32).WithArguments("BadProperty", "Foo", "NotSerializableClass")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute";
|
||||
private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute";
|
||||
private const string ViewVariablesNamespace = "Robust.Shared.ViewVariables.ViewVariablesAttribute";
|
||||
private const string NotYamlSerializableName = "Robust.Shared.Serialization.Manager.Attributes.NotYamlSerializableAttribute";
|
||||
private const string DataFieldAttributeName = "DataField";
|
||||
private const string ViewVariablesAttributeName = "ViewVariables";
|
||||
|
||||
@@ -81,9 +82,19 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure to remove the ViewVariables attribute."
|
||||
);
|
||||
|
||||
public static readonly DiagnosticDescriptor DataFieldYamlSerializableRule = new(
|
||||
Diagnostics.IdDataFieldYamlSerializable,
|
||||
"Data field type is not YAML serializable",
|
||||
"Data field {0} in data definition {1} is type {2}, which is not YAML serializable",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Error,
|
||||
true,
|
||||
"Make sure to use a type that is YAML serializable."
|
||||
);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule,
|
||||
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule
|
||||
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule, DataFieldYamlSerializableRule
|
||||
);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
@@ -164,6 +175,19 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
TryGetAttributeLocation(field, ViewVariablesAttributeName, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, fieldSymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (context.SemanticModel.GetSymbolInfo(field.Declaration.Type).Symbol is not ITypeSymbol fieldTypeSymbol)
|
||||
continue;
|
||||
|
||||
if (IsNotYamlSerializable(fieldSymbol, fieldTypeSymbol))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
|
||||
(context.Node as FieldDeclarationSyntax)?.Declaration.Type.GetLocation(),
|
||||
fieldSymbol.Name,
|
||||
type.Name,
|
||||
fieldTypeSymbol.MetadataName
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +225,19 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
TryGetAttributeLocation(property, ViewVariablesAttributeName, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, propertySymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (context.SemanticModel.GetSymbolInfo(property.Type).Symbol is not ITypeSymbol propertyTypeSymbol)
|
||||
return;
|
||||
|
||||
if (IsNotYamlSerializable(propertySymbol, propertyTypeSymbol))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
|
||||
(context.Node as PropertyDeclarationSyntax)?.Type.GetLocation(),
|
||||
propertySymbol.Name,
|
||||
type.Name,
|
||||
propertyTypeSymbol.Name
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
|
||||
@@ -383,6 +420,14 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
return (VVAccess)accessByte == VVAccess.ReadWrite;
|
||||
}
|
||||
|
||||
private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type)
|
||||
{
|
||||
if (!IsDataField(field, out _, out _))
|
||||
return false;
|
||||
|
||||
return HasAttribute(type, NotYamlSerializableName);
|
||||
}
|
||||
|
||||
private static bool IsImplicitDataDefinition(ITypeSymbol type)
|
||||
{
|
||||
if (HasAttribute(type, ImplicitDataDefinitionNamespace))
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Client.HWId;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Localization;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.Player;
|
||||
@@ -36,6 +37,7 @@ using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics;
|
||||
@@ -104,6 +106,8 @@ namespace Robust.Client
|
||||
deps.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
|
||||
deps.Register<NetworkResourceManager>();
|
||||
deps.Register<IReloadManager, ReloadManager>();
|
||||
deps.Register<ILocalizationManager, ClientLocalizationManager>();
|
||||
deps.Register<ILocalizationManagerInternal, ClientLocalizationManager>();
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Numerics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
@@ -9,25 +10,7 @@ namespace Robust.Client.ComponentTrees;
|
||||
|
||||
public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent, SpriteComponent>
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SpriteComponent, QueueSpriteTreeUpdateEvent>(OnQueueUpdate);
|
||||
}
|
||||
|
||||
private void OnQueueUpdate(EntityUid uid, SpriteComponent component, ref QueueSpriteTreeUpdateEvent args)
|
||||
=> QueueTreeUpdate(uid, component, args.Xform);
|
||||
|
||||
// TODO remove this when finally ECSing sprite components
|
||||
[ByRefEvent]
|
||||
internal readonly struct QueueSpriteTreeUpdateEvent
|
||||
{
|
||||
public readonly TransformComponent Xform;
|
||||
public QueueSpriteTreeUpdateEvent(TransformComponent xform)
|
||||
{
|
||||
Xform = xform;
|
||||
}
|
||||
}
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
#region Component Tree Overrides
|
||||
protected override bool DoFrameUpdate => true;
|
||||
@@ -36,6 +19,11 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
|
||||
protected override int InitialCapacity => 1024;
|
||||
|
||||
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
|
||||
=> entry.Component.CalculateRotatedBoundingBox(pos, rot, default).CalcBoundingBox();
|
||||
{
|
||||
// TODO SPRITE optimize this
|
||||
// Because the just take the BB of the rotated BB, I'mt pretty sure we do a lot of unnecessary maths.
|
||||
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
@@ -94,6 +95,7 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IReplayRecordingManagerInternal _replayRecording = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
private IWebViewManagerHook? _webViewHook;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
return layer.Bounds;
|
||||
|
||||
layer.Bounds = CalculateLocalBounds(layer);
|
||||
layer.BoundsDirty = false;
|
||||
return layer.Bounds;
|
||||
}
|
||||
|
||||
internal Box2 CalculateLocalBounds(Layer layer)
|
||||
{
|
||||
if (layer.Blank || layer.CopyToShaderParameters == null)
|
||||
return Box2.Empty;
|
||||
|
||||
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
|
||||
|
||||
if (!layer.Blank)
|
||||
{
|
||||
layer.BoundsDirty = true;
|
||||
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
|
||||
}
|
||||
301
Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerMap.cs
Normal file
301
Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerMap.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
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>
|
||||
/// Create a new blank layer and map the given key to it.
|
||||
/// </summary>
|
||||
public int LayerMapReserve(Entity<SpriteComponent?> sprite, Enum key)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return -1;
|
||||
|
||||
if (LayerExists(sprite, key))
|
||||
throw new Exception("Layer already exists");
|
||||
|
||||
var layer = AddBlankLayer(sprite!);
|
||||
LayerMapSet(sprite, key, layer.Index);
|
||||
return layer.Index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A create a new blank layer and map the given key to it. 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 int LayerMapReserve(Entity<SpriteComponent?> sprite, string key)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return -1;
|
||||
|
||||
if (LayerExists(sprite, key))
|
||||
throw new Exception("Layer already exists");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
183
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs
Normal file
183
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
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 static Robust.Client.GameObjects.SpriteComponent;
|
||||
using Vector4 = Robust.Shared.Maths.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);
|
||||
|
||||
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;
|
||||
|
||||
@@ -1306,6 +1306,11 @@ namespace Robust.Client.GameStates
|
||||
meta.LastStateApplied = lastStateApplied.Value;
|
||||
|
||||
var xform = xforms.GetComponent(ent.Value);
|
||||
|
||||
// TODO PVS DETACH
|
||||
// Why is this if block here again? If a null-space entity gets sent to a player via some PVS override,
|
||||
// and then later on it gets removed, you would assume that the client marks it as detached?
|
||||
// I.e., modifying the metadata flag & pausing the entity should probably happen outside of this block.
|
||||
if (xform.ParentUid.IsValid())
|
||||
{
|
||||
lookupSys.RemoveFromEntityTree(ent.Value, xform);
|
||||
@@ -1326,6 +1331,13 @@ namespace Robust.Client.GameStates
|
||||
xformSys.DetachEntity(ent.Value, xform);
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
|
||||
|
||||
// We mark the entity as paused, without raising a pause-event.
|
||||
// The entity gets un-paused when the metadata's comp-state is reapplied (which also does not raise
|
||||
// an un-pause event). The assumption is that game logic that has to handle the pausing should be
|
||||
// getting networked anyway. And if its some client-side timer on a networked entity, the timer
|
||||
// shouldn't actually be getting paused just because the entity has left the players view.
|
||||
meta.PauseTime = TimeSpan.Zero;
|
||||
|
||||
if (container != null)
|
||||
containerSys.AddExpectedEntity(netEntity, container);
|
||||
}
|
||||
|
||||
@@ -254,7 +254,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
region = regionMaybe[tile.Variant];
|
||||
}
|
||||
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
|
||||
var rotationMirroring = _tileDefinitionManager[tile.TypeId].AllowRotationMirror
|
||||
? tile.RotationMirroring
|
||||
: 0;
|
||||
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, rotationMirroring);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
@@ -325,7 +329,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
continue;
|
||||
|
||||
var region = regionMaybe[0];
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, 0);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
@@ -445,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace Robust.Client.Placement
|
||||
|
||||
sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
|
||||
var rot = args.Viewport.Eye?.Rotation ?? default;
|
||||
spriteSys.Render(uid.Value, sprite, args.WorldHandle, rot, worldRot, worldPos);
|
||||
spriteSys.RenderSprite((uid.Value, sprite), args.WorldHandle, rot, worldRot, worldPos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ namespace Robust.Client.Prototypes
|
||||
public sealed class ClientPrototypeManager : PrototypeManager
|
||||
{
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
#pragma warning disable CS0414
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
#pragma warning restore CS0414
|
||||
[Dependency] private readonly IGameControllerInternal _controller = default!;
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,6 +9,7 @@ using Robust.Client.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Graphics;
|
||||
@@ -59,7 +61,14 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
try
|
||||
{
|
||||
TextureResource.LoadPreTexture(_manager, data);
|
||||
TextureResource.LoadTextureParameters(_manager, data);
|
||||
if (!data.LoadParameters.Preload)
|
||||
{
|
||||
data.Skip = true;
|
||||
return;
|
||||
}
|
||||
|
||||
TextureResource.LoadPreTextureData(_manager, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -72,7 +81,7 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
foreach (var data in texList)
|
||||
{
|
||||
if (data.Bad)
|
||||
if (data.Bad || data.Skip)
|
||||
continue;
|
||||
|
||||
try
|
||||
@@ -87,6 +96,7 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
|
||||
var errors = 0;
|
||||
var skipped = 0;
|
||||
foreach (var data in texList)
|
||||
{
|
||||
if (data.Bad)
|
||||
@@ -95,6 +105,12 @@ namespace Robust.Client.ResourceManagement
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data.Skip)
|
||||
{
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var texResource = new TextureResource();
|
||||
@@ -110,9 +126,10 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
|
||||
sawmill.Debug(
|
||||
"Preloaded {CountLoaded} textures ({CountErrored} errored) in {LoadTime}",
|
||||
texList.Length,
|
||||
"Preloaded {CountLoaded} textures ({CountErrored} errored, {CountSkipped} skipped) in {LoadTime}",
|
||||
texList.Length - skipped - errors,
|
||||
errors,
|
||||
skipped,
|
||||
sw.Elapsed);
|
||||
}
|
||||
|
||||
@@ -176,65 +193,143 @@ namespace Robust.Client.ResourceManagement
|
||||
// TODO allow RSIs to opt out (useful for very big & rare RSIs)
|
||||
// TODO combine with (non-rsi) texture atlas?
|
||||
|
||||
Array.Sort(atlasList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
|
||||
// We now need to insert the RSIs into the atlas. This specific problem is 2BP|O|F - the items are oriented
|
||||
// and cutting is free. The sorting is done by a slightly modified FFDH algorithm. The algorithm is exactly
|
||||
// the same as the standard FFDH algorithm with one main difference: We create new "levels" above placed
|
||||
// blocks. For example if the first block was 10x20, then the second was 10x10 units, we would create a
|
||||
// 10x10 level above the second block that would be treated as a normal level. This increases the packing
|
||||
// efficiency from ~85% to ~95% with very little extra computational effort. The algorithm appears to be
|
||||
// ~97% effective for storing SS14s RSIs.
|
||||
//
|
||||
// Here are some more resources about the strip packing problem!
|
||||
// - https://en.wikipedia.org/w/index.php?title=Strip_packing_problem&oldid=1263496949#First-fit_decreasing-height_(FFDH)
|
||||
// - https://www.csc.liv.ac.uk/~epa/surveyhtml.html
|
||||
// - https://www.dei.unipd.it/~fisch/ricop/tesi/tesi_dottorato_Lodi_1999.pdf
|
||||
|
||||
// The array must be sorted from biggest to smallest first.
|
||||
Array.Sort(atlasList, (b, a) => a.AtlasSheet.Height.CompareTo(b.AtlasSheet.Height));
|
||||
|
||||
// Each RSI sub atlas has a different size.
|
||||
// Even if we iterate through them once to estimate total area, I have NFI how to sanely estimate an optimal square-texture size.
|
||||
// So fuck it, just default to letting it be as large as it needs to and crop it as needed?
|
||||
var maxSize = Math.Min(GL.GetInteger(GetPName.MaxTextureSize), _configurationManager.GetCVar(CVars.ResRSIAtlasSize));
|
||||
var sheet = new Image<Rgba32>(maxSize, maxSize);
|
||||
|
||||
var deltaY = 0;
|
||||
Vector2i offset = default;
|
||||
int finalized = -1;
|
||||
int atlasCount = 0;
|
||||
for (int i = 0; i < atlasList.Length; i++)
|
||||
// THIS IS NOT GUARANTEED TO HAVE ANY PARTICULARLY LOGICAL ORDERING.
|
||||
// E.G you could have atlas 1 RSIs appear *before* you're done seeing atlas 2 RSIs.
|
||||
var levels = new ValueList<Level>();
|
||||
|
||||
// List of all the image atlases.
|
||||
var imageAtlases = new ValueList<Image<Rgba32>>();
|
||||
|
||||
// List of all the actual atlases.
|
||||
var finalAtlases = new ValueList<OwnedTexture>();
|
||||
|
||||
// Number of total pixels in each atlas.
|
||||
var finalPixels = new ValueList<int>();
|
||||
|
||||
// First we just find the location of all the RSIs in the atlas before actually placing them.
|
||||
// This allows us to effectively determine how much space we need to allocate for the images.
|
||||
var currentHeight = 0;
|
||||
var currentAtlasIndex = 0;
|
||||
foreach (var rsi in atlasList)
|
||||
{
|
||||
var rsi = atlasList[i];
|
||||
if (rsi.Bad)
|
||||
var insertHeight = rsi.AtlasSheet.Height;
|
||||
var insertWidth = rsi.AtlasSheet.Width;
|
||||
|
||||
var found = false;
|
||||
for (var i = 0; i < levels.Count && !found; i++)
|
||||
{
|
||||
var levelPosition = levels[i].Position;
|
||||
var levelWidth = levels[i].Width;
|
||||
var levelHeight = levels[i].Height;
|
||||
|
||||
// Check if it can fit in this level.
|
||||
if (levelHeight < insertHeight || levelWidth + insertWidth > levels[i].MaxWidth)
|
||||
continue;
|
||||
|
||||
found = true;
|
||||
|
||||
levels[i].Width += insertWidth;
|
||||
rsi.AtlasOffset = levelPosition + new Vector2i(levelWidth, 0);
|
||||
levels[i].RSIList.Add(rsi);
|
||||
|
||||
// Creating the extra "free" space above blocks that can be used for inserting more items.
|
||||
// This differs from the FFDH spec which just ignores this space.
|
||||
Debug.Assert(levelHeight >= insertHeight); // Must be true because the array needs to be sorted
|
||||
if (levelHeight - insertHeight == 0)
|
||||
continue;
|
||||
|
||||
var freeLevel = new Level
|
||||
{
|
||||
AtlasId = levels[i].AtlasId,
|
||||
Position = levelPosition + new Vector2i(levelWidth, insertHeight),
|
||||
Height = levelHeight - insertHeight,
|
||||
Width = 0,
|
||||
MaxWidth = insertWidth,
|
||||
RSIList = [ ]
|
||||
};
|
||||
|
||||
levels.Add(freeLevel);
|
||||
}
|
||||
|
||||
if (found)
|
||||
continue;
|
||||
|
||||
DebugTools.Assert(rsi.AtlasSheet.Width < sheet.Width);
|
||||
DebugTools.Assert(rsi.AtlasSheet.Height < sheet.Height);
|
||||
|
||||
if (offset.X + rsi.AtlasSheet.Width > sheet.Width)
|
||||
// Ran out of space, we need to move on to the next atlas.
|
||||
// This also isn't in the normal FFDH algorithm (obviously) but its close enough.
|
||||
if (currentHeight + insertHeight > maxSize)
|
||||
{
|
||||
offset.X = 0;
|
||||
offset.Y += deltaY;
|
||||
imageAtlases.Add(new Image<Rgba32>(maxSize, currentHeight));
|
||||
finalPixels.Add(0);
|
||||
currentHeight = 0;
|
||||
currentAtlasIndex++;
|
||||
}
|
||||
|
||||
if (offset.Y + rsi.AtlasSheet.Height > sheet.Height)
|
||||
{
|
||||
FinalizeMetaAtlas(i-1, sheet);
|
||||
sheet = new Image<Rgba32>(maxSize, maxSize);
|
||||
deltaY = 0;
|
||||
offset = default;
|
||||
}
|
||||
rsi.AtlasOffset = new Vector2i(0, currentHeight);
|
||||
|
||||
deltaY = Math.Max(deltaY, rsi.AtlasSheet.Height);
|
||||
var box = new UIBox2i(0, 0, rsi.AtlasSheet.Width, rsi.AtlasSheet.Height);
|
||||
rsi.AtlasSheet.Blit(box, sheet, offset);
|
||||
rsi.AtlasOffset = offset;
|
||||
offset.X += rsi.AtlasSheet.Width;
|
||||
var newLevel = new Level
|
||||
{
|
||||
AtlasId = currentAtlasIndex,
|
||||
Position = new Vector2i(0, currentHeight),
|
||||
Height = insertHeight,
|
||||
Width = insertWidth,
|
||||
MaxWidth = maxSize,
|
||||
RSIList = [ rsi ]
|
||||
};
|
||||
levels.Add(newLevel);
|
||||
|
||||
currentHeight += insertHeight;
|
||||
}
|
||||
|
||||
var height = offset.Y + deltaY;
|
||||
var croppedSheet = new Image<Rgba32>(maxSize, height);
|
||||
sheet.Blit(new UIBox2i(0, 0, maxSize, height), croppedSheet, default);
|
||||
FinalizeMetaAtlas(atlasList.Length - 1, croppedSheet);
|
||||
// This allocation takes a long time.
|
||||
imageAtlases.Add(new Image<Rgba32>(maxSize, currentHeight));
|
||||
finalPixels.Add(0);
|
||||
|
||||
void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
|
||||
// Put all textures on the atlases
|
||||
foreach (var level in levels)
|
||||
{
|
||||
var fromIndex = finalized + 1;
|
||||
var atlas = Clyde.LoadTextureFromImage(sheet, $"Meta atlas {fromIndex}-{toIndex}");
|
||||
for (int i = fromIndex; i <= toIndex; i++)
|
||||
foreach (var rsi in level.RSIList)
|
||||
{
|
||||
var rsi = atlasList[i];
|
||||
rsi.AtlasTexture = atlas;
|
||||
}
|
||||
var box = new UIBox2i(0, 0, rsi.AtlasSheet.Width, rsi.AtlasSheet.Height);
|
||||
|
||||
finalized = toIndex;
|
||||
atlasCount++;
|
||||
rsi.AtlasSheet.Blit(box, imageAtlases[level.AtlasId], rsi.AtlasOffset);
|
||||
finalPixels[level.AtlasId] += rsi.AtlasSheet.Width * rsi.AtlasSheet.Height;
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize the atlases.
|
||||
for (var i = 0; i < imageAtlases.Count; i++)
|
||||
{
|
||||
var atlasTexture = Clyde.LoadTextureFromImage(imageAtlases[i], $"Meta atlas {i}");
|
||||
finalAtlases.Add(atlasTexture);
|
||||
|
||||
sawmill.Debug($"(Meta atlas {i}) - cropped utilization: {(float)finalPixels[i] / (maxSize * imageAtlases[i].Height):P2}, fill percentage: {(float)imageAtlases[i].Height / maxSize:P2}");
|
||||
}
|
||||
|
||||
// Finally, reference the actual atlas from the RSIs.
|
||||
foreach (var level in levels)
|
||||
{
|
||||
foreach (var rsi in level.RSIList)
|
||||
{
|
||||
rsi.AtlasTexture = finalAtlases[level.AtlasId];
|
||||
}
|
||||
}
|
||||
|
||||
Parallel.ForEach(rsiList, data =>
|
||||
@@ -279,7 +374,7 @@ namespace Robust.Client.ResourceManagement
|
||||
sawmill.Debug(
|
||||
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountNotAtlas} not atlassed, {CountErrored} errored) in {LoadTime}",
|
||||
rsiList.Length,
|
||||
atlasCount,
|
||||
finalAtlases.Count,
|
||||
nonAtlasList.Length,
|
||||
errors,
|
||||
sw.Elapsed);
|
||||
@@ -290,4 +385,38 @@ namespace Robust.Client.ResourceManagement
|
||||
return rsi.MetaAtlas && rsi.LoadParameters == TextureLoadParameters.Default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A "Level" to place boxes. Similar to FFDH levels, but with more parameters so we can fit in "free" levels
|
||||
/// above placed boxes.
|
||||
/// </summary>
|
||||
internal sealed class Level
|
||||
{
|
||||
/// <summary>
|
||||
/// Index of the atlas this is located.
|
||||
/// </summary>
|
||||
public required int AtlasId;
|
||||
/// <summary>
|
||||
/// Bottom left of the location for the RSIs.
|
||||
/// </summary>
|
||||
public required Vector2i Position;
|
||||
/// <summary>
|
||||
/// The current width of the level.
|
||||
/// </summary>
|
||||
/// <remarks>This can (and will) be 0. Will change.</remarks>
|
||||
public required int Width;
|
||||
/// <summary>
|
||||
/// The current height of the level.
|
||||
/// </summary>
|
||||
/// <remarks>This value should never change.</remarks>
|
||||
public required int Height;
|
||||
/// <summary>
|
||||
/// Maximum width of the level.
|
||||
/// </summary>
|
||||
public required int MaxWidth;
|
||||
/// <summary>
|
||||
/// List of all the RSIs stored in this level. RSIs are ordered from tallest to smallest per level.
|
||||
/// </summary>
|
||||
public required List<RSIResource.LoadStepData> RSIList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -11,6 +11,13 @@ namespace Robust.Client.ResourceManagement;
|
||||
|
||||
public sealed class AudioResource : BaseResource
|
||||
{
|
||||
// from: https://en.wikipedia.org/wiki/List_of_file_signatures
|
||||
private static readonly byte[] OggSignature = "OggS"u8.ToArray();
|
||||
private static readonly byte[] RiffSignature = "RIFF"u8.ToArray();
|
||||
private const int WavSignatureStart = 8; // RIFF????
|
||||
private static readonly byte[] WavSignature = "WAVE"u8.ToArray();
|
||||
private const int MaxSignatureLength = 12; // RIFF????WAVE
|
||||
|
||||
public AudioStream AudioStream { get; private set; } = default!;
|
||||
|
||||
public void Load(AudioStream stream)
|
||||
@@ -28,14 +35,19 @@ public sealed class AudioResource : BaseResource
|
||||
}
|
||||
|
||||
using var fileStream = cache.ContentFileRead(path);
|
||||
var seekableStream = fileStream.CanSeek ? fileStream : fileStream.CopyToMemoryStream();
|
||||
byte[] signature = seekableStream.ReadExact(MaxSignatureLength);
|
||||
seekableStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
var audioManager = dependencies.Resolve<IAudioInternal>();
|
||||
if (path.Extension == "ogg")
|
||||
if (signature[..OggSignature.Length].SequenceEqual(OggSignature))
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(seekableStream, path.ToString());
|
||||
}
|
||||
else if (path.Extension == "wav")
|
||||
else if (signature[..RiffSignature.Length].SequenceEqual(RiffSignature)
|
||||
&& signature[WavSignatureStart..MaxSignatureLength].SequenceEqual(WavSignature))
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
|
||||
AudioStream = audioManager.LoadAudioWav(seekableStream, path.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -32,18 +32,22 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
var data = new LoadStepData {Path = path};
|
||||
|
||||
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadTextureParameters(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadPreTextureData(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadTexture(dependencies.Resolve<IClyde>(), data);
|
||||
LoadFinish(dependencies.Resolve<IResourceCache>(), data);
|
||||
}
|
||||
|
||||
internal static void LoadPreTexture(IResourceManager cache, LoadStepData data)
|
||||
internal static void LoadPreTextureData(IResourceManager cache, LoadStepData data)
|
||||
{
|
||||
using (var stream = cache.ContentFileRead(data.Path))
|
||||
{
|
||||
data.Image = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void LoadTextureParameters(IResourceManager cache, LoadStepData data)
|
||||
{
|
||||
data.LoadParameters = TryLoadTextureParameters(cache, data.Path) ?? TextureLoadParameters.Default;
|
||||
}
|
||||
|
||||
@@ -95,7 +99,8 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
var data = new LoadStepData {Path = path};
|
||||
|
||||
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadTextureParameters(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadPreTextureData(dependencies.Resolve<IResourceManager>(), data);
|
||||
|
||||
if (data.Image.Width == Texture.Width && data.Image.Height == Texture.Height)
|
||||
{
|
||||
@@ -119,6 +124,7 @@ namespace Robust.Client.ResourceManagement
|
||||
public Image<Rgba32> Image = default!;
|
||||
public TextureLoadParameters LoadParameters;
|
||||
public OwnedTexture Texture = default!;
|
||||
public bool Skip;
|
||||
public bool Bad;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
@@ -21,12 +20,14 @@ internal sealed class ReloadManager : IReloadManager
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
#pragma warning disable CS0414
|
||||
[Dependency] private readonly ITaskManager _tasks = default!;
|
||||
#pragma warning restore CS0414
|
||||
|
||||
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
|
||||
private CancellationTokenSource _reloadToken = new();
|
||||
private readonly HashSet<ResPath> _reloadQueue = new();
|
||||
private List<FileSystemWatcher> _watchers = new();
|
||||
private List<FileSystemWatcher> _watchers = new(); // this list is never used but needed to prevent them from being garbage collected
|
||||
|
||||
public event Action<ResPath>? OnChanged;
|
||||
|
||||
@@ -69,6 +70,11 @@ internal sealed class ReloadManager : IReloadManager
|
||||
_reloadQueue.Clear();
|
||||
}
|
||||
|
||||
public void Register(ResPath directory, string filter)
|
||||
{
|
||||
Register(directory.ToString(), filter);
|
||||
}
|
||||
|
||||
public void Register(string directory, string filter)
|
||||
{
|
||||
if (!_cfg.GetCVar(CVars.ResPrototypeReloadWatch))
|
||||
@@ -90,7 +96,7 @@ internal sealed class ReloadManager : IReloadManager
|
||||
NotifyFilter = NotifyFilters.LastWrite
|
||||
};
|
||||
|
||||
_watchers.Add(watcher);
|
||||
_watchers.Add(watcher); // prevent garbage collection
|
||||
|
||||
watcher.Changed += OnWatch;
|
||||
|
||||
@@ -100,7 +106,7 @@ internal sealed class ReloadManager : IReloadManager
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.Error($"Watching resources in path {path} threw an exception:\n{ex}");
|
||||
_sawmill.Error($"Watching resources in path {path} threw an exception:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +142,6 @@ internal sealed class ReloadManager : IReloadManager
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Robust.Client.Utility
|
||||
/// </summary>
|
||||
public static class SpriteSpecifierExt
|
||||
{
|
||||
[Obsolete("Use SpriteSystem.GetTexture() instead")]
|
||||
public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, IResourceCache cache)
|
||||
{
|
||||
return cache
|
||||
@@ -24,13 +25,14 @@ namespace Robust.Client.Utility
|
||||
.Texture;
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem")]
|
||||
[Obsolete("Use SpriteSystem.GetState() instead")]
|
||||
public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourceCache cache)
|
||||
{
|
||||
if (!cache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / rsiSpecifier.RsiPath, out var theRsi))
|
||||
{
|
||||
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
Logger.Error("SpriteSpecifier failed to load RSI {0}", rsiSpecifier.RsiPath);
|
||||
return SpriteComponent.GetFallbackState(cache);
|
||||
return sys.GetFallbackState();
|
||||
}
|
||||
|
||||
if (theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
|
||||
@@ -39,21 +41,22 @@ namespace Robust.Client.Utility
|
||||
}
|
||||
|
||||
Logger.Error($"SpriteSpecifier has invalid RSI state '{rsiSpecifier.RsiState}' for RSI: {rsiSpecifier.RsiPath}");
|
||||
return SpriteComponent.GetFallbackState(cache);
|
||||
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>().GetFallbackState();
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem")]
|
||||
[Obsolete("Use SpriteSystem.Frame0() instead")]
|
||||
public static Texture Frame0(this SpriteSpecifier specifier)
|
||||
{
|
||||
return specifier.RsiStateLike().Default;
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem.RsiStateLike() instead")]
|
||||
public static IDirectionalTextureProvider DirFrame0(this SpriteSpecifier specifier)
|
||||
{
|
||||
return specifier.RsiStateLike();
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem")]
|
||||
[Obsolete("Use SpriteSystem.RsiStateLike() instead")]
|
||||
public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier)
|
||||
{
|
||||
var resC = IoCManager.Resolve<IResourceCache>();
|
||||
@@ -67,10 +70,11 @@ namespace Robust.Client.Utility
|
||||
|
||||
case SpriteSpecifier.EntityPrototype prototypeIcon:
|
||||
var protMgr = IoCManager.Resolve<IPrototypeManager>();
|
||||
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
if (!protMgr.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
|
||||
{
|
||||
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
|
||||
return SpriteComponent.GetFallbackState(resC);
|
||||
return sys.GetFallbackState();
|
||||
}
|
||||
|
||||
return SpriteComponent.GetPrototypeIcon(prototype, resC);
|
||||
|
||||
@@ -39,6 +39,7 @@ public static class Diagnostics
|
||||
public const string IdForbidLiteral = "RA0033";
|
||||
public const string IdObsoleteInheritance = "RA0034";
|
||||
public const string IdObsoleteInheritanceWithMessage = "RA0035";
|
||||
public const string IdDataFieldYamlSerializable = "RA0036";
|
||||
|
||||
public static SuppressionDescriptor MeansImplicitAssignment =>
|
||||
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");
|
||||
|
||||
@@ -331,6 +331,7 @@ namespace Robust.Server
|
||||
// TODO: solve this properly.
|
||||
_serializer.Initialize();
|
||||
|
||||
_loc.Initialize();
|
||||
_loc.AddLoadedToStringSerializer(_stringSerializer);
|
||||
|
||||
//IoCManager.Resolve<IMapLoader>().LoadedMapData +=
|
||||
|
||||
@@ -14,6 +14,19 @@ public sealed class PointLightSystem : SharedPointLightSystem
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentGetState>(OnLightGetState);
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentStartup>(OnLightStartup);
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentShutdown>(OnLightShutdown);
|
||||
SubscribeLocalEvent<PointLightComponent, MetaFlagRemoveAttemptEvent>(OnFlagRemoveAttempt);
|
||||
}
|
||||
|
||||
private void OnLightShutdown(Entity<PointLightComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
UpdatePriority(ent.Owner, ent.Comp, MetaData(ent.Owner));
|
||||
}
|
||||
|
||||
private void OnFlagRemoveAttempt(Entity<PointLightComponent> ent, ref MetaFlagRemoveAttemptEvent args)
|
||||
{
|
||||
if (IsHighPriority(ent.Comp))
|
||||
args.ToRemove &= ~MetaDataFlags.PvsPriority;
|
||||
}
|
||||
|
||||
private void OnLightStartup(EntityUid uid, PointLightComponent component, ComponentStartup args)
|
||||
@@ -21,24 +34,14 @@ public sealed class PointLightSystem : SharedPointLightSystem
|
||||
UpdatePriority(uid, component, MetaData(uid));
|
||||
}
|
||||
|
||||
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
|
||||
private bool IsHighPriority(SharedPointLightComponent comp)
|
||||
{
|
||||
var isHighPriority = comp.Enabled && comp.CastShadows && (comp.Radius > 7);
|
||||
_metadata.SetFlag((uid, meta), MetaDataFlags.PvsPriority, isHighPriority);
|
||||
return comp is {Enabled: true, CastShadows: true, Radius: > 7, LifeStage: <= ComponentLifeStage.Running};
|
||||
}
|
||||
|
||||
private void OnLightGetState(EntityUid uid, PointLightComponent component, ref ComponentGetState args)
|
||||
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
|
||||
{
|
||||
args.State = new PointLightComponentState()
|
||||
{
|
||||
Color = component.Color,
|
||||
Enabled = component.Enabled,
|
||||
Energy = component.Energy,
|
||||
Offset = component.Offset,
|
||||
Radius = component.Radius,
|
||||
Softness = component.Softness,
|
||||
CastShadows = component.CastShadows,
|
||||
};
|
||||
_metadata.SetFlag((uid, meta), MetaDataFlags.PvsPriority, IsHighPriority(comp));
|
||||
}
|
||||
|
||||
public override SharedPointLightComponent EnsureLight(EntityUid uid)
|
||||
|
||||
@@ -9,12 +9,30 @@ public sealed class ServerOccluderSystem : OccluderSystem
|
||||
{
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<OccluderComponent, MetaFlagRemoveAttemptEvent>(OnFlagRemoveAttempt);
|
||||
}
|
||||
|
||||
private void OnFlagRemoveAttempt(Entity<OccluderComponent> ent, ref MetaFlagRemoveAttemptEvent args)
|
||||
{
|
||||
if (ent.Comp is {Enabled: true, LifeStage: <= ComponentLifeStage.Running})
|
||||
args.ToRemove &= ~MetaDataFlags.PvsPriority;
|
||||
}
|
||||
|
||||
protected override void OnCompStartup(EntityUid uid, OccluderComponent component, ComponentStartup args)
|
||||
{
|
||||
base.OnCompStartup(uid, component, args);
|
||||
_metadata.SetFlag(uid, MetaDataFlags.PvsPriority, component.Enabled);
|
||||
}
|
||||
|
||||
protected override void OnCompRemoved(EntityUid uid, OccluderComponent component, ComponentRemove args)
|
||||
{
|
||||
base.OnCompRemoved(uid, component, args);
|
||||
_metadata.SetFlag(uid, MetaDataFlags.PvsPriority, false);
|
||||
}
|
||||
|
||||
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
|
||||
@@ -166,11 +166,11 @@ internal sealed partial class PvsSystem
|
||||
var session = pvsSession.Session;
|
||||
if (session.Status != SessionStatus.InGame)
|
||||
{
|
||||
pvsSession.Viewers = Array.Empty<Entity<TransformComponent, EyeComponent?>>();
|
||||
pvsSession.Viewers = Array.Empty<Entity<TransformComponent, EyeComponent?>>();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fast path
|
||||
// The majority of players will have no view subscriptions
|
||||
if (session.ViewSubscriptions.Count == 0)
|
||||
{
|
||||
if (session.AttachedEntity is not {} attached)
|
||||
@@ -184,15 +184,21 @@ internal sealed partial class PvsSystem
|
||||
return;
|
||||
}
|
||||
|
||||
var count = session.ViewSubscriptions.Count;
|
||||
var i = 0;
|
||||
if (session.AttachedEntity is { } local)
|
||||
{
|
||||
Array.Resize(ref pvsSession.Viewers, session.ViewSubscriptions.Count + 1);
|
||||
if (!session.ViewSubscriptions.Contains(local))
|
||||
count += 1;
|
||||
|
||||
Array.Resize(ref pvsSession.Viewers, count);
|
||||
|
||||
// Attached entity is always the first viewer, to prioritize it and help reduce pop-in for the "main" eye.
|
||||
pvsSession.Viewers[i++] = (local, Transform(local), _eyeQuery.CompOrNull(local));
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Resize(ref pvsSession.Viewers, session.ViewSubscriptions.Count);
|
||||
Array.Resize(ref pvsSession.Viewers, count);
|
||||
}
|
||||
|
||||
foreach (var ent in session.ViewSubscriptions)
|
||||
@@ -200,6 +206,8 @@ internal sealed partial class PvsSystem
|
||||
if (ent != session.AttachedEntity)
|
||||
pvsSession.Viewers[i++] = (ent, Transform(ent), _eyeQuery.CompOrNull(ent));
|
||||
}
|
||||
|
||||
DebugTools.AssertEqual(i, pvsSession.Viewers.Length);
|
||||
}
|
||||
|
||||
private void ProcessVisibleChunks()
|
||||
|
||||
8
Robust.Server/Localization/ServerLocalizationManager.cs
Normal file
8
Robust.Server/Localization/ServerLocalizationManager.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Server.Localization;
|
||||
|
||||
internal sealed class ServerLocalizationManager : LocalizationManager, ILocalizationManager
|
||||
{
|
||||
void ILocalizationManager.Initialize() => Initialize();
|
||||
}
|
||||
@@ -179,11 +179,14 @@ namespace Robust.Server.Placement
|
||||
}
|
||||
else
|
||||
{
|
||||
PlaceNewTile(tileType, coordinates, msg.MsgChannel.UserId);
|
||||
if (_tileDefinitionManager[tileType].AllowRotationMirror)
|
||||
PlaceNewTile(tileType, coordinates, msg.MsgChannel.UserId, Tile.DirectionToByte(dirRcv), msg.Mirrored);
|
||||
else
|
||||
PlaceNewTile(tileType, coordinates, msg.MsgChannel.UserId, Tile.DirectionToByte(Direction.South), false);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId placingUserId)
|
||||
private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId placingUserId, byte direction, bool mirrored)
|
||||
{
|
||||
if (!coordinates.IsValid(_entityManager)) return;
|
||||
|
||||
@@ -193,7 +196,7 @@ namespace Robust.Server.Placement
|
||||
if (_entityManager.TryGetComponent(coordinates.EntityId, out grid)
|
||||
|| _mapManager.TryFindGridAt(_xformSystem.ToMapCoordinates(coordinates), out gridId, out grid))
|
||||
{
|
||||
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType));
|
||||
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0))));
|
||||
|
||||
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
|
||||
@@ -207,7 +210,7 @@ namespace Robust.Server.Placement
|
||||
|
||||
_xformSystem.SetWorldPosition(newGridXform, coordinates.Position - newGrid.Comp.TileSizeHalfVector); // assume bottom left tile origin
|
||||
var tilePos = _maps.WorldToTile(newGrid.Owner, newGrid.Comp, coordinates.Position);
|
||||
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType));
|
||||
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0))));
|
||||
|
||||
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
|
||||
|
||||
@@ -13,8 +13,10 @@ namespace Robust.Server.Prototypes
|
||||
{
|
||||
public sealed class ServerPrototypeManager : PrototypeManager
|
||||
{
|
||||
#pragma warning disable CS0414
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IConGroupController _conGroups = default!;
|
||||
#pragma warning restore CS0414
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly IBaseServerInternal _server = default!;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Robust.Server.Console;
|
||||
using Robust.Server.DataMetrics;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Server.Localization;
|
||||
using Robust.Server.Placement;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Server.Prototypes;
|
||||
@@ -21,6 +22,7 @@ using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
@@ -97,7 +99,9 @@ namespace Robust.Server
|
||||
deps.Register<NetworkResourceManager>();
|
||||
deps.Register<IHttpClientHolder, HttpClientHolder>();
|
||||
deps.Register<UploadedContentManager>();
|
||||
deps.Register<IHWId, DummyHWId>();
|
||||
deps.Register<IHWId, DummyHWId>();
|
||||
deps.Register<ILocalizationManager, ServerLocalizationManager>();
|
||||
deps.Register<ILocalizationManagerInternal, ServerLocalizationManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,16 @@ namespace Robust.Shared.Maths
|
||||
return (Direction) (Math.Floor((ang + CardinalOffset) / CardinalSegment) * 2 % 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the angle to the nearest cardinal direction. This behaves similarly to a combination of
|
||||
/// <see cref="GetCardinalDir"/> and Direction.ToAngle(), however this may return an angle outside of the range
|
||||
/// returned by those methods (-pi to pi).
|
||||
/// </summary>
|
||||
public Angle RoundToCardinalAngle()
|
||||
{
|
||||
return new Angle(CardinalSegment * Math.Floor((Theta + CardinalOffset) / CardinalSegment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates the vector counter-clockwise around its origin by the value of Theta.
|
||||
/// </summary>
|
||||
|
||||
@@ -111,6 +111,11 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
|
||||
component.TreeUpdateQueued = true;
|
||||
_updateQueue.Enqueue((component, xform));
|
||||
}
|
||||
|
||||
public void QueueTreeUpdate(Entity<TComp> entity, TransformComponent? xform = null)
|
||||
{
|
||||
QueueTreeUpdate(entity.Owner, entity.Comp, xform);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Component Management
|
||||
|
||||
@@ -16,6 +16,9 @@ public abstract class ContainerAttemptEventBase : CancellableEntityEventArgs
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on the container when attempting to insert an entity.
|
||||
/// </summary>
|
||||
public sealed class ContainerIsInsertingAttemptEvent : ContainerAttemptEventBase
|
||||
{
|
||||
/// <summary>
|
||||
@@ -23,7 +26,7 @@ public sealed class ContainerIsInsertingAttemptEvent : ContainerAttemptEventBase
|
||||
/// I.e., could the entity be inserted if the container doesn't contain anything else?
|
||||
/// </summary>
|
||||
public bool AssumeEmpty { get; set; }
|
||||
|
||||
|
||||
public ContainerIsInsertingAttemptEvent(BaseContainer container, EntityUid entityUid, bool assumeEmpty)
|
||||
: base(container, entityUid)
|
||||
{
|
||||
@@ -31,6 +34,9 @@ public sealed class ContainerIsInsertingAttemptEvent : ContainerAttemptEventBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on the entity being inserted into the container.
|
||||
/// </summary>
|
||||
public sealed class ContainerGettingInsertedAttemptEvent : ContainerAttemptEventBase
|
||||
{
|
||||
/// <summary>
|
||||
@@ -46,6 +52,9 @@ public sealed class ContainerGettingInsertedAttemptEvent : ContainerAttemptEvent
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on the container when attempting to remove an entity.
|
||||
/// </summary>
|
||||
public sealed class ContainerIsRemovingAttemptEvent : ContainerAttemptEventBase
|
||||
{
|
||||
public ContainerIsRemovingAttemptEvent(BaseContainer container, EntityUid entityUid) : base(container, entityUid)
|
||||
@@ -53,6 +62,9 @@ public sealed class ContainerIsRemovingAttemptEvent : ContainerAttemptEventBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on the entity being removed from the container.
|
||||
/// </summary>
|
||||
public sealed class ContainerGettingRemovedAttemptEvent : ContainerAttemptEventBase
|
||||
{
|
||||
public ContainerGettingRemovedAttemptEvent(BaseContainer container, EntityUid entityUid) : base(container, entityUid)
|
||||
|
||||
@@ -913,6 +913,7 @@ Types:
|
||||
System.Threading:
|
||||
CancellationToken: { All: True }
|
||||
CancellationTokenSource: { All: True }
|
||||
CancellationTokenRegistration: { All: True }
|
||||
Interlocked: { All: True }
|
||||
Monitor: { All: True }
|
||||
System.Web:
|
||||
|
||||
@@ -72,19 +72,42 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
node.TryGetValue("version", out var versionNode);
|
||||
var version = ((ValueDataNode?) versionNode)?.AsInt() ?? 1;
|
||||
|
||||
for (ushort y = 0; y < chunk.ChunkSize; y++)
|
||||
// Old map files will throw an error if they do not have a rotation/mirror byte stored.
|
||||
if (version >= 7)
|
||||
{
|
||||
for (ushort x = 0; x < chunk.ChunkSize; x++)
|
||||
for (ushort y = 0; y < chunk.ChunkSize; y++)
|
||||
{
|
||||
var id = version < 6 ? reader.ReadUInt16() : reader.ReadInt32();
|
||||
var flags = reader.ReadByte();
|
||||
var variant = reader.ReadByte();
|
||||
for (ushort x = 0; x < chunk.ChunkSize; x++)
|
||||
{
|
||||
var id = reader.ReadInt32();
|
||||
var flags = reader.ReadByte();
|
||||
var variant = reader.ReadByte();
|
||||
var rotationMirroring = reader.ReadByte();
|
||||
|
||||
var defName = tileMap[id];
|
||||
id = tileDefinitionManager[defName].TileId;
|
||||
var defName = tileMap[id];
|
||||
id = tileDefinitionManager[defName].TileId;
|
||||
|
||||
var tile = new Tile(id, flags, variant);
|
||||
chunk.TrySetTile(x, y, tile, out _, out _);
|
||||
var tile = new Tile(id, flags, variant, rotationMirroring);
|
||||
chunk.TrySetTile(x, y, tile, out _, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (ushort y = 0; y < chunk.ChunkSize; y++)
|
||||
{
|
||||
for (ushort x = 0; x < chunk.ChunkSize; x++)
|
||||
{
|
||||
var id = version < 6 ? reader.ReadUInt16() : reader.ReadInt32();
|
||||
var flags = reader.ReadByte();
|
||||
var variant = reader.ReadByte();
|
||||
|
||||
var defName = tileMap[id];
|
||||
id = tileDefinitionManager[defName].TileId;
|
||||
|
||||
var tile = new Tile(id, flags, variant);
|
||||
chunk.TrySetTile(x, y, tile, out _, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +129,7 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
var gridNode = new ValueDataNode();
|
||||
root.Add("tiles", gridNode);
|
||||
|
||||
root.Add("version", new ValueDataNode("6"));
|
||||
root.Add("version", new ValueDataNode("7"));
|
||||
|
||||
gridNode.Value = SerializeTiles(value, context as EntitySerializer);
|
||||
|
||||
@@ -116,7 +139,7 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
private static string SerializeTiles(MapChunk chunk, EntitySerializer? serializer)
|
||||
{
|
||||
// number of bytes written per tile, because sizeof(Tile) is useless.
|
||||
const int structSize = 6;
|
||||
const int structSize = 7;
|
||||
|
||||
var nTiles = chunk.ChunkSize * chunk.ChunkSize * structSize;
|
||||
var barr = new byte[nTiles];
|
||||
@@ -134,6 +157,7 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
writer.Write(tile.TypeId);
|
||||
writer.Write((byte) tile.Flags);
|
||||
writer.Write(tile.Variant);
|
||||
writer.Write(tile.RotationMirroring);
|
||||
}
|
||||
}
|
||||
return Convert.ToBase64String(barr);
|
||||
@@ -153,6 +177,7 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
writer.Write(yamlId);
|
||||
writer.Write((byte) tile.Flags);
|
||||
writer.Write(tile.Variant);
|
||||
writer.Write(tile.RotationMirroring);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,11 @@ namespace Robust.Shared.GameObjects
|
||||
/// a grid or map.
|
||||
/// </summary>
|
||||
PvsPriority = 1 << 4,
|
||||
|
||||
/// <summary>
|
||||
/// If set, transform system will raise events directed at this entity whenever the GridUid or MapUid are modified.
|
||||
/// </summary>
|
||||
ExtraTransformEvents = 1 << 5,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -80,4 +80,8 @@ public enum LayerRenderingStrategy
|
||||
SnapToCardinals,
|
||||
NoRotation,
|
||||
UseSpriteStrategy
|
||||
// TODO SPRITE
|
||||
// Refactor this make the sprites strategy the actual default.
|
||||
// That way layers have to opt in to having a custom strategy, instead of opt out.
|
||||
// Also rename default to make it clear that its not actually the default, instead I guess its "WithRotation"?
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace Robust.Shared.GameObjects
|
||||
if (!Initialized)
|
||||
return;
|
||||
|
||||
_entMan.System<SharedTransformSystem>().RaiseMoveEvent((Owner, this, meta), _parent, _localPosition, oldRotation, MapUid);
|
||||
_entMan.System<SharedTransformSystem>().RaiseMoveEvent((Owner, this, meta), _parent, _localPosition, oldRotation, MapUid, checkTraversal: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,32 +402,6 @@ namespace Robust.Shared.GameObjects
|
||||
_entMan.EntitySysManager.GetEntitySystem<SharedTransformSystem>().AttachToGridOrMap(Owner, this);
|
||||
}
|
||||
|
||||
internal void UpdateChildMapIdsRecursive(
|
||||
MapId newMapId,
|
||||
EntityUid? newUid,
|
||||
bool mapPaused,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
EntityQuery<MetaDataComponent> metaQuery,
|
||||
MetaDataSystem system)
|
||||
{
|
||||
foreach (var child in _children)
|
||||
{
|
||||
//Set Paused state
|
||||
var metaData = metaQuery.GetComponent(child);
|
||||
system.SetEntityPaused(child, mapPaused, metaData);
|
||||
|
||||
var concrete = xformQuery.GetComponent(child);
|
||||
|
||||
concrete.MapUid = newUid;
|
||||
concrete.MapID = newMapId;
|
||||
|
||||
if (concrete.ChildCount != 0)
|
||||
{
|
||||
concrete.UpdateChildMapIdsRecursive(newMapId, newUid, mapPaused, xformQuery, metaQuery, system);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use TransformSystem.SetParent() instead")]
|
||||
public void AttachParent(EntityUid parent)
|
||||
{
|
||||
|
||||
@@ -47,9 +47,10 @@ public record struct Entity<T> : IFluentEntityUid, IAsType<EntityUid>
|
||||
comp = Comp;
|
||||
}
|
||||
|
||||
public EntityUid AsType() => Owner;
|
||||
|
||||
public override int GetHashCode() => Owner.GetHashCode();
|
||||
public Entity<T?> AsNullable() => new(Owner, Comp);
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
[NotYamlSerializable]
|
||||
@@ -118,6 +119,8 @@ public record struct Entity<T1, T2> : IFluentEntityUid, IAsType<EntityUid>
|
||||
return new Entity<T1>(ent.Owner, ent.Comp1);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Owner.GetHashCode();
|
||||
public Entity<T1?, T2?> AsNullable() => new(Owner, Comp1, Comp2);
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
@@ -223,6 +226,8 @@ public record struct Entity<T1, T2, T3> : IFluentEntityUid, IAsType<EntityUid>
|
||||
|
||||
#endregion
|
||||
|
||||
public override int GetHashCode() => Owner.GetHashCode();
|
||||
public Entity<T1?, T2?, T3?> AsNullable() => new(Owner, Comp1, Comp2, Comp3);
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
@@ -352,6 +357,8 @@ public record struct Entity<T1, T2, T3, T4> : IFluentEntityUid, IAsType<EntityUi
|
||||
|
||||
#endregion
|
||||
|
||||
public override int GetHashCode() => Owner.GetHashCode();
|
||||
public Entity<T1?, T2?, T3?, T4?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4);
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
@@ -505,6 +512,8 @@ public record struct Entity<T1, T2, T3, T4, T5> : IFluentEntityUid, IAsType<Enti
|
||||
|
||||
#endregion
|
||||
|
||||
public override int GetHashCode() => Owner.GetHashCode();
|
||||
public Entity<T1?, T2?, T3?, T4?, T5?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5);
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
@@ -682,6 +691,8 @@ public record struct Entity<T1, T2, T3, T4, T5, T6> : IFluentEntityUid, IAsType<
|
||||
|
||||
#endregion
|
||||
|
||||
public override int GetHashCode() => Owner.GetHashCode();
|
||||
public Entity<T1?, T2?, T3?, T4?, T5?, T6?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6);
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
@@ -883,8 +894,9 @@ public record struct Entity<T1, T2, T3, T4, T5, T6, T7> : IFluentEntityUid, IAsT
|
||||
|
||||
#endregion
|
||||
|
||||
public override int GetHashCode() => Owner.GetHashCode();
|
||||
public Entity<T1?, T2?, T3?, T4?, T5?, T6?, T7?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6, Comp7);
|
||||
public EntityUid AsType() => Owner;
|
||||
|
||||
}
|
||||
|
||||
[NotYamlSerializable]
|
||||
@@ -1109,5 +1121,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6, T7, T8> : IFluentEntityUid,
|
||||
|
||||
#endregion
|
||||
|
||||
public override int GetHashCode() => Owner.GetHashCode();
|
||||
public Entity<T1?, T2?, T3?, T4?, T5?, T6?, T7?, T8?> AsNullable() => new(Owner, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6, Comp7, Comp8);
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
public static class EntityExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an Entity{T} to Entity{T?}, making it compatible with methods that take nullable components.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The component type. Must implement IComponent.</typeparam>
|
||||
/// <param name="ent">The source entity to convert.</param>
|
||||
/// <returns>An Entity{T?} with the same owner and component as the source entity.</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // Instead of:
|
||||
/// Entity{MyComponent?} nullable = (ent, ent.Comp);
|
||||
///
|
||||
/// // You can write:
|
||||
/// Entity{MyComponent?} nullable = ent.AsNullable();
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static Entity<T?> AsNullable<T>(this Entity<T> ent) where T : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?> AsNullable<T1, T2>(this Entity<T1, T2> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?> AsNullable<T1, T2, T3>(this Entity<T1, T2, T3> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?, T4?> AsNullable<T1, T2, T3, T4>(this Entity<T1, T2, T3, T4> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3, ent.Comp4);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?, T4?, T5?> AsNullable<T1, T2, T3, T4, T5>(this Entity<T1, T2, T3, T4, T5> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
where T5 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3, ent.Comp4, ent.Comp5);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?, T4?, T5?, T6?> AsNullable<T1, T2, T3, T4, T5, T6>(
|
||||
this Entity<T1, T2, T3, T4, T5, T6> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
where T5 : IComponent
|
||||
where T6 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3, ent.Comp4, ent.Comp5, ent.Comp6);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?, T4?, T5?, T6?, T7?> AsNullable<T1, T2, T3, T4, T5, T6, T7>(
|
||||
this Entity<T1, T2, T3, T4, T5, T6, T7> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
where T5 : IComponent
|
||||
where T6 : IComponent
|
||||
where T7 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3, ent.Comp4, ent.Comp5, ent.Comp6, ent.Comp7);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?, T4?, T5?, T6?, T7?, T8?> AsNullable<T1, T2, T3, T4, T5, T6, T7, T8>(
|
||||
this Entity<T1, T2, T3, T4, T5, T6, T7, T8> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
where T5 : IComponent
|
||||
where T6 : IComponent
|
||||
where T7 : IComponent
|
||||
where T8 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3, ent.Comp4, ent.Comp5, ent.Comp6, ent.Comp7, ent.Comp8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed at an entity when <see cref="TransformComponent.GridUid"/> is modified.
|
||||
/// This event is only raised if the entity has the <see cref="MetaDataFlags.ExtraTransformEvents"/> flag set.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Event handlers should not modify positions or delete the entity, because the move event that triggered this event
|
||||
/// is still being processed. This may also mean that the entity's current position/parent has not yet been updated,
|
||||
/// and that positional entity queries are not reliable.
|
||||
/// </remarks>
|
||||
[ByRefEvent]
|
||||
public readonly record struct GridUidChangedEvent(
|
||||
Entity<TransformComponent, MetaDataComponent> Entity,
|
||||
EntityUid? OldGrid)
|
||||
{
|
||||
public EntityUid? NewGrid => Entity.Comp1.GridUid;
|
||||
public EntityUid Uid => Entity.Owner;
|
||||
public TransformComponent Transform => Entity.Comp1;
|
||||
public MetaDataComponent Meta => Entity.Comp2;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed at an entity when <see cref="TransformComponent.MapUid"/> is modified.
|
||||
/// This event is only raised if the entity has the <see cref="MetaDataFlags.ExtraTransformEvents"/> flag set.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Event handlers should not modify positions or delete the entity, because the move event that triggered this event
|
||||
/// is still being processed. This may also mean that the entity's current position/parent has not yet been updated,
|
||||
/// and that positional entity queries are not reliable.
|
||||
/// </remarks>
|
||||
[ByRefEvent]
|
||||
public readonly record struct MapUidChangedEvent(
|
||||
Entity<TransformComponent, MetaDataComponent> Entity,
|
||||
EntityUid? OldMap,
|
||||
MapId OldMapId)
|
||||
{
|
||||
public EntityUid? NewMap => Entity.Comp1.MapUid;
|
||||
public MapId? NewMapId => Entity.Comp1.MapID;
|
||||
public EntityUid Uid => Entity.Owner;
|
||||
public TransformComponent Transform => Entity.Comp1;
|
||||
public MetaDataComponent Meta => Entity.Comp2;
|
||||
}
|
||||
@@ -159,6 +159,8 @@ public abstract class MetaDataSystem : EntitySystem
|
||||
if (toRemove == 0x0)
|
||||
return;
|
||||
|
||||
// TODO PERF
|
||||
// does this need to be a broadcast event?
|
||||
var ev = new MetaFlagRemoveAttemptEvent(toRemove);
|
||||
RaiseLocalEvent(uid, ref ev, true);
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ public sealed class SharedGridTraversalSystem : EntitySystem
|
||||
|
||||
public void CheckTraversal(EntityUid entity, TransformComponent xform, EntityUid map)
|
||||
{
|
||||
// TODO: This should probably check center of mass.
|
||||
DebugTools.Assert(!HasComp<MapGridComponent>(entity));
|
||||
DebugTools.Assert(!HasComp<MapComponent>(entity));
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
@@ -9,6 +10,7 @@ using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
@@ -55,6 +57,27 @@ namespace Robust.Shared.GameObjects
|
||||
SubscribeLocalEvent<MapLightComponent, ComponentGetState>(OnMapLightGetState);
|
||||
SubscribeLocalEvent<MapLightComponent, ComponentHandleState>(OnMapLightHandleState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the specified index to a bitmask with the specified chunksize.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public static ulong ToBitmask(Vector2i index, byte chunkSize = 8)
|
||||
{
|
||||
DebugTools.Assert(chunkSize <= 8);
|
||||
DebugTools.Assert((index.X + index.Y * chunkSize) < 64);
|
||||
|
||||
return (ulong) 1 << (index.X + index.Y * chunkSize);
|
||||
}
|
||||
|
||||
/// <returns>True if the specified bitflag is set for this index.</returns>
|
||||
[Pure]
|
||||
public static bool FromBitmask(Vector2i index, ulong bitmask, byte chunkSize = 8)
|
||||
{
|
||||
var flag = ToBitmask(index, chunkSize);
|
||||
|
||||
return (flag & bitmask) == flag;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
@@ -87,4 +88,21 @@ public abstract class SharedPointLightSystem : EntitySystem
|
||||
comp.Softness = value;
|
||||
Dirty(uid, comp);
|
||||
}
|
||||
|
||||
protected static void OnLightGetState(
|
||||
EntityUid uid,
|
||||
SharedPointLightComponent component,
|
||||
ref ComponentGetState args)
|
||||
{
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ public abstract partial class SharedTransformSystem
|
||||
xform._localPosition = tilePos + newGrid.TileSizeHalfVector;
|
||||
xform._localRotation += rotation;
|
||||
|
||||
SetGridId(uid, xform, newGridUid, XformQuery);
|
||||
var meta = MetaData(uid);
|
||||
SetGridId((uid, xform, meta), newGridUid);
|
||||
RaiseMoveEvent((uid, xform, meta), oldGridUid, oldPos, oldRot, oldMap);
|
||||
|
||||
DebugTools.Assert(XformQuery.GetComponent(oldGridUid).MapID == XformQuery.GetComponent(newGridUid).MapID);
|
||||
@@ -354,32 +354,49 @@ public abstract partial class SharedTransformSystem
|
||||
|
||||
#region GridId
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="GridId"/> for the transformcomponent without updating its children. Does not Dirty it.
|
||||
/// </summary>
|
||||
internal void SetGridIdNoRecursive(EntityUid uid, TransformComponent xform, EntityUid? gridUid)
|
||||
/// <inheritdoc cref="SetGridId(Entity{TransformComponent,MetaDataComponent},EntityUid?)"/>
|
||||
public void SetGridId(EntityUid uid, TransformComponent xform, EntityUid? gridId, EntityQuery<TransformComponent>? xformQuery = null)
|
||||
{
|
||||
DebugTools.Assert(gridUid == uid || !HasComp<MapGridComponent>(uid));
|
||||
if (xform._gridUid == gridUid)
|
||||
return;
|
||||
|
||||
DebugTools.Assert(gridUid == null || HasComp<MapGridComponent>(gridUid));
|
||||
xform._gridUid = gridUid;
|
||||
SetGridId((uid, xform, MetaData(uid)), gridId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="GridId"/> for the transformcomponent. Does not Dirty it.
|
||||
/// Sets <see cref="TransformComponent.GridUid"/> for the entity and any children. Note that this does not dirty
|
||||
/// the component, as this is implicitly networked via the transform hierarchy.
|
||||
/// </summary>
|
||||
public void SetGridId(EntityUid uid, TransformComponent xform, EntityUid? gridId, EntityQuery<TransformComponent>? xformQuery = null)
|
||||
public void SetGridId(Entity<TransformComponent, MetaDataComponent?> ent, EntityUid? gridId)
|
||||
{
|
||||
if (!xform._gridInitialized || xform._gridUid == gridId || xform.GridUid == uid)
|
||||
if (!ent.Comp1._gridInitialized || ent.Comp1._gridUid == gridId || ent.Comp1.GridUid == ent.Owner)
|
||||
return;
|
||||
|
||||
DebugTools.Assert(!HasComp<MapGridComponent>(uid) || gridId == uid);
|
||||
xform._gridUid = gridId;
|
||||
foreach (var child in xform._children)
|
||||
DebugTools.Assert(!HasComp<MapGridComponent>(ent.Owner) || gridId == ent.Owner);
|
||||
|
||||
// ReSharper disable once ReturnValueOfPureMethodIsNotUsed
|
||||
_metaQuery.ResolveInternal(ent.Owner, ref ent.Comp2);
|
||||
if ((ent.Comp2!.Flags & MetaDataFlags.ExtraTransformEvents) != 0)
|
||||
{
|
||||
SetGridId(child, XformQuery.GetComponent(child), gridId);
|
||||
#if DEBUG
|
||||
var childCount = ent.Comp1.ChildCount;
|
||||
var oldParent = ent.Comp1.ParentUid;
|
||||
#endif
|
||||
|
||||
var ev = new GridUidChangedEvent((ent.Owner, ent.Comp1, ent.Comp2), ent.Comp1._gridUid);
|
||||
ent.Comp1._gridUid = gridId;
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
|
||||
#if DEBUG
|
||||
// Lets check content didn't do anything silly with the event.
|
||||
DebugTools.AssertEqual(ent.Comp1._gridUid, gridId);
|
||||
DebugTools.AssertEqual(ent.Comp1.ChildCount, childCount);
|
||||
DebugTools.AssertEqual(ent.Comp1.ParentUid, oldParent);
|
||||
#endif
|
||||
}
|
||||
|
||||
ent.Comp1._gridUid = gridId;
|
||||
|
||||
foreach (var child in ent.Comp1._children)
|
||||
{
|
||||
SetGridId((child, XformQuery.GetComponent(child), null), gridId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,7 +587,10 @@ public abstract partial class SharedTransformSystem
|
||||
|
||||
if (newParent != null)
|
||||
{
|
||||
ChangeMapId(uid, xform, meta, newParent.MapID);
|
||||
// TODO PERF
|
||||
// if both map & grid id change, we should simultaneously update both.
|
||||
|
||||
ChangeMapId(entity, newParent.MapID);
|
||||
|
||||
if (!xform._gridInitialized)
|
||||
InitializeGridUid(uid, xform);
|
||||
@@ -578,16 +598,19 @@ public abstract partial class SharedTransformSystem
|
||||
{
|
||||
if (!newParent._gridInitialized)
|
||||
InitializeGridUid(value.EntityId, newParent);
|
||||
SetGridId(uid, xform, newParent.GridUid);
|
||||
SetGridId(entity!, newParent.GridUid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ChangeMapId(uid, xform, meta, MapId.Nullspace);
|
||||
// TODO PERF
|
||||
// if both map & grid id change, we should simultaneously update both.
|
||||
|
||||
ChangeMapId(entity, MapId.Nullspace);
|
||||
if (!xform._gridInitialized)
|
||||
InitializeGridUid(uid, xform);
|
||||
else
|
||||
SetGridId(uid, xform, null, XformQuery);
|
||||
SetGridId(entity!, null);
|
||||
}
|
||||
|
||||
if (xform.Initialized)
|
||||
@@ -623,20 +646,65 @@ public abstract partial class SharedTransformSystem
|
||||
SetCoordinates((uid, xform, _metaQuery.GetComponent(uid)), value, rotation, unanchor, newParent, oldParent);
|
||||
}
|
||||
|
||||
private void ChangeMapId(EntityUid uid, TransformComponent xform, MetaDataComponent meta, MapId newMapId)
|
||||
private void ChangeMapId(Entity<TransformComponent, MetaDataComponent> ent, MapId newMapId)
|
||||
{
|
||||
if (newMapId == xform.MapID)
|
||||
if (newMapId == ent.Comp1.MapID)
|
||||
return;
|
||||
|
||||
EntityUid? newUid = newMapId == MapId.Nullspace ? null : _map.GetMap(newMapId);
|
||||
bool? mapPaused = null;
|
||||
|
||||
//Set Paused state
|
||||
var mapPaused = _map.IsPaused(newMapId);
|
||||
_metaData.SetEntityPaused(uid, mapPaused, meta);
|
||||
// Client may be moving entities across maps due to things leaving or entering PVS range.
|
||||
// In that case, we don't want to pause or unpause entities.
|
||||
if (!_gameTiming.ApplyingState)
|
||||
{
|
||||
mapPaused = _map.IsPaused(newMapId);
|
||||
_metaData.SetEntityPaused(ent.Owner, mapPaused.Value, ent.Comp2);
|
||||
}
|
||||
|
||||
xform.MapUid = newUid;
|
||||
xform.MapID = newMapId;
|
||||
xform.UpdateChildMapIdsRecursive(newMapId, newUid, mapPaused, XformQuery, _metaQuery, _metaData);
|
||||
ChangeMapIdRecursive(ent, newUid, newMapId, mapPaused);
|
||||
}
|
||||
|
||||
private void ChangeMapIdRecursive(
|
||||
Entity<TransformComponent, MetaDataComponent> ent,
|
||||
EntityUid? newMap,
|
||||
MapId newMapId,
|
||||
bool? paused)
|
||||
{
|
||||
if (paused is { } p)
|
||||
{
|
||||
_metaData.SetEntityPaused(ent.Owner, p, ent.Comp2);
|
||||
}
|
||||
|
||||
if ((ent.Comp2.Flags & MetaDataFlags.ExtraTransformEvents) != 0)
|
||||
{
|
||||
#if DEBUG
|
||||
var childCount = ent.Comp1.ChildCount;
|
||||
var oldParent = ent.Comp1.ParentUid;
|
||||
#endif
|
||||
|
||||
var ev = new MapUidChangedEvent(ent, ent.Comp1.MapUid, ent.Comp1.MapID);
|
||||
ent.Comp1.MapUid = newMap;
|
||||
ent.Comp1.MapID = newMapId;
|
||||
RaiseLocalEvent(ent.Owner, ref ev);
|
||||
|
||||
#if DEBUG
|
||||
// Lets check content didn't do anything silly with the event.
|
||||
DebugTools.AssertEqual(ent.Comp1.MapUid, newMap);
|
||||
DebugTools.AssertEqual(ent.Comp1.MapID, newMapId);
|
||||
DebugTools.AssertEqual(ent.Comp1.ChildCount, childCount);
|
||||
DebugTools.AssertEqual(ent.Comp1.ParentUid, oldParent);
|
||||
#endif
|
||||
}
|
||||
|
||||
ent.Comp1.MapUid = newMap;
|
||||
ent.Comp1.MapID = newMapId;
|
||||
|
||||
foreach (var uid in ent.Comp1._children)
|
||||
{
|
||||
var child = new Entity<TransformComponent, MetaDataComponent>(uid, Transform(uid), MetaData(uid));
|
||||
ChangeMapIdRecursive(child, newMap, newMapId, paused);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -1201,7 +1269,13 @@ public abstract partial class SharedTransformSystem
|
||||
if (!xform.Initialized)
|
||||
return;
|
||||
|
||||
RaiseMoveEvent((uid, xform, meta), oldParent, oldPosition, oldRotation, xform.MapUid);
|
||||
RaiseMoveEvent(
|
||||
(uid, xform, meta),
|
||||
oldParent,
|
||||
oldPosition,
|
||||
oldRotation,
|
||||
xform.MapUid,
|
||||
checkTraversal: !oldPosition.Equals(pos));
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -1501,9 +1575,10 @@ public abstract partial class SharedTransformSystem
|
||||
private void OnGridAdd(EntityUid uid, TransformComponent component, GridAddEvent args)
|
||||
{
|
||||
// Added to existing map so need to update all children too.
|
||||
if (LifeStage(uid) > EntityLifeStage.Initialized)
|
||||
var meta = MetaData(uid);
|
||||
if (meta.EntityLifeStage > EntityLifeStage.Initialized)
|
||||
{
|
||||
SetGridId(uid, component, uid, XformQuery);
|
||||
SetGridId((uid, component, meta), uid);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -265,7 +265,8 @@ namespace Robust.Shared.GameObjects
|
||||
EntityUid oldParent,
|
||||
Vector2 oldPosition,
|
||||
Angle oldRotation,
|
||||
EntityUid? oldMap)
|
||||
EntityUid? oldMap,
|
||||
bool checkTraversal = true)
|
||||
{
|
||||
var pos = ent.Comp1._parent == EntityUid.Invalid
|
||||
? default
|
||||
@@ -295,7 +296,10 @@ namespace Robust.Shared.GameObjects
|
||||
// Finally, handle grid traversal. This is handled separately to avoid out-of-order move events.
|
||||
// I.e., if the traversal raises its own move event, this ensures that all the old move event handlers
|
||||
// have finished running first. Ideally this shouldn't be required, but this is here just in case
|
||||
_traversal.CheckTraverse(ent);
|
||||
if (checkTraversal)
|
||||
{
|
||||
_traversal.CheckTraverse(ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ public struct TextureLoadParameters : IEquatable<TextureLoadParameters>
|
||||
/// </summary>
|
||||
public bool Srgb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If false, this texture should not be preloaded on game startup.
|
||||
/// </summary>
|
||||
public bool Preload { get; set; }
|
||||
|
||||
public static TextureLoadParameters FromYaml(YamlMappingNode yaml)
|
||||
{
|
||||
var loadParams = Default;
|
||||
@@ -34,18 +39,24 @@ public struct TextureLoadParameters : IEquatable<TextureLoadParameters>
|
||||
loadParams.Srgb = srgb.AsBool();
|
||||
}
|
||||
|
||||
if (yaml.TryGetNode("preload", out var preload))
|
||||
{
|
||||
loadParams.Preload = preload.AsBool();
|
||||
}
|
||||
|
||||
return loadParams;
|
||||
}
|
||||
|
||||
public static readonly TextureLoadParameters Default = new()
|
||||
{
|
||||
SampleParameters = TextureSampleParameters.Default,
|
||||
Srgb = true
|
||||
Srgb = true,
|
||||
Preload = true
|
||||
};
|
||||
|
||||
public bool Equals(TextureLoadParameters other)
|
||||
{
|
||||
return SampleParameters.Equals(other.SampleParameters) && Srgb == other.Srgb;
|
||||
return SampleParameters.Equals(other.SampleParameters) && Srgb == other.Srgb && Preload == other.Preload;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
|
||||
@@ -146,6 +146,13 @@ namespace Robust.Shared.Localization
|
||||
/// Gets localization data for an entity prototype.
|
||||
/// </summary>
|
||||
EntityLocData GetEntityData(string prototypeId);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the <see cref="LocalizationManager"/>.
|
||||
/// </summary>
|
||||
void Initialize()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal interface ILocalizationManagerInternal : ILocalizationManager
|
||||
|
||||
@@ -12,7 +12,7 @@ using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Shared.Localization
|
||||
{
|
||||
internal sealed partial class LocalizationManager
|
||||
internal abstract partial class LocalizationManager
|
||||
{
|
||||
// Concurrent dict so that multiple threads "reading" .Name won't cause a concurrent write issue
|
||||
// when the cache gets populated.
|
||||
@@ -134,7 +134,7 @@ namespace Robust.Shared.Localization
|
||||
.Select(x => GetString(x.Suffix!));
|
||||
suffix = string.Join(", ", suffixes);
|
||||
}
|
||||
|
||||
|
||||
return new EntityLocData(
|
||||
name ?? "",
|
||||
desc ?? "",
|
||||
|
||||
@@ -12,7 +12,7 @@ using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Shared.Localization
|
||||
{
|
||||
internal sealed partial class LocalizationManager
|
||||
internal abstract partial class LocalizationManager
|
||||
{
|
||||
private static readonly Regex RegexWordMatch = new Regex(@"\w+");
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Localization
|
||||
{
|
||||
internal sealed partial class LocalizationManager : ILocalizationManagerInternal, IPostInjectInit
|
||||
internal abstract partial class LocalizationManager : ILocalizationManagerInternal
|
||||
{
|
||||
private static readonly ResPath LocaleDirPath = new("/Locale");
|
||||
protected static readonly ResPath LocaleDirPath = new("/Locale");
|
||||
|
||||
[Dependency] private readonly IConfigurationManager _configuration = default!;
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
@@ -40,7 +40,9 @@ namespace Robust.Shared.Localization
|
||||
private (CultureInfo, FluentBundle)? _defaultCulture;
|
||||
private (CultureInfo, FluentBundle)[] _fallbackCultures = Array.Empty<(CultureInfo, FluentBundle)>();
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
void ILocalizationManager.Initialize() => Initialize();
|
||||
|
||||
public virtual void Initialize()
|
||||
{
|
||||
_logSawmill = _log.GetSawmill("loc");
|
||||
_prototype.PrototypesReloaded += OnPrototypesReloaded;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
@@ -29,7 +30,7 @@ namespace Robust.Shared.Map.Components
|
||||
// the grid section now writes the grid's EntityUID. as long as existing maps get updated (just a load+save),
|
||||
// this can be removed
|
||||
|
||||
[DataField("chunkSize")] internal ushort ChunkSize = 16;
|
||||
[DataField] internal ushort ChunkSize = 16;
|
||||
|
||||
[ViewVariables]
|
||||
public int ChunkCount => Chunks.Count;
|
||||
@@ -261,6 +262,13 @@ namespace Robust.Shared.Map.Components
|
||||
{
|
||||
return MapSystem.TryGetTileRef(Owner, this, coords, out tile);
|
||||
}
|
||||
|
||||
/// <returns>True if the specified chunk exists on this grid.</returns>
|
||||
[Pure]
|
||||
public bool HasChunk(Vector2i indices)
|
||||
{
|
||||
return Chunks.ContainsKey(indices);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -51,6 +51,11 @@ namespace Robust.Shared.Map
|
||||
/// </summary>
|
||||
byte Variants { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Allows the tile to be rotated/mirrored when placed on a grid.
|
||||
/// </summary>
|
||||
bool AllowRotationMirror => false;
|
||||
|
||||
/// <summary>
|
||||
/// Assign a new value to <see cref="TileId"/>, used when registering the tile definition.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Map;
|
||||
@@ -26,6 +27,11 @@ public readonly struct Tile : IEquatable<Tile>, ISpanFormattable
|
||||
/// </summary>
|
||||
public readonly byte Variant;
|
||||
|
||||
/// <summary>
|
||||
/// Rotation and mirroring of this tile to render. 0-3 is normal, 4-7 is mirrored.
|
||||
/// </summary>
|
||||
public readonly byte RotationMirroring;
|
||||
|
||||
/// <summary>
|
||||
/// An empty tile that can be compared against.
|
||||
/// </summary>
|
||||
@@ -42,11 +48,33 @@ public readonly struct Tile : IEquatable<Tile>, ISpanFormattable
|
||||
/// <param name="typeId">Internal type ID.</param>
|
||||
/// <param name="flags">Custom tile flags for usage.</param>
|
||||
/// <param name="variant">The visual variant this tile is using.</param>
|
||||
public Tile(int typeId, byte flags = 0, byte variant = 0)
|
||||
/// <param name="rotationMirroring">The rotation and mirroring this tile is using.</param>
|
||||
public Tile(int typeId, byte flags = 0, byte variant = 0, byte rotationMirroring = 0)
|
||||
{
|
||||
TypeId = typeId;
|
||||
Flags = flags;
|
||||
Variant = variant;
|
||||
RotationMirroring = rotationMirroring;
|
||||
}
|
||||
|
||||
public static byte DirectionToByte(Direction direction, bool throwIfDiagonal = false)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.South:
|
||||
return 0;
|
||||
case Direction.East:
|
||||
return 1;
|
||||
case Direction.North:
|
||||
return 2;
|
||||
case Direction.West:
|
||||
return 3;
|
||||
default:
|
||||
if (throwIfDiagonal)
|
||||
throw new InvalidEnumArgumentException("direction", (int)direction, typeof(Direction));
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -71,7 +99,7 @@ public readonly struct Tile : IEquatable<Tile>, ISpanFormattable
|
||||
/// <returns>String representation of this Tile.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Tile {TypeId}, {Flags}, {Variant}";
|
||||
return $"Tile {TypeId}, {Flags}, {Variant}, {RotationMirroring}";
|
||||
}
|
||||
|
||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||
@@ -94,7 +122,7 @@ public readonly struct Tile : IEquatable<Tile>, ISpanFormattable
|
||||
/// <inheritdoc />
|
||||
public bool Equals(Tile other)
|
||||
{
|
||||
return TypeId == other.TypeId && Flags == other.Flags && Variant == other.Variant;
|
||||
return TypeId == other.TypeId && Flags == other.Flags && Variant == other.Variant && RotationMirroring == other.RotationMirroring;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -110,7 +138,7 @@ public readonly struct Tile : IEquatable<Tile>, ISpanFormattable
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (TypeId.GetHashCode() * 397) ^ Flags.GetHashCode() ^ Variant.GetHashCode();
|
||||
return (TypeId.GetHashCode() * 397) ^ Flags.GetHashCode() ^ Variant.GetHashCode() ^ RotationMirroring.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,11 @@ namespace Robust.Shared.Network.Messages
|
||||
public string AlignOption { get; set; }
|
||||
public Vector2 RectSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to determine tile sprite mirroring
|
||||
/// </summary>
|
||||
public bool Mirrored { get; set; }
|
||||
|
||||
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
PlaceType = (PlacementManagerMessage) buffer.ReadByte();
|
||||
@@ -49,6 +54,7 @@ namespace Robust.Shared.Network.Messages
|
||||
|
||||
NetCoordinates = buffer.ReadNetCoordinates();
|
||||
DirRcv = (Direction)buffer.ReadByte();
|
||||
Mirrored = buffer.ReadBoolean();
|
||||
break;
|
||||
case PlacementManagerMessage.StartPlacement:
|
||||
Range = buffer.ReadInt32();
|
||||
@@ -84,6 +90,7 @@ namespace Robust.Shared.Network.Messages
|
||||
|
||||
buffer.Write(NetCoordinates);
|
||||
buffer.Write((byte)DirRcv);
|
||||
buffer.Write(Mirrored);
|
||||
break;
|
||||
case PlacementManagerMessage.StartPlacement:
|
||||
buffer.Write(Range);
|
||||
|
||||
@@ -24,7 +24,9 @@ namespace Robust.Shared.Physics.Systems
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
#pragma warning disable CS0414
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
#pragma warning restore CS0414
|
||||
private EntityQuery<PhysicsMapComponent> _mapQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private EntityQuery<FixturesComponent> _fixtureQuery;
|
||||
|
||||
@@ -350,10 +350,10 @@ public abstract partial class SharedPhysicsSystem
|
||||
|
||||
if (contact.Manifold.PointCount > 0 && contact.FixtureA?.Hard == true && contact.FixtureB?.Hard == true)
|
||||
{
|
||||
if (bodyA.CanCollide)
|
||||
if (bodyA.CanCollide && !TerminatingOrDeleted(aUid))
|
||||
SetAwake((aUid, bodyA), true);
|
||||
|
||||
if (bodyB.CanCollide)
|
||||
if (bodyB.CanCollide && !TerminatingOrDeleted(bUid))
|
||||
SetAwake((bUid, bodyB), true);
|
||||
}
|
||||
|
||||
@@ -621,24 +621,6 @@ public abstract partial class SharedPhysicsSystem
|
||||
ArrayPool<Vector2>.Shared.Return(worldPoints);
|
||||
}
|
||||
|
||||
private record struct UpdateTreesJob : IRobustJob
|
||||
{
|
||||
public IEntityManager EntManager;
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
var query = EntManager.AllEntityQueryEnumerator<BroadphaseComponent>();
|
||||
|
||||
while (query.MoveNext(out var broadphase))
|
||||
{
|
||||
broadphase.DynamicTree.Rebuild(false);
|
||||
broadphase.StaticTree.Rebuild(false);
|
||||
broadphase.SundriesTree._b2Tree.Rebuild(false);
|
||||
broadphase.StaticSundriesTree._b2Tree.Rebuild(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, Vector2[] worldPoints)
|
||||
{
|
||||
if (count == 0)
|
||||
|
||||
@@ -21,8 +21,10 @@ namespace Robust.Shared.Physics.Systems
|
||||
*/
|
||||
public partial class SharedPhysicsSystem
|
||||
{
|
||||
#pragma warning disable CS0414
|
||||
[Dependency] private readonly SharedDebugRayDrawingSystem _sharedDebugRaySystem = default!;
|
||||
[Dependency] private readonly INetManager _netMan = default!;
|
||||
#pragma warning restore CS0414
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if the specified collision rectangle collides with any of the physBodies under management.
|
||||
|
||||
@@ -9,15 +9,13 @@ namespace Robust.Shared
|
||||
public static void Setup(IConfigurationManager cfg)
|
||||
{
|
||||
// Disabled on non-release since I don't want this to start creating files in Steam's bin directory.
|
||||
#if RELEASE
|
||||
return;
|
||||
#endif
|
||||
|
||||
#if !RELEASE
|
||||
if (!cfg.GetCVar(CVars.SysProfileOpt))
|
||||
return;
|
||||
|
||||
ProfileOptimization.SetProfileRoot(PathHelpers.GetExecutableDirectory());
|
||||
ProfileOptimization.StartProfile("profile_opt");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,6 +306,11 @@ public interface IPrototypeManager
|
||||
/// </summary>
|
||||
void RegisterIgnore(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given gind name has been marked as ignored via <see cref="RegisterIgnore"/>
|
||||
/// </summary>
|
||||
bool IsIgnored(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Loads several prototype kinds into the manager. Note that this will re-build a frozen dictionary and should be avoided if possible.
|
||||
/// </summary>
|
||||
|
||||
@@ -920,6 +920,8 @@ namespace Robust.Shared.Prototypes
|
||||
return TryGetKindFrom(typeof(T), out kind);
|
||||
}
|
||||
|
||||
public bool IsIgnored(string name) => _ignoredPrototypeTypes.Contains(name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterIgnore(string name)
|
||||
{
|
||||
@@ -973,6 +975,17 @@ namespace Robust.Shared.Prototypes
|
||||
|
||||
var name = attribute.Type ?? CalculatePrototypeName(kind);
|
||||
|
||||
if (_ignoredPrototypeTypes.Contains(name))
|
||||
{
|
||||
// For whatever reason, we are registering a prototype despite it having been marked as ignored.
|
||||
// This often happens when someone is moving a server or client prototype to shared. Maybe this should
|
||||
// log an error, but I want to avoid breaking changes and maaaaybe there some weird instance where you
|
||||
// want the client to know that a prototype kind exists, without having the client load information
|
||||
// about the individual prototypes? So for now lets just log a warning instead of introducing breaking
|
||||
// changes.
|
||||
Sawmill.Warning($"Registering an ignored prototype {kind}");
|
||||
}
|
||||
|
||||
if (_kindNames.TryGetValue(name, out var existing))
|
||||
{
|
||||
throw new InvalidImplementationException(kind,
|
||||
|
||||
@@ -3,6 +3,9 @@ using JetBrains.Annotations;
|
||||
|
||||
namespace Robust.Shared.Serialization.Manager.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a <see cref="Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeSerializer"/> as a default serializer for its type
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
[MeansImplicitUse]
|
||||
public sealed class TypeSerializerAttribute : Attribute
|
||||
|
||||
@@ -3,6 +3,7 @@ using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
|
||||
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype
|
||||
@@ -30,10 +31,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Pro
|
||||
public virtual ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node,
|
||||
IDependencyCollection dependencies, ISerializationContext? context = null)
|
||||
{
|
||||
var protoMan = dependencies.Resolve<IPrototypeManager>();
|
||||
return protoMan.TryGetKindFrom<TPrototype>(out _) && protoMan.HasIndex<TPrototype>(node.Value)
|
||||
? new ValidatedValueNode(node)
|
||||
: new ErrorNode(node, $"PrototypeID {node.Value} for type {typeof(TPrototype)} at {node.Start} not found");
|
||||
return ProtoIdSerializer<TPrototype>.Validate(dependencies, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,19 @@ public sealed class ProtoIdSerializer<T> : ITypeSerializer<ProtoId<T>, ValueData
|
||||
{
|
||||
public ValidationNode Validate(ISerializationManager serialization, ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context = null)
|
||||
{
|
||||
var prototypes = dependencies.Resolve<IPrototypeManager>();
|
||||
if (prototypes.TryGetKindFrom<T>(out _) && prototypes.HasMapping<T>(node.Value))
|
||||
return Validate(dependencies, node);
|
||||
}
|
||||
|
||||
public static ValidationNode Validate(IDependencyCollection deps, ValueDataNode node)
|
||||
{
|
||||
var proto = deps.Resolve<IPrototypeManager>();
|
||||
if (!proto.TryGetKindFrom<T>(out var kind))
|
||||
return new ErrorNode(node, $"Unknown prototype kind: {typeof(T)}");
|
||||
|
||||
if (proto.IsIgnored(kind))
|
||||
return new ErrorNode(node,$"Attempting to validate an ignored prototype: {typeof(T)}.\nDid you forget to remove the IPrototypeManager.RegisterIgnore(\"{kind}\") call when moving a prototype to Shared?");
|
||||
|
||||
if (proto.HasMapping<T>(node.Value))
|
||||
return new ValidatedValueNode(node);
|
||||
|
||||
return new ErrorNode(node, $"No {typeof(T)} found with id {node.Value}");
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations
|
||||
ITypeCopier<Rsi>,
|
||||
ITypeCopier<Texture>
|
||||
{
|
||||
// Should probably be in SpriteComponent, but is needed for server to validate paths.
|
||||
// Should probably be in SpriteSystem, but is needed for server to validate paths.
|
||||
// So I guess it might as well go here?
|
||||
public static readonly ResPath TextureRoot = new("/Textures");
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -9,42 +10,98 @@ using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
|
||||
namespace Robust.Shared.Serialization.TypeSerializers.Implementations
|
||||
namespace Robust.Shared.Serialization.TypeSerializers.Implementations;
|
||||
|
||||
[TypeSerializer]
|
||||
public sealed class TimespanSerializer : ITypeSerializer<TimeSpan, ValueDataNode>, ITypeCopyCreator<TimeSpan>
|
||||
{
|
||||
[TypeSerializer]
|
||||
public sealed class TimespanSerializer : ITypeSerializer<TimeSpan, ValueDataNode>, ITypeCopyCreator<TimeSpan>
|
||||
public TimeSpan Read(
|
||||
ISerializationManager serializationManager,
|
||||
ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context = null,
|
||||
ISerializationManager.InstantiationDelegate<TimeSpan>? instanceProvider = null)
|
||||
{
|
||||
public TimeSpan Read(ISerializationManager serializationManager, ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context = null,
|
||||
ISerializationManager.InstantiationDelegate<TimeSpan>? instanceProvider = null)
|
||||
if (TryTimeSpan(node, out var time))
|
||||
return time.Value;
|
||||
|
||||
var seconds = double.Parse(node.Value, CultureInfo.InvariantCulture);
|
||||
return TimeSpan.FromSeconds(seconds);
|
||||
}
|
||||
|
||||
public ValidationNode Validate(
|
||||
ISerializationManager serializationManager,
|
||||
ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
return TryTimeSpan(node, out _)
|
||||
|| double.TryParse(node.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out _)
|
||||
? new ValidatedValueNode(node)
|
||||
: new ErrorNode(node, "Failed parsing TimeSpan");
|
||||
}
|
||||
|
||||
public DataNode Write(
|
||||
ISerializationManager serializationManager,
|
||||
TimeSpan value,
|
||||
IDependencyCollection dependencies,
|
||||
bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
return new ValueDataNode(value.TotalSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[MustUseReturnValue]
|
||||
public TimeSpan CreateCopy(
|
||||
ISerializationManager serializationManager,
|
||||
TimeSpan source,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert strings from the compatible format (or just numbers) into TimeSpan and output it. Returns true if successful.
|
||||
/// The string must start with a number and end with a single letter referring to the time unit used.
|
||||
/// It can NOT combine multiple types (like "1h30m"), but it CAN use decimals ("1.5h")
|
||||
/// </summary>
|
||||
private bool TryTimeSpan(ValueDataNode node, [NotNullWhen(true)] out TimeSpan? timeSpan)
|
||||
{
|
||||
timeSpan = null;
|
||||
|
||||
// A lot of the checks will be for plain numbers, so might as well rule them out right away, instead of
|
||||
// running all the other checks on them. They will need to get parsed later anyway, if they weren't now.
|
||||
if (double.TryParse(node.Value, out var v))
|
||||
{
|
||||
var seconds = double.Parse(node.Value, CultureInfo.InvariantCulture);
|
||||
return TimeSpan.FromSeconds(seconds);
|
||||
timeSpan = TimeSpan.FromSeconds(v);
|
||||
return true;
|
||||
}
|
||||
|
||||
public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
return double.TryParse(node.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out _)
|
||||
? new ValidatedValueNode(node)
|
||||
: new ErrorNode(node, "Failed parsing TimeSpan");
|
||||
}
|
||||
// If there aren't even enough characters for a number and a time unit, exit
|
||||
if (node.Value.Length <= 1)
|
||||
return false;
|
||||
|
||||
public DataNode Write(ISerializationManager serializationManager, TimeSpan value,
|
||||
IDependencyCollection dependencies, bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
return new ValueDataNode(value.TotalSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
// If the input without the last character is still not a valid number, exit
|
||||
if (!double.TryParse(node.Value.AsSpan()[..^1], out var number))
|
||||
return false;
|
||||
|
||||
[MustUseReturnValue]
|
||||
public TimeSpan CreateCopy(ISerializationManager serializationManager, TimeSpan source,
|
||||
IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null)
|
||||
// Check the last character of the input for time unit indicators
|
||||
switch (node.Value[^1])
|
||||
{
|
||||
return source;
|
||||
case 's':
|
||||
timeSpan = TimeSpan.FromSeconds(number);
|
||||
return true;
|
||||
case 'm':
|
||||
timeSpan = TimeSpan.FromMinutes(number);
|
||||
return true;
|
||||
case 'h':
|
||||
timeSpan = TimeSpan.FromHours(number);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user