mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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.2.2" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<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 -->
|
||||
|
||||
<Project>
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<PropertyGroup><Version>263.0.0</Version></PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -54,10 +54,69 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 261.2.2
|
||||
## 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.
|
||||
|
||||
|
||||
## 261.2.1
|
||||
## 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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
internal static class Program
|
||||
public 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,7 +5,6 @@ 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;
|
||||
@@ -25,7 +24,6 @@ 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!;
|
||||
@@ -63,10 +61,7 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
var cachePath = "";
|
||||
if (_resourceManager.UserData is WritableDirProvider userData)
|
||||
{
|
||||
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
|
||||
cachePath = Path.Combine(rootDir, "cef_cache", "0");
|
||||
}
|
||||
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
|
||||
|
||||
var settings = new CefSettings()
|
||||
{
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -387,7 +387,7 @@ namespace Robust.Client
|
||||
|
||||
_prof.Initialize();
|
||||
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
|
||||
|
||||
var mountOptions = _commandLineArgs != null
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
|
||||
|
||||
@@ -1224,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);
|
||||
@@ -1791,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -42,6 +42,7 @@ public sealed class ColorSelectorSliders : Control
|
||||
break;
|
||||
}
|
||||
_currentType = value;
|
||||
_typeSelector.Select(_types.IndexOf(value));
|
||||
UpdateType();
|
||||
Update();
|
||||
}
|
||||
|
||||
@@ -107,6 +107,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_scrollBar.Value = 0;
|
||||
}
|
||||
|
||||
public FormattedMessage GetMessage(Index index)
|
||||
{
|
||||
return new FormattedMessage(_entries[index].Message);
|
||||
}
|
||||
|
||||
public void RemoveEntry(Index index)
|
||||
{
|
||||
var entry = _entries[index];
|
||||
@@ -138,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)
|
||||
{
|
||||
@@ -147,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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -18,12 +18,15 @@ 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;
|
||||
@@ -267,7 +270,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
if (_kDialogAvailable)
|
||||
{
|
||||
Logger.DebugS("filedialog", "kdialog available.");
|
||||
_sawmill.Debug("kdialog available.");
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -404,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,
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace Robust.Client.UserInterface
|
||||
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(
|
||||
|
||||
@@ -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, hideUserDataDir: false);
|
||||
_resources.Initialize(dataDir);
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,10 +127,10 @@ 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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -391,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>
|
||||
@@ -418,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).
|
||||
@@ -459,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>
|
||||
|
||||
@@ -60,7 +60,9 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
internal string GetPath(ResPath relPath)
|
||||
{
|
||||
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
|
||||
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
|
||||
// Sanitise platform-specific path and standardize it for engine use.
|
||||
.Replace(Path.DirectorySeparatorChar, '/');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -14,11 +14,7 @@ namespace Robust.Shared.ContentPack
|
||||
/// The directory to use for user data.
|
||||
/// If null, a virtual temporary file system is used instead.
|
||||
/// </param>
|
||||
/// <param name="hideUserDataDir">
|
||||
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
|
||||
/// <see cref="IResourceManager.UserData"/>.
|
||||
/// </param>
|
||||
void Initialize(string? userData, bool hideUserDataDir);
|
||||
void Initialize(string? userData);
|
||||
|
||||
/// <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 or the path is protected (e.g. on the client).
|
||||
/// Can be null if it's a virtual provider.
|
||||
/// </summary>
|
||||
string? RootDir { get; }
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.ContentPack
|
||||
{
|
||||
@@ -64,27 +63,5 @@ 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, bool hideRootDir)
|
||||
public virtual void Initialize(string? userData)
|
||||
{
|
||||
Sawmill = _logManager.GetSawmill("res");
|
||||
|
||||
if (userData != null)
|
||||
{
|
||||
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
|
||||
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -379,10 +379,6 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,22 +10,17 @@ namespace Robust.Shared.ContentPack
|
||||
/// <inheritdoc />
|
||||
internal sealed class WritableDirProvider : IWritableDirProvider
|
||||
{
|
||||
private readonly bool _hideRootDir;
|
||||
|
||||
/// <inheritdoc />
|
||||
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>
|
||||
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
|
||||
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
|
||||
public WritableDirProvider(DirectoryInfo rootDir)
|
||||
{
|
||||
// FullName does not have a trailing separator, and we MUST have a separator.
|
||||
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
|
||||
_hideRootDir = hideRootDir;
|
||||
}
|
||||
|
||||
#region File Access
|
||||
@@ -124,7 +119,7 @@ namespace Robust.Shared.ContentPack
|
||||
throw new FileNotFoundException();
|
||||
|
||||
var dirInfo = new DirectoryInfo(GetFullPath(path));
|
||||
return new WritableDirProvider(dirInfo, _hideRootDir);
|
||||
return new WritableDirProvider(dirInfo);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -185,7 +180,20 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
path = path.Clean();
|
||||
|
||||
return PathHelpers.SafeGetResourcePath(RootDir, path);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,20 +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;
|
||||
#else
|
||||
ToDelete.Add(entity);
|
||||
_log.Error($"Encountered error while loading entity. Yaml uid: {data.YamlId}. Loaded loaded entity: {EntMan.ToPrettyString(entity)}. Error:\n{e}.");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
CurrentReadingEntity = null;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -161,7 +161,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// <summary>
|
||||
/// Returns true if the entity's data (apart from transform) is default.
|
||||
/// </summary>
|
||||
public bool IsDefault(EntityUid uid)
|
||||
public bool IsDefault(EntityUid uid, ICollection<string>? ignoredComps = null)
|
||||
{
|
||||
if (!MetaQuery.TryGetComponent(uid, out var metadata) || metadata.EntityPrototype == null)
|
||||
return false;
|
||||
@@ -195,6 +195,9 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
var compName = _componentFactory.GetComponentName(compType);
|
||||
|
||||
if (ignoredComps?.Contains(compName) == true)
|
||||
continue;
|
||||
|
||||
// If the component isn't on the prototype then it's custom.
|
||||
if (!protoData.TryGetValue(compName, out var protoMapping))
|
||||
return false;
|
||||
|
||||
@@ -665,7 +665,7 @@ public partial class EntitySystem
|
||||
|
||||
/// <inheritdoc cref="IEntityManager.AddComponent<T>(EntityUid)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected T AddComp<T>(EntityUid uid) where T : Component, new()
|
||||
protected T AddComp<T>(EntityUid uid) where T : IComponent, new()
|
||||
{
|
||||
return EntityManager.AddComponent<T>(uid);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
private void OnStartup(EntityUid uid, CollideOnAnchorComponent component, ComponentStartup args)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(uid, out TransformComponent? transformComponent)) return;
|
||||
if (!TryComp(uid, out TransformComponent? transformComponent)) return;
|
||||
|
||||
SetCollide(uid, component, transformComponent.Anchored);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
private void SetCollide(EntityUid uid, CollideOnAnchorComponent component, bool anchored)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? body)) return;
|
||||
if (!TryComp(uid, out PhysicsComponent? body)) return;
|
||||
|
||||
var enabled = component.Enable;
|
||||
|
||||
|
||||
@@ -73,8 +73,8 @@ internal sealed class PrototypeReloadSystem : EntitySystem
|
||||
var data = newPrototype.Components[name];
|
||||
var component = _componentFactory.GetComponent(name);
|
||||
|
||||
if (!EntityManager.HasComponent(entity, component.GetType()))
|
||||
EntityManager.AddComponent(entity, component);
|
||||
if (!HasComp(entity, component.GetType()))
|
||||
AddComp(entity, component);
|
||||
}
|
||||
|
||||
// Update entity metadata
|
||||
|
||||
@@ -69,19 +69,19 @@ namespace Robust.Shared.GameObjects
|
||||
if (!_enabled)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? body))
|
||||
if (!TryComp(uid, out PhysicsComponent? body))
|
||||
{
|
||||
Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(body)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(uid, out FixturesComponent? manager))
|
||||
if (!TryComp(uid, out FixturesComponent? manager))
|
||||
{
|
||||
Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(manager)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(uid, out TransformComponent? xform))
|
||||
if (!TryComp(uid, out TransformComponent? xform))
|
||||
{
|
||||
Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(TransformComponent)}");
|
||||
return;
|
||||
|
||||
@@ -158,7 +158,7 @@ public abstract partial class SharedMapSystem
|
||||
// Just MapLoader things.
|
||||
if (component.MapProxy == DynamicTree.Proxy.Free) return;
|
||||
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(uid);
|
||||
var xform = Comp<TransformComponent>(uid);
|
||||
var aabb = GetWorldAABB(uid, component, xform);
|
||||
|
||||
if (TryComp<GridTreeComponent>(xform.MapUid, out var gridTree))
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.Shared.Map.Components;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
|
||||
@@ -636,7 +636,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (!_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key));
|
||||
RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -685,7 +685,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
{
|
||||
// Not guaranteed to open so rely upon the event handling it.
|
||||
// Also lets client request it to be opened remotely too.
|
||||
EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key));
|
||||
RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key));
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Robust.Shared.GameObjects
|
||||
base.Update(frameTime);
|
||||
|
||||
// Avoid a collection was modified while enumerating.
|
||||
var timers = EntityManager.EntityQueryEnumerator<TimerComponent>();
|
||||
var timers = EntityQueryEnumerator<TimerComponent>();
|
||||
var timersList = new ValueList<(EntityUid, TimerComponent)>();
|
||||
while (timers.MoveNext(out var uid, out var timer))
|
||||
{
|
||||
@@ -28,7 +28,7 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
if (!timer.Deleted && !EntityManager.Deleted(uid) && timer.RemoveOnEmpty && timer.TimerCount == 0)
|
||||
{
|
||||
EntityManager.RemoveComponent<TimerComponent>(uid);
|
||||
RemComp<TimerComponent>(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,10 +170,8 @@ namespace Robust.Shared.Localization
|
||||
if (args.Args.Count < 1) return new LocValueString(nameof(Gender.Neuter));
|
||||
|
||||
ILocValue entity0 = args.Args[0];
|
||||
if (entity0.Value != null)
|
||||
if (entity0.Value is EntityUid entity)
|
||||
{
|
||||
EntityUid entity = (EntityUid)entity0.Value;
|
||||
|
||||
if (_entMan.TryGetComponent(entity, out GrammarComponent? grammar) && grammar.Gender.HasValue)
|
||||
{
|
||||
return new LocValueString(grammar.Gender.Value.ToString().ToLowerInvariant());
|
||||
@@ -246,10 +244,8 @@ namespace Robust.Shared.Localization
|
||||
if (args.Args.Count < 1) return new LocValueString(GetString("zzzz-counter-default"));
|
||||
|
||||
ILocValue entity0 = args.Args[0];
|
||||
if (entity0.Value != null)
|
||||
if (entity0.Value is EntityUid entity)
|
||||
{
|
||||
EntityUid entity = (EntityUid)entity0.Value;
|
||||
|
||||
if (TryGetEntityLocAttrib(entity, "counter", out var counter))
|
||||
{
|
||||
return new LocValueString(counter);
|
||||
@@ -292,9 +288,8 @@ namespace Robust.Shared.Localization
|
||||
if (args.Args.Count < 2) return new LocValueString("other");
|
||||
|
||||
ILocValue entity0 = args.Args[0];
|
||||
if (entity0.Value != null)
|
||||
if (entity0.Value is EntityUid entity)
|
||||
{
|
||||
EntityUid entity = (EntityUid)entity0.Value;
|
||||
ILocValue attrib0 = args.Args[1];
|
||||
if (TryGetEntityLocAttrib(entity, attrib0.Format(new LocContext(bundle)), out var attrib))
|
||||
{
|
||||
@@ -313,10 +308,8 @@ namespace Robust.Shared.Localization
|
||||
if (args.Args.Count < 1) return new LocValueString("false");
|
||||
|
||||
ILocValue entity0 = args.Args[0];
|
||||
if (entity0.Value != null)
|
||||
if (entity0.Value is EntityUid entity)
|
||||
{
|
||||
EntityUid entity = (EntityUid)entity0.Value;
|
||||
|
||||
if (_entMan.TryGetComponent(entity, out GrammarComponent? grammar) && grammar.ProperNoun.HasValue)
|
||||
{
|
||||
return new LocValueString(grammar.ProperNoun.Value.ToString().ToLowerInvariant());
|
||||
|
||||
@@ -257,7 +257,7 @@ namespace Robust.Shared.Physics.Systems
|
||||
if (args.Current is not FixtureManagerComponentState state)
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? physics))
|
||||
if (!TryComp(uid, out PhysicsComponent? physics))
|
||||
{
|
||||
Log.Error($"Tried to apply fixture state for an entity without physics: {ToPrettyString(uid)}");
|
||||
return;
|
||||
|
||||
@@ -121,7 +121,7 @@ public abstract partial class SharedJointSystem : EntitySystem
|
||||
foreach (var joint in _dirtyJoints)
|
||||
{
|
||||
if (joint.Comp.Deleted || joint.Comp.JointCount != 0) continue;
|
||||
EntityManager.RemoveComponent<JointComponent>(joint);
|
||||
RemComp<JointComponent>(joint);
|
||||
}
|
||||
|
||||
_dirtyJoints.Clear();
|
||||
@@ -551,7 +551,7 @@ public abstract partial class SharedJointSystem : EntitySystem
|
||||
_physics.WakeBody(uidA);
|
||||
}
|
||||
|
||||
if (EntityManager.TryGetComponent<PhysicsComponent>(bodyBUid, out var bodyB) &&
|
||||
if (TryComp<PhysicsComponent>(bodyBUid, out var bodyB) &&
|
||||
MetaData(bodyBUid).EntityLifeStage < EntityLifeStage.Terminating)
|
||||
{
|
||||
var uidB = jointComponentB.Relay ?? bodyBUid;
|
||||
|
||||
@@ -633,7 +633,7 @@ public abstract partial class SharedPhysicsSystem
|
||||
);
|
||||
|
||||
// We'll sort islands from internally parallel (due to lots of contacts) to running all the islands in parallel
|
||||
islands.Sort((x, y) => InternalParallel(y).CompareTo(InternalParallel(x)));
|
||||
islands.Sort(static (x, y) => InternalParallel(y).CompareTo(InternalParallel(x)));
|
||||
|
||||
var totalBodies = 0;
|
||||
var actualIslands = islands.ToArray();
|
||||
@@ -904,16 +904,48 @@ public abstract partial class SharedPhysicsSystem
|
||||
|
||||
if (options != null)
|
||||
{
|
||||
const int FinaliseBodies = 32;
|
||||
var batches = (int)MathF.Ceiling((float) bodyCount / FinaliseBodies);
|
||||
|
||||
Parallel.For(0, batches, options, i =>
|
||||
// Isolate to avoid delegate capture allocation unless we're actually processing parallel here.
|
||||
static void ProcessParallelInternal(
|
||||
SharedPhysicsSystem system,
|
||||
ParallelOptions options,
|
||||
int bodyCount,
|
||||
int offset,
|
||||
List<Entity<PhysicsComponent, TransformComponent>> bodies,
|
||||
Vector2[] positions,
|
||||
float[] angles,
|
||||
Vector2[] solvedPositions,
|
||||
float[] solvedAngles)
|
||||
{
|
||||
var start = i * FinaliseBodies;
|
||||
var end = Math.Min(bodyCount, start + FinaliseBodies);
|
||||
const int FinaliseBodies = 32;
|
||||
var batches = (int)MathF.Ceiling((float) bodyCount / FinaliseBodies);
|
||||
|
||||
FinalisePositions(start, end, offset, bodies, positions, angles, solvedPositions, solvedAngles);
|
||||
});
|
||||
Parallel.For(0, batches, options, i =>
|
||||
{
|
||||
var start = i * FinaliseBodies;
|
||||
var end = Math.Min(bodyCount, start + FinaliseBodies);
|
||||
|
||||
system.FinalisePositions(
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
bodies,
|
||||
positions,
|
||||
angles,
|
||||
solvedPositions,
|
||||
solvedAngles);
|
||||
});
|
||||
}
|
||||
|
||||
ProcessParallelInternal(
|
||||
this,
|
||||
options,
|
||||
bodyCount,
|
||||
offset,
|
||||
bodies,
|
||||
positions,
|
||||
angles,
|
||||
solvedPositions,
|
||||
solvedAngles);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@ public partial class PrototypeManager
|
||||
Dictionary<Type, HashSet<string>> prototypes)
|
||||
{
|
||||
DebugTools.Assert(field.IsStatic);
|
||||
DebugTools.Assert(!field.HasCustomAttribute<DataFieldAttribute>(), "Datafields should not be static");
|
||||
DebugTools.Assert(!field.HasCustomAttribute<DataFieldAttribute>(), $"Datafields should not be static. Field: {field.Name} in Type: {field.DeclaringType}");
|
||||
|
||||
// Is this even a prototype id related field?
|
||||
if (!TryGetFieldPrototype(field, out var proto))
|
||||
|
||||
@@ -9,6 +9,7 @@ using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Serialization.TypeSerializers.Implementations;
|
||||
|
||||
@@ -74,7 +75,7 @@ public sealed class TimespanSerializer : ITypeSerializer<TimeSpan, ValueDataNode
|
||||
|
||||
// A lot of the checks will be for plain numbers, so might as well rule them out right away, instead of
|
||||
// running all the other checks on them. They will need to get parsed later anyway, if they weren't now.
|
||||
if (double.TryParse(node.Value, out var v))
|
||||
if (Parse.TryDouble(node.Value, out var v))
|
||||
{
|
||||
timeSpan = TimeSpan.FromSeconds(v);
|
||||
return true;
|
||||
@@ -85,7 +86,7 @@ public sealed class TimespanSerializer : ITypeSerializer<TimeSpan, ValueDataNode
|
||||
return false;
|
||||
|
||||
// If the input without the last character is still not a valid number, exit
|
||||
if (!double.TryParse(node.Value.AsSpan()[..^1], out var number))
|
||||
if (!Parse.TryDouble(node.Value.AsSpan()[..^1], out var number))
|
||||
return false;
|
||||
|
||||
// Check the last character of the input for time unit indicators
|
||||
|
||||
@@ -46,8 +46,9 @@ public sealed class PipedArgumentAttribute : Attribute;
|
||||
[MeansImplicitUse]
|
||||
public sealed class CommandArgumentAttribute : Attribute
|
||||
{
|
||||
public CommandArgumentAttribute(Type? customParser = null)
|
||||
public CommandArgumentAttribute(Type? customParser = null, bool unparseable = false)
|
||||
{
|
||||
Unparseable = unparseable;
|
||||
if (customParser == null)
|
||||
return;
|
||||
|
||||
@@ -56,6 +57,13 @@ public sealed class CommandArgumentAttribute : Attribute
|
||||
$"Custom parser {customParser.PrettyName()} does not inherit from {typeof(CustomTypeParser<>).PrettyName()}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Command initialization will validate that all of a command's arguments are parseable (i.e., have a type parser).
|
||||
/// In some niche situations you might want to have a command that accepts unparseable arguments that must be
|
||||
/// supplied via a toolshed variable or command block, in which case the check can be disabled via this property.
|
||||
/// </summary>
|
||||
public bool Unparseable { get; }
|
||||
|
||||
public Type? CustomParser { get; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Shared.Toolshed.Commands.Entities;
|
||||
@@ -10,7 +13,10 @@ namespace Robust.Shared.Toolshed.Commands.Entities;
|
||||
[ToolshedCommand]
|
||||
internal sealed class SpawnCommand : ToolshedCommand
|
||||
{
|
||||
private SharedContainerSystem? sharedContainerSystem = null;
|
||||
|
||||
#region spawn:at implementations
|
||||
|
||||
[CommandImplementation("at")]
|
||||
public EntityUid SpawnAt([PipedArgument] EntityCoordinates target, EntProtoId proto)
|
||||
{
|
||||
@@ -20,9 +26,11 @@ internal sealed class SpawnCommand : ToolshedCommand
|
||||
[CommandImplementation("at")]
|
||||
public IEnumerable<EntityUid> SpawnAt([PipedArgument] IEnumerable<EntityCoordinates> target, EntProtoId proto)
|
||||
=> target.Select(x => SpawnAt(x, proto));
|
||||
|
||||
#endregion
|
||||
|
||||
#region spawn:on implementations
|
||||
|
||||
[CommandImplementation("on")]
|
||||
public EntityUid SpawnOn([PipedArgument] EntityUid target, EntProtoId proto)
|
||||
{
|
||||
@@ -32,9 +40,38 @@ internal sealed class SpawnCommand : ToolshedCommand
|
||||
[CommandImplementation("on")]
|
||||
public IEnumerable<EntityUid> SpawnOn([PipedArgument] IEnumerable<EntityUid> target, EntProtoId proto)
|
||||
=> target.Select(x => SpawnOn(x, proto));
|
||||
|
||||
#endregion
|
||||
|
||||
#region spawn:in implementations
|
||||
|
||||
[CommandImplementation("in")]
|
||||
public EntityUid SpawnIn([PipedArgument] EntityUid target, string containerId, EntProtoId proto)
|
||||
{
|
||||
var spawned = SpawnOn(target, proto);
|
||||
if (!TryComp<TransformComponent>(spawned, out var transformComponent) ||
|
||||
!TryComp<MetaDataComponent>(spawned, out var metaDataComp) ||
|
||||
!TryComp<PhysicsComponent>(spawned, out var physicsComponent))
|
||||
return spawned;
|
||||
sharedContainerSystem ??= EntityManager.System<SharedContainerSystem>();
|
||||
var container = sharedContainerSystem.GetContainer(target, containerId);
|
||||
sharedContainerSystem.InsertOrDrop((spawned, transformComponent, metaDataComp, physicsComponent),
|
||||
container
|
||||
);
|
||||
return spawned;
|
||||
}
|
||||
|
||||
[CommandImplementation("in")]
|
||||
public IEnumerable<EntityUid> SpawnIn(
|
||||
[PipedArgument] IEnumerable<EntityUid> target,
|
||||
string containerId,
|
||||
EntProtoId proto)
|
||||
=> target.Select(x => SpawnIn(x, containerId, proto));
|
||||
|
||||
#endregion
|
||||
|
||||
#region spawn:attached implementations
|
||||
|
||||
[CommandImplementation("attached")]
|
||||
public EntityUid SpawnIn([PipedArgument] EntityUid target, EntProtoId proto)
|
||||
{
|
||||
@@ -44,5 +81,6 @@ internal sealed class SpawnCommand : ToolshedCommand
|
||||
[CommandImplementation("attached")]
|
||||
public IEnumerable<EntityUid> SpawnIn([PipedArgument] IEnumerable<EntityUid> target, EntProtoId proto)
|
||||
=> target.Select(x => SpawnIn(x, proto));
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ internal sealed class TpCommand : ToolshedCommand
|
||||
{
|
||||
private SharedTransformSystem? _xform;
|
||||
|
||||
// TODO TOOLSHED
|
||||
// add EntityCoordinates parser
|
||||
|
||||
[CommandImplementation("coords")]
|
||||
public EntityUid TpCoords([PipedArgument] EntityUid teleporter, EntityCoordinates target)
|
||||
public EntityUid TpCoords([PipedArgument] EntityUid teleporter, [CommandArgument(unparseable:true)] EntityCoordinates target)
|
||||
{
|
||||
_xform ??= GetSys<SharedTransformSystem>();
|
||||
_xform.SetCoordinates(teleporter, target);
|
||||
@@ -20,7 +23,7 @@ internal sealed class TpCommand : ToolshedCommand
|
||||
}
|
||||
|
||||
[CommandImplementation("coords")]
|
||||
public IEnumerable<EntityUid> TpCoords([PipedArgument] IEnumerable<EntityUid> teleporters, EntityCoordinates target)
|
||||
public IEnumerable<EntityUid> TpCoords([PipedArgument] IEnumerable<EntityUid> teleporters, [CommandArgument(unparseable:true)] EntityCoordinates target)
|
||||
=> teleporters.Select(x => TpCoords(x, target));
|
||||
|
||||
[CommandImplementation("to")]
|
||||
|
||||
@@ -124,12 +124,12 @@ public abstract partial class ToolshedCommand
|
||||
{
|
||||
var hasAnyAttribute = false;
|
||||
|
||||
if (param.HasCustomAttribute<CommandArgumentAttribute>())
|
||||
if (param.GetCustomAttribute<CommandArgumentAttribute>() is {} cmdAttr)
|
||||
{
|
||||
if (param.Name == null || !argNames.Add(param.Name))
|
||||
throw new InvalidCommandImplementation($"Command arguments must have a unique name");
|
||||
hasAnyAttribute = true;
|
||||
ValidateArg(param);
|
||||
ValidateArg(param, cmdAttr);
|
||||
}
|
||||
|
||||
if (param.HasCustomAttribute<PipedArgumentAttribute>())
|
||||
@@ -232,8 +232,18 @@ public abstract partial class ToolshedCommand
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateArg(ParameterInfo arg)
|
||||
private void ValidateArg(ParameterInfo arg, CommandArgumentAttribute? cmdAttr = null)
|
||||
{
|
||||
if (cmdAttr == null || cmdAttr.CustomParser == null && !cmdAttr.Unparseable)
|
||||
{
|
||||
// This checks that each argument has a corresponding type parser, as people have sometimes created a command
|
||||
// without realising that the type is unparseable.
|
||||
var t = Nullable.GetUnderlyingType(arg.ParameterType) ?? arg.ParameterType;
|
||||
var ignore = t.IsGenericType || t.IsArray || t.ContainsGenericParameters;
|
||||
if (!ignore && Toolshed.GetParserForType(t) == null)
|
||||
throw new InvalidCommandImplementation($"{Name} command argument of type {t.PrettyName()} has no type parser. You either need to add a type parser or explicitly mark the argument as unparseable.");
|
||||
}
|
||||
|
||||
var isParams = arg.HasCustomAttribute<ParamArrayAttribute>();
|
||||
if (!isParams)
|
||||
return;
|
||||
|
||||
@@ -231,7 +231,10 @@ public sealed partial class ToolshedManager
|
||||
/// <returns>Success.</returns>
|
||||
public bool TryParse<T>(ParserContext parserContext, [NotNullWhen(true)] out T? parsed)
|
||||
{
|
||||
var res = TryParse(parserContext, typeof(T), out var p);
|
||||
var t = typeof(T);
|
||||
t = Nullable.GetUnderlyingType(t) ?? t;
|
||||
var res = TryParse(parserContext, t, out var p);
|
||||
|
||||
if (p is not null)
|
||||
parsed = (T?) p;
|
||||
else
|
||||
|
||||
@@ -81,7 +81,7 @@ public abstract class SpanLikeTypeParser<T, TElem> : TypeParser<T>
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return CompletionResult.FromHint(typeof(T).PrettyName());
|
||||
return CompletionResult.FromHint(GetArgHint(arg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ using Robust.Shared.Player;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Utility;
|
||||
using AppearanceSystem = Robust.Client.GameObjects.AppearanceSystem;
|
||||
using InputSystem = Robust.Server.GameObjects.InputSystem;
|
||||
using MapSystem = Robust.Server.GameObjects.MapSystem;
|
||||
using PointLightComponent = Robust.Client.GameObjects.PointLightComponent;
|
||||
@@ -141,6 +142,7 @@ namespace Robust.UnitTesting
|
||||
systems.LoadExtraSystemType<RecursiveMoveSystem>();
|
||||
systems.LoadExtraSystemType<SpriteSystem>();
|
||||
systems.LoadExtraSystemType<SpriteTreeSystem>();
|
||||
systems.LoadExtraSystemType<AppearanceSystem>();
|
||||
systems.LoadExtraSystemType<GridChunkBoundsDebugSystem>();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -136,35 +136,39 @@ public sealed class PvsPauseTest : RobustIntegrationTest
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: false, clientPaused: true);
|
||||
|
||||
// Unpause the entity while out of range
|
||||
{
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: true, clientPaused: true);
|
||||
// Unpause the entity while out of range
|
||||
{
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: true, clientPaused: true);
|
||||
|
||||
await server.WaitPost(() => metaSys.SetEntityPaused(sEnt, false));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: true, clientPaused: true);
|
||||
await server.WaitPost(() => metaSys.SetEntityPaused(sEnt, false));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: true, clientPaused: true);
|
||||
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: false, clientPaused: false);
|
||||
}
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: false, clientPaused: false);
|
||||
}
|
||||
|
||||
// Pause the entity while out of range
|
||||
{
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: true, clientPaused: true);
|
||||
// Pause the entity while out of range
|
||||
{
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: false, detached: true, clientPaused: true);
|
||||
|
||||
await server.WaitPost(() => metaSys.SetEntityPaused(sEnt, true));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: true, clientPaused: true);
|
||||
await server.WaitPost(() => metaSys.SetEntityPaused(sEnt, true));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: true, clientPaused: true);
|
||||
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: false, clientPaused: true);
|
||||
}
|
||||
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
|
||||
await RunTicks();
|
||||
AssertEnt(paused: true, detached: false, clientPaused: true);
|
||||
}
|
||||
|
||||
await client.WaitPost(() => netMan.ClientDisconnect(""));
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
134
Robust.UnitTesting/Server/GameStates/PvsResetTest.cs
Normal file
134
Robust.UnitTesting/Server/GameStates/PvsResetTest.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Robust.UnitTesting.Server.GameStates;
|
||||
|
||||
public sealed class PvsResetTest : RobustIntegrationTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Check that the client doesn't reset dirty detached entities. They should remain in nullspace.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ResetTest()
|
||||
{
|
||||
var server = StartServer();
|
||||
var client = StartClient();
|
||||
|
||||
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
|
||||
|
||||
var sEntMan = server.EntMan;
|
||||
var confMan = server.CfgMan;
|
||||
var sPlayerMan = server.PlayerMan;
|
||||
var xforms = sEntMan.System<SharedTransformSystem>();
|
||||
|
||||
var cEntMan = client.EntMan;
|
||||
var cPlayerMan = client.PlayerMan;
|
||||
var netMan = client.ResolveDependency<IClientNetManager>();
|
||||
|
||||
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
|
||||
client.Post(() => netMan.ClientConnect(null!, 0, null!));
|
||||
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
|
||||
|
||||
async Task RunTicks()
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
await server.WaitRunTicks(1);
|
||||
await client.WaitRunTicks(1);
|
||||
}
|
||||
}
|
||||
|
||||
await RunTicks();
|
||||
|
||||
// Set up map and spawn player
|
||||
EntityUid sMap = default;
|
||||
EntityUid playerUid = default;
|
||||
EntityUid sEnt = default;
|
||||
EntityCoordinates coords = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
sMap = server.System<SharedMapSystem>().CreateMap();
|
||||
coords = new(sMap, default);
|
||||
|
||||
playerUid = sEntMan.SpawnEntity(null, coords);
|
||||
sEnt = sEntMan.SpawnEntity(null, coords);
|
||||
// Attach player.
|
||||
var session = sPlayerMan.Sessions.First();
|
||||
server.PlayerMan.SetAttachedEntity(session, playerUid);
|
||||
sPlayerMan.JoinGame(session);
|
||||
});
|
||||
|
||||
await RunTicks();
|
||||
var farAway = new EntityCoordinates(sMap, new Vector2(100, 100));
|
||||
var netEnt = sEntMan.GetNetEntity(sEnt);
|
||||
var player = sEntMan.GetNetEntity(playerUid);
|
||||
Assert.That(player, Is.Not.EqualTo(NetEntity.Invalid));
|
||||
Assert.That(netEnt, Is.Not.EqualTo(NetEntity.Invalid));
|
||||
|
||||
// Check player got properly attached, and has received the other entity.
|
||||
Assert.That(cEntMan.TryGetEntity(netEnt, out var uid));
|
||||
Assert.That(cEntMan.TryGetEntity(player, out var cPlayerUid));
|
||||
var cEnt = uid!.Value;
|
||||
Assert.That(cPlayerMan.LocalEntity, Is.EqualTo(cPlayerUid));
|
||||
var cMap = cEntMan.GetEntity(sEntMan.GetNetEntity(sMap));
|
||||
|
||||
void AssertDetached(bool detached)
|
||||
{
|
||||
var cXform = client.Transform(cEnt);
|
||||
var sXform = server.Transform(sEnt);
|
||||
var meta = client.MetaData(cEnt);
|
||||
|
||||
Assert.That(sXform.MapUid, Is.EqualTo(sMap));
|
||||
Assert.That(sXform.ParentUid, Is.EqualTo(sMap));
|
||||
|
||||
if (detached)
|
||||
{
|
||||
Assert.That(meta.Flags.HasFlag(MetaDataFlags.Detached));
|
||||
Assert.That(cXform.MapUid, Is.Null);
|
||||
Assert.That(cXform.ParentUid, Is.EqualTo(EntityUid.Invalid));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(!meta.Flags.HasFlag(MetaDataFlags.Detached));
|
||||
Assert.That(cXform.MapUid, Is.EqualTo(cMap));
|
||||
Assert.That(cXform.ParentUid, Is.EqualTo(cMap));
|
||||
}
|
||||
}
|
||||
|
||||
// Entity is initially in view
|
||||
AssertDetached(false);
|
||||
|
||||
// Move the player out of the entity's PVS range
|
||||
await server.WaitPost(() => xforms.SetCoordinates(playerUid, farAway));
|
||||
await RunTicks();
|
||||
|
||||
// Client should now have detached the entity, moving it into nullspace
|
||||
AssertDetached(true);
|
||||
|
||||
// Marking the entity as dirty due to client-side prediction should have effect
|
||||
await client.WaitPost(() => client.EntMan.Dirty(cEnt, client.Transform(cEnt)));
|
||||
await RunTicks();
|
||||
AssertDetached(true);
|
||||
|
||||
// Move the player back into range
|
||||
await server.WaitPost( () => xforms.SetCoordinates(playerUid, coords));
|
||||
await RunTicks();
|
||||
AssertDetached(false);
|
||||
|
||||
// Marking the entity as dirty due to client-side prediction should have no real effect
|
||||
await client.WaitPost(() => client.EntMan.Dirty(cEnt, client.Transform(cEnt)));
|
||||
await RunTicks();
|
||||
AssertDetached(false);
|
||||
|
||||
await client.WaitPost(() => netMan.ClientDisconnect(""));
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Robust.UnitTesting.Shared.Resources
|
||||
_testDir = Directory.CreateDirectory(_testDirPath);
|
||||
var subDir = Path.Combine(_testDirPath, "writable");
|
||||
|
||||
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir), hideRootDir: false);
|
||||
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir));
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Toolshed.TypeParsers;
|
||||
@@ -43,6 +44,7 @@ public sealed class ToolshedValidationTest : ToolshedTest
|
||||
foreach (var type in types)
|
||||
{
|
||||
var instance = (ToolshedCommand)Activator.CreateInstance(type)!;
|
||||
Server.Resolve<IDependencyCollection>().InjectDependencies(instance, oneOff: true);
|
||||
Assert.Throws<InvalidCommandImplementation>(instance.Init, $"{type.PrettyName()} did not throw a {nameof(InvalidCommandImplementation)} exception");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ class License(Validator):
|
||||
"CC-BY-NC-SA-4.0",
|
||||
"CC-BY-ND-3.0",
|
||||
"CC-BY-ND-4.0",
|
||||
"CC-BY-NC-ND-4.0",
|
||||
"CC0-1.0",
|
||||
"MIT",
|
||||
"Custom" # implies that the license is described in the copyright field.
|
||||
@@ -27,4 +28,4 @@ class Url(Validator):
|
||||
|
||||
def _is_valid(self, value):
|
||||
# Source field is required to ensure its not neglected, but there may be no applicable URL
|
||||
return (value == "NA") or validators.url(value)
|
||||
return (value == "NA") or validators.url(value)
|
||||
|
||||
Reference in New Issue
Block a user