mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5adf0cdfa3 | ||
|
|
320543c2a6 | ||
|
|
0d0d949752 | ||
|
|
d43fc89055 | ||
|
|
f9d0dd551a | ||
|
|
b2540a6e08 | ||
|
|
66d898ee91 | ||
|
|
310dc676ea | ||
|
|
41844d2d30 | ||
|
|
c6f3af20d6 | ||
|
|
5501209b35 | ||
|
|
9b2ef75762 | ||
|
|
196e59b7e4 | ||
|
|
2c936b5973 | ||
|
|
7765e71dca | ||
|
|
d8ae71d8cd | ||
|
|
a74812ce5b | ||
|
|
a7f9b0a6db | ||
|
|
3aac92e4b2 | ||
|
|
c152fb8953 | ||
|
|
10ea5498cf | ||
|
|
324606e5a3 | ||
|
|
a8227f7faa | ||
|
|
9f55400c58 | ||
|
|
8b971f7ae7 | ||
|
|
e3c7e361ae | ||
|
|
5c48dcb211 | ||
|
|
694de028c2 | ||
|
|
d41c9e7662 | ||
|
|
76134e0f8d | ||
|
|
2983517e43 | ||
|
|
18849be0b4 | ||
|
|
c6a1d82bb1 | ||
|
|
d89e1a43c6 | ||
|
|
d894ef70ef | ||
|
|
c7ea2793ca | ||
|
|
0c61ff2bee | ||
|
|
343a34eac7 | ||
|
|
7be41f4890 | ||
|
|
293470a5fe | ||
|
|
2b8057acf0 | ||
|
|
bec3caa5da | ||
|
|
ea6126563b | ||
|
|
00494ad9eb | ||
|
|
6672b7b1bd | ||
|
|
8dc55e8748 | ||
|
|
44ea2cd396 | ||
|
|
2c5604432b | ||
|
|
c696466522 | ||
|
|
01bb98e400 | ||
|
|
af08e747de | ||
|
|
8c35c2c380 | ||
|
|
6d46d3f4a5 | ||
|
|
50e06e43fa | ||
|
|
986b0f979d | ||
|
|
a51d786dee | ||
|
|
5f5fed5d6c | ||
|
|
e475cc7898 | ||
|
|
ee8ea4ec3b | ||
|
|
7482451ec4 | ||
|
|
dddf5cd2fb | ||
|
|
01979c451d | ||
|
|
181a5ef0b4 | ||
|
|
e7c7011cc0 | ||
|
|
dc97615fd4 | ||
|
|
3b4944376b | ||
|
|
fa6bd8f7ba | ||
|
|
2398cbcf26 | ||
|
|
38ce48a83f | ||
|
|
4e7de2f272 | ||
|
|
b61075c660 | ||
|
|
7b571dc80e | ||
|
|
f1c76ca899 | ||
|
|
84dcd658aa | ||
|
|
a634d6bd04 | ||
|
|
36f9df3079 | ||
|
|
824c018a69 | ||
|
|
4b6b688c72 | ||
|
|
71df25b251 | ||
|
|
be14a3c249 | ||
|
|
3c2a4d5c79 | ||
|
|
44180b3ee0 | ||
|
|
bb0e77e937 | ||
|
|
684b9bc852 | ||
|
|
9f3db6693e | ||
|
|
40d869948d | ||
|
|
5c97b15849 | ||
|
|
3d8a9a41fa | ||
|
|
92fc8722da | ||
|
|
73f6555624 | ||
|
|
2ac7bc3ce4 | ||
|
|
05cb4bb1c9 | ||
|
|
a393efc87a | ||
|
|
4d47cfa1a6 | ||
|
|
2b1d755d9f |
@@ -57,7 +57,7 @@
|
||||
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
|
||||
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
217
RELEASE-NOTES.md
217
RELEASE-NOTES.md
@@ -54,6 +54,223 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 263.0.2
|
||||
|
||||
|
||||
## 263.0.1
|
||||
|
||||
|
||||
## 263.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Fully removed some non-`Entity<T>` container methods.
|
||||
|
||||
### New features
|
||||
|
||||
* `IMidiRenderer.LoadSoundfont` has been split into `LoadSoundfontResource` and `LoadSoundfontUser`, the original now being deprecated.
|
||||
* Client command execution now properly catches errors instead of letting them bubble up through the input stack.
|
||||
* Added `CompletionHelper.PrototypeIdsLimited` API to allow commands to autocomplete entity prototype IDs.
|
||||
* Added `spawn:in` Toolshed command.
|
||||
* Added `MapLoaderSystem.TryLoadGeneric` overload to load from a `Stream`.
|
||||
* Added `OutputPanel.GetMessage()` and `OutputPanel.SetMessage()` to allow replacing individual messages.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed debug asserts when using MIDI on Windows.
|
||||
* Fixed an error getting logged on startup on macOS related to window icons.
|
||||
* `CC-BY-NC-ND-4.0` is now a valid license for the RGA validator.
|
||||
* Fixed `TabContainer.CurrentTab` clamping against the wrong value.
|
||||
* Fix culture-based parsing in `TimespanSerializer`.
|
||||
* Fixed grid rendering blowing up on tile IDs that aren't registered.
|
||||
* Fixed debug assert when loading MIDI soundfonts on Windows.
|
||||
* Make `ColorSelectorSliders` properly update the dropdown when changing `SelectorType`.
|
||||
* Fixed `tpto` allowing teleports to oneself, thereby causing them to be deleted.
|
||||
* Fix OpenAL extensions being requested incorrectly, causing an error on macOS.
|
||||
* Fixed horizontal measuring of markup controls in rich text.
|
||||
|
||||
### Other
|
||||
|
||||
* Improved logging for some audio entity errors.
|
||||
* Avoided more server stutters when using `csci`.
|
||||
* Improved physics performance.
|
||||
* Made various localization functions like `GENDER()` not throw if passed a string instead of an `EntityUid`.
|
||||
* The generic clause on `EntitySystem.AddComp<T>` has been changed to `IComponent` (from `Component`) for consistency with `IEntityManager.AddComponent<T>`.
|
||||
* `DataDefinitionAnalyzer` has been optimized somewhat.
|
||||
* Improved assert logging error message when static data fields are encountered.
|
||||
|
||||
### Internal
|
||||
|
||||
* Warning cleanup.
|
||||
* Added more tests for `DataDefinitionAnalyzer`.
|
||||
* Consistently use `EntitySystem` proxy methods in engine.
|
||||
|
||||
|
||||
## 262.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Toolshed commands will now validate that each non-generic command argument is parseable (i.e., has a corresponding type parser). This check can be disabled by explicitly marking the argument as unparseable via `CommandArgumentAttribute.Unparseable`.
|
||||
|
||||
### New features
|
||||
|
||||
* `ToolshedManager.TryParse` now also supports nullable value types.
|
||||
* Add an ignoredComponents arg to IsDefault.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `SpriteComponent.Layer.Visible` setter not marking a sprite's bounding box as dirty.
|
||||
* The audio params in the passed SoundSpecifier for PlayStatic(SoundSpecifier, Filter, ...) will now be used as a default like other PlayStatic overrides.
|
||||
* Fix windows not saving their positions correctly when their x position is <= 0.
|
||||
* Fix transform state handling overriding PVS detachment.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@@ -195,6 +195,8 @@ command-description-spawn-at =
|
||||
Spawns an entity at the given coordinates.
|
||||
command-description-spawn-on =
|
||||
Spawns an entity on the given entity, at it's coordinates.
|
||||
command-description-spawn-in =
|
||||
Spawns an entity in the given container on the given entity, dropping it at its coordinates if it doesn't fit
|
||||
command-description-spawn-attached =
|
||||
Spawns an entity attached to the given entity, at (0 0) relative to it.
|
||||
command-description-mappos =
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
namespace Robust.Shared.Serialization.Manager.Attributes
|
||||
{
|
||||
public class DataFieldBaseAttribute : Attribute;
|
||||
public class DataFieldAttribute : DataFieldBaseAttribute;
|
||||
public class DataFieldAttribute(string? tag = null) : DataFieldBaseAttribute;
|
||||
public sealed class DataDefinitionAttribute : Attribute;
|
||||
public sealed class NotYamlSerializableAttribute : Attribute;
|
||||
}
|
||||
@@ -117,6 +117,61 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PartialDataDefinitionTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed class Foo { }
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(4,15): error RA0017: Type Foo is a DataDefinition but is not partial
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataDefinitionPartialRule).WithSpan(4, 15, 4, 20).WithArguments("Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NestedPartialDataDefinitionTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
public sealed class Foo
|
||||
{
|
||||
[DataDefinition]
|
||||
public sealed partial class Nested { }
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(3,15): error RA0018: Type Foo contains nested data definition Nested but is not partial
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.NestedDataDefinitionPartialRule).WithSpan(3, 15, 3, 20).WithArguments("Foo", "Nested")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RedundantDataFieldTagTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[DataField("someValue")]
|
||||
public int SomeValue;
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(6,6): info RA0027: Data field SomeValue in data definition Foo has an explicitly set tag that matches autogenerated tag
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldRedundantTagRule).WithSpan(6, 6, 6, 28).WithArguments("SomeValue", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ReadOnlyPropertyTest()
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
private const string DataFieldAttributeName = "DataField";
|
||||
private const string ViewVariablesAttributeName = "ViewVariables";
|
||||
|
||||
private static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
|
||||
public static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
|
||||
Diagnostics.IdDataDefinitionPartial,
|
||||
"Type must be partial",
|
||||
"Type {0} is a DataDefinition but is not partial",
|
||||
@@ -32,7 +32,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure to mark any type that is a data definition as partial."
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
|
||||
public static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
|
||||
Diagnostics.IdNestedDataDefinitionPartial,
|
||||
"Type must be partial",
|
||||
"Type {0} contains nested data definition {1} but is not partial",
|
||||
@@ -62,7 +62,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure to add a setter."
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
|
||||
public static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
|
||||
Diagnostics.IdDataFieldRedundantTag,
|
||||
"Data field has redundant tag specified",
|
||||
"Data field {0} in data definition {1} has an explicitly set tag that matches autogenerated tag",
|
||||
@@ -102,14 +102,23 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
|
||||
context.RegisterSymbolStartAction(symbolContext =>
|
||||
{
|
||||
if (symbolContext.Symbol is not INamedTypeSymbol typeSymbol)
|
||||
return;
|
||||
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
|
||||
if (!IsDataDefinition(typeSymbol))
|
||||
return;
|
||||
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
|
||||
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
|
||||
}, SymbolKind.NamedType);
|
||||
}
|
||||
|
||||
private void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
|
||||
@@ -117,8 +126,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
if (context.Node is not TypeDeclarationSyntax declaration)
|
||||
return;
|
||||
|
||||
var type = context.SemanticModel.GetDeclaredSymbol(declaration)!;
|
||||
if (!IsDataDefinition(type))
|
||||
if (context.ContainingSymbol is not INamedTypeSymbol type)
|
||||
return;
|
||||
|
||||
if (!IsPartial(declaration))
|
||||
@@ -129,7 +137,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
var containingType = type.ContainingType;
|
||||
while (containingType != null)
|
||||
{
|
||||
var containingTypeDeclaration = (TypeDeclarationSyntax) containingType.DeclaringSyntaxReferences[0].GetSyntax();
|
||||
var containingTypeDeclaration = (TypeDeclarationSyntax)containingType.DeclaringSyntaxReferences[0].GetSyntax();
|
||||
if (!IsPartial(containingTypeDeclaration))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(NestedDataDefinitionPartialRule, containingTypeDeclaration.Keyword.GetLocation(), containingType.Name, type.Name));
|
||||
@@ -144,27 +152,26 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
if (context.Node is not FieldDeclarationSyntax field)
|
||||
return;
|
||||
|
||||
var typeDeclaration = field.FirstAncestorOrSelf<TypeDeclarationSyntax>();
|
||||
if (typeDeclaration == null)
|
||||
return;
|
||||
|
||||
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
|
||||
if (!IsDataDefinition(type))
|
||||
if (context.ContainingSymbol?.ContainingType is not INamedTypeSymbol type)
|
||||
return;
|
||||
|
||||
foreach (var variable in field.Declaration.Variables)
|
||||
{
|
||||
var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable);
|
||||
|
||||
if (fieldSymbol == null)
|
||||
continue;
|
||||
|
||||
if (!IsDataField(fieldSymbol, out _, out var datafieldAttribute))
|
||||
continue;
|
||||
|
||||
if (IsReadOnlyDataField(type, fieldSymbol))
|
||||
{
|
||||
TryGetModifierLocation(field, SyntaxKind.ReadOnlyKeyword, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, location, fieldSymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (HasRedundantTag(fieldSymbol))
|
||||
if (HasRedundantTag(fieldSymbol, datafieldAttribute))
|
||||
{
|
||||
TryGetAttributeLocation(field, DataFieldAttributeName, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, fieldSymbol.Name, type.Name));
|
||||
@@ -196,25 +203,28 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
if (context.Node is not PropertyDeclarationSyntax property)
|
||||
return;
|
||||
|
||||
var typeDeclaration = property.FirstAncestorOrSelf<TypeDeclarationSyntax>();
|
||||
if (typeDeclaration == null)
|
||||
if (context.ContainingSymbol is not IPropertySymbol propertySymbol)
|
||||
return;
|
||||
|
||||
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
|
||||
if (!IsDataDefinition(type) || type.IsRecord || type.IsValueType)
|
||||
if (propertySymbol.ContainingType is not INamedTypeSymbol type)
|
||||
return;
|
||||
|
||||
if (type.IsRecord || type.IsValueType)
|
||||
return;
|
||||
|
||||
var propertySymbol = context.SemanticModel.GetDeclaredSymbol(property);
|
||||
if (propertySymbol == null)
|
||||
return;
|
||||
|
||||
if (!IsDataField(propertySymbol, out _, out var datafieldAttribute))
|
||||
return;
|
||||
|
||||
if (IsReadOnlyDataField(type, propertySymbol))
|
||||
{
|
||||
var location = property.AccessorList != null ? property.AccessorList.GetLocation() : property.GetLocation();
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, location, propertySymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (HasRedundantTag(propertySymbol))
|
||||
if (HasRedundantTag(propertySymbol, datafieldAttribute))
|
||||
{
|
||||
TryGetAttributeLocation(property, DataFieldAttributeName, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, propertySymbol.Name, type.Name));
|
||||
@@ -242,9 +252,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
|
||||
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
|
||||
{
|
||||
if (!IsDataField(field, out _, out _))
|
||||
return false;
|
||||
|
||||
return IsReadOnlyMember(type, field);
|
||||
}
|
||||
|
||||
@@ -369,17 +376,14 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasRedundantTag(ISymbol symbol)
|
||||
private static bool HasRedundantTag(ISymbol symbol, AttributeData datafieldAttribute)
|
||||
{
|
||||
if (!IsDataField(symbol, out var _, out var attribute))
|
||||
return false;
|
||||
|
||||
// No args, no problem
|
||||
if (attribute.ConstructorArguments.Length == 0)
|
||||
if (datafieldAttribute.ConstructorArguments.Length == 0)
|
||||
return false;
|
||||
|
||||
// If a tag is explicitly specified, it will be the first argument...
|
||||
var tagArgument = attribute.ConstructorArguments[0];
|
||||
var tagArgument = datafieldAttribute.ConstructorArguments[0];
|
||||
// ...but the first arg could also something else, since tag is optional
|
||||
// so we make sure that it's a string
|
||||
if (tagArgument.Value is not string explicitName)
|
||||
@@ -394,9 +398,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
|
||||
private static bool HasVVReadWrite(ISymbol symbol)
|
||||
{
|
||||
if (!IsDataField(symbol, out _, out _))
|
||||
return false;
|
||||
|
||||
// Make sure it has ViewVariablesAttribute
|
||||
AttributeData? viewVariablesAttribute = null;
|
||||
foreach (var attr in symbol.GetAttributes())
|
||||
@@ -422,9 +423,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
|
||||
private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type)
|
||||
{
|
||||
if (!IsDataField(field, out _, out _))
|
||||
return false;
|
||||
|
||||
return HasAttribute(type, NotYamlSerializableName);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
public static class Program
|
||||
internal static class Program
|
||||
{
|
||||
// This was supposed to be the main entry for the subprocess program... It doesn't work.
|
||||
public static int Main(string[] args)
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IGameControllerInternal _gameController = default!;
|
||||
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
var cachePath = "";
|
||||
if (_resourceManager.UserData is WritableDirProvider userData)
|
||||
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
|
||||
{
|
||||
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
|
||||
cachePath = Path.Combine(rootDir, "cef_cache", "0");
|
||||
}
|
||||
|
||||
var settings = new CefSettings()
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Robust.Client.Animations
|
||||
/// </summary>
|
||||
public abstract class AnimationTrackProperty : AnimationTrack
|
||||
{
|
||||
public List<KeyFrame> KeyFrames { get; protected set; } = new();
|
||||
public List<KeyFrame> KeyFrames { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// How to interpolate values when between two keyframes.
|
||||
|
||||
@@ -57,8 +57,8 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
_checkAlError();
|
||||
|
||||
// Load up AL context extensions.
|
||||
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' '))
|
||||
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
_alContextExtensions.Add(extension);
|
||||
}
|
||||
|
||||
@@ -582,7 +582,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
{
|
||||
if (TerminatingOrDeleted(entity))
|
||||
{
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entity)}");
|
||||
LogAudioPlaybackOnInvalidEntity(specifier, entity);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -626,7 +626,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
{
|
||||
if (TerminatingOrDeleted(coordinates.EntityId))
|
||||
{
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
|
||||
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -753,6 +753,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return _resourceCache.GetResource<AudioResource>(filename).AudioStream.Length;
|
||||
}
|
||||
|
||||
private void LogAudioPlaybackOnInvalidEntity(ResolvedSoundSpecifier? specifier, EntityUid entityId)
|
||||
{
|
||||
var soundInfo = specifier?.ToString() ?? "unknown sound";
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entityId)}. Sound: {soundInfo}. Trace: {Environment.StackTrace}");
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
private record struct UpdateAudioJob : IParallelRobustJob
|
||||
|
||||
@@ -6,6 +6,7 @@ using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
@@ -156,8 +157,13 @@ public interface IMidiRenderer : IDisposable
|
||||
/// <summary>
|
||||
/// Loads a new soundfont into the renderer.
|
||||
/// </summary>
|
||||
[Obsolete("Use LoadSoundfontResource or LoadSoundfontUser instead")]
|
||||
void LoadSoundfont(string filename, bool resetPresets = false);
|
||||
|
||||
void LoadSoundfontResource(ResPath path, bool resetPresets = false);
|
||||
|
||||
void LoadSoundfontUser(ResPath path, bool resetPresets = false);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked whenever a new midi event is registered.
|
||||
/// </summary>
|
||||
|
||||
262
Robust.Client/Audio/Midi/MidiManager.SoundFontLoad.cs
Normal file
262
Robust.Client/Audio/Midi/MidiManager.SoundFontLoad.cs
Normal file
@@ -0,0 +1,262 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using NFluidsynth;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
internal sealed partial class MidiManager
|
||||
{
|
||||
// For loading sound fonts, we have to use a callback model where we can only parse a string.
|
||||
// This API, frankly, fucking sucks.
|
||||
//
|
||||
// These prefixes are used to separate the various places a file *can* be loaded from.
|
||||
//
|
||||
// We cannot prevent Fluidsynth from trying to load prefixed paths itself if they are invalid
|
||||
// So if content specifies "/foobar.sf2" to be loaded and it doesn't exist,
|
||||
// Fluidsynth *will* try to fopen("RES:/foobar.sf2"). For this reason I'm putting in some nonsense characters
|
||||
// that will pass through Fluidsynth fine, but make sure the filename is *never* a practically valid OS path.
|
||||
//
|
||||
// NOTE: Raw disk paths *cannot* be prefixed as Fluidsynth needs to load those itself.
|
||||
// Specifically, their .dls loader doesn't respect file callbacks.
|
||||
// If you're curious why this is: it's two-fold:
|
||||
// * The Fluidsynth C code for the .dls loader just doesn't use the file callbacks, period.
|
||||
// * Even if it did, we're not specifying those file callbacks, as they're per loader,
|
||||
// and we're only adding a *new* sound font loader with file callbacks, not modifying the existing ones.
|
||||
// The loader for .sfX format and .dls format are different loader objects in Fluidsynth.
|
||||
internal const string PrefixCommon = "!/ -?\x0001";
|
||||
internal const string PrefixLegacy = PrefixCommon + "LEGACY";
|
||||
internal const string PrefixUser = PrefixCommon + "USER";
|
||||
internal const string PrefixResources = PrefixCommon + "RES";
|
||||
|
||||
private void LoadSoundFontSetup(MidiRenderer renderer)
|
||||
{
|
||||
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
|
||||
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
|
||||
renderer.LoadSoundfontResource(FallbackSoundfont);
|
||||
|
||||
// Load system-specific soundfonts.
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
foreach (var filepath in LinuxSoundfonts)
|
||||
{
|
||||
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {filepath}");
|
||||
renderer.LoadSoundfontDisk(filepath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {OsxSoundfont}");
|
||||
renderer.LoadSoundfontDisk(OsxSoundfont);
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {WindowsSoundfont}");
|
||||
renderer.LoadSoundfontDisk(WindowsSoundfont);
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe load soundfont specified in environment variable.
|
||||
// Load it here so it can override system soundfonts but not content or user data soundfonts.
|
||||
if (Environment.GetEnvironmentVariable(SoundfontEnvironmentVariable) is { } soundfontOverride)
|
||||
{
|
||||
// Just to avoid funny shit: avoid people smuggling a prefix in here.
|
||||
// I wish I could separate this properly...
|
||||
var (prefix, _) = SplitPrefix(soundfontOverride);
|
||||
if (IsValidPrefix(prefix))
|
||||
{
|
||||
_midiSawmill.Error($"Not respecting {SoundfontEnvironmentVariable} env variable: invalid file path");
|
||||
}
|
||||
else if (File.Exists(soundfontOverride) && SoundFont.IsSoundFont(soundfontOverride))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading environment variable soundfont {soundfontOverride}");
|
||||
renderer.LoadSoundfontDisk(soundfontOverride);
|
||||
}
|
||||
}
|
||||
|
||||
// Load content-specific custom soundfonts, which should override the system/fallback soundfont.
|
||||
_midiSawmill.Debug($"Loading soundfonts from content directory {ContentCustomSoundfontDirectory}");
|
||||
foreach (var file in _resourceManager.ContentFindFiles(ContentCustomSoundfontDirectory))
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
|
||||
_midiSawmill.Debug($"Loading content soundfont {file}");
|
||||
renderer.LoadSoundfontResource(file);
|
||||
}
|
||||
|
||||
// Load every soundfont from the user data directory last, since those may override any other soundfont.
|
||||
_midiSawmill.Debug($"Loading soundfonts from user data directory {CustomSoundfontDirectory}");
|
||||
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath()}*").Item1;
|
||||
foreach (var file in enumerator)
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
|
||||
_midiSawmill.Debug($"Loading user soundfont {file}");
|
||||
renderer.LoadSoundfontUser(file);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string PrefixPath(string prefix, string value)
|
||||
{
|
||||
return $"{prefix}:{value}";
|
||||
}
|
||||
|
||||
internal static (string prefix, string? value) SplitPrefix(string filename)
|
||||
{
|
||||
var filenameSplit = filename.Split(':', 2);
|
||||
if (filenameSplit.Length == 1)
|
||||
return (filenameSplit[0], null);
|
||||
|
||||
return (filenameSplit[0], filenameSplit[1]);
|
||||
}
|
||||
|
||||
internal static bool IsValidPrefix(string prefix)
|
||||
{
|
||||
return prefix is PrefixLegacy or PrefixUser or PrefixResources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class is used to load soundfonts.
|
||||
/// </summary>
|
||||
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
|
||||
{
|
||||
private readonly MidiManager _parent;
|
||||
private readonly Dictionary<int, Stream> _openStreams = new();
|
||||
private int _nextStreamId = 1;
|
||||
|
||||
public ResourceLoaderCallbacks(MidiManager parent)
|
||||
{
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public override IntPtr Open(string filename)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
Stream stream;
|
||||
try
|
||||
{
|
||||
stream = OpenCore(filename);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_parent._midiSawmill.Error($"Error while opening sound font: {e}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
var id = _nextStreamId++;
|
||||
|
||||
_openStreams.Add(id, stream);
|
||||
|
||||
return (IntPtr) id;
|
||||
}
|
||||
|
||||
private Stream OpenCore(string filename)
|
||||
{
|
||||
var (prefix, value) = SplitPrefix(filename);
|
||||
|
||||
if (!IsValidPrefix(prefix) || value == null)
|
||||
return File.OpenRead(filename);
|
||||
|
||||
var resourceCache = _parent._resourceManager;
|
||||
var resourcePath = new ResPath(value);
|
||||
|
||||
switch (prefix)
|
||||
{
|
||||
case PrefixUser:
|
||||
return resourceCache.UserData.OpenRead(resourcePath);
|
||||
case PrefixResources:
|
||||
return resourceCache.ContentFileRead(resourcePath);
|
||||
case PrefixLegacy:
|
||||
// Try resources first, then try user data.
|
||||
if (resourceCache.TryContentFileRead(resourcePath, out var stream))
|
||||
return stream;
|
||||
|
||||
return resourceCache.UserData.OpenRead(resourcePath);
|
||||
default:
|
||||
throw new UnreachableException("Invalid prefix specified!");
|
||||
}
|
||||
}
|
||||
|
||||
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
|
||||
{
|
||||
var length = (int) count;
|
||||
var span = new Span<byte>(buf.ToPointer(), length);
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
|
||||
try
|
||||
{
|
||||
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
|
||||
if (count < 1024)
|
||||
{
|
||||
// ReSharper disable once SuggestVarOrType_Elsewhere
|
||||
Span<byte> buffer = stackalloc byte[(int)count];
|
||||
|
||||
stream.ReadExact(buffer);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
var buffer = stream.ReadExact(length);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
}
|
||||
catch (EndOfStreamException)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override int Seek(IntPtr sfHandle, long offset, SeekOrigin origin)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
stream.Seek(offset, origin);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override long Tell(IntPtr sfHandle)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
return (long) stream.Position;
|
||||
}
|
||||
|
||||
public override int Close(IntPtr sfHandle)
|
||||
{
|
||||
if (!_openStreams.Remove((int) sfHandle, out var stream))
|
||||
return -1;
|
||||
|
||||
stream.Dispose();
|
||||
return 0;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
private const string OsxSoundfont =
|
||||
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
|
||||
|
||||
private const string FallbackSoundfont = "/Midi/fallback.sf2";
|
||||
private static readonly ResPath FallbackSoundfont = new ResPath("/Midi/fallback.sf2");
|
||||
|
||||
private const string ContentCustomSoundfontDirectory = "/Audio/MidiCustom/";
|
||||
|
||||
@@ -265,81 +265,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
|
||||
|
||||
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
|
||||
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
|
||||
renderer.LoadSoundfont(FallbackSoundfont);
|
||||
|
||||
// Load system-specific soundfonts.
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
foreach (var filepath in LinuxSoundfonts)
|
||||
{
|
||||
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {filepath}");
|
||||
renderer.LoadSoundfont(filepath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {OsxSoundfont}");
|
||||
renderer.LoadSoundfont(OsxSoundfont);
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {WindowsSoundfont}");
|
||||
renderer.LoadSoundfont(WindowsSoundfont);
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe load soundfont specified in environment variable.
|
||||
// Load it here so it can override system soundfonts but not content or user data soundfonts.
|
||||
if (Environment.GetEnvironmentVariable(SoundfontEnvironmentVariable) is {} soundfontOverride)
|
||||
{
|
||||
if (File.Exists(soundfontOverride) && SoundFont.IsSoundFont(soundfontOverride))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading environment variable soundfont {soundfontOverride}");
|
||||
renderer.LoadSoundfont(soundfontOverride);
|
||||
}
|
||||
}
|
||||
|
||||
// Load content-specific custom soundfonts, which should override the system/fallback soundfont.
|
||||
_midiSawmill.Debug($"Loading soundfonts from content directory {ContentCustomSoundfontDirectory}");
|
||||
foreach (var file in _resourceManager.ContentFindFiles(ContentCustomSoundfontDirectory))
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
|
||||
_midiSawmill.Debug($"Loading content soundfont {file}");
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
|
||||
var userDataPath = _resourceManager.UserData.RootDir == null
|
||||
? CustomSoundfontDirectory
|
||||
: new ResPath(_resourceManager.UserData.RootDir) / CustomSoundfontDirectory.ToRelativePath();
|
||||
|
||||
// Load every soundfont from the user data directory last, since those may override any other soundfont.
|
||||
_midiSawmill.Debug($"Loading soundfonts from user data directory {userDataPath}");
|
||||
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath()}*").Item1;
|
||||
foreach (var file in enumerator)
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
|
||||
_midiSawmill.Debug($"Loading user soundfont {file}");
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
LoadSoundFontSetup(renderer);
|
||||
|
||||
renderer.Source.Gain = _gain;
|
||||
|
||||
@@ -572,130 +498,6 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
midiEvent.Velocity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class is used to load soundfonts.
|
||||
/// </summary>
|
||||
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
|
||||
{
|
||||
private readonly MidiManager _parent;
|
||||
private readonly Dictionary<int, Stream> _openStreams = new();
|
||||
private int _nextStreamId = 1;
|
||||
|
||||
public ResourceLoaderCallbacks(MidiManager parent)
|
||||
{
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public override IntPtr Open(string filename)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
Stream? stream;
|
||||
var resourceCache = _parent._resourceManager;
|
||||
var resourcePath = new ResPath(filename);
|
||||
|
||||
if (resourcePath.IsRooted)
|
||||
{
|
||||
// is it in content?
|
||||
if (resourceCache.ContentFileExists(filename))
|
||||
{
|
||||
if (!resourceCache.TryContentFileRead(filename, out stream))
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
// is it in userdata?
|
||||
else if (resourceCache.UserData.Exists(resourcePath))
|
||||
{
|
||||
stream = resourceCache.UserData.OpenRead(resourcePath);
|
||||
}
|
||||
else if (File.Exists(filename))
|
||||
{
|
||||
stream = File.OpenRead(filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
else if (File.Exists(filename))
|
||||
{
|
||||
stream = File.OpenRead(filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
var id = _nextStreamId++;
|
||||
|
||||
_openStreams.Add(id, stream);
|
||||
|
||||
return (IntPtr) id;
|
||||
}
|
||||
|
||||
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
|
||||
{
|
||||
var length = (int) count;
|
||||
var span = new Span<byte>(buf.ToPointer(), length);
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
|
||||
try
|
||||
{
|
||||
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
|
||||
if (count < 1024)
|
||||
{
|
||||
// ReSharper disable once SuggestVarOrType_Elsewhere
|
||||
Span<byte> buffer = stackalloc byte[(int)count];
|
||||
|
||||
stream.ReadExact(buffer);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
var buffer = stream.ReadExact(length);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
}
|
||||
catch (EndOfStreamException)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override int Seek(IntPtr sfHandle, long offset, SeekOrigin origin)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
stream.Seek(offset, origin);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override long Tell(IntPtr sfHandle)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
return (long) stream.Position;
|
||||
}
|
||||
|
||||
public override int Close(IntPtr sfHandle)
|
||||
{
|
||||
if (!_openStreams.Remove((int) sfHandle, out var stream))
|
||||
return -1;
|
||||
|
||||
stream.Dispose();
|
||||
return 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
private record struct MidiUpdateJob : IParallelRobustJob
|
||||
|
||||
45
Robust.Client/Audio/Midi/MidiRenderer.SoundFontLoad.cs
Normal file
45
Robust.Client/Audio/Midi/MidiRenderer.SoundFontLoad.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
internal sealed partial class MidiRenderer
|
||||
{
|
||||
[Obsolete("Use LoadSoundfontResource or LoadSoundfontUser instead")]
|
||||
public void LoadSoundfont(string filename, bool resetPresets = true)
|
||||
{
|
||||
LoadSoundfontCore(
|
||||
MidiManager.PrefixPath(MidiManager.PrefixLegacy, filename),
|
||||
resetPresets);
|
||||
}
|
||||
|
||||
public void LoadSoundfontResource(ResPath path, bool resetPresets = false)
|
||||
{
|
||||
LoadSoundfontCore(
|
||||
MidiManager.PrefixPath(MidiManager.PrefixResources, path.ToString()),
|
||||
resetPresets);
|
||||
}
|
||||
|
||||
public void LoadSoundfontUser(ResPath path, bool resetPresets = false)
|
||||
{
|
||||
LoadSoundfontCore(
|
||||
MidiManager.PrefixPath(MidiManager.PrefixUser, path.ToString()),
|
||||
resetPresets);
|
||||
}
|
||||
|
||||
internal void LoadSoundfontDisk(string path, bool resetPresets = false)
|
||||
{
|
||||
LoadSoundfontCore(
|
||||
path,
|
||||
resetPresets);
|
||||
}
|
||||
|
||||
private void LoadSoundfontCore(string filenameString, bool resetPresets)
|
||||
{
|
||||
lock (_playerStateLock)
|
||||
{
|
||||
_synth.LoadSoundFont(filenameString, resetPresets);
|
||||
MidiSoundfont = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
internal sealed class MidiRenderer : IMidiRenderer
|
||||
internal sealed partial class MidiRenderer : IMidiRenderer
|
||||
{
|
||||
private readonly IMidiManager _midiManager;
|
||||
private readonly ITaskManager _taskManager;
|
||||
@@ -435,15 +435,6 @@ internal sealed class MidiRenderer : IMidiRenderer
|
||||
_sequencer.RemoveEvents(SequencerClientId.Wildcard, SequencerClientId.Wildcard, -1);
|
||||
}
|
||||
|
||||
public void LoadSoundfont(string filename, bool resetPresets = true)
|
||||
{
|
||||
lock (_playerStateLock)
|
||||
{
|
||||
_synth.LoadSoundFont(filename, resetPresets);
|
||||
MidiSoundfont = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void IMidiRenderer.Render()
|
||||
{
|
||||
Render();
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
|
||||
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.
|
||||
// Because the just take the BB of the rotated BB, I'm pretty sure we do a lot of unnecessary maths.
|
||||
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
|
||||
}
|
||||
|
||||
|
||||
@@ -191,8 +191,16 @@ namespace Robust.Client.Console
|
||||
var shell = new ConsoleShell(this, session ?? _player.LocalSession, session == null);
|
||||
var cmdArgs = args.ToArray();
|
||||
|
||||
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
|
||||
cmd.Execute(shell, command, cmdArgs);
|
||||
try
|
||||
{
|
||||
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
|
||||
cmd.Execute(shell, command, cmdArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_conLogger.Error($"ExecuteError - {command}:\n{e}");
|
||||
shell.WriteError($"There was an error while executing the command: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanExecute(string cmdName)
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace Robust.Client.Debugging
|
||||
|
||||
foreach (var ent in _mapSystem.GetAnchoredEntities(gridUid, grid, spot))
|
||||
{
|
||||
if (EntityManager.TryGetComponent<MetaDataComponent>(ent, out var meta))
|
||||
if (TryComp<MetaDataComponent>(ent, out var meta))
|
||||
{
|
||||
text.AppendLine($"uid: {ent}, {meta.EntityName}");
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ namespace Robust.Client
|
||||
}
|
||||
|
||||
_serializationManager.Initialize();
|
||||
_loc.Initialize();
|
||||
|
||||
// Call Init in game assemblies.
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
|
||||
@@ -182,7 +183,6 @@ namespace Robust.Client
|
||||
_serializer.Initialize();
|
||||
_inputManager.Initialize();
|
||||
_console.Initialize();
|
||||
_loc.Initialize();
|
||||
|
||||
// Make sure this is done before we try to load prototypes,
|
||||
// avoid any possibility of race conditions causing the check to not finish
|
||||
@@ -387,7 +387,7 @@ namespace Robust.Client
|
||||
|
||||
_prof.Initialize();
|
||||
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
|
||||
|
||||
var mountOptions = _commandLineArgs != null
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
|
||||
|
||||
@@ -296,7 +296,7 @@ namespace Robust.Client.GameObjects
|
||||
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
|
||||
{
|
||||
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|
||||
|| ent.Comp1.EntityDeleted
|
||||
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
|
||||
{
|
||||
return;
|
||||
@@ -322,7 +322,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
if (IsQueuedForDeletion(ent.Owner)
|
||||
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|
||||
|| ent.Comp1.EntityDeleted
|
||||
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -24,9 +24,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public sealed class SpriteBoundsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
|
||||
|
||||
private SpriteBoundsOverlay? _overlay;
|
||||
|
||||
@@ -42,7 +40,7 @@ namespace Robust.Client.GameObjects
|
||||
if (_enabled)
|
||||
{
|
||||
DebugTools.AssertNull(_overlay);
|
||||
_overlay = new SpriteBoundsOverlay(_spriteTree, _xformSystem);
|
||||
_overlay = new SpriteBoundsOverlay(EntityManager);
|
||||
_overlayManager.AddOverlay(_overlay);
|
||||
}
|
||||
else
|
||||
@@ -57,18 +55,13 @@ namespace Robust.Client.GameObjects
|
||||
private bool _enabled;
|
||||
}
|
||||
|
||||
public sealed class SpriteBoundsOverlay : Overlay
|
||||
public sealed class SpriteBoundsOverlay(IEntityManager entMan) : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
private readonly SharedTransformSystem _xformSystem;
|
||||
private SpriteTreeSystem _renderTree;
|
||||
|
||||
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, SharedTransformSystem xformSystem)
|
||||
{
|
||||
_renderTree = renderTree;
|
||||
_xformSystem = xformSystem;
|
||||
}
|
||||
private readonly SharedTransformSystem _xformSystem = entMan.System<SharedTransformSystem>();
|
||||
private readonly SpriteSystem _spriteSystem = entMan.System<SpriteSystem>();
|
||||
private readonly SpriteTreeSystem _renderTree = entMan.System<SpriteTreeSystem>();
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
@@ -76,10 +69,11 @@ namespace Robust.Client.GameObjects
|
||||
var currentMap = args.MapId;
|
||||
var viewport = args.WorldBounds;
|
||||
|
||||
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
|
||||
foreach (var entry in _renderTree.QueryAabb(currentMap, viewport))
|
||||
{
|
||||
var (sprite, xform) = entry;
|
||||
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
|
||||
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
|
||||
var bounds = _spriteSystem.CalculateBounds((entry.Uid, sprite), worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
|
||||
|
||||
// Get scaled down bounds used to indicate the "south" of a sprite.
|
||||
var localBound = bounds.Box;
|
||||
|
||||
@@ -39,7 +39,6 @@ namespace Robust.Client.GameObjects
|
||||
[RegisterComponent]
|
||||
public sealed partial class SpriteComponent : Component, IComponentDebug, ISerializationHooks, IComponentTreeEntry<SpriteComponent>, IAnimationProperties
|
||||
{
|
||||
#region ECSd
|
||||
public const string LogCategory = "go.comp.sprite";
|
||||
|
||||
[Dependency] private readonly IResourceCache resourceCache = default!;
|
||||
@@ -59,12 +58,13 @@ namespace Robust.Client.GameObjects
|
||||
[DataField] // TODO Sprite access restrict.
|
||||
public bool GranularLayersRendering = false;
|
||||
|
||||
[DataField]
|
||||
[DataField("visible")]
|
||||
internal bool _visible = true;
|
||||
|
||||
// VV convenience variable to examine layer objects using layer keys
|
||||
// ReSharper disable once UnusedMember.Local
|
||||
[ViewVariables]
|
||||
private Dictionary<object, Layer> _mappedLayers => LayerMap.ToDictionary(x => x.Key, x => Layers[x.Value]);
|
||||
private Dictionary<object, Layer> MappedLayers => LayerMap.ToDictionary(x => x.Key, x => Layers[x.Value]);
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Visible
|
||||
@@ -93,7 +93,7 @@ namespace Robust.Client.GameObjects
|
||||
set => Sys.SetDrawDepth((Owner, this), value);
|
||||
}
|
||||
|
||||
[DataField]
|
||||
[DataField("scale")] // Explicit name, in case this field ever gets renamed
|
||||
internal Vector2 scale = Vector2.One;
|
||||
|
||||
/// <summary>
|
||||
@@ -108,7 +108,7 @@ namespace Robust.Client.GameObjects
|
||||
set => Sys.SetScale((Owner, this), value);
|
||||
}
|
||||
|
||||
[DataField]
|
||||
[DataField("rotation")] // Explicit name, in case this field ever gets renamed
|
||||
internal Angle rotation = Angle.Zero;
|
||||
|
||||
[Animatable]
|
||||
@@ -120,7 +120,7 @@ namespace Robust.Client.GameObjects
|
||||
set => Sys.SetRotation((Owner, this), value);
|
||||
}
|
||||
|
||||
[DataField]
|
||||
[DataField("offset")] // Explicit name, in case this field ever gets renamed
|
||||
internal Vector2 offset = Vector2.Zero;
|
||||
|
||||
/// <summary>
|
||||
@@ -135,7 +135,7 @@ namespace Robust.Client.GameObjects
|
||||
set => Sys.SetOffset((Owner, this), value);
|
||||
}
|
||||
|
||||
[DataField]
|
||||
[DataField("color")] // Explicit name, in case this field ever gets renamed
|
||||
internal Color color = Color.White;
|
||||
|
||||
[Animatable]
|
||||
@@ -1052,8 +1052,6 @@ namespace Robust.Client.GameObjects
|
||||
return Sys.CalculateBounds((Owner, this), worldPosition, worldRotation, eyeRot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Enum to "offset" a cardinal direction.
|
||||
/// </summary>
|
||||
@@ -1226,6 +1224,8 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
_visible = value;
|
||||
|
||||
Owner.Comp.BoundsDirty = true;
|
||||
|
||||
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
|
||||
if (_parent.Owner != EntityUid.Invalid)
|
||||
Owner.Comp.Sys?.QueueUpdateIsInert(Owner);
|
||||
@@ -1793,76 +1793,15 @@ namespace Robust.Client.GameObjects
|
||||
[Obsolete("Use SpriteSystem.GetPrototypeTextures() instead")]
|
||||
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot)
|
||||
{
|
||||
var results = new List<IDirectionalTextureProvider>();
|
||||
noRot = false;
|
||||
|
||||
// TODO when moving to a non-static method in a system, pass in IComponentFactory
|
||||
if (prototype.TryGetComponent(out IconComponent? icon))
|
||||
{
|
||||
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
results.Add(sys.GetIcon(icon));
|
||||
return results;
|
||||
}
|
||||
|
||||
if (!prototype.Components.TryGetValue("Sprite", out _))
|
||||
{
|
||||
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
|
||||
return results;
|
||||
}
|
||||
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
|
||||
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
|
||||
EntitySystem.Get<AppearanceSystem>().OnChangeData(dummy, spriteComponent);
|
||||
|
||||
foreach (var layer in spriteComponent.AllLayers)
|
||||
{
|
||||
if (!layer.Visible) continue;
|
||||
|
||||
if (layer.Texture != null)
|
||||
{
|
||||
results.Add(layer.Texture);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!layer.RsiState.IsValid) continue;
|
||||
|
||||
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
|
||||
if (rsi == null ||
|
||||
!rsi.TryGetState(layer.RsiState, out var state))
|
||||
continue;
|
||||
|
||||
results.Add(state);
|
||||
}
|
||||
|
||||
noRot = spriteComponent.NoRotation;
|
||||
|
||||
entityManager.DeleteEntity(dummy);
|
||||
|
||||
if (results.Count == 0)
|
||||
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
|
||||
|
||||
return results;
|
||||
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
return sys.GetPrototypeTextures(prototype, out noRot);
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem.GetPrototypeIcon() instead")]
|
||||
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
|
||||
{
|
||||
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
|
||||
// TODO when moving to a non-static method in a system, pass in IComponentFactory
|
||||
if (prototype.TryGetComponent(out IconComponent? icon))
|
||||
return sys.GetIcon(icon);
|
||||
|
||||
if (!prototype.Components.ContainsKey("Sprite"))
|
||||
return sys.GetFallbackState();
|
||||
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
|
||||
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
|
||||
var result = spriteComponent.Icon ?? sys.GetFallbackState();
|
||||
entityManager.DeleteEntity(dummy);
|
||||
|
||||
return result;
|
||||
return sys.GetPrototypeIcon(prototype);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace Robust.Client.GameObjects
|
||||
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
|
||||
public void Play(EntityUid uid, AnimationPlayerComponent? component, Animation animation, string key)
|
||||
{
|
||||
component ??= EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
|
||||
component ??= EnsureComp<AnimationPlayerComponent>(uid);
|
||||
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public bool HasRunningAnimation(EntityUid uid, string key)
|
||||
{
|
||||
return EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? component) &&
|
||||
return TryComp(uid, out AnimationPlayerComponent? component) &&
|
||||
component.PlayingAnimations.ContainsKey(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Shared.Containers.ContainerManagerComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
@@ -58,7 +57,7 @@ namespace Robust.Client.GameObjects
|
||||
if (!RemoveExpectedEntity(meta.NetEntity, out var container))
|
||||
return;
|
||||
|
||||
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container);
|
||||
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container, force: true);
|
||||
}
|
||||
|
||||
public override void ShutdownContainer(BaseContainer container)
|
||||
@@ -232,7 +231,7 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
Insert(message.Entity, container);
|
||||
Insert(message.Entity, container, force: true);
|
||||
}
|
||||
|
||||
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)
|
||||
|
||||
@@ -223,12 +223,12 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private void SetEntityContextActive(IInputManager inputMan, EntityUid entity)
|
||||
{
|
||||
if(entity == default || !EntityManager.EntityExists(entity))
|
||||
if(entity == default || !Exists(entity))
|
||||
throw new ArgumentNullException(nameof(entity));
|
||||
|
||||
if (!EntityManager.TryGetComponent(entity, out InputComponent? inputComp))
|
||||
if (!TryComp(entity, out InputComponent? inputComp))
|
||||
{
|
||||
_sawmillInputContext.Debug($"AttachedEnt has no InputComponent: entId={entity}, entProto={EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype}. Setting default \"{InputContextContainer.DefaultContextName}\" context...");
|
||||
_sawmillInputContext.Debug($"AttachedEnt has no InputComponent: entId={entity}, entProto={Comp<MetaDataComponent>(entity).EntityPrototype}. Setting default \"{InputContextContainer.DefaultContextName}\" context...");
|
||||
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
|
||||
return;
|
||||
}
|
||||
@@ -239,7 +239,7 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
else
|
||||
{
|
||||
_sawmillInputContext.Error($"Unknown context: entId={entity}, entProto={EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context...");
|
||||
_sawmillInputContext.Error($"Unknown context: entId={entity}, entProto={Comp<MetaDataComponent>(entity).EntityPrototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context...");
|
||||
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public sealed class ShowPlayerVelocityDebugSystem : EntitySystem
|
||||
|
||||
var player = _playerManager.LocalEntity;
|
||||
|
||||
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
|
||||
if (player == null || !TryComp(player.Value, out PhysicsComponent? body))
|
||||
{
|
||||
_label.Visible = false;
|
||||
return;
|
||||
|
||||
@@ -56,10 +56,6 @@ public sealed partial class SpriteSystem
|
||||
/// </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.
|
||||
@@ -67,11 +63,7 @@ public sealed partial class SpriteSystem
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
// Generate the icon and cache it in case it's ever needed again.
|
||||
var result = GetPrototypeIcon(entityPrototype);
|
||||
_cachedPrototypeIcons[prototype] = result;
|
||||
|
||||
return result;
|
||||
return GetPrototypeIcon(entityPrototype);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,13 +71,19 @@ public sealed partial class SpriteSystem
|
||||
/// This method does NOT cache the result.
|
||||
/// </summary>
|
||||
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
|
||||
{
|
||||
// This method may spawn & delete an entity to get an accruate RSI state, hence we cache the results
|
||||
if (_cachedPrototypeIcons.TryGetValue(prototype.ID, out var cachedResult))
|
||||
return cachedResult;
|
||||
|
||||
return _cachedPrototypeIcons[prototype.ID] = GetPrototypeIconInternal(prototype);
|
||||
}
|
||||
|
||||
private IRsiStateLike GetPrototypeIconInternal(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)
|
||||
{
|
||||
if (prototype.TryGetComponent(out IconComponent? icon, _factory))
|
||||
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"))
|
||||
@@ -102,6 +100,63 @@ public sealed partial class SpriteSystem
|
||||
return result;
|
||||
}
|
||||
|
||||
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto) =>
|
||||
GetPrototypeTextures(proto, out _);
|
||||
|
||||
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto, out bool noRot)
|
||||
{
|
||||
var results = new List<IDirectionalTextureProvider>();
|
||||
noRot = false;
|
||||
|
||||
if (proto.TryGetComponent(out IconComponent? icon, _factory))
|
||||
{
|
||||
results.Add(GetIcon(icon));
|
||||
return results;
|
||||
}
|
||||
|
||||
if (!proto.Components.ContainsKey("Sprite"))
|
||||
{
|
||||
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
|
||||
return results;
|
||||
}
|
||||
|
||||
var dummy = Spawn(proto.ID, MapCoordinates.Nullspace);
|
||||
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
|
||||
|
||||
// TODO SPRITE is this needed?
|
||||
// And if it is, shouldn't GetPrototypeIconInternal also use this?
|
||||
_appearance.OnChangeData(dummy, spriteComponent);
|
||||
|
||||
foreach (var layer in spriteComponent.AllLayers)
|
||||
{
|
||||
if (!layer.Visible)
|
||||
continue;
|
||||
|
||||
if (layer.Texture != null)
|
||||
{
|
||||
results.Add(layer.Texture);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!layer.RsiState.IsValid)
|
||||
continue;
|
||||
|
||||
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
|
||||
if (rsi == null || !rsi.TryGetState(layer.RsiState, out var state))
|
||||
continue;
|
||||
|
||||
results.Add(state);
|
||||
}
|
||||
|
||||
noRot = spriteComponent.NoRotation;
|
||||
Del(dummy);
|
||||
|
||||
if (results.Count == 0)
|
||||
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public RSI.State GetFallbackState()
|
||||
{
|
||||
|
||||
@@ -213,32 +213,30 @@ public sealed partial class SpriteSystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new blank layer and map the given key to it.
|
||||
/// 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 (LayerExists(sprite, key))
|
||||
throw new Exception("Layer already exists");
|
||||
if (LayerMapTryGet(sprite, key, out var layerIndex, false))
|
||||
return layerIndex;
|
||||
|
||||
var layer = AddBlankLayer(sprite!);
|
||||
LayerMapSet(sprite, key, layer.Index);
|
||||
return layer.Index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A create a new blank layer and map the given key to it. If possible, it is preferred to use an enum key.
|
||||
/// string keys mainly exist to make it easier to define custom layer keys in yaml.
|
||||
/// </summary>
|
||||
/// <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 (LayerExists(sprite, key))
|
||||
throw new Exception("Layer already exists");
|
||||
if (LayerMapTryGet(sprite, key, out var layerIndex, false))
|
||||
return layerIndex;
|
||||
|
||||
var layer = AddBlankLayer(sprite!);
|
||||
LayerMapSet(sprite, key, layer.Index);
|
||||
|
||||
@@ -34,8 +34,12 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IComponentFactory _factory = default!;
|
||||
|
||||
// Note that any new system dependencies have to be added to RobustUnitTest.BaseSetup()
|
||||
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||
[Dependency] private readonly SpriteTreeSystem _tree = default!;
|
||||
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
||||
|
||||
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
|
||||
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
|
||||
|
||||
@@ -11,6 +11,7 @@ public abstract class VisualizerSystem<T> : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly AppearanceSystem AppearanceSystem = default!;
|
||||
[Dependency] protected readonly AnimationPlayerSystem AnimationSystem = default!;
|
||||
[Dependency] protected readonly SpriteSystem SpriteSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -631,7 +631,7 @@ namespace Robust.Client.GameStates
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
|
||||
|
||||
if (compState != null)
|
||||
if ((meta.Flags & MetaDataFlags.Detached) == 0 && compState != null)
|
||||
{
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
_entities.EventBus.RaiseComponentEvent(entity, comp, ref handleState);
|
||||
|
||||
@@ -254,9 +254,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
region = regionMaybe[tile.Variant];
|
||||
}
|
||||
|
||||
var rotationMirroring = _tileDefinitionManager[tile.TypeId].AllowRotationMirror
|
||||
? tile.RotationMirroring
|
||||
: 0;
|
||||
var rotationMirroring = (_tileDefinitionManager.TryGetDefinition(tile.TypeId, out var tileDef) && tileDef.AllowRotationMirror) ?
|
||||
tile.RotationMirroring
|
||||
: 0;
|
||||
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, rotationMirroring);
|
||||
i += 1;
|
||||
@@ -412,8 +412,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
|
||||
{
|
||||
var gridData = _mapChunkData.GetOrNew(args.Entity);
|
||||
if (gridData.TryGetValue(args.ChunkIndex, out var data))
|
||||
data.Dirty = true;
|
||||
foreach (var change in args.Changes)
|
||||
{
|
||||
if (gridData.TryGetValue(change.ChunkIndex, out var data))
|
||||
data.Dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void _updateOnGridCreated(GridStartupEvent ev)
|
||||
|
||||
@@ -153,7 +153,7 @@ internal partial class Clyde
|
||||
|
||||
// special casing angle = n*pi/2 to avoid box rotation & bounding calculations doesn't seem to give significant speedups.
|
||||
data.SpriteScreenBB = TransformCenteredBox(
|
||||
data.Sprite.Bounds,
|
||||
_spriteSystem.GetLocalBounds((data.Uid, data.Sprite)),
|
||||
finalRotation,
|
||||
pos + batch.PreScaleViewOffset,
|
||||
batch.ViewScale);
|
||||
|
||||
@@ -10,6 +10,7 @@ internal sealed partial class Clyde
|
||||
private MapSystem _mapSystem = default!;
|
||||
private LightTreeSystem _lightTreeSystem = default!;
|
||||
private TransformSystem _transformSystem = default!;
|
||||
private SpriteSystem _spriteSystem = default!;
|
||||
private SpriteTreeSystem _spriteTreeSystem = default!;
|
||||
private ClientOccluderSystem _occluderSystem = default!;
|
||||
|
||||
@@ -24,6 +25,7 @@ internal sealed partial class Clyde
|
||||
_mapSystem = _entitySystemManager.GetEntitySystem<MapSystem>();
|
||||
_lightTreeSystem = _entitySystemManager.GetEntitySystem<LightTreeSystem>();
|
||||
_transformSystem = _entitySystemManager.GetEntitySystem<TransformSystem>();
|
||||
_spriteSystem = _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||
_spriteTreeSystem = _entitySystemManager.GetEntitySystem<SpriteTreeSystem>();
|
||||
_occluderSystem = _entitySystemManager.GetEntitySystem<ClientOccluderSystem>();
|
||||
}
|
||||
@@ -33,6 +35,7 @@ internal sealed partial class Clyde
|
||||
_mapSystem = null!;
|
||||
_lightTreeSystem = null!;
|
||||
_transformSystem = null!;
|
||||
_spriteSystem = null!;
|
||||
_spriteTreeSystem = null!;
|
||||
_occluderSystem = null!;
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_windowing!.RunOnWindowThread(a);
|
||||
}
|
||||
|
||||
public IFileDialogManager? FileDialogImpl => _windowing as IFileDialogManager;
|
||||
public IFileDialogManagerImplementation? FileDialogImpl => _windowing as IFileDialogManagerImplementation;
|
||||
|
||||
private abstract class WindowReg
|
||||
{
|
||||
|
||||
@@ -307,7 +307,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
action();
|
||||
}
|
||||
|
||||
public IFileDialogManager? FileDialogImpl => null;
|
||||
public IFileDialogManagerImplementation? FileDialogImpl => null;
|
||||
|
||||
private sealed class DummyCursor : ICursor
|
||||
{
|
||||
|
||||
@@ -662,6 +662,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
var icons = _clyde.LoadWindowIcons().ToArray();
|
||||
|
||||
// Done if no icon (e.g., macOS)
|
||||
if (icons.Length == 0)
|
||||
return;
|
||||
|
||||
// Turn each image into a byte[] so we can actually pin their contents.
|
||||
// Wish I knew a clean way to do this without allocations.
|
||||
var images = icons
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -12,31 +11,16 @@ namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl : IFileDialogManager
|
||||
private sealed partial class Sdl3WindowingImpl : IFileDialogManagerImplementation
|
||||
{
|
||||
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
public async Task<string?> OpenFile(FileDialogFilters? filters)
|
||||
{
|
||||
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
|
||||
if (fileName == null)
|
||||
return null;
|
||||
|
||||
return File.OpenRead(fileName);
|
||||
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
|
||||
}
|
||||
|
||||
public async Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
|
||||
public async Task<string?> SaveFile(FileDialogFilters? filters)
|
||||
{
|
||||
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
|
||||
if (fileName == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return (File.Open(fileName, truncate ? FileMode.Truncate : FileMode.Open), true);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return (File.Open(fileName, FileMode.Create), false);
|
||||
}
|
||||
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
|
||||
}
|
||||
|
||||
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
|
||||
@@ -74,6 +58,8 @@ internal partial class Clyde
|
||||
NativeMemory.Free(filter.name);
|
||||
NativeMemory.Free(filter.pattern);
|
||||
}
|
||||
|
||||
NativeMemory.Free(filtersAlloc);
|
||||
}
|
||||
|
||||
return task;
|
||||
|
||||
@@ -71,6 +71,6 @@ namespace Robust.Client.Graphics
|
||||
|
||||
void RunOnWindowThread(Action action);
|
||||
|
||||
IFileDialogManager? FileDialogImpl { get; }
|
||||
IFileDialogManagerImplementation? FileDialogImpl { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Robust.Client.Physics
|
||||
* This will draw above every body involved in a particular island solve.
|
||||
*/
|
||||
|
||||
public readonly Queue<(TimeSpan Time, List<PhysicsComponent> Bodies)> IslandSolve = new();
|
||||
public readonly Queue<(TimeSpan Time, List<Entity<PhysicsComponent, TransformComponent>> Bodies)> IslandSolve = new();
|
||||
public const float SolveDuration = 0.1f;
|
||||
|
||||
public override void Initialize()
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Robust.Client.Physics
|
||||
|
||||
// Add new joint (if possible).
|
||||
// Need to wait for BOTH joint components to come in first before we can add it. Yay dependencies!
|
||||
if (!EntityManager.HasComponent<JointComponent>(other))
|
||||
if (!HasComp<JointComponent>(other))
|
||||
continue;
|
||||
|
||||
// TODO: if (other entity is outside of PVS range) continue;
|
||||
|
||||
@@ -90,9 +90,10 @@ public sealed partial class PhysicsSystem
|
||||
// existing contacts for predicted entities before performing any actual prediction.
|
||||
|
||||
var contacts = new List<Contact>();
|
||||
var maps = new HashSet<EntityUid>();
|
||||
|
||||
var enumerator = AllEntityQuery<PredictedPhysicsComponent, PhysicsComponent, TransformComponent>();
|
||||
_broadphase.FindNewContacts();
|
||||
|
||||
while (enumerator.MoveNext(out _, out var physics, out var xform))
|
||||
{
|
||||
DebugTools.Assert(physics.Predict);
|
||||
@@ -100,10 +101,6 @@ public sealed partial class PhysicsSystem
|
||||
if (xform.MapUid is not { } map)
|
||||
continue;
|
||||
|
||||
if (maps.Add(map) && PhysMapQuery.TryGetComponent(map, out var physMap) &&
|
||||
MapQuery.TryGetComponent(map, out var mapComp))
|
||||
_broadphase.FindNewContacts(physMap, mapComp.MapId);
|
||||
|
||||
contacts.AddRange(physics.Contacts);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Physics;
|
||||
@@ -23,21 +24,23 @@ namespace Robust.Client.Physics
|
||||
SimulateWorld(frameTime, _gameTiming.InPrediction);
|
||||
}
|
||||
|
||||
protected override void Cleanup(PhysicsMapComponent component, float frameTime)
|
||||
protected override void Cleanup(float frameTime)
|
||||
{
|
||||
var toRemove = new List<Entity<PhysicsComponent>>();
|
||||
var toRemove = new ValueList<Entity<PhysicsComponent, TransformComponent>>();
|
||||
|
||||
// Because we're not predicting 99% of bodies its sleep timer never gets incremented so we'll just do it ourselves.
|
||||
// (and serializing it over the network isn't necessary?)
|
||||
// This is a client-only problem.
|
||||
// Also need to suss out having the client build the island anyway and just... not solving it?
|
||||
foreach (var body in component.AwakeBodies)
|
||||
foreach (var ent in AwakeBodies)
|
||||
{
|
||||
var body = ent.Comp1;
|
||||
|
||||
if (!body.SleepingAllowed || body.LinearVelocity.Length() > LinearToleranceSqr / 2f || body.AngularVelocity * body.AngularVelocity > AngularToleranceSqr / 2f) continue;
|
||||
body.SleepTime += frameTime;
|
||||
if (body.SleepTime > TimeToSleep)
|
||||
{
|
||||
toRemove.Add(new Entity<PhysicsComponent>(body.Owner, body));
|
||||
toRemove.Add(ent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,37 +49,38 @@ namespace Robust.Client.Physics
|
||||
SetAwake(body, false);
|
||||
}
|
||||
|
||||
base.Cleanup(component, frameTime);
|
||||
base.Cleanup(frameTime);
|
||||
}
|
||||
|
||||
protected override void UpdateLerpData(PhysicsMapComponent component, List<PhysicsComponent> bodies, EntityQuery<TransformComponent> xformQuery)
|
||||
protected override void UpdateLerpData(List<Entity<PhysicsComponent, TransformComponent>> bodies)
|
||||
{
|
||||
foreach (var body in bodies)
|
||||
foreach (var bodyEnt in bodies)
|
||||
{
|
||||
var body = bodyEnt.Comp1;
|
||||
var xform = bodyEnt.Comp2;
|
||||
|
||||
if (body.BodyType == BodyType.Static ||
|
||||
component.LerpData.TryGetValue(body.Owner, out var lerpData) ||
|
||||
!xformQuery.TryGetComponent(body.Owner, out var xform) ||
|
||||
lerpData.ParentUid == xform.ParentUid)
|
||||
LerpData.TryGetValue(bodyEnt, out var lerpData) ||
|
||||
lerpData == xform.ParentUid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
component.LerpData[xform.Owner] = (xform.ParentUid, xform.LocalPosition, xform.LocalRotation);
|
||||
LerpData[bodyEnt.Owner] = xform.ParentUid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush all of our lerping data.
|
||||
/// </summary>
|
||||
protected override void FinalStep(PhysicsMapComponent component)
|
||||
protected override void FinalStep()
|
||||
{
|
||||
base.FinalStep(component);
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
base.FinalStep();
|
||||
|
||||
foreach (var (uid, (parentUid, position, rotation)) in component.LerpData)
|
||||
foreach (var (uid, parentUid) in LerpData)
|
||||
{
|
||||
if (!xformQuery.TryGetComponent(uid, out var xform) ||
|
||||
!parentUid.IsValid())
|
||||
// Can't just re-use xform from before as movement events may cause event subs to fire.
|
||||
if (!XformQuery.TryGetComponent(uid, out var xform) || !parentUid.IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -85,7 +89,7 @@ namespace Robust.Client.Physics
|
||||
_transform.SetLocalPositionRotation(uid, xform.LocalPosition, xform.LocalRotation, xform);
|
||||
}
|
||||
|
||||
component.LerpData.Clear();
|
||||
LerpData.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace Robust.Client.Placement
|
||||
|
||||
private SharedMapSystem Maps => EntityManager.System<SharedMapSystem>();
|
||||
private SharedTransformSystem XformSystem => EntityManager.System<SharedTransformSystem>();
|
||||
private SpriteSystem Sprite => EntityManager.System<SpriteSystem>();
|
||||
|
||||
/// <summary>
|
||||
/// How long before a pending tile change is dropped.
|
||||
@@ -359,12 +360,15 @@ namespace Robust.Client.Placement
|
||||
|
||||
private void HandleTileChanged(ref TileChangedEvent args)
|
||||
{
|
||||
var coords = Maps.GridTileToLocal(
|
||||
args.NewTile.GridUid,
|
||||
EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid),
|
||||
args.NewTile.GridIndices);
|
||||
foreach (var change in args.Changes)
|
||||
{
|
||||
var coords = Maps.GridTileToLocal(
|
||||
args.Entity,
|
||||
args.Entity.Comp,
|
||||
change.GridIndices);
|
||||
|
||||
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
|
||||
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -708,11 +712,11 @@ namespace Robust.Client.Placement
|
||||
CurrentPlacementOverlayEntity = null;
|
||||
}
|
||||
|
||||
private SpriteComponent SetupPlacementOverlayEntity()
|
||||
private Entity<SpriteComponent> SetupPlacementOverlayEntity()
|
||||
{
|
||||
EnsureNoPlacementOverlayEntity();
|
||||
CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
return EntityManager.EnsureComponent<SpriteComponent>(CurrentPlacementOverlayEntity.Value);
|
||||
return (CurrentPlacementOverlayEntity.Value, EntityManager.EnsureComponent<SpriteComponent>(CurrentPlacementOverlayEntity.Value));
|
||||
}
|
||||
|
||||
private void PreparePlacement(string templateName)
|
||||
@@ -729,10 +733,16 @@ namespace Robust.Client.Placement
|
||||
EntityManager.GetComponent<MetaDataComponent>(CurrentPlacementOverlayEntity.Value));
|
||||
}
|
||||
|
||||
public void PreparePlacementSprite(SpriteComponent sprite)
|
||||
public void PreparePlacementSprite(Entity<SpriteComponent> sprite)
|
||||
{
|
||||
var sc = SetupPlacementOverlayEntity();
|
||||
sc.CopyFrom(sprite);
|
||||
Sprite.CopySprite(sprite.AsNullable(), sc.AsNullable());
|
||||
}
|
||||
|
||||
[Obsolete("Use the Entity<SpriteComponent> overload.")]
|
||||
public void PreparePlacementSprite(SpriteComponent sprite)
|
||||
{
|
||||
PreparePlacementSprite((sprite.Owner, sprite));
|
||||
}
|
||||
|
||||
public void PreparePlacementTexList(List<IDirectionalTextureProvider>? texs, bool noRot, EntityPrototype? prototype)
|
||||
@@ -743,27 +753,27 @@ namespace Robust.Client.Placement
|
||||
// This one covers most cases (including Construction)
|
||||
foreach (var v in texs)
|
||||
{
|
||||
if (v is RSI.State)
|
||||
if (v is RSI.State st)
|
||||
{
|
||||
var st = (RSI.State) v;
|
||||
sc.AddLayer(st.StateId, st.RSI);
|
||||
Sprite.AddRsiLayer(sc.AsNullable(), st.StateId, st.RSI);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
sc.AddLayer(v.Default);
|
||||
Sprite.AddTextureLayer(sc.AsNullable(), v.Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sc.AddLayer(new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
Sprite.AddTextureLayer(sc.AsNullable(), new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
}
|
||||
sc.NoRotation = noRot;
|
||||
|
||||
sc.Comp.NoRotation = noRot;
|
||||
|
||||
if (prototype != null && prototype.TryGetComponent<SpriteComponent>("Sprite", out var spriteComp))
|
||||
{
|
||||
sc.Scale = spriteComp.Scale;
|
||||
Sprite.SetScale(sc.AsNullable(), spriteComp.Scale);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -771,7 +781,7 @@ namespace Robust.Client.Placement
|
||||
private void PreparePlacementTile()
|
||||
{
|
||||
var sc = SetupPlacementOverlayEntity();
|
||||
sc.AddLayer(new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
Sprite.AddTextureLayer(sc.AsNullable(), new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ namespace Robust.Client.Player
|
||||
{
|
||||
if (_client.RunLevel != ClientRunLevel.SinglePlayerGame)
|
||||
Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues.");
|
||||
var eye = (EyeComponent) Factory.GetComponent(typeof(EyeComponent));
|
||||
var eye = Factory.GetComponent<EyeComponent>();
|
||||
eye.NetSyncEnabled = false;
|
||||
EntManager.AddComponent(uid.Value, eye);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class LoadPrototypeCommand : IConsoleCommand
|
||||
var dialogManager = IoCManager.Resolve<IFileDialogManager>();
|
||||
var loadManager = IoCManager.Resolve<IGamePrototypeLoadManager>();
|
||||
|
||||
var stream = await dialogManager.OpenFile();
|
||||
var stream = await dialogManager.OpenFile(access: FileAccess.Read);
|
||||
if (stream is null)
|
||||
return;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -36,7 +37,7 @@ public sealed class UploadFileCommand : IConsoleCommand
|
||||
var path = new ResPath(args[0]).ToRelativePath();
|
||||
|
||||
var filters = new FileDialogFilters(new FileDialogFilters.Group(path.Extension));
|
||||
await using var file = await _dialog.OpenFile(filters);
|
||||
await using var file = await _dialog.OpenFile(filters, FileAccess.Read);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
|
||||
@@ -994,6 +994,9 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
internal int DoFrameUpdateRecursive(FrameEventArgs args)
|
||||
{
|
||||
if (!Visible)
|
||||
return 0;
|
||||
|
||||
var total = 1;
|
||||
FrameUpdate(args);
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controllers;
|
||||
@@ -11,11 +14,14 @@ namespace Robust.Client.UserInterface.Controllers;
|
||||
/// and <see cref="UISystemDependencyAttribute"/> to depend on <see cref="EntitySystem"/>s, which will be automatically
|
||||
/// injected once they are created.
|
||||
/// </summary>
|
||||
public abstract partial class UIController
|
||||
public abstract partial class UIController : IPostInjectInit
|
||||
{
|
||||
[Dependency] protected readonly IUserInterfaceManager UIManager = default!;
|
||||
[Dependency] protected readonly IEntitySystemManager EntitySystemManager = default!;
|
||||
[Dependency] protected readonly IEntityManager EntityManager = default!;
|
||||
[Dependency] protected readonly ILogManager LogManager = default!;
|
||||
|
||||
public ISawmill Log { get; protected set; } = default!;
|
||||
|
||||
public virtual void Initialize()
|
||||
{
|
||||
@@ -24,4 +30,39 @@ public abstract partial class UIController
|
||||
public virtual void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual string SawmillName
|
||||
{
|
||||
get
|
||||
{
|
||||
var name = GetType().Name;
|
||||
|
||||
// Strip trailing "UIController"
|
||||
if (name.EndsWith("UIController"))
|
||||
name = name.Substring(0, name.Length - "UIController".Length);
|
||||
|
||||
// Convert CamelCase to snake_case
|
||||
// Ignore if all uppercase, assume acronym (e.g. NPC or HTN)
|
||||
if (name.All(char.IsUpper))
|
||||
{
|
||||
name = name.ToLower(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
name = string.Concat(name.Select(x => char.IsUpper(x) ? $"_{char.ToLower(x)}" : x.ToString()));
|
||||
name = name.Trim('_');
|
||||
}
|
||||
|
||||
return $"ui.{name}";
|
||||
}
|
||||
}
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
Log = LogManager.GetSawmill(SawmillName);
|
||||
|
||||
#if !DEBUG
|
||||
Log.Level = LogLevel.Info;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ public sealed class ColorSelectorSliders : Control
|
||||
break;
|
||||
}
|
||||
_currentType = value;
|
||||
_typeSelector.Select(_types.IndexOf(value));
|
||||
UpdateType();
|
||||
Update();
|
||||
}
|
||||
|
||||
@@ -95,15 +95,27 @@ 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);
|
||||
_scrollBar.Value = 0;
|
||||
}
|
||||
|
||||
public FormattedMessage GetMessage(Index index)
|
||||
{
|
||||
return new FormattedMessage(_entries[index].Message);
|
||||
}
|
||||
|
||||
public void RemoveEntry(Index index)
|
||||
{
|
||||
var entry = _entries[index];
|
||||
entry.RemoveControls();
|
||||
_entries.RemoveAt(index.GetOffset(_entries.Count));
|
||||
|
||||
var font = _getFont();
|
||||
@@ -131,6 +143,31 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
_entries.Add(entry);
|
||||
var font = _getFont();
|
||||
AddNewItemHeight(font, entry);
|
||||
|
||||
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
|
||||
if (_isAtBottom && ScrollFollowing)
|
||||
{
|
||||
_scrollBar.MoveToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMessage(Index index, FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
|
||||
{
|
||||
var oldEntry = _entries[index];
|
||||
var font = _getFont();
|
||||
_totalContentHeight -= oldEntry.Height + font.GetLineSeparation(UIScale);
|
||||
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
|
||||
|
||||
var entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor);
|
||||
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
|
||||
_entries[index] = entry;
|
||||
|
||||
AddNewItemHeight(font, in entry);
|
||||
}
|
||||
|
||||
private void AddNewItemHeight(Font font, in RichTextEntry entry)
|
||||
{
|
||||
_totalContentHeight += entry.Height;
|
||||
if (_firstLine)
|
||||
{
|
||||
@@ -140,12 +177,6 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
_totalContentHeight += font.GetLineSeparation(UIScale);
|
||||
}
|
||||
|
||||
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
|
||||
if (_isAtBottom && ScrollFollowing)
|
||||
{
|
||||
_scrollBar.MoveToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public void ScrollToBottom()
|
||||
@@ -189,6 +220,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (entryOffset > contentBox.Height)
|
||||
{
|
||||
entry.HideControls();
|
||||
|
||||
// We know that every subsequent entry will also fail the test, but we also need to
|
||||
// hide all the controls, so we cannot simply break out of the loop
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,27 @@ namespace Robust.Client.UserInterface.Controls
|
||||
private StyleBox? _backgroundStyleBoxOverride;
|
||||
private StyleBox? _foregroundStyleBoxOverride;
|
||||
|
||||
private bool _vertical;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the progress bar is oriented vertically.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A vertical progress bar fills from bottom to top.
|
||||
/// </remarks>
|
||||
public bool Vertical
|
||||
{
|
||||
get => _vertical;
|
||||
set
|
||||
{
|
||||
if (_vertical != value)
|
||||
{
|
||||
_vertical = value;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public StyleBox? BackgroundStyleBoxOverride
|
||||
{
|
||||
get => _backgroundStyleBoxOverride;
|
||||
@@ -70,11 +91,23 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
return;
|
||||
}
|
||||
var minSize = fg.MinimumSize;
|
||||
var size = PixelWidth * GetAsRatio() - minSize.X;
|
||||
if (size > 0)
|
||||
|
||||
if (_vertical)
|
||||
{
|
||||
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, PixelHeight), UIScale);
|
||||
var size = PixelHeight * GetAsRatio();
|
||||
if (size > 0)
|
||||
{
|
||||
fg.Draw(handle, UIBox2.FromDimensions(0, PixelHeight - size, PixelWidth, size), UIScale);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var minSize = fg.MinimumSize;
|
||||
var size = PixelWidth * GetAsRatio() - minSize.X;
|
||||
if (size > 0)
|
||||
{
|
||||
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, PixelHeight), UIScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -15,8 +17,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
[Dependency] private readonly MarkupTagManager _tagManager = default!;
|
||||
|
||||
private FormattedMessage? _message;
|
||||
private RichTextEntry _entry;
|
||||
private RichTextEntry? _entry;
|
||||
private float _lineHeightScale = 1;
|
||||
private bool _lineHeightOverride;
|
||||
|
||||
@@ -40,19 +41,26 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public string? Text
|
||||
{
|
||||
get => _message?.ToMarkup();
|
||||
get => _entry?.Message.ToMarkup();
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
_message?.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
SetMessage(FormattedMessage.FromMarkupPermissive(value));
|
||||
Clear();
|
||||
else
|
||||
SetMessage(FormattedMessage.FromMarkupPermissive(value));
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_entry?.RemoveControls();
|
||||
_entry = null;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
|
||||
public IEnumerable<Control> Controls => _entry?.Controls?.Values ?? Enumerable.Empty<Control>();
|
||||
public IReadOnlyList<MarkupNode> Nodes => _entry?.Message.Nodes ?? Array.Empty<MarkupNode>();
|
||||
|
||||
public RichTextLabel()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
@@ -61,8 +69,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public void SetMessage(FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
|
||||
{
|
||||
_message = message;
|
||||
_entry = new RichTextEntry(_message, this, _tagManager, tagsAllowed, defaultColor);
|
||||
_entry?.RemoveControls();
|
||||
_entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
|
||||
@@ -73,31 +81,31 @@ namespace Robust.Client.UserInterface.Controls
|
||||
SetMessage(msg, tagsAllowed, defaultColor);
|
||||
}
|
||||
|
||||
public string? GetMessage() => _message?.ToMarkup();
|
||||
public string? GetMessage() => _entry?.Message.ToMarkup();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the currently used formatted message.
|
||||
/// </summary>
|
||||
public FormattedMessage? GetFormattedMessage() => _entry == null ? null : new FormattedMessage(_entry.Value.Message);
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
if (_message == null)
|
||||
{
|
||||
if (_entry == null)
|
||||
return Vector2.Zero;
|
||||
}
|
||||
|
||||
var font = _getFont();
|
||||
_entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
|
||||
|
||||
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
|
||||
// _entry is nullable struct.
|
||||
// cannot just call _entry.Value.Update() as that doesn't actually update _entry.
|
||||
_entry = _entry.Value.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
|
||||
|
||||
return new Vector2(_entry.Value.Width / UIScale, _entry.Value.Height / UIScale);
|
||||
}
|
||||
|
||||
protected internal override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (_message == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entry.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
|
||||
_entry?.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
|
||||
@@ -30,12 +30,12 @@ namespace Robust.Client.UserInterface.Controls
|
||||
get => _currentTab;
|
||||
set
|
||||
{
|
||||
if (_currentTab < 0)
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, "Current tab must be positive.");
|
||||
}
|
||||
|
||||
if (_currentTab >= ChildCount)
|
||||
if (value >= ChildCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value,
|
||||
"Current tab must less than the amount of tabs.");
|
||||
|
||||
@@ -145,6 +145,11 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
// This is to avoid unnecessarily setting a position where our size isn't yet fully updated.
|
||||
// This most commonly happens with saved window positions if your window position is <= 0.
|
||||
if (!IsMeasureValid)
|
||||
return;
|
||||
|
||||
var (spaceX, spaceY) = Parent!.Size;
|
||||
|
||||
var maxX = spaceX - ((AllowOffScreen & DirectionFlag.West) == 0 ? Size.X : WindowEdgeSeparation);
|
||||
|
||||
@@ -8,12 +8,19 @@ namespace Robust.Client.UserInterface
|
||||
/// </summary>
|
||||
internal sealed class DummyFileDialogManager : IFileDialogManager
|
||||
{
|
||||
public Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
public Task<Stream?> OpenFile(
|
||||
FileDialogFilters? filters = null,
|
||||
FileAccess access = FileAccess.ReadWrite,
|
||||
FileShare? share = null)
|
||||
{
|
||||
return Task.FromResult<Stream?>(null);
|
||||
}
|
||||
|
||||
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
|
||||
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
|
||||
FileDialogFilters? filters = null,
|
||||
bool truncate = true,
|
||||
FileAccess access = FileAccess.ReadWrite,
|
||||
FileShare share = FileShare.None)
|
||||
{
|
||||
return Task.FromResult<(Stream fileStream, bool alreadyExisted)?>(null);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -20,28 +18,41 @@ namespace Robust.Client.UserInterface
|
||||
[SuppressMessage("ReSharper", "IdentifierTypo")]
|
||||
[SuppressMessage("ReSharper", "CommentTypo")]
|
||||
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
||||
internal sealed class FileDialogManager : IFileDialogManager
|
||||
internal sealed class FileDialogManager : IFileDialogManager, IPostInjectInit
|
||||
{
|
||||
// Uses nativefiledialog to open the file dialogs cross platform.
|
||||
// On Linux, if the kdialog command is found, it will be used instead.
|
||||
// TODO: Should we maybe try to avoid running kdialog if the DE isn't KDE?
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
[Dependency] private readonly ILogManager _log = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private bool _kDialogAvailable;
|
||||
private bool _checkedKDialogAvailable;
|
||||
|
||||
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
public async Task<Stream?> OpenFile(
|
||||
FileDialogFilters? filters = null,
|
||||
FileAccess access = FileAccess.ReadWrite,
|
||||
FileShare? share = null)
|
||||
{
|
||||
if ((access & FileAccess.ReadWrite) != access)
|
||||
throw new ArgumentException("Invalid file access specified");
|
||||
|
||||
var realShare = share ?? (access == FileAccess.Read ? FileShare.Read : FileShare.None);
|
||||
if ((realShare & (FileShare.ReadWrite | FileShare.Delete)) != realShare)
|
||||
throw new ArgumentException("Invalid file share specified");
|
||||
|
||||
string? name;
|
||||
if (_clyde.FileDialogImpl is { } clydeImpl)
|
||||
return await clydeImpl.OpenFile(filters);
|
||||
name = await clydeImpl.OpenFile(filters);
|
||||
else
|
||||
name = await GetOpenFileName(filters);
|
||||
|
||||
var name = await GetOpenFileName(filters);
|
||||
if (name == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return File.Open(name, FileMode.Open);
|
||||
return File.Open(name, FileMode.Open, access, realShare);
|
||||
}
|
||||
|
||||
private async Task<string?> GetOpenFileName(FileDialogFilters? filters)
|
||||
@@ -54,24 +65,34 @@ namespace Robust.Client.UserInterface
|
||||
return await OpenFileNfd(filters);
|
||||
}
|
||||
|
||||
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
|
||||
public async Task<(Stream, bool)?> SaveFile(
|
||||
FileDialogFilters? filters,
|
||||
bool truncate = true,
|
||||
FileAccess access = FileAccess.ReadWrite,
|
||||
FileShare share = FileShare.None)
|
||||
{
|
||||
if (_clyde.FileDialogImpl is { } clydeImpl)
|
||||
return await clydeImpl.SaveFile(filters);
|
||||
if ((access & FileAccess.ReadWrite) != access)
|
||||
throw new ArgumentException("Invalid file access specified");
|
||||
|
||||
if ((share & (FileShare.ReadWrite | FileShare.Delete)) != share)
|
||||
throw new ArgumentException("Invalid file share specified");
|
||||
|
||||
string? name;
|
||||
if (_clyde.FileDialogImpl is { } clydeImpl)
|
||||
name = await clydeImpl.SaveFile(filters);
|
||||
else
|
||||
name = await GetSaveFileName(filters);
|
||||
|
||||
var name = await GetSaveFileName(filters);
|
||||
if (name == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open), true);
|
||||
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open, access, share), true);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return (File.Open(name, FileMode.Create), false);
|
||||
return (File.Open(name, FileMode.Create, access, share), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +270,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
if (_kDialogAvailable)
|
||||
{
|
||||
Logger.DebugS("filedialog", "kdialog available.");
|
||||
_sawmill.Debug("kdialog available.");
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -386,6 +407,11 @@ namespace Robust.Client.UserInterface
|
||||
[DllImport("swnfd.dll")]
|
||||
private static extern unsafe void sw_NFD_Free(void* ptr);
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
_sawmill = _log.GetSawmill("filedialog");
|
||||
}
|
||||
|
||||
private enum sw_nfdresult
|
||||
{
|
||||
SW_NFD_ERROR,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Robust.Client.UserInterface
|
||||
{
|
||||
@@ -19,7 +20,17 @@ namespace Robust.Client.UserInterface
|
||||
/// The file stream for the file the user opened.
|
||||
/// <see langword="null" /> if the user cancelled the action.
|
||||
/// </returns>
|
||||
Task<Stream?> OpenFile(FileDialogFilters? filters = null);
|
||||
/// <param name="filters">Filters for file types that the user can select.</param>
|
||||
/// <param name="access">What access is desired from the file operation.</param>
|
||||
/// <param name="share">
|
||||
/// What sharing mode is desired from the file operation.
|
||||
/// If null is provided and <paramref name="access"/> is <see cref="FileAccess.Read"/>,
|
||||
/// <see cref="FileShare.Read"/> is selected, otherwise <see cref="FileShare.None"/>.
|
||||
/// </param>
|
||||
Task<Stream?> OpenFile(
|
||||
FileDialogFilters? filters = null,
|
||||
FileAccess access = FileAccess.ReadWrite,
|
||||
FileShare? share = null);
|
||||
|
||||
/// <summary>
|
||||
/// Open a file dialog used for saving a single file.
|
||||
@@ -29,6 +40,21 @@ namespace Robust.Client.UserInterface
|
||||
/// Null if the user cancelled the action.
|
||||
/// </returns>
|
||||
/// <param name="truncate">Should we truncate an existing file to 0-size then write or append.</param>
|
||||
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true);
|
||||
/// <param name="access">What access is desired from the file operation.</param>
|
||||
/// <param name="share">Sharing mode for the opened file.</param>
|
||||
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
|
||||
FileDialogFilters? filters = null,
|
||||
bool truncate = true,
|
||||
FileAccess access = FileAccess.ReadWrite,
|
||||
FileShare share = FileShare.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal implementation interface used to connect <see cref="FileDialogManager"/> and <see cref="IClydeInternal"/>.
|
||||
/// </summary>
|
||||
internal interface IFileDialogManagerImplementation
|
||||
{
|
||||
Task<string?> OpenFile(FileDialogFilters? filters);
|
||||
Task<string?> SaveFile(FileDialogFilters? filters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class BoldItalicTag : IMarkupTag
|
||||
public sealed class BoldItalicTag : IMarkupTagHandler
|
||||
{
|
||||
public const string BoldItalicFont = "DefaultBoldItalic";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class BoldTag : IMarkupTag
|
||||
public sealed class BoldTag : IMarkupTagHandler
|
||||
{
|
||||
public const string BoldFont = "DefaultBold";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class BulletTag : IMarkupTag
|
||||
public sealed class BulletTag : IMarkupTagHandler
|
||||
{
|
||||
public string Name => "bullet";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Robust.Client.UserInterface.RichText;
|
||||
/// <summary>
|
||||
/// Colors the text inside its opening and closing nodes
|
||||
/// </summary>
|
||||
public sealed class ColorTag : IMarkupTag
|
||||
public sealed class ColorTag : IMarkupTagHandler
|
||||
{
|
||||
public static readonly Color DefaultColor = new(200, 200, 200);
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class CommandLinkTag : IMarkupTag
|
||||
public sealed class CommandLinkTag : IMarkupTagHandler
|
||||
{
|
||||
[Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!;
|
||||
|
||||
public string Name => "cmdlink";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
if (!node.Value.TryGetString(out var text)
|
||||
|| !node.Attributes.TryGetValue("command", out var commandParameter)
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Robust.Client.UserInterface.RichText;
|
||||
/// Applies the font provided as the tags parameter to the markup drawing context.
|
||||
/// Definitely not save for user supplied markup
|
||||
/// </summary>
|
||||
public sealed class FontTag : IMarkupTag
|
||||
public sealed class FontTag : IMarkupTagHandler
|
||||
{
|
||||
public const string DefaultFont = "Default";
|
||||
public const int DefaultSize = 12;
|
||||
|
||||
@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class HeadingTag : IMarkupTag
|
||||
public sealed class HeadingTag : IMarkupTagHandler
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public interface IMarkupTag
|
||||
/// <summary>
|
||||
/// Classes that implement this interface will be instantiated by <see cref="MarkupTagManager"/> and used to handle
|
||||
/// the parsing and behaviour of markup tags. Note that each class is only ever instantiated once by the tag manager,
|
||||
/// and wil be used to handle all tags of that kind, and thus should not contain state information relevant to a
|
||||
/// specific tag.
|
||||
/// </summary>
|
||||
public interface IMarkupTagHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The string used as the tags name when writing rich text
|
||||
@@ -54,17 +61,32 @@ public interface IMarkupTag
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called inside the constructor of <see cref="RichTextEntry"/> to
|
||||
/// supply a control that gets rendered inline before this tags children<br/>
|
||||
/// Text continues to the right of the control until the next line and then continues bellow it
|
||||
/// Called inside the constructor of <see cref="RichTextEntry"/> to supply a control that gets rendered inline
|
||||
/// before this tags children. The returned control must be new instance to avoid issues with shallow cloning
|
||||
/// <see cref="FormattedMessage"/> nodes. Text continues to the right of the control until the next line and
|
||||
/// then continues bellow it.
|
||||
/// </summary>
|
||||
/// <param name="node">The markup node containing the parameter and attributes</param>
|
||||
/// <param name="control">A UI control for placing in line with this tags children</param>
|
||||
/// <returns>true if this tag supplies a control</returns>
|
||||
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use IMarkupTagHandler")]
|
||||
public interface IMarkupTag : IMarkupTagHandler
|
||||
{
|
||||
bool IMarkupTagHandler.TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
return TryGetControl(node, out control);
|
||||
}
|
||||
|
||||
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
public sealed class ItalicTag : IMarkupTag
|
||||
public sealed class ItalicTag : IMarkupTagHandler
|
||||
{
|
||||
public const string ItalicFont = "DefaultItalic";
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class MarkupTagManager
|
||||
/// <summary>
|
||||
/// Tags defined in engine need to be instantiated here because of sandboxing
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, IMarkupTag> _markupTagTypes = new IMarkupTag[] {
|
||||
private readonly Dictionary<string, IMarkupTagHandler> _markupTagTypes = new IMarkupTagHandler[] {
|
||||
new BoldItalicTag(),
|
||||
new BoldTag(),
|
||||
new BulletTag(),
|
||||
@@ -44,13 +44,13 @@ public sealed class MarkupTagManager
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTag>())
|
||||
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTagHandler>())
|
||||
{
|
||||
//Prevent tags defined inside engine from being instantiated
|
||||
if (_engineTypes.Contains(type))
|
||||
continue;
|
||||
|
||||
var instance = (IMarkupTag)_sandboxHelper.CreateInstance(type);
|
||||
var instance = (IMarkupTagHandler)_sandboxHelper.CreateInstance(type);
|
||||
_markupTagTypes[instance.Name.ToLower()] = instance;
|
||||
}
|
||||
|
||||
@@ -60,22 +60,48 @@ public sealed class MarkupTagManager
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use GetMarkupTagHandler")]
|
||||
public IMarkupTag? GetMarkupTag(string name)
|
||||
{
|
||||
return _markupTagTypes.GetValueOrDefault(name) as IMarkupTag;
|
||||
}
|
||||
|
||||
public IMarkupTagHandler? GetMarkupTagHandler(string name)
|
||||
{
|
||||
return _markupTagTypes.GetValueOrDefault(name);
|
||||
}
|
||||
|
||||
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
|
||||
/// <summary>
|
||||
/// Attempt to get the tag handler with the corresponding name.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the tag, as specified by <see cref="IMarkupTag.Name"/></param>
|
||||
/// <param name="tagsAllowed">List of allowed tag types. If null, all types are allowed.</param>
|
||||
/// <param name="handler">The instance responsible for handling tags of this type.</param>
|
||||
/// <returns></returns>
|
||||
public bool TryGetMarkupTagHandler(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTagHandler? handler)
|
||||
{
|
||||
if (_markupTagTypes.TryGetValue(name, out var markupTag)
|
||||
// Using a whitelist prevents new tags from sneaking in.
|
||||
&& (tagsAllowed == null || Array.IndexOf(tagsAllowed, markupTag.GetType()) != -1))
|
||||
{
|
||||
tag = markupTag;
|
||||
handler = markupTag;
|
||||
return true;
|
||||
}
|
||||
|
||||
tag = null;
|
||||
handler = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
[Obsolete("Use TryGetMarkupTagHandler")]
|
||||
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
|
||||
{
|
||||
if (!TryGetMarkupTagHandler(name, tagsAllowed, out var handler) || handler is not IMarkupTag cast)
|
||||
{
|
||||
tag = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
tag = cast;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Used by <see cref="OutputPanel"/> and <see cref="RichTextLabel"/> to handle rich text layout.
|
||||
/// Note that if this text is ever removed or modified without removing the owning control,
|
||||
/// then <see cref="RemoveControls"/> should be called to ensure that any controls that were added by this
|
||||
/// entry are also removed.
|
||||
/// </summary>
|
||||
internal struct RichTextEntry
|
||||
{
|
||||
@@ -36,7 +39,7 @@ namespace Robust.Client.UserInterface
|
||||
/// </summary>
|
||||
public ValueList<int> LineBreaks;
|
||||
|
||||
private readonly Dictionary<int, Control>? _tagControls;
|
||||
public readonly Dictionary<int, Control>? Controls;
|
||||
|
||||
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
|
||||
{
|
||||
@@ -56,15 +59,35 @@ namespace Robust.Client.UserInterface
|
||||
if (node.Name == null)
|
||||
continue;
|
||||
|
||||
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
|
||||
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var handler) || !handler.TryCreateControl(node, out var control))
|
||||
continue;
|
||||
|
||||
// Markup tag handler instances are shared across controls. We need to ensure that the hanlder doesn't
|
||||
// store state information and return the same control for each rich text entry.
|
||||
DebugTools.Assert(handler.TryCreateControl(node, out var other) && other != control);
|
||||
|
||||
parent.Children.Add(control);
|
||||
tagControls ??= new Dictionary<int, Control>();
|
||||
tagControls.Add(nodeIndex, control);
|
||||
}
|
||||
|
||||
_tagControls = tagControls;
|
||||
Controls = tagControls;
|
||||
}
|
||||
|
||||
// TODO RICH TEXT
|
||||
// Somehow ensure that this **has** to be called when removing rich text from some control.
|
||||
/// <summary>
|
||||
/// Remove all owned controls from their parents.
|
||||
/// </summary>
|
||||
public readonly void RemoveControls()
|
||||
{
|
||||
if (Controls == null)
|
||||
return;
|
||||
|
||||
foreach (var ctrl in Controls.Values)
|
||||
{
|
||||
ctrl.Orphan();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,7 +97,7 @@ namespace Robust.Client.UserInterface
|
||||
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
|
||||
/// <param name="uiScale"></param>
|
||||
/// <param name="lineHeightScale"></param>
|
||||
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
|
||||
public RichTextEntry Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
|
||||
{
|
||||
// This method is gonna suck due to complexity.
|
||||
// Bear with me here.
|
||||
@@ -112,13 +135,13 @@ namespace Robust.Client.UserInterface
|
||||
continue;
|
||||
|
||||
if (ProcessMetric(ref this, metrics, out breakLine))
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
|
||||
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
|
||||
continue;
|
||||
|
||||
control.Measure(new Vector2(Width, Height));
|
||||
control.Measure(new Vector2(maxSizeX, Height));
|
||||
|
||||
var desiredSize = control.DesiredPixelSize;
|
||||
var controlMetrics = new CharMetrics(
|
||||
@@ -128,12 +151,14 @@ namespace Robust.Client.UserInterface
|
||||
desiredSize.Y);
|
||||
|
||||
if (ProcessMetric(ref this, controlMetrics, out breakLine))
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
|
||||
Width = wordWrap.FinalizeText(out breakLine);
|
||||
CheckLineBreak(ref this, breakLine);
|
||||
|
||||
return this;
|
||||
|
||||
bool ProcessRune(ref RichTextEntry src, Rune rune, out int? outBreakLine)
|
||||
{
|
||||
wordWrap.NextRune(rune, out breakLine, out var breakNewLine, out var skip);
|
||||
@@ -166,9 +191,10 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
internal readonly void HideControls()
|
||||
{
|
||||
if (_tagControls == null)
|
||||
if (Controls == null)
|
||||
return;
|
||||
foreach (var control in _tagControls.Values)
|
||||
|
||||
foreach (var control in Controls.Values)
|
||||
{
|
||||
control.Visible = false;
|
||||
}
|
||||
@@ -220,7 +246,7 @@ namespace Robust.Client.UserInterface
|
||||
globalBreakCounter += 1;
|
||||
}
|
||||
|
||||
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
|
||||
continue;
|
||||
|
||||
// Controls may have been previously hidden via HideControls due to being "out-of frame".
|
||||
@@ -243,7 +269,7 @@ namespace Robust.Client.UserInterface
|
||||
return node.Value.StringValue ?? "";
|
||||
|
||||
//Skip the node if there is no markup tag for it.
|
||||
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
|
||||
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var tag))
|
||||
return "";
|
||||
|
||||
if (!node.Closing)
|
||||
|
||||
@@ -137,7 +137,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
|
||||
if (TerminatingOrDeleted(coordinates.EntityId))
|
||||
{
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}. Trace: {Environment.StackTrace}");
|
||||
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
|
||||
if (TerminatingOrDeleted(coordinates.EntityId))
|
||||
{
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}. Trace: {Environment.StackTrace}");
|
||||
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -281,4 +281,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
{
|
||||
// TODO: Yeah remove this...
|
||||
}
|
||||
|
||||
private void LogAudioPlaybackOnInvalidEntity(ResolvedSoundSpecifier? specifier, EntityUid entityId)
|
||||
{
|
||||
var soundInfo = specifier?.ToString() ?? "unknown sound";
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entityId)}. Sound: {soundInfo}. Trace: {Environment.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ namespace Robust.Server
|
||||
: null;
|
||||
|
||||
// Set up the VFS
|
||||
_resources.Initialize(dataDir);
|
||||
_resources.Initialize(dataDir, hideUserDataDir: false);
|
||||
|
||||
var mountOptions = _commandLineArgs != null
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
foreach (var uid in toDelete)
|
||||
{
|
||||
EntityManager.DeleteEntity(uid);
|
||||
Del(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ namespace Robust.Server.GameObjects
|
||||
if (!_deleteEmptyGrids || TerminatingOrDeleted(uid) || HasComp<MapComponent>(uid))
|
||||
return;
|
||||
|
||||
EntityManager.DeleteEntity(args.GridId);
|
||||
Del(args.GridId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class ViewSubscriberSystem : SharedViewSubscriberSystem
|
||||
public override void AddViewSubscriber(EntityUid uid, ICommonSession session)
|
||||
{
|
||||
// If the entity doesn't have the component, it will be added.
|
||||
var viewSubscriber = EntityManager.EnsureComponent<Shared.GameObjects.ViewSubscriberComponent>(uid);
|
||||
var viewSubscriber = EnsureComp<Shared.GameObjects.ViewSubscriberComponent>(uid);
|
||||
|
||||
if (viewSubscriber.SubscribedSessions.Contains(session))
|
||||
return; // Already subscribed, do nothing else.
|
||||
@@ -36,7 +36,7 @@ public sealed class ViewSubscriberSystem : SharedViewSubscriberSystem
|
||||
/// </summary>
|
||||
public override void RemoveViewSubscriber(EntityUid uid, ICommonSession session)
|
||||
{
|
||||
if(!EntityManager.TryGetComponent(uid, out Shared.GameObjects.ViewSubscriberComponent? viewSubscriber))
|
||||
if(!TryComp(uid, out Shared.GameObjects.ViewSubscriberComponent? viewSubscriber))
|
||||
return; // Entity didn't have any subscriptions, do nothing.
|
||||
|
||||
if (!viewSubscriber.SubscribedSessions.Remove(session))
|
||||
|
||||
@@ -132,7 +132,7 @@ internal sealed partial class PvsSystem
|
||||
|
||||
if (enumerateAll)
|
||||
{
|
||||
var query = EntityManager.AllEntityQueryEnumerator<MetaDataComponent>();
|
||||
var query = AllEntityQuery<MetaDataComponent>();
|
||||
while (query.MoveNext(out var uid, out var md))
|
||||
{
|
||||
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");
|
||||
|
||||
@@ -199,7 +199,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
foreach (var uid in _toDelete)
|
||||
{
|
||||
EntityManager.QueueDeleteEntity(uid);
|
||||
QueueDel(uid);
|
||||
}
|
||||
_toDelete.Clear();
|
||||
|
||||
|
||||
82
Robust.Server/Physics/Commands/BoxStackCommand.cs
Normal file
82
Robust.Server/Physics/Commands/BoxStackCommand.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Controllers;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
|
||||
namespace Robust.Server.Physics.Commands;
|
||||
|
||||
public sealed class BoxStackCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public string Command => "boxstack";
|
||||
public string Description => string.Empty;
|
||||
public string Help => "boxstack [mapid] [columns] [rows]";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 3 ||
|
||||
!int.TryParse(args[0], out var mapInt) ||
|
||||
!int.TryParse(args[1], out var columns) ||
|
||||
!int.TryParse(args[2], out var rows))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mapId = new MapId(mapInt);
|
||||
|
||||
var fixtureSystem = _entManager.System<FixtureSystem>();
|
||||
var physSystem = _entManager.System<SharedPhysicsSystem>();
|
||||
physSystem.SetGravity(new Vector2(0f, -9.8f));
|
||||
|
||||
var groundUid = _entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
|
||||
var ground = _entManager.AddComponent<PhysicsComponent>(groundUid);
|
||||
var groundManager = _entManager.EnsureComponent<FixturesComponent>(groundUid);
|
||||
|
||||
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
|
||||
fixtureSystem.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 1, 1, true), manager: groundManager, body: ground);
|
||||
|
||||
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
|
||||
fixtureSystem.CreateFixture(groundUid, "fix2", new Fixture(vertical, 1, 1, true), manager: groundManager, body: ground);
|
||||
|
||||
physSystem.WakeBody(groundUid, manager: groundManager, body: ground);
|
||||
|
||||
var xs = new[]
|
||||
{
|
||||
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
|
||||
};
|
||||
|
||||
for (var j = 0; j < columns; j++)
|
||||
{
|
||||
for (var i = 0; i < rows; i++)
|
||||
{
|
||||
var x = 0.0f;
|
||||
|
||||
var boxUid = _entManager.SpawnEntity(null,
|
||||
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 2.1f * i), mapId));
|
||||
var box = _entManager.AddComponent<PhysicsComponent>(boxUid);
|
||||
var manager = _entManager.EnsureComponent<FixturesComponent>(boxUid);
|
||||
|
||||
physSystem.SetBodyType(boxUid, BodyType.Dynamic, manager: manager, body: box);
|
||||
var poly = new PolygonShape(0.001f);
|
||||
poly.Set(new List<Vector2>()
|
||||
{
|
||||
new(0.5f, -0.5f),
|
||||
new(0.5f, 0.5f),
|
||||
new(-0.5f, 0.5f),
|
||||
new(-0.5f, -0.5f),
|
||||
});
|
||||
|
||||
fixtureSystem.CreateFixture(boxUid, "fix1", new Fixture(poly, 1, 1, true), manager: manager, body: box);
|
||||
physSystem.WakeBody(boxUid, manager: manager, body: box);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ namespace Robust.Server.Placement
|
||||
{
|
||||
// Replace existing entities if relevant.
|
||||
if (msg.Replacement && _prototype.Index<EntityPrototype>(entityTemplateName).Components.TryGetValue(
|
||||
_factory.GetComponentName(typeof(PlacementReplacementComponent)), out var compRegistry))
|
||||
_factory.GetComponentName<PlacementReplacementComponent>(), out var compRegistry))
|
||||
{
|
||||
var key = ((PlacementReplacementComponent)compRegistry.Component).Key;
|
||||
var gridUid = _xformSystem.GetGrid(coordinates);
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Robust.Server.Player
|
||||
{
|
||||
foreach (var uid in entities)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(uid, out ActorComponent? actor))
|
||||
if (TryComp(uid, out ActorComponent? actor))
|
||||
filter.AddPlayer(actor.PlayerSession);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -188,13 +189,20 @@ namespace Robust.Server.Scripting
|
||||
}
|
||||
|
||||
// Compile ahead of time so that we can do syntax highlighting correctly for the echo.
|
||||
newScript.Compile();
|
||||
await Task.Run(() =>
|
||||
{
|
||||
newScript.Compile();
|
||||
|
||||
// Echo entered script.
|
||||
var echoMessage = new FormattedMessage();
|
||||
ScriptInstanceShared.AddWithSyntaxHighlighting(newScript, echoMessage, code, instance.HighlightWorkspace);
|
||||
// Echo entered script.
|
||||
var echoMessage = new FormattedMessage();
|
||||
ScriptInstanceShared.AddWithSyntaxHighlighting(
|
||||
newScript,
|
||||
echoMessage,
|
||||
code,
|
||||
instance.HighlightWorkspace.Value);
|
||||
|
||||
replyMessage.Echo = echoMessage;
|
||||
replyMessage.Echo = echoMessage;
|
||||
});
|
||||
|
||||
var msg = new FormattedMessage();
|
||||
|
||||
@@ -332,7 +340,7 @@ namespace Robust.Server.Scripting
|
||||
|
||||
private sealed class ScriptInstance
|
||||
{
|
||||
public Workspace HighlightWorkspace { get; } = new AdhocWorkspace();
|
||||
public Lazy<Workspace> HighlightWorkspace { get; } = new(() => new AdhocWorkspace());
|
||||
public StringBuilder InputBuffer { get; } = new();
|
||||
public FormattedMessage OutputBuffer { get; } = new();
|
||||
public bool RunningScript { get; set; }
|
||||
@@ -373,7 +381,7 @@ namespace Robust.Server.Scripting
|
||||
script.Compile();
|
||||
|
||||
var syntax = new FormattedMessage();
|
||||
ScriptInstanceShared.AddWithSyntaxHighlighting(script, syntax, code, _scriptInstance.HighlightWorkspace);
|
||||
ScriptInstanceShared.AddWithSyntaxHighlighting(script, syntax, code, _scriptInstance.HighlightWorkspace.Value);
|
||||
|
||||
_scriptInstance.OutputBuffer.AddMessage(syntax);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -101,6 +102,7 @@ namespace Robust.Shared.Maths
|
||||
/// <summary>
|
||||
/// Returns the smallest rectangle that contains both of the rectangles.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public readonly Box2i Union(in Box2i other)
|
||||
{
|
||||
var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft);
|
||||
@@ -207,6 +209,7 @@ namespace Robust.Shared.Maths
|
||||
/// <summary>
|
||||
/// Multiplies each side of the box by the scalar.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public Box2i Scale(int scalar)
|
||||
{
|
||||
return new Box2i(
|
||||
@@ -215,6 +218,21 @@ namespace Robust.Shared.Maths
|
||||
Right * scalar,
|
||||
Top * scalar);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public bool Intersects(in Box2i other)
|
||||
{
|
||||
return other.Bottom <= this.Top && other.Top >= this.Bottom && other.Right >= this.Left &&
|
||||
other.Left <= this.Right;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public readonly Box2i Enlarged(int size)
|
||||
{
|
||||
return new(Left - size, Bottom - size, Right + size, Top + size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace Robust.Shared.Audio;
|
||||
/// <seealso cref="ResolvedPathSpecifier"/>
|
||||
/// <seealso cref="ResolvedCollectionSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public abstract partial class ResolvedSoundSpecifier {
|
||||
public abstract partial class ResolvedSoundSpecifier
|
||||
{
|
||||
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
|
||||
public static implicit operator ResolvedSoundSpecifier(string s) => new ResolvedPathSpecifier(s);
|
||||
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
|
||||
@@ -22,8 +23,10 @@ public abstract partial class ResolvedSoundSpecifier {
|
||||
/// <summary>
|
||||
/// Returns whether <c>s</c> is null, or if it contains an empty path/collection ID.
|
||||
/// </summary>
|
||||
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s) {
|
||||
return s switch {
|
||||
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s)
|
||||
{
|
||||
return s switch
|
||||
{
|
||||
null => true,
|
||||
ResolvedPathSpecifier path => path.Path.ToString() == "",
|
||||
ResolvedCollectionSpecifier collection => string.IsNullOrEmpty(collection.Collection),
|
||||
@@ -37,7 +40,8 @@ public abstract partial class ResolvedSoundSpecifier {
|
||||
/// </summary>
|
||||
/// <seealso cref="ResolvedCollectionSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
|
||||
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedPathSpecifier>
|
||||
{
|
||||
/// <summary>
|
||||
/// The resource path of the sound.
|
||||
/// </summary>
|
||||
@@ -57,6 +61,21 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
|
||||
public ResolvedPathSpecifier(string path) : this(new ResPath(path))
|
||||
{
|
||||
}
|
||||
|
||||
public bool Equals(ResolvedPathSpecifier? other)
|
||||
{
|
||||
return Path.Equals(other?.Path);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return Equals(obj as ResolvedPathSpecifier);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Path.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,7 +83,9 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
|
||||
/// </summary>
|
||||
/// <seealso cref="ResolvedPathSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier {
|
||||
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedCollectionSpecifier>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the <see cref="SoundCollectionPrototype">sound collection</see> to look up.
|
||||
/// </summary>
|
||||
@@ -87,4 +108,19 @@ public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier
|
||||
Collection = collection;
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public bool Equals(ResolvedCollectionSpecifier? other)
|
||||
{
|
||||
return Collection.Equals(other?.Collection) && Index.Equals(other?.Index);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return Equals(obj as ResolvedCollectionSpecifier);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Collection, Index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,7 +665,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams);
|
||||
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -127,19 +127,20 @@ public sealed class TeleportToCommand : LocalizedEntityCommands
|
||||
{
|
||||
foreach (var victim in args)
|
||||
{
|
||||
if (victim == target)
|
||||
if (!TryGetTransformFromUidOrUsername(victim, shell, out var uid, out var victimTransform))
|
||||
continue;
|
||||
|
||||
if (!TryGetTransformFromUidOrUsername(victim, shell, out var uid, out var victimTransform))
|
||||
if (uid == targetUid)
|
||||
continue;
|
||||
|
||||
victims.Add((uid.Value, victimTransform));
|
||||
}
|
||||
}
|
||||
|
||||
var targetMapCoords = _transform.ToMapCoordinates(targetCoords);
|
||||
foreach (var victim in victims)
|
||||
{
|
||||
_transform.SetCoordinates(victim.Entity, targetCoords);
|
||||
_transform.SetMapCoordinates(victim.Entity, targetMapCoords);
|
||||
_transform.AttachToGridOrMap(victim.Entity, victim.Transform);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -155,7 +156,7 @@ public static class CompletionHelper
|
||||
/// Returns a completion list for all prototype IDs of the given type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Don't use this for prototypes types that likely have a large number of entries, like <see cref="EntityPrototype"/>.
|
||||
/// Don't use this for prototypes types that likely have a large number of entries, like <see cref="EntityPrototype"/>, use <see cref="PrototypeIdsLimited{T}"/> instead.
|
||||
/// </remarks>
|
||||
public static IEnumerable<CompletionOption> PrototypeIDs<T>(bool sorted = true, IPrototypeManager? proto = null)
|
||||
where T: class, IPrototype
|
||||
@@ -166,6 +167,36 @@ public static class CompletionHelper
|
||||
return sorted ? protoOptions.OrderBy(o => o.Value) : protoOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a completion list for all prototype IDs of the given type, limited to avoid performance problems.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a limited alternative to <see cref="PrototypeIDs{T}"/>.
|
||||
/// The limit is applied before sorting of results, so the unfiltered results are somewhat arbitrary.
|
||||
/// </remarks>
|
||||
/// <param name="currentArgument">The argument being currently typed for the completion.</param>
|
||||
/// <param name="proto">The <see cref="IPrototypeManager"/>.</param>
|
||||
/// <param name="sorted">Whether to sort the results or not.</param>
|
||||
/// <param name="maxCount">The maximum amount of results to return at once.</param>
|
||||
/// <typeparam name="T">The type of prototype to search through.</typeparam>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<CompletionOption> PrototypeIdsLimited<T>(
|
||||
string currentArgument,
|
||||
IPrototypeManager proto,
|
||||
bool sorted = true,
|
||||
int maxCount = 30) where T : class, IPrototype
|
||||
{
|
||||
var protoOptions = proto.EnumeratePrototypes<T>()
|
||||
.Where(p => p.ID.StartsWith(currentArgument, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(maxCount)
|
||||
.Select(p => new CompletionOption(p.ID));
|
||||
|
||||
if (sorted)
|
||||
protoOptions = protoOptions.OrderBy(o => o.Value);
|
||||
|
||||
return protoOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of connected session names.
|
||||
/// </summary>
|
||||
|
||||
@@ -279,16 +279,6 @@ namespace Robust.Shared.Containers
|
||||
|
||||
#region Container Helpers
|
||||
|
||||
[Obsolete("Use Entity<T> variant")]
|
||||
public bool TryGetContainingContainer(
|
||||
EntityUid uid,
|
||||
[NotNullWhen(true)] out BaseContainer? container,
|
||||
MetaDataComponent? meta = null,
|
||||
TransformComponent? transform = null)
|
||||
{
|
||||
return TryGetContainingContainer((uid, transform, meta), out container);
|
||||
}
|
||||
|
||||
public bool TryGetContainingContainer(
|
||||
Entity<TransformComponent?, MetaDataComponent?> ent,
|
||||
[NotNullWhen(true)] out BaseContainer? container)
|
||||
@@ -401,13 +391,6 @@ namespace Robust.Shared.Containers
|
||||
return TryFindComponentsOnEntityContainerOrParent(xform.ParentUid, entityQuery, foundComponents);
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("Use Entity<T> variant")]
|
||||
public bool IsInSameOrNoContainer(EntityUid user, EntityUid other)
|
||||
{
|
||||
return IsInSameOrNoContainer((user, null, null), (other, null, null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the two entities are not contained, or are contained in the same container.
|
||||
/// </summary>
|
||||
@@ -428,13 +411,6 @@ namespace Robust.Shared.Containers
|
||||
return userContainer == otherContainer;
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("Use Entity<T> variant")]
|
||||
public bool IsInSameOrParentContainer(EntityUid user, EntityUid other)
|
||||
{
|
||||
return IsInSameOrParentContainer((user, null), other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the two entities are not contained, or are contained in the same container, or if one
|
||||
/// entity contains the other (i.e., is the parent).
|
||||
@@ -469,21 +445,6 @@ namespace Robust.Shared.Containers
|
||||
return userContainer == otherContainer;
|
||||
}
|
||||
|
||||
[Obsolete("Use Entity<T> variant")]
|
||||
public bool IsInSameOrTransparentContainer(
|
||||
EntityUid user,
|
||||
EntityUid other,
|
||||
BaseContainer? userContainer = null,
|
||||
BaseContainer? otherContainer = null,
|
||||
bool userSeeInsideSelf = false)
|
||||
{
|
||||
return IsInSameOrTransparentContainer((user, null),
|
||||
other,
|
||||
userContainer,
|
||||
otherContainer,
|
||||
userSeeInsideSelf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a given entity can see another entity despite whatever containers they may be in.
|
||||
/// </summary>
|
||||
@@ -606,7 +567,7 @@ namespace Robust.Shared.Containers
|
||||
/// <param name="force">Whether to forcibly remove the entity from the container.</param>
|
||||
/// <param name="wasInContainer">Whether the entity was actually inside a container or not.</param>
|
||||
/// <returns>If the entity could be removed. Also returns false if it wasn't inside a container.</returns>
|
||||
public bool TryRemoveFromContainer(EntityUid entity, bool force, out bool wasInContainer)
|
||||
public bool TryRemoveFromContainer(Entity<TransformComponent?, MetaDataComponent?> entity, bool force, out bool wasInContainer)
|
||||
{
|
||||
DebugTools.Assert(Exists(entity));
|
||||
|
||||
@@ -631,7 +592,7 @@ namespace Robust.Shared.Containers
|
||||
/// <param name="entity">Entity that might be inside a container.</param>
|
||||
/// <param name="force">Whether to forcibly remove the entity from the container.</param>
|
||||
/// <returns>If the entity could be removed. Also returns false if it wasn't inside a container.</returns>
|
||||
public bool TryRemoveFromContainer(EntityUid entity, bool force = false)
|
||||
public bool TryRemoveFromContainer(Entity<TransformComponent?, MetaDataComponent?> entity, bool force = false)
|
||||
{
|
||||
return TryRemoveFromContainer(entity, force, out _);
|
||||
}
|
||||
@@ -678,9 +639,8 @@ namespace Robust.Shared.Containers
|
||||
{
|
||||
// TODO make this check upwards for any container, and parent to that.
|
||||
// Currently this just checks the direct parent, so entities will still teleport through containers.
|
||||
|
||||
if (!transform.Comp.ParentUid.IsValid()
|
||||
|| !TryGetContainingContainer(transform.Comp.ParentUid, out var container)
|
||||
|| !TryGetContainingContainer((transform.Comp.ParentUid, Transform(transform.Comp.ParentUid)), out var container)
|
||||
|| !TryInsertIntoContainer(transform, container))
|
||||
{
|
||||
_transform.AttachToGridOrMap(transform, transform.Comp);
|
||||
@@ -692,8 +652,9 @@ namespace Robust.Shared.Containers
|
||||
if (Insert((transform.Owner, transform.Comp, null, null), container))
|
||||
return true;
|
||||
|
||||
if (Transform(container.Owner).ParentUid.IsValid()
|
||||
&& TryGetContainingContainer(container.Owner, out var newContainer))
|
||||
var ownerXform = Transform(container.Owner);
|
||||
if (ownerXform.ParentUid.IsValid()
|
||||
&& TryGetContainingContainer((container.Owner, ownerXform), out var newContainer))
|
||||
return TryInsertIntoContainer(transform, newContainer);
|
||||
|
||||
return false;
|
||||
|
||||
@@ -60,9 +60,7 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
internal string GetPath(ResPath relPath)
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
|
||||
// Sanitise platform-specific path and standardize it for engine use.
|
||||
.Replace(Path.DirectorySeparatorChar, '/');
|
||||
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -14,7 +14,11 @@ namespace Robust.Shared.ContentPack
|
||||
/// The directory to use for user data.
|
||||
/// If null, a virtual temporary file system is used instead.
|
||||
/// </param>
|
||||
void Initialize(string? userData);
|
||||
/// <param name="hideUserDataDir">
|
||||
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
|
||||
/// <see cref="IResourceManager.UserData"/>.
|
||||
/// </param>
|
||||
void Initialize(string? userData, bool hideUserDataDir);
|
||||
|
||||
/// <summary>
|
||||
/// Mounts a single stream as a content file. Useful for unit testing.
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
|
||||
{
|
||||
/// <summary>
|
||||
/// The root path of this provider.
|
||||
/// Can be null if it's a virtual provider.
|
||||
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
|
||||
/// </summary>
|
||||
string? RootDir { get; }
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.ContentPack
|
||||
{
|
||||
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
|
||||
!OperatingSystem.IsWindows()
|
||||
&& !OperatingSystem.IsMacOS();
|
||||
|
||||
|
||||
internal static string SafeGetResourcePath(string baseDir, ResPath path)
|
||||
{
|
||||
var relSysPath = path.ToRelativeSystemPath();
|
||||
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
|
||||
{
|
||||
// Hard cap on any exploit smuggling a .. in there.
|
||||
// Since that could allow leaving sandbox.
|
||||
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
|
||||
}
|
||||
|
||||
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
|
||||
// better safe than sorry check
|
||||
if (!retPath.StartsWith(baseDir))
|
||||
{
|
||||
// Allow path to match if it's just missing the directory separator at the end.
|
||||
if (retPath != baseDir.TrimEnd(Path.DirectorySeparatorChar))
|
||||
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
|
||||
}
|
||||
|
||||
return retPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
|
||||
public IWritableDirProvider UserData { get; private set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Initialize(string? userData)
|
||||
public virtual void Initialize(string? userData, bool hideRootDir)
|
||||
{
|
||||
Sawmill = _logManager.GetSawmill("res");
|
||||
|
||||
if (userData != null)
|
||||
{
|
||||
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
|
||||
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -379,6 +379,10 @@ namespace Robust.Shared.ContentPack
|
||||
{
|
||||
var rootDir = loader.GetPath(new ResPath(@"/"));
|
||||
|
||||
// TODO: GET RID OF THIS.
|
||||
// This code shouldn't be passing OS disk paths through ResPath.
|
||||
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
yield return new ResPath(rootDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -875,6 +875,7 @@ Types:
|
||||
- "System.Text.StringBuilder Insert(int, object)"
|
||||
- "System.Text.StringBuilder Insert(int, sbyte)"
|
||||
- "System.Text.StringBuilder Insert(int, short)"
|
||||
- "System.Text.StringBuilder Insert(int, string)"
|
||||
- "System.Text.StringBuilder Insert(int, string, int)"
|
||||
- "System.Text.StringBuilder Insert(int, System.Decimal)"
|
||||
- "System.Text.StringBuilder Insert(int, System.ReadOnlySpan`1<char>)"
|
||||
|
||||
@@ -10,17 +10,22 @@ namespace Robust.Shared.ContentPack
|
||||
/// <inheritdoc />
|
||||
internal sealed class WritableDirProvider : IWritableDirProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
private readonly bool _hideRootDir;
|
||||
|
||||
public string RootDir { get; }
|
||||
|
||||
string? IWritableDirProvider.RootDir => _hideRootDir ? null : RootDir;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="WritableDirProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="rootDir">Root file system directory to allow writing.</param>
|
||||
public WritableDirProvider(DirectoryInfo rootDir)
|
||||
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
|
||||
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
|
||||
{
|
||||
// FullName does not have a trailing separator, and we MUST have a separator.
|
||||
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
|
||||
_hideRootDir = hideRootDir;
|
||||
}
|
||||
|
||||
#region File Access
|
||||
@@ -119,7 +124,7 @@ namespace Robust.Shared.ContentPack
|
||||
throw new FileNotFoundException();
|
||||
|
||||
var dirInfo = new DirectoryInfo(GetFullPath(path));
|
||||
return new WritableDirProvider(dirInfo);
|
||||
return new WritableDirProvider(dirInfo, _hideRootDir);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -180,20 +185,7 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
path = path.Clean();
|
||||
|
||||
return GetFullPath(RootDir, path);
|
||||
}
|
||||
|
||||
private static string GetFullPath(string root, ResPath path)
|
||||
{
|
||||
var relPath = path.ToRelativeSystemPath();
|
||||
if (relPath.Contains("\\..") || relPath.Contains("/.."))
|
||||
{
|
||||
// Hard cap on any exploit smuggling a .. in there.
|
||||
// Since that could allow leaving sandbox.
|
||||
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Path.Combine(root, relPath));
|
||||
return PathHelpers.SafeGetResourcePath(RootDir, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,19 +547,20 @@ public sealed class EntityDeserializer :
|
||||
_stopwatch.Restart();
|
||||
foreach (var (entity, data) in Entities)
|
||||
{
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
{
|
||||
#endif
|
||||
CurrentReadingEntity = data;
|
||||
LoadEntity(entity, _metaQuery.Comp(entity), data.Components, data.MissingComponents);
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
ToDelete.Add(entity);
|
||||
_log.Error($"Encountered error while loading entity. Yaml uid: {data.YamlId}. Loaded loaded entity: {EntMan.ToPrettyString(entity)}. Error:\n{e}.");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
CurrentReadingEntity = null;
|
||||
|
||||
@@ -162,8 +162,8 @@ public sealed class EntitySerializer : ISerializationContext,
|
||||
_log = _logMan.GetSawmill("entity_serializer");
|
||||
SerializerProvider.RegisterSerializer(this);
|
||||
|
||||
_metaName = _factory.GetComponentName(typeof(MetaDataComponent));
|
||||
_xformName = _factory.GetComponentName(typeof(TransformComponent));
|
||||
_metaName = _factory.GetComponentName<MetaDataComponent>();
|
||||
_xformName = _factory.GetComponentName<TransformComponent>();
|
||||
_emptyMetaNode = _serialization.WriteValueAs<MappingDataNode>(typeof(MetaDataComponent), new MetaDataComponent(), alwaysWrite: true, context: this);
|
||||
|
||||
CurrentComponent = _xformName;
|
||||
|
||||
@@ -41,6 +41,31 @@ public sealed partial class MapLoaderSystem
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load entities from a YAML file, taking in a raw byte stream.
|
||||
/// </summary>
|
||||
/// <param name="file">The file contents to load from.</param>
|
||||
/// <param name="fileName">
|
||||
/// The name of the file being loaded. This is used purely for logging/informational purposes.
|
||||
/// </param>
|
||||
/// <param name="result">The result of the load operation.</param>
|
||||
/// <param name="options">Options for the load operation.</param>
|
||||
/// <returns>True if the load succeeded, false otherwise.</returns>
|
||||
/// <seealso cref="M:Robust.Shared.EntitySerialization.Systems.MapLoaderSystem.TryLoadGeneric(Robust.Shared.Utility.ResPath,Robust.Shared.EntitySerialization.LoadResult@,System.Nullable{Robust.Shared.EntitySerialization.MapLoadOptions})"/>
|
||||
public bool TryLoadGeneric(
|
||||
Stream file,
|
||||
string fileName,
|
||||
[NotNullWhen(true)] out LoadResult? result,
|
||||
MapLoadOptions? options = null)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (!TryReadFile(new StreamReader(file), out var data))
|
||||
return false;
|
||||
|
||||
return TryLoadGeneric(data, fileName, out result, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load entities from a yaml file. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
|
||||
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
|
||||
@@ -55,6 +80,17 @@ public sealed partial class MapLoaderSystem
|
||||
if (!TryReadFile(file, out var data))
|
||||
return false;
|
||||
|
||||
return TryLoadGeneric(data, file.ToString(), out result, options);
|
||||
}
|
||||
|
||||
private bool TryLoadGeneric(
|
||||
MappingDataNode data,
|
||||
string fileName,
|
||||
[NotNullWhen(true)] out LoadResult? result,
|
||||
MapLoadOptions? options = null)
|
||||
{
|
||||
result = null;
|
||||
|
||||
_stopwatch.Restart();
|
||||
var ev = new BeforeEntityReadEvent();
|
||||
RaiseLocalEvent(ev);
|
||||
@@ -85,7 +121,7 @@ public sealed partial class MapLoaderSystem
|
||||
|
||||
if (!deserializer.TryProcessData())
|
||||
{
|
||||
Log.Debug($"Failed to process entity data in {file}");
|
||||
Log.Debug($"Failed to process entity data in {fileName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -95,7 +131,7 @@ public sealed partial class MapLoaderSystem
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Caught exception while creating entities for map {file}: {e}");
|
||||
Log.Error($"Caught exception while creating entities for map {fileName}: {e}");
|
||||
Delete(deserializer.Result);
|
||||
throw;
|
||||
}
|
||||
@@ -103,7 +139,7 @@ public sealed partial class MapLoaderSystem
|
||||
if (opts.ExpectedCategory is { } exp && exp != deserializer.Result.Category)
|
||||
{
|
||||
// Did someone try to load a map file as a grid or vice versa?
|
||||
Log.Error($"Map {file} does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
|
||||
Log.Error($"Map {fileName} does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
|
||||
Delete(deserializer.Result);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,13 @@ public sealed partial class MapLoaderSystem : EntitySystem
|
||||
return false;
|
||||
|
||||
Log.Info($"Loading file: {resPath}");
|
||||
return TryReadFile(reader, out data);
|
||||
}
|
||||
|
||||
private bool TryReadFile(TextReader reader, [NotNullWhen(true)] out MappingDataNode? data)
|
||||
{
|
||||
data = null;
|
||||
|
||||
_stopwatch.Restart();
|
||||
|
||||
using var textReader = reader;
|
||||
|
||||
@@ -273,11 +273,11 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
public IComponent GetComponent(Type componentType)
|
||||
{
|
||||
if (!_types.ContainsKey(componentType))
|
||||
if (!_types.TryGetValue(componentType, out var value))
|
||||
{
|
||||
throw new InvalidOperationException($"{componentType} is not a registered component.");
|
||||
}
|
||||
return _typeFactory.CreateInstanceUnchecked<IComponent>(_types[componentType].Type);
|
||||
return _typeFactory.CreateInstanceUnchecked<IComponent>(value.Type);
|
||||
}
|
||||
|
||||
public IComponent GetComponent(CompIdx componentType)
|
||||
@@ -287,11 +287,11 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
public T GetComponent<T>() where T : IComponent, new()
|
||||
{
|
||||
if (!_types.ContainsKey(typeof(T)))
|
||||
if (!_types.TryGetValue(typeof(T), out var reg))
|
||||
{
|
||||
throw new InvalidOperationException($"{typeof(T)} is not a registered component.");
|
||||
}
|
||||
return _typeFactory.CreateInstanceUnchecked<T>(_types[typeof(T)].Type);
|
||||
return _typeFactory.CreateInstanceUnchecked<T>(reg.Type);
|
||||
}
|
||||
|
||||
public IComponent GetComponent(ComponentRegistration reg)
|
||||
|
||||
@@ -150,6 +150,7 @@ namespace Robust.Shared.GameObjects
|
||||
[ViewVariables, Access(typeof(EntityManager), Other = AccessPermissions.ReadExecute)]
|
||||
public EntityLifeStage EntityLifeStage { get; internal set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public MetaDataFlags Flags
|
||||
{
|
||||
get => _flags;
|
||||
|
||||
@@ -633,7 +633,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// An invalid entity UID indicates that this entity has intentionally been removed from broadphases and should
|
||||
/// not automatically be re-added by movement events.
|
||||
/// </remarks>
|
||||
internal record struct BroadphaseData(EntityUid Uid, EntityUid PhysicsMap, bool CanCollide, bool Static)
|
||||
internal record struct BroadphaseData(EntityUid Uid, bool CanCollide, bool Static)
|
||||
{
|
||||
public bool IsValid() => Uid.IsValid();
|
||||
public bool Valid => IsValid();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user