Compare commits

..

2 Commits

Author SHA1 Message Date
PJB3005
6abe4eb29e Version: 255.0.1 2025-09-14 14:55:55 +02:00
PJB3005
9469e4f5ef Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

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

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

    Move CEF cache out of data directory

    Don't want content messing with this...

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

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

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

    Update SpaceWizards.NFluidSynth to 0.2.2

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

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
2025-09-14 14:55:54 +02:00
228 changed files with 3111 additions and 7755 deletions

View File

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

View File

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

View File

@@ -54,246 +54,7 @@ END TEMPLATE-->
*None yet*
## 261.2.2
## 261.2.1
## 261.2.0
### New features
* Implement IEquatable for ResolvedPathSpecifier & ResolvedCollectionSpecifier.
* Add NearestChunkEnumerator.
### Bugfixes
* Fix static entities not having the center of mass updated.
* Fix TryQueueDelete.
* Fix tpto potentially parenting grids to non-map entities.
### Other
* TileChangedEvent is now raised once in clientside grid state handling rather than per tile.
* Removed ITileDefinition.ID as it was redundant.
* Change the lifestage checks on predicted entity deletion to check for terminating.
### Internal
* Update some `GetComponentName<T>` uses to generic.
## 261.1.0
### New features
* Automatically create logger sawmills for `UIController`s similar to `EntitySystem`s.
### Bugfixes
* Fix physics forces not auto-clearing / respecting the cvar.
### Internal
* Cleanup more compiler warnings in unit tests.
## 261.0.0
### Breaking changes
* Remove unused TryGetContainingContainer override.
* Stop recursive FrameUpdates for controls that are not visible.
* Initialize LocMgr earlier in the callstack for GameController.
* Fix FastNoiseLise fractal bounding and remove its DataField property as it should be derived on other properties updating.
* Make RaiseMoveEvent internal.
* MovedGridsComponent and PhysicsMapComponent are now purged and properties on `SharedPhysicsSystem`. Additionally the TransformComponent for Awake entities is stored alongside the PhysicsComponent for them.
* TransformComponent is now stored on physics contacts.
* Gravity2DComponent and Gravity2DController were moved to SharedPhysicsSystem.
### New features
* `IFileDialogManager` now allows specifying `FileAccess` and `FileShare` modes.
* Add Intersects and Enlarged to Box2i in line with Box2.
* Make `KeyFrame`s on `AnimationTrackProperty` public settable.
* Add the spawned entities to a returned array from `SpawnEntitiesAttachedTo`.
### Bugfixes
* Fixed SDL3 file dialog implementation having a memory leak and not opening files read-write.
* Fix GetMapLinearVelocity.
### Other
* `uploadfile` and `loadprototype` commands now only open files with read access.
* Optimize `ToMapCoordinates`.
### Internal
* Cleanup on internals of `IFileDialogManager`, removing duplicate code.
* Fix Contacts not correctly being marked as `Touching` while contact is ongoing.
## 260.2.0
### New features
* Add `StringBuilder.Insert(int, string)` to sandbox.
* Add the WorldNormal to the StartCollideEvent.
## 260.1.0
### New features
* `ComponentFactory` is now exposed to `EntitySystem` as `Factory`
### Other
* Cleanup warnings in PLacementManager
* Cleanup warnings in Clide.Sprite
## 260.0.0
### Breaking changes
* Fix / change `StartCollideEvent.WorldPoint` to return all points for the collision which may be up to 2 instead of 1.
### New features
* Add SpriteSystem dependency to VisualizerSystem.
* Add Vertical property to progress bars
* Add some `EntProtoId` overloads for group entity spawn methods.
## 259.0.0
### Breaking changes
* TileChangedEvent now has an array of tile changed entries rather than raising an individual event for every single tile changed.
### Other
* `Entity<T>` methods were marked as `readonly` as appropriate.
## 258.0.1
### Bugfixes
* Fix static physics bodies not generating contacts if they spawn onto sleeping bodies.
## 258.0.0
### Breaking changes
* `IMarkupTag` and related methods in `MarkupTagManager` have been obsoleted and should be replaced with the new `IMarkupTagHandler` interface. Various engine tags (e.g., `BoldTag`, `ColorTag`, etc) no longer implement the old interface.
### New features
* Add IsValidPath to ResPath and make some minor performance improvements.
### Bugfixes
* OutputPanel and RichTextLabel now remove controls associated with rich text tags when the text is updated.
* Fix `SpriteComponent.Visible` datafield not being read from yaml.
* Fix container state handling not forcing inserts.
### Other
* `SpriteSystem.LayerMapReserve()` no longer throws an exception if the specified layer already exists. This makes it behave like the obsoleted `SpriteComponent.LayerMapReserveBlank()`.
## 257.0.2
### Bugfixes
* Fix unshaded sprite layers not rendering correctly.
## 257.0.1
### Bugfixes
* Fix sprite layer bounding box calculations. This was causing various sprite rendering & render-tree lookup issues.
## 257.0.0
### Breaking changes
* The client will now automatically pause any entities that leave their PVS range.
* Contacts for terminating entities no longer raise wake events.
### New features
* Added `IPrototypeManager.IsIgnored()` for checking whether a given prototype kind has been marked as ignored via `RegisterIgnore()`.
* Added `PoolManager` & `TestPair` classes to `Robust.UnitTesting`. These classes make it easier to create & use pooled server/client instance pairs in integration tests.
* Catch NotYamlSerializable DataFields with an analyzer.
* Optimized RSI preloading and texture atlas creation.
### Bugfixes
* Fix clients unintentionally un-pausing paused entities that re-enter pvs range
### Other
* The yaml prototype id serialiser now provides better feedback when trying to validate an id for a prototype kind that has been ignored via `IPrototypeManager.RegisterIgnore()`
* Several SpriteComponent methods have been marked as obsolete, and should be replaced with new methods in SpriteSystem.
* Rotation events no longer check for grid traversal.
## 256.0.0
### Breaking changes
* `ITypeReaderWriter<TType, TNode>` has been removed due to being unused. Implement `ITypeSerializer<TType, TNode>` instead
* Moved AsNullable extension methods to the Entity struct.
### New features
* Add DevWindow tab to show all loaded textures.
* Add Vector2i / bitmask converfsion helpers.
* Allow texture preload to be skipped for some textures.
* Check audio file signatures instead of extensions.
* Add CancellationTokenRegistration to sandbox.
* Add the ability to serialize TimeSpan from text.
* Add support for rotated / mirrored tiles.
### Bugfixes
* Fix yaml hot reloading.
* Fix a linear dictionary lookup in PlacementManager.
### Other
* Make ItemList not run deselection callback on all items if they aren't selected.
* Cleanup warnings for CS0649 & CS0414.
### Internal
* Move PointLight component states to shared.
## 255.1.0
### New features
* The client localisation manager now supports hot-reloading ftl files.
* TransformSystem can now raise `GridUidChangedEvent` and `MapUidChangedEvent` when a entity's grid or map changes. This event is only raised if the `ExtraTransformEvents` metadata flag is enabled.
### Bugfixes
* Fixed a server crash due to a `NullReferenceException` in PVS system when a player's local entity is also one of their view subscriptions.
* Fix CompileRobustXamlTask for benchmarks.
* .ftl files will now hot reload.
* Fix placementmanager sometimes not clearing.
### Other
* Container events are now documented.
## 255.0.1
## 255.0.0

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute";
private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute";
private const string ViewVariablesNamespace = "Robust.Shared.ViewVariables.ViewVariablesAttribute";
private const string NotYamlSerializableName = "Robust.Shared.Serialization.Manager.Attributes.NotYamlSerializableAttribute";
private const string DataFieldAttributeName = "DataField";
private const string ViewVariablesAttributeName = "ViewVariables";
@@ -82,19 +81,9 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to remove the ViewVariables attribute."
);
public static readonly DiagnosticDescriptor DataFieldYamlSerializableRule = new(
Diagnostics.IdDataFieldYamlSerializable,
"Data field type is not YAML serializable",
"Data field {0} in data definition {1} is type {2}, which is not YAML serializable",
"Usage",
DiagnosticSeverity.Error,
true,
"Make sure to use a type that is YAML serializable."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule,
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule, DataFieldYamlSerializableRule
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule
);
public override void Initialize(AnalysisContext context)
@@ -175,19 +164,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
TryGetAttributeLocation(field, ViewVariablesAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, fieldSymbol.Name, type.Name));
}
if (context.SemanticModel.GetSymbolInfo(field.Declaration.Type).Symbol is not ITypeSymbol fieldTypeSymbol)
continue;
if (IsNotYamlSerializable(fieldSymbol, fieldTypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
(context.Node as FieldDeclarationSyntax)?.Declaration.Type.GetLocation(),
fieldSymbol.Name,
type.Name,
fieldTypeSymbol.MetadataName
));
}
}
}
@@ -225,19 +201,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
TryGetAttributeLocation(property, ViewVariablesAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, propertySymbol.Name, type.Name));
}
if (context.SemanticModel.GetSymbolInfo(property.Type).Symbol is not ITypeSymbol propertyTypeSymbol)
return;
if (IsNotYamlSerializable(propertySymbol, propertyTypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
(context.Node as PropertyDeclarationSyntax)?.Type.GetLocation(),
propertySymbol.Name,
type.Name,
propertyTypeSymbol.Name
));
}
}
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
@@ -420,14 +383,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return (VVAccess)accessByte == VVAccess.ReadWrite;
}
private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type)
{
if (!IsDataField(field, out _, out _))
return false;
return HasAttribute(type, NotYamlSerializableName);
}
private static bool IsImplicitDataDefinition(ITypeSymbol type)
{
if (HasAttribute(type, ImplicitDataDefinitionNamespace))

View File

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

View File

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

View File

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

View File

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

View File

@@ -296,7 +296,7 @@ namespace Robust.Client.GameObjects
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| ent.Comp1.EntityDeleted
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
@@ -322,7 +322,7 @@ namespace Robust.Client.GameObjects
{
if (IsQueuedForDeletion(ent.Owner)
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| ent.Comp1.EntityDeleted
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,15 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Serialization;
using static Robust.Shared.Containers.ContainerManagerComponent;
namespace Robust.Client.GameObjects
@@ -57,7 +58,7 @@ namespace Robust.Client.GameObjects
if (!RemoveExpectedEntity(meta.NetEntity, out var container))
return;
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container, force: true);
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container);
}
public override void ShutdownContainer(BaseContainer container)
@@ -231,7 +232,7 @@ namespace Robust.Client.GameObjects
return;
}
Insert(message.Entity, container, force: true);
Insert(message.Entity, container);
}
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1306,11 +1306,6 @@ namespace Robust.Client.GameStates
meta.LastStateApplied = lastStateApplied.Value;
var xform = xforms.GetComponent(ent.Value);
// TODO PVS DETACH
// Why is this if block here again? If a null-space entity gets sent to a player via some PVS override,
// and then later on it gets removed, you would assume that the client marks it as detached?
// I.e., modifying the metadata flag & pausing the entity should probably happen outside of this block.
if (xform.ParentUid.IsValid())
{
lookupSys.RemoveFromEntityTree(ent.Value, xform);
@@ -1331,13 +1326,6 @@ namespace Robust.Client.GameStates
xformSys.DetachEntity(ent.Value, xform);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
// We mark the entity as paused, without raising a pause-event.
// The entity gets un-paused when the metadata's comp-state is reapplied (which also does not raise
// an un-pause event). The assumption is that game logic that has to handle the pausing should be
// getting networked anyway. And if its some client-side timer on a networked entity, the timer
// shouldn't actually be getting paused just because the entity has left the players view.
meta.PauseTime = TimeSpan.Zero;
if (container != null)
containerSys.AddExpectedEntity(netEntity, container);
}

