Compare commits

..

36 Commits

Author SHA1 Message Date
PJB3005
f9d0dd551a Version: 263.0.0 2025-06-22 13:36:07 +02:00
Aiden
b2540a6e08 Static Field Assert (#5926)
* Static Field Assert

* Update Robust.Shared/Prototypes/PrototypeManager.ValidateFields.cs

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>

---------

Co-authored-by: GoobBot <uristmchands@proton.me>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-06-21 23:51:28 +02:00
DrSmugleaf
66d898ee91 Add GetMessage and SetMessage methods to OutputPanel (#5956)
* Add GetMessage and SetMessage methods to OutputPanel

* Copy paste bad

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-06-21 23:33:03 +02:00
ThereDrD
310dc676ea fix: use maxSizeX instead of Width in rich text entry measure (#5989)
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-06-21 15:16:38 +02:00
Perry Fraser
41844d2d30 Adjust how OpenAL extensions are requested (#6000)
* fix: use correct device in OAL extension lookup

* fix: don't try to set non-existent window icons

* Revert "fix: don't try to set non-existent window icons"

This reverts commit 793958fb8c.

Moving to other PR.
2025-06-21 15:16:02 +02:00
slarticodefast
c6f3af20d6 fully obsolete container methods (#6007) 2025-06-21 14:42:57 +02:00
Pieter-Jan Briers
5501209b35 Add API to load maps from byte stream (#6029)
In case you don't want to load from a ResPath.
2025-06-21 14:41:32 +02:00
Tayrtahn
9b2ef75762 Optimize DataDefinitionAnalyzer a bit (#6035)
* Skip fields/properties not in DataDefs

* Only check IsDataField once per field/property

* Remove pointless foreach loop

* Remove an extra IsDataDefinition check

* Revert unneeded changes from testing

* Revert "Remove pointless foreach loop"

This reverts commit f05d566904.

* Restore analysis of multiple declarations
2025-06-21 01:15:59 +02:00
Tayrtahn
196e59b7e4 Clean up all missing EntitySystem proxy method uses (#6027)
* Clean up all missing EntitySystem proxy method uses

* Restore comment

* Fix bad change that caused closure allocation

* tuple

* Revert "tuple"

This reverts commit 14581a40aa.

* Revert "Fix bad change that caused closure allocation"

This reverts commit 215b2559ed.

* Revert "Restore comment"

This reverts commit 4a47a36557.

* Revert "Clean up all missing EntitySystem proxy method uses"

This reverts commit 3b1fe4ce7f.

* Redo with improved code fixer.
Let's see how it fares this time
2025-06-21 00:05:09 +02:00
Perry Fraser
2c936b5973 fix: don't delete people who are teleported to themselves (#6040) 2025-06-20 14:00:27 +02:00
Tayrtahn
7765e71dca Add tests for remaining DataDefinitionAnalyzer diagnostics (#6034)
* Add tests for partial datadefs and partial nested datadefs

* Add test for redundant datafield tag

* Blank lines upset me
2025-06-20 02:26:08 +02:00
TrixxedHeart
d8ae71d8cd adds typeselector (thanks pbj) (#6038) 2025-06-20 02:24:03 +02:00
Tayrtahn
a74812ce5b Make AddComp where clause consistent with AddComponent (#6028) 2025-06-19 10:21:13 +10:00
PJB3005
a7f9b0a6db Fix debug assert when loading MIDI on Windows.
Fixes #6020

The assert was caused by the native OS path (C:\Windows\...) being passed through a ResPath. Bad. While looking at this I realized the sound font loader callback system was a mess and I should probably clean it up, so I did.

The file name is now properly namespaced in the loader callback, which should avoid spaghetti like this in the future. The details of how this works are a pain in the ass because Fluidsynth isn't well-designed.

I split LoadSoundfont() into two functions: one for resource, one for user paths. The other is kept there but compatible.

I can't believe I spent 3 hours on dealing with this nonsense and most of it is just due to Fluidsynth being poorly designed...
2025-06-18 03:25:58 +02:00
Amy
3aac92e4b2 soft only plz (#6030) 2025-06-17 18:56:44 +02:00
PJB3005
c152fb8953 Fix culture-based parsing in TimespanSerializer 2025-06-17 16:03:20 +02:00
DrSmugleaf
10ea5498cf Fix error in some localization functions when an argument is not an EntityUid (#6022) 2025-06-15 14:07:13 +02:00
Walker Fowlkes
324606e5a3 Add a new toolshed command spawn:in (#6021)
* added spawn:in command.

* better annotations

* use EntityManager.System

* ftl

* make it lazy cached.

* Fix typo (it's -> its)

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-06-15 14:05:27 +02:00
Tayrtahn
a8227f7faa Replace static logger call in FileDialogManager (#6018) 2025-06-13 23:20:23 +02:00
PJB3005
9f55400c58 Avoid closure allocation in physics SolveIsland()
There's a parallel call in there that's only used when the island should be processed parallel internally. This isn't done for all islands, so allocating the closure in every case is a massive waste.
2025-06-13 15:00:47 +02:00
PJB3005
8b971f7ae7 Add CompletionHelper.PrototypeIdsLimited API
Somebody ignored the doc comment saying "don't use this with EntityPrototype" so now just *typing* a Tippy command causes the server to lag. Great.

This still isn't too great for performance but at least it's better, and I don't want to commit to making PrototypeManager semi-thread-safe.
2025-06-13 00:15:17 +02:00
PJB3005
e3c7e361ae Avoid server stutters from scsi init
Task.Run go brr
2025-06-12 23:33:48 +02:00
Tayrtahn
5c48dcb211 Fix TabContainer.CurrentTab setter (#6017) 2025-06-12 00:31:03 +02:00
B_Kirill
694de028c2 AudioSystem logging extension (#5959)
* AudioSystem logging extension

* Redo

* Fix

* review
2025-06-11 02:17:49 +02:00
PJB3005
d41c9e7662 Properly catch errors when executing client commands.
Previously these errors propagated all the way into Clyde. Guh.

Probably still need more error handling around the input system, but this is important regardless.
2025-06-11 02:11:32 +02:00
B3CKDOOR
76134e0f8d Adding "Attribution-NonCommercial-NoDerivatives 4.0 International" (#6008)
* Adding "Attribution-NonCommercial-NoDerivatives 4.0 International"

Adding the "Attribution-NonCommercial-NoDerivatives 4.0 International" License type, this is getting marked as an "invalid" license when its actually a valid license.

[License link](https://creativecommons.org/licenses/by-nc-nd/4.0/)

* Darn, forgot a comma
2025-06-09 20:57:54 +02:00
Perry Fraser
2983517e43 fix: don't try to set non-existent window icons (#6016) 2025-06-09 20:56:01 +02:00
metalgearsloth
18849be0b4 Version: 262.0.0 2025-06-09 23:56:58 +10:00
Leon Friedrich
c6a1d82bb1 Validate that Toolshed command arguments have parsers (#6014)
* Add Nullable<T> support to ToolshedManager.TryParse

* Check that command arguments are parseable

* release notes

* a

* A is for Array

* Fix test

* Fix indentation
2025-06-09 23:47:29 +10:00
Leon Friedrich
d89e1a43c6 Add PvsResetTest (#6015) 2025-06-09 23:39:06 +10:00
Leon Friedrich
d894ef70ef Misc SpriteSystem fixes (#6001)
* Move GetPrototypeTextures to SpriteSystem

* Fix tests

* Fix #6002

* release notes

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-06-09 18:59:37 +10:00
DrSmugleaf
c7ea2793ca Fix TransformComponent state handling changing the coordinates of detached entities (#6006)
* Fix TransformComponent state handling changing the coordinates of detached entities

* Make ResetPredictedEntities not handle state for detached entities
2025-06-09 17:44:36 +10:00
Perry Fraser
0c61ff2bee fix: default audio params for PlayStatic (#6011) 2025-06-09 00:28:12 +02:00
Tayrtahn
343a34eac7 Fix warning CS0168 in EntityDeserializer (#6013)
* Fix warning CS0168 in EntityDeserializer

* Actually, why use try-catch just to rethrow anyway?
2025-06-09 00:04:03 +02:00
metalgearsloth
7be41f4890 Add ignoredcomps to IsDefault (#5998) 2025-06-05 23:54:47 +10:00
metalgearsloth
293470a5fe Fix incorrect saved window positions (#5927)
* Fix incorrect saved window positions

As of however many UI PRs ago windows store their last position on the client and it re-opens windows at that position.

The issue is that the code to avoid windows being able to go off-screen was immediately bulldozing this value, at least if the x <= 0. Now we just don't run it until we have a valid measure (probably the frame after) and avoid unnecessarily having an incorrect position applied.

* Explainer
2025-06-05 23:35:28 +10:00
83 changed files with 1101 additions and 558 deletions

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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

View File

@@ -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 =

View File

@@ -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()
{

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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()
{

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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>

View 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;
}
}
}

View File

@@ -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

View 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;
}
}
}

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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)

View File

@@ -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}");
}

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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()
{

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -42,6 +42,7 @@ public sealed class ColorSelectorSliders : Control
break;
}
_currentType = value;
_typeSelector.Select(_types.IndexOf(value));
UpdateType();
Update();
}

View File

@@ -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()

View File

@@ -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.");

View File

@@ -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);

View File

@@ -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,

View File

@@ -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(

View File

@@ -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}");
}
}

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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))

View File

@@ -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");

View File

@@ -199,7 +199,7 @@ internal sealed partial class PvsSystem : EntitySystem
foreach (var uid in _toDelete)
{
EntityManager.QueueDeleteEntity(uid);
QueueDel(uid);
}
_toDelete.Clear();

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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));

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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.

View File

@@ -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; }

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -665,7 +665,7 @@ public partial class EntitySystem
/// <inheritdoc cref="IEntityManager.AddComponent&lt;T&gt;(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);
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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))

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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());

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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))

View File

@@ -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

View File

@@ -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; }
}

View File

@@ -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
}

View File

@@ -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")]

View File

@@ -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;

View File

@@ -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

View File

@@ -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));
}
}

View File

@@ -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

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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]

View File

@@ -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");
}
});

View File

@@ -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)