View File

@@ -254,11 +254,7 @@ namespace Robust.Client.Graphics.Clyde
region = regionMaybe[tile.Variant];
}
var rotationMirroring = _tileDefinitionManager[tile.TypeId].AllowRotationMirror
? tile.RotationMirroring
: 0;
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, rotationMirroring);
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
i += 1;
}
}
@@ -329,7 +325,7 @@ namespace Robust.Client.Graphics.Clyde
continue;
var region = regionMaybe[0];
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, 0);
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
i += 1;
}
}
@@ -412,11 +408,8 @@ namespace Robust.Client.Graphics.Clyde
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
{
var gridData = _mapChunkData.GetOrNew(args.Entity);
foreach (var change in args.Changes)
{
if (gridData.TryGetValue(change.ChunkIndex, out var data))
data.Dirty = true;
}
if (gridData.TryGetValue(args.ChunkIndex, out var data))
data.Dirty = true;
}
private void _updateOnGridCreated(GridStartupEvent ev)
@@ -452,57 +445,13 @@ namespace Robust.Client.Graphics.Clyde
int gridY,
Span<Vertex2D> vertexBuffer,
Span<ushort> indexBuffer,
Box2 region,
int rotationMirroring)
Box2 region)
{
var rLeftBottom = (region.Left, region.Bottom);
var rRightBottom = (region.Right, region.Bottom);
var rRightTop = (region.Right, region.Top);
var rLeftTop = (region.Left, region.Top);
// The vertices must be changed if there's any rotation or mirroring to the tile
if (rotationMirroring != 0)
{
// Rotate the tile
for (int r = 0; r < rotationMirroring % 4; r++)
{
(rLeftBottom, rRightBottom, rRightTop, rLeftTop) =
(rLeftTop, rLeftBottom, rRightBottom, rRightTop);
}
// Mirror on the x-axis
if (rotationMirroring >= 4)
{
if (rotationMirroring % 2 == 0)
{
rLeftBottom = (rLeftBottom.Item1.Equals(region.Left) ? region.Right : region.Left,
rLeftBottom.Item2);
rRightBottom = (rRightBottom.Item1.Equals(region.Left) ? region.Right : region.Left,
rRightBottom.Item2);
rRightTop = (rRightTop.Item1.Equals(region.Left) ? region.Right : region.Left,
rRightTop.Item2);
rLeftTop = (rLeftTop.Item1.Equals(region.Left) ? region.Right : region.Left,
rLeftTop.Item2);
}
else
{
rLeftBottom = (rLeftBottom.Item1,
rLeftBottom.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
rRightBottom = (rRightBottom.Item1,
rRightBottom.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
rRightTop = (rRightTop.Item1,
rRightTop.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
rLeftTop = (rLeftTop.Item1,
rLeftTop.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
}
}
}
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(gridX, gridY, rLeftBottom.Left, rLeftBottom.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gridX + 1, gridY, rRightBottom.Right, rRightBottom.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gridX + 1, gridY + 1, rRightTop.Right, rRightTop.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gridX, gridY + 1, rLeftTop.Left, rLeftTop.Top, Color.White);
vertexBuffer[vIdx + 0] = new Vertex2D(gridX, gridY, region.Left, region.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gridX + 1, gridY, region.Right, region.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gridX + 1, gridY + 1, region.Right, region.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gridX, gridY + 1, region.Left, region.Top, Color.White);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort)(i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);

View File

@@ -318,7 +318,7 @@ namespace Robust.Client.Graphics.Clyde
screenSpriteSize.Y++;
bool exit = false;
if (entry.Sprite.GetScreenTexture && entry.Sprite.PostShader != null)
if (entry.Sprite.GetScreenTexture)
{
FlushRenderQueue();
var tex = CopyScreenTexture(viewport.RenderTarget);
@@ -369,7 +369,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite), _renderHandle.DrawingHandleWorld, eye.Rotation, entry.WorldRot, entry.WorldPos);
spriteSystem.Render(entry.Uid, entry.Sprite, _renderHandle.DrawingHandleWorld, eye.Rotation, in entry.WorldRot, in entry.WorldPos);
if (entry.Sprite.PostShader != null && entityPostRenderTarget != null)
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -126,7 +126,7 @@ namespace Robust.Client.Placement
sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
var rot = args.Viewport.Eye?.Rotation ?? default;
spriteSys.RenderSprite((uid.Value, sprite), args.WorldHandle, rot, worldRot, worldPos);
spriteSys.Render(uid.Value, sprite, args.WorldHandle, rot, worldRot, worldPos);
}
}

View File

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

View File

@@ -13,9 +13,7 @@ namespace Robust.Client.Prototypes
public sealed class ClientPrototypeManager : PrototypeManager
{
[Dependency] private readonly INetManager _netManager = default!;
#pragma warning disable CS0414
[Dependency] private readonly IClientGameTiming _timing = default!;
#pragma warning restore CS0414
[Dependency] private readonly IGameControllerInternal _controller = default!;
[Dependency] private readonly IReloadManager _reload = default!;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -741,7 +741,7 @@ namespace Robust.Client.UserInterface.Controls
get => _selected;
set
{
if (!Selectable || _selected == value) return;
if (!Selectable) return;
_selected = value;
if(_selected) OnSelected?.Invoke(this);
else OnDeselected?.Invoke(this);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class HeadingTag : IMarkupTagHandler
public sealed class HeadingTag : IMarkupTag
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;

View File

@@ -1,16 +1,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
/// <summary>
/// Classes that implement this interface will be instantiated by <see cref="MarkupTagManager"/> and used to handle
/// the parsing and behaviour of markup tags. Note that each class is only ever instantiated once by the tag manager,
/// and wil be used to handle all tags of that kind, and thus should not contain state information relevant to a
/// specific tag.
/// </summary>
public interface IMarkupTagHandler
public interface IMarkupTag
{
/// <summary>
/// The string used as the tags name when writing rich text
@@ -61,32 +54,17 @@ public interface IMarkupTagHandler
}
/// <summary>
/// Called inside the constructor of <see cref="RichTextEntry"/> to supply a control that gets rendered inline
/// before this tags children. The returned control must be new instance to avoid issues with shallow cloning
/// <see cref="FormattedMessage"/> nodes. Text continues to the right of the control until the next line and
/// then continues bellow it.
/// Called inside the constructor of <see cref="RichTextEntry"/> to
/// supply a control that gets rendered inline before this tags children<br/>
/// Text continues to the right of the control until the next line and then continues bellow it
/// </summary>
/// <param name="node">The markup node containing the parameter and attributes</param>
/// <param name="control">A UI control for placing in line with this tags children</param>
/// <returns>true if this tag supplies a control</returns>
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
control = null;
return false;
}
}
[Obsolete("Use IMarkupTagHandler")]
public interface IMarkupTag : IMarkupTagHandler
{
bool IMarkupTagHandler.TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
return TryGetControl(node, out control);
}
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
control = null;
return false;
}
}

View File

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

View File

@@ -16,7 +16,7 @@ public sealed class MarkupTagManager
/// <summary>
/// Tags defined in engine need to be instantiated here because of sandboxing
/// </summary>
private readonly Dictionary<string, IMarkupTagHandler> _markupTagTypes = new IMarkupTagHandler[] {
private readonly Dictionary<string, IMarkupTag> _markupTagTypes = new IMarkupTag[] {
new BoldItalicTag(),
new BoldTag(),
new BulletTag(),
@@ -44,13 +44,13 @@ public sealed class MarkupTagManager
public void Initialize()
{
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTagHandler>())
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTag>())
{
//Prevent tags defined inside engine from being instantiated
if (_engineTypes.Contains(type))
continue;
var instance = (IMarkupTagHandler)_sandboxHelper.CreateInstance(type);
var instance = (IMarkupTag)_sandboxHelper.CreateInstance(type);
_markupTagTypes[instance.Name.ToLower()] = instance;
}
@@ -60,48 +60,22 @@ public sealed class MarkupTagManager
}
}
[Obsolete("Use GetMarkupTagHandler")]
public IMarkupTag? GetMarkupTag(string name)
{
return _markupTagTypes.GetValueOrDefault(name) as IMarkupTag;
}
public IMarkupTagHandler? GetMarkupTagHandler(string name)
{
return _markupTagTypes.GetValueOrDefault(name);
}
/// <summary>
/// Attempt to get the tag handler with the corresponding name.
/// </summary>
/// <param name="name">The name of the tag, as specified by <see cref="IMarkupTag.Name"/></param>
/// <param name="tagsAllowed">List of allowed tag types. If null, all types are allowed.</param>
/// <param name="handler">The instance responsible for handling tags of this type.</param>
/// <returns></returns>
public bool TryGetMarkupTagHandler(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTagHandler? handler)
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
{
if (_markupTagTypes.TryGetValue(name, out var markupTag)
// Using a whitelist prevents new tags from sneaking in.
&& (tagsAllowed == null || Array.IndexOf(tagsAllowed, markupTag.GetType()) != -1))
{
handler = markupTag;
tag = markupTag;
return true;
}
handler = null;
tag = null;
return false;
}
[Obsolete("Use TryGetMarkupTagHandler")]
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
{
if (!TryGetMarkupTagHandler(name, tagsAllowed, out var handler) || handler is not IMarkupTag cast)
{
tag = null;
return false;
}
tag = cast;
return true;
}
}

View File

@@ -13,9 +13,6 @@ namespace Robust.Client.UserInterface
{
/// <summary>
/// Used by <see cref="OutputPanel"/> and <see cref="RichTextLabel"/> to handle rich text layout.
/// Note that if this text is ever removed or modified without removing the owning control,
/// then <see cref="RemoveControls"/> should be called to ensure that any controls that were added by this
/// entry are also removed.
/// </summary>
internal struct RichTextEntry
{
@@ -39,7 +36,7 @@ namespace Robust.Client.UserInterface
/// </summary>
public ValueList<int> LineBreaks;
public readonly Dictionary<int, Control>? Controls;
private readonly Dictionary<int, Control>? _tagControls;
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
@@ -59,35 +56,15 @@ namespace Robust.Client.UserInterface
if (node.Name == null)
continue;
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var handler) || !handler.TryCreateControl(node, out var control))
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
continue;
// Markup tag handler instances are shared across controls. We need to ensure that the hanlder doesn't
// store state information and return the same control for each rich text entry.
DebugTools.Assert(handler.TryCreateControl(node, out var other) && other != control);
parent.Children.Add(control);
tagControls ??= new Dictionary<int, Control>();
tagControls.Add(nodeIndex, control);
}
Controls = tagControls;
}
// TODO RICH TEXT
// Somehow ensure that this **has** to be called when removing rich text from some control.
/// <summary>
/// Remove all owned controls from their parents.
/// </summary>
public readonly void RemoveControls()
{
if (Controls == null)
return;
foreach (var ctrl in Controls.Values)
{
ctrl.Orphan();
}
_tagControls = tagControls;
}
/// <summary>
@@ -97,7 +74,7 @@ namespace Robust.Client.UserInterface
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
/// <param name="uiScale"></param>
/// <param name="lineHeightScale"></param>
public RichTextEntry Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
{
// This method is gonna suck due to complexity.
// Bear with me here.
@@ -135,10 +112,10 @@ namespace Robust.Client.UserInterface
continue;
if (ProcessMetric(ref this, metrics, out breakLine))
return this;
return;
}
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
continue;
control.Measure(new Vector2(Width, Height));
@@ -151,14 +128,12 @@ namespace Robust.Client.UserInterface
desiredSize.Y);
if (ProcessMetric(ref this, controlMetrics, out breakLine))
return this;
return;
}
Width = wordWrap.FinalizeText(out breakLine);
CheckLineBreak(ref this, breakLine);
return this;
bool ProcessRune(ref RichTextEntry src, Rune rune, out int? outBreakLine)
{
wordWrap.NextRune(rune, out breakLine, out var breakNewLine, out var skip);
@@ -191,10 +166,9 @@ namespace Robust.Client.UserInterface
internal readonly void HideControls()
{
if (Controls == null)
if (_tagControls == null)
return;
foreach (var control in Controls.Values)
foreach (var control in _tagControls.Values)
{
control.Visible = false;
}
@@ -246,7 +220,7 @@ namespace Robust.Client.UserInterface
globalBreakCounter += 1;
}
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
continue;
// Controls may have been previously hidden via HideControls due to being "out-of frame".
@@ -269,7 +243,7 @@ namespace Robust.Client.UserInterface
return node.Value.StringValue ?? "";
//Skip the node if there is no markup tag for it.
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var tag))
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
return "";
if (!node.Closing)

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Robust.Client.Graphics;
using Robust.Shared;
@@ -20,14 +21,12 @@ 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(); // this list is never used but needed to prevent them from being garbage collected
private List<FileSystemWatcher> _watchers = new();
public event Action<ResPath>? OnChanged;
@@ -70,11 +69,6 @@ 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))
@@ -96,7 +90,7 @@ internal sealed class ReloadManager : IReloadManager
NotifyFilter = NotifyFilters.LastWrite
};
_watchers.Add(watcher); // prevent garbage collection
_watchers.Add(watcher);
watcher.Changed += OnWatch;
@@ -106,7 +100,7 @@ internal sealed class ReloadManager : IReloadManager
}
catch (IOException ex)
{
_sawmill.Error($"Watching resources in path {path} threw an exception:\n{ex}");
Logger.Error($"Watching resources in path {path} threw an exception:\n{ex}");
}
}
@@ -142,6 +136,6 @@ internal sealed class ReloadManager : IReloadManager
}
});
}
#endif
#endif
}
}

View File

@@ -17,7 +17,6 @@ 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
@@ -25,14 +24,13 @@ namespace Robust.Client.Utility
.Texture;
}
[Obsolete("Use SpriteSystem.GetState() instead")]
[Obsolete("Use SpriteSystem")]
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 sys.GetFallbackState();
return SpriteComponent.GetFallbackState(cache);
}
if (theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
@@ -41,22 +39,21 @@ namespace Robust.Client.Utility
}
Logger.Error($"SpriteSpecifier has invalid RSI state '{rsiSpecifier.RsiState}' for RSI: {rsiSpecifier.RsiPath}");
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>().GetFallbackState();
return SpriteComponent.GetFallbackState(cache);
}
[Obsolete("Use SpriteSystem.Frame0() instead")]
[Obsolete("Use SpriteSystem")]
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.RsiStateLike() instead")]
[Obsolete("Use SpriteSystem")]
public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier)
{
var resC = IoCManager.Resolve<IResourceCache>();
@@ -70,11 +67,10 @@ 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 sys.GetFallbackState();
return SpriteComponent.GetFallbackState(resC);
}
return SpriteComponent.GetPrototypeIcon(prototype, resC);

View File

@@ -39,7 +39,6 @@ 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.");

View File

@@ -331,7 +331,6 @@ namespace Robust.Server
// TODO: solve this properly.
_serializer.Initialize();
_loc.Initialize();
_loc.AddLoadedToStringSerializer(_stringSerializer);
//IoCManager.Resolve<IMapLoader>().LoadedMapData +=

View File

@@ -14,19 +14,6 @@ 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)
@@ -34,14 +21,24 @@ public sealed class PointLightSystem : SharedPointLightSystem
UpdatePriority(uid, component, MetaData(uid));
}
private bool IsHighPriority(SharedPointLightComponent comp)
{
return comp is {Enabled: true, CastShadows: true, Radius: > 7, LifeStage: <= ComponentLifeStage.Running};
}
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
{
_metadata.SetFlag((uid, meta), MetaDataFlags.PvsPriority, IsHighPriority(comp));
var isHighPriority = comp.Enabled && comp.CastShadows && (comp.Radius > 7);
_metadata.SetFlag((uid, meta), MetaDataFlags.PvsPriority, isHighPriority);
}
private void OnLightGetState(EntityUid uid, PointLightComponent 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,
};
}
public override SharedPointLightComponent EnsureLight(EntityUid uid)

View File

@@ -9,30 +9,12 @@ 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))

View File

@@ -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;
}
// The majority of players will have no view subscriptions
// Fast path
if (session.ViewSubscriptions.Count == 0)
{
if (session.AttachedEntity is not {} attached)
@@ -184,21 +184,15 @@ internal sealed partial class PvsSystem
return;
}
var count = session.ViewSubscriptions.Count;
var i = 0;
if (session.AttachedEntity is { } local)
{
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.
Array.Resize(ref pvsSession.Viewers, session.ViewSubscriptions.Count + 1);
pvsSession.Viewers[i++] = (local, Transform(local), _eyeQuery.CompOrNull(local));
}
else
{
Array.Resize(ref pvsSession.Viewers, count);
Array.Resize(ref pvsSession.Viewers, session.ViewSubscriptions.Count);
}
foreach (var ent in session.ViewSubscriptions)
@@ -206,8 +200,6 @@ 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()

View File

@@ -1,8 +0,0 @@
using Robust.Shared.Localization;
namespace Robust.Server.Localization;
internal sealed class ServerLocalizationManager : LocalizationManager, ILocalizationManager
{
void ILocalizationManager.Initialize() => Initialize();
}

View File

@@ -1,82 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
namespace Robust.Server.Physics.Commands;
public sealed class BoxStackCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _entManager = default!;
public string Command => "boxstack";
public string Description => string.Empty;
public string Help => "boxstack [mapid] [columns] [rows]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 3 ||
!int.TryParse(args[0], out var mapInt) ||
!int.TryParse(args[1], out var columns) ||
!int.TryParse(args[2], out var rows))
{
return;
}
var mapId = new MapId(mapInt);
var fixtureSystem = _entManager.System<FixtureSystem>();
var physSystem = _entManager.System<SharedPhysicsSystem>();
physSystem.SetGravity(new Vector2(0f, -9.8f));
var groundUid = _entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var ground = _entManager.AddComponent<PhysicsComponent>(groundUid);
var groundManager = _entManager.EnsureComponent<FixturesComponent>(groundUid);
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
fixtureSystem.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 1, 1, true), manager: groundManager, body: ground);
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
fixtureSystem.CreateFixture(groundUid, "fix2", new Fixture(vertical, 1, 1, true), manager: groundManager, body: ground);
physSystem.WakeBody(groundUid, manager: groundManager, body: ground);
var xs = new[]
{
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
};
for (var j = 0; j < columns; j++)
{
for (var i = 0; i < rows; i++)
{
var x = 0.0f;
var boxUid = _entManager.SpawnEntity(null,
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 2.1f * i), mapId));
var box = _entManager.AddComponent<PhysicsComponent>(boxUid);
var manager = _entManager.EnsureComponent<FixturesComponent>(boxUid);
physSystem.SetBodyType(boxUid, BodyType.Dynamic, manager: manager, body: box);
var poly = new PolygonShape(0.001f);
poly.Set(new List<Vector2>()
{
new(0.5f, -0.5f),
new(0.5f, 0.5f),
new(-0.5f, 0.5f),
new(-0.5f, -0.5f),
});
fixtureSystem.CreateFixture(boxUid, "fix1", new Fixture(poly, 1, 1, true), manager: manager, body: box);
physSystem.WakeBody(boxUid, manager: manager, body: box);
}
}
}
}

View File

@@ -140,7 +140,7 @@ namespace Robust.Server.Placement
{
// Replace existing entities if relevant.
if (msg.Replacement && _prototype.Index<EntityPrototype>(entityTemplateName).Components.TryGetValue(
_factory.GetComponentName<PlacementReplacementComponent>(), out var compRegistry))
_factory.GetComponentName(typeof(PlacementReplacementComponent)), out var compRegistry))
{
var key = ((PlacementReplacementComponent)compRegistry.Component).Key;
var gridUid = _xformSystem.GetGrid(coordinates);
@@ -179,14 +179,11 @@ namespace Robust.Server.Placement
}
else
{
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);
PlaceNewTile(tileType, coordinates, msg.MsgChannel.UserId);
}
}
private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId placingUserId, byte direction, bool mirrored)
private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId placingUserId)
{
if (!coordinates.IsValid(_entityManager)) return;
@@ -196,7 +193,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, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0))));
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType));
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
@@ -210,7 +207,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, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0))));
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType));
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);

View File

@@ -13,10 +13,8 @@ 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!;

View File

@@ -4,7 +4,6 @@ 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;
@@ -22,7 +21,6 @@ using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
@@ -99,9 +97,7 @@ namespace Robust.Server
deps.Register<NetworkResourceManager>();
deps.Register<IHttpClientHolder, HttpClientHolder>();
deps.Register<UploadedContentManager>();
deps.Register<IHWId, DummyHWId>();
deps.Register<ILocalizationManager, ServerLocalizationManager>();
deps.Register<ILocalizationManagerInternal, ServerLocalizationManager>();
deps.Register<IHWId, DummyHWId>();
}
}
}

View File

@@ -104,16 +104,6 @@ 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>

View File

@@ -1,5 +1,4 @@
using System;
using System.Diagnostics.Contracts;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -102,7 +101,6 @@ namespace Robust.Shared.Maths
/// <summary>
/// Returns the smallest rectangle that contains both of the rectangles.
/// </summary>
[Pure]
public readonly Box2i Union(in Box2i other)
{
var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft);
@@ -209,7 +207,6 @@ namespace Robust.Shared.Maths
/// <summary>
/// Multiplies each side of the box by the scalar.
/// </summary>
[Pure]
public Box2i Scale(int scalar)
{
return new Box2i(
@@ -218,21 +215,6 @@ namespace Robust.Shared.Maths
Right * scalar,
Top * scalar);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool Intersects(in Box2i other)
{
return other.Bottom <= this.Top && other.Top >= this.Bottom && other.Right >= this.Left &&
other.Left <= this.Right;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public readonly Box2i Enlarged(int size)
{
return new(Left - size, Bottom - size, Right + size, Top + size);
}
}
/// <summary>

View File

@@ -13,8 +13,7 @@ namespace Robust.Shared.Audio;
/// <seealso cref="ResolvedPathSpecifier"/>
/// <seealso cref="ResolvedCollectionSpecifier"/>
[Serializable, NetSerializable]
public abstract partial class ResolvedSoundSpecifier
{
public abstract partial class ResolvedSoundSpecifier {
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
public static implicit operator ResolvedSoundSpecifier(string s) => new ResolvedPathSpecifier(s);
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
@@ -23,10 +22,8 @@ public abstract partial class ResolvedSoundSpecifier
/// <summary>
/// Returns whether <c>s</c> is null, or if it contains an empty path/collection ID.
/// </summary>
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s)
{
return s switch
{
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s) {
return s switch {
null => true,
ResolvedPathSpecifier path => path.Path.ToString() == "",
ResolvedCollectionSpecifier collection => string.IsNullOrEmpty(collection.Collection),
@@ -40,8 +37,7 @@ public abstract partial class ResolvedSoundSpecifier
/// </summary>
/// <seealso cref="ResolvedCollectionSpecifier"/>
[Serializable, NetSerializable]
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedPathSpecifier>
{
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
/// <summary>
/// The resource path of the sound.
/// </summary>
@@ -61,21 +57,6 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier, IEqu
public ResolvedPathSpecifier(string path) : this(new ResPath(path))
{
}
public bool Equals(ResolvedPathSpecifier? other)
{
return Path.Equals(other?.Path);
}
public override bool Equals(object? obj)
{
return Equals(obj as ResolvedPathSpecifier);
}
public override int GetHashCode()
{
return Path.GetHashCode();
}
}
/// <summary>
@@ -83,9 +64,7 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier, IEqu
/// </summary>
/// <seealso cref="ResolvedPathSpecifier"/>
[Serializable, NetSerializable]
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedCollectionSpecifier>
{
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier {
/// <summary>
/// The ID of the <see cref="SoundCollectionPrototype">sound collection</see> to look up.
/// </summary>
@@ -108,19 +87,4 @@ public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier
Collection = collection;
Index = index;
}
public bool Equals(ResolvedCollectionSpecifier? other)
{
return Collection.Equals(other?.Collection) && Index.Equals(other?.Index);
}
public override bool Equals(object? obj)
{
return Equals(obj as ResolvedCollectionSpecifier);
}
public override int GetHashCode()
{
return HashCode.Combine(Collection, Index);
}
}

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