Compare commits

..

4 Commits

Author SHA1 Message Date
PJB3005
7d6def6adf Version: 261.2.2 2025-09-19 09:17:27 +02:00
Skye
f733a9efa5 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:27 +02:00
PJB3005
8a68dce987 Version: 261.2.1 2025-09-14 14:55:51 +02:00
PJB3005
b8d840437e Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

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

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

    Move CEF cache out of data directory

    Don't want content messing with this...

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

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

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

    Update SpaceWizards.NFluidSynth to 0.2.2

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

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
2025-09-14 14:55:51 +02:00
83 changed files with 558 additions and 1101 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.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

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

View File

@@ -54,69 +54,10 @@ END TEMPLATE-->
*None yet*
## 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.2
## 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.1
## 261.2.0

View File

@@ -195,8 +195,6 @@ 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(string? tag = null) : DataFieldBaseAttribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
public sealed class NotYamlSerializableAttribute : Attribute;
}
@@ -117,61 +117,6 @@ 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";
public static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
private 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."
);
public static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
private 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."
);
public static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
private 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,23 +102,14 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolStartAction(symbolContext =>
{
if (symbolContext.Symbol is not INamedTypeSymbol typeSymbol)
return;
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);
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);
context.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
}
private void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
@@ -126,7 +117,8 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (context.Node is not TypeDeclarationSyntax declaration)
return;
if (context.ContainingSymbol is not INamedTypeSymbol type)
var type = context.SemanticModel.GetDeclaredSymbol(declaration)!;
if (!IsDataDefinition(type))
return;
if (!IsPartial(declaration))
@@ -137,7 +129,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));
@@ -152,26 +144,27 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (context.Node is not FieldDeclarationSyntax field)
return;
if (context.ContainingSymbol?.ContainingType is not INamedTypeSymbol type)
var typeDeclaration = field.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration == null)
return;
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
if (!IsDataDefinition(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, datafieldAttribute))
if (HasRedundantTag(fieldSymbol))
{
TryGetAttributeLocation(field, DataFieldAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, fieldSymbol.Name, type.Name));
@@ -203,28 +196,25 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (context.Node is not PropertyDeclarationSyntax property)
return;
if (context.ContainingSymbol is not IPropertySymbol propertySymbol)
var typeDeclaration = property.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration == null)
return;
if (propertySymbol.ContainingType is not INamedTypeSymbol type)
return;
if (type.IsRecord || type.IsValueType)
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
if (!IsDataDefinition(type) || 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, datafieldAttribute))
if (HasRedundantTag(propertySymbol))
{
TryGetAttributeLocation(property, DataFieldAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, propertySymbol.Name, type.Name));
@@ -252,6 +242,9 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
{
if (!IsDataField(field, out _, out _))
return false;
return IsReadOnlyMember(type, field);
}
@@ -376,14 +369,17 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return false;
}
private static bool HasRedundantTag(ISymbol symbol, AttributeData datafieldAttribute)
private static bool HasRedundantTag(ISymbol symbol)
{
if (!IsDataField(symbol, out var _, out var attribute))
return false;
// No args, no problem
if (datafieldAttribute.ConstructorArguments.Length == 0)
if (attribute.ConstructorArguments.Length == 0)
return false;
// If a tag is explicitly specified, it will be the first argument...
var tagArgument = datafieldAttribute.ConstructorArguments[0];
var tagArgument = attribute.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)
@@ -398,6 +394,9 @@ 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())
@@ -423,6 +422,9 @@ 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
{
public static class Program
internal static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameControllerInternal _gameController = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var settings = new CefSettings()
{

View File

@@ -57,8 +57,8 @@ internal sealed partial class AudioManager : IAudioInternal
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' ', StringSplitOptions.RemoveEmptyEntries))
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alContextExtensions.Add(extension);
}

View File

@@ -582,7 +582,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
if (TerminatingOrDeleted(entity))
{
LogAudioPlaybackOnInvalidEntity(specifier, entity);
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entity)}");
return null;
}
@@ -626,7 +626,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
return null;
}
@@ -753,12 +753,6 @@ 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,7 +6,6 @@ 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;
@@ -157,13 +156,8 @@ 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

@@ -1,262 +0,0 @@
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 static readonly ResPath FallbackSoundfont = new ResPath("/Midi/fallback.sf2");
private const string FallbackSoundfont = "/Midi/fallback.sf2";
private const string ContentCustomSoundfontDirectory = "/Audio/MidiCustom/";
@@ -265,7 +265,81 @@ internal sealed partial class MidiManager : IMidiManager
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
LoadSoundFontSetup(renderer);
_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());
}
renderer.Source.Gain = _gain;
@@ -498,6 +572,130 @@ 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

@@ -1,45 +0,0 @@
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 partial class MidiRenderer : IMidiRenderer
internal sealed class MidiRenderer : IMidiRenderer
{
private readonly IMidiManager _midiManager;
private readonly ITaskManager _taskManager;
@@ -435,6 +435,15 @@ internal sealed partial 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'm pretty sure we do a lot of unnecessary maths.
// Because the just take the BB of the rotated BB, I'mt pretty sure we do a lot of unnecessary maths.
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
}

View File

@@ -191,16 +191,8 @@ namespace Robust.Client.Console
var shell = new ConsoleShell(this, session ?? _player.LocalSession, session == null);
var cmdArgs = args.ToArray();
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}");
}
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
cmd.Execute(shell, command, cmdArgs);
}
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 (TryComp<MetaDataComponent>(ent, out var meta))
if (EntityManager.TryGetComponent<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);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)

View File

@@ -1224,8 +1224,6 @@ namespace Robust.Client.GameObjects
return;
_visible = value;
Owner.Comp.BoundsDirty = true;
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
if (_parent.Owner != EntityUid.Invalid)
Owner.Comp.Sys?.QueueUpdateIsInert(Owner);
@@ -1793,15 +1791,76 @@ namespace Robust.Client.GameObjects
[Obsolete("Use SpriteSystem.GetPrototypeTextures() instead")]
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot)
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
return sys.GetPrototypeTextures(prototype, out 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;
}
[Obsolete("Use SpriteSystem.GetPrototypeIcon() instead")]
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
return sys.GetPrototypeIcon(prototype);
// 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;
}
}
}

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 ??= EnsureComp<AnimationPlayerComponent>(uid);
component ??= EntityManager.EnsureComponent<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 TryComp(uid, out AnimationPlayerComponent? component) &&
return EntityManager.TryGetComponent(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 || !Exists(entity))
if(entity == default || !EntityManager.EntityExists(entity))
throw new ArgumentNullException(nameof(entity));
if (!TryComp(entity, out InputComponent? inputComp))
if (!EntityManager.TryGetComponent(entity, out InputComponent? inputComp))
{
_sawmillInputContext.Debug($"AttachedEnt has no InputComponent: entId={entity}, entProto={Comp<MetaDataComponent>(entity).EntityPrototype}. Setting default \"{InputContextContainer.DefaultContextName}\" context...");
_sawmillInputContext.Debug($"AttachedEnt has no InputComponent: entId={entity}, entProto={EntityManager.GetComponent<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={Comp<MetaDataComponent>(entity).EntityPrototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context...");
_sawmillInputContext.Error($"Unknown context: entId={entity}, entProto={EntityManager.GetComponent<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 || !TryComp(player.Value, out PhysicsComponent? body))
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{
_label.Visible = false;
return;

View File

@@ -56,6 +56,10 @@ 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.
@@ -63,7 +67,11 @@ public sealed partial class SpriteSystem
return GetFallbackState();
}
return GetPrototypeIcon(entityPrototype);
// Generate the icon and cache it in case it's ever needed again.
var result = GetPrototypeIcon(entityPrototype);
_cachedPrototypeIcons[prototype] = result;
return result;
}
/// <summary>
@@ -71,19 +79,13 @@ 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.TryGetComponent(out IconComponent? icon, _factory))
if (prototype.Components.TryGetValue("Icon", out var compData)
&& compData.Component is IconComponent icon)
{
return GetIcon(icon);
}
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
if (!prototype.Components.ContainsKey("Sprite"))
@@ -100,63 +102,6 @@ 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,12 +34,8 @@ 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 ((meta.Flags & MetaDataFlags.Detached) == 0 && compState != null)
if (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.TryGetDefinition(tile.TypeId, out var tileDef) && tileDef.AllowRotationMirror) ?
tile.RotationMirroring
: 0;
var rotationMirroring = _tileDefinitionManager[tile.TypeId].AllowRotationMirror
? tile.RotationMirroring
: 0;
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, rotationMirroring);
i += 1;

View File

@@ -662,10 +662,6 @@ 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 (!HasComp<JointComponent>(other))
if (!EntityManager.HasComponent<JointComponent>(other))
continue;
// TODO: if (other entity is outside of PVS range) continue;

View File

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

View File

@@ -107,11 +107,6 @@ 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];
@@ -143,31 +138,6 @@ 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)
{
@@ -177,6 +147,12 @@ 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 (value < 0)
if (_currentTab < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Current tab must be positive.");
}
if (value >= ChildCount)
if (_currentTab >= ChildCount)
{
throw new ArgumentOutOfRangeException(nameof(value), value,
"Current tab must less than the amount of tabs.");

View File

@@ -145,11 +145,6 @@ 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,15 +18,12 @@ namespace Robust.Client.UserInterface
[SuppressMessage("ReSharper", "IdentifierTypo")]
[SuppressMessage("ReSharper", "CommentTypo")]
[SuppressMessage("ReSharper", "StringLiteralTypo")]
internal sealed class FileDialogManager : IFileDialogManager, IPostInjectInit
internal sealed class FileDialogManager : IFileDialogManager
{
// 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;
@@ -270,7 +267,7 @@ namespace Robust.Client.UserInterface
if (_kDialogAvailable)
{
_sawmill.Debug("kdialog available.");
Logger.DebugS("filedialog", "kdialog available.");
}
}
catch
@@ -407,11 +404,6 @@ 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(maxSizeX, Height));
control.Measure(new Vector2(Width, 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))
{
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}. Trace: {Environment.StackTrace}");
return null;
}
@@ -160,7 +160,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (TerminatingOrDeleted(coordinates.EntityId))
{
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}. Trace: {Environment.StackTrace}");
return null;
}
@@ -281,10 +281,4 @@ 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);
_resources.Initialize(dataDir, hideUserDataDir: false);
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)
{
Del(uid);
EntityManager.DeleteEntity(uid);
}
}
}
@@ -75,7 +75,7 @@ namespace Robust.Server.GameObjects
if (!_deleteEmptyGrids || TerminatingOrDeleted(uid) || HasComp<MapComponent>(uid))
return;
Del(args.GridId);
EntityManager.DeleteEntity(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 = EnsureComp<Shared.GameObjects.ViewSubscriberComponent>(uid);
var viewSubscriber = EntityManager.EnsureComponent<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(!TryComp(uid, out Shared.GameObjects.ViewSubscriberComponent? viewSubscriber))
if(!EntityManager.TryGetComponent(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 = AllEntityQuery<MetaDataComponent>();
var query = EntityManager.AllEntityQueryEnumerator<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)
{
QueueDel(uid);
EntityManager.QueueDeleteEntity(uid);
}
_toDelete.Clear();

View File

@@ -10,7 +10,7 @@ namespace Robust.Server.Player
{
foreach (var uid in entities)
{
if (TryComp(uid, out ActorComponent? actor))
if (EntityManager.TryGetComponent(uid, out ActorComponent? actor))
filter.AddPlayer(actor.PlayerSession);
}

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
@@ -189,20 +188,13 @@ namespace Robust.Server.Scripting
}
// Compile ahead of time so that we can do syntax highlighting correctly for the echo.
await Task.Run(() =>
{
newScript.Compile();
newScript.Compile();
// Echo entered script.
var echoMessage = new FormattedMessage();
ScriptInstanceShared.AddWithSyntaxHighlighting(
newScript,
echoMessage,
code,
instance.HighlightWorkspace.Value);
// Echo entered script.
var echoMessage = new FormattedMessage();
ScriptInstanceShared.AddWithSyntaxHighlighting(newScript, echoMessage, code, instance.HighlightWorkspace);
replyMessage.Echo = echoMessage;
});
replyMessage.Echo = echoMessage;
var msg = new FormattedMessage();
@@ -340,7 +332,7 @@ namespace Robust.Server.Scripting
private sealed class ScriptInstance
{
public Lazy<Workspace> HighlightWorkspace { get; } = new(() => new AdhocWorkspace());
public Workspace HighlightWorkspace { get; } = new AdhocWorkspace();
public StringBuilder InputBuffer { get; } = new();
public FormattedMessage OutputBuffer { get; } = new();
public bool RunningScript { get; set; }
@@ -381,7 +373,7 @@ namespace Robust.Server.Scripting
script.Compile();
var syntax = new FormattedMessage();
ScriptInstanceShared.AddWithSyntaxHighlighting(script, syntax, code, _scriptInstance.HighlightWorkspace.Value);
ScriptInstanceShared.AddWithSyntaxHighlighting(script, syntax, code, _scriptInstance.HighlightWorkspace);
_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 ?? sound.Params);
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams);
}
/// <summary>

View File

@@ -127,10 +127,10 @@ public sealed class TeleportToCommand : LocalizedEntityCommands
{
foreach (var victim in args)
{
if (!TryGetTransformFromUidOrUsername(victim, shell, out var uid, out var victimTransform))
if (victim == target)
continue;
if (uid == targetUid)
if (!TryGetTransformFromUidOrUsername(victim, shell, out var uid, out var victimTransform))
continue;
victims.Add((uid.Value, victimTransform));

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -156,7 +155,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"/>, use <see cref="PrototypeIdsLimited{T}"/> instead.
/// Don't use this for prototypes types that likely have a large number of entries, like <see cref="EntityPrototype"/>.
/// </remarks>
public static IEnumerable<CompletionOption> PrototypeIDs<T>(bool sorted = true, IPrototypeManager? proto = null)
where T: class, IPrototype
@@ -167,36 +166,6 @@ 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,6 +391,13 @@ 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>
@@ -411,6 +418,13 @@ 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).
@@ -445,6 +459,21 @@ 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,9 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
// Sanitise platform-specific path and standardize it for engine use.
.Replace(Path.DirectorySeparatorChar, '/');
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
}
/// <inheritdoc />

View File

@@ -14,7 +14,11 @@ namespace Robust.Shared.ContentPack
/// The directory to use for user data.
/// If null, a virtual temporary file system is used instead.
/// </param>
void Initialize(string? userData);
/// <param name="hideUserDataDir">
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
/// <see cref="IResourceManager.UserData"/>.
/// </param>
void Initialize(string? userData, bool hideUserDataDir);
/// <summary>
/// Mounts a single stream as a content file. Useful for unit testing.

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.
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
/// </summary>
string? RootDir { get; }

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
{
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
!OperatingSystem.IsWindows()
&& !OperatingSystem.IsMacOS();
internal static string SafeGetResourcePath(string baseDir, ResPath path)
{
var relSysPath = path.ToRelativeSystemPath();
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
// better safe than sorry check
if (!retPath.StartsWith(baseDir))
{
// Allow path to match if it's just missing the directory separator at the end.
if (retPath != baseDir.TrimEnd(Path.DirectorySeparatorChar))
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return retPath;
}
}
}

View File

@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
public IWritableDirProvider UserData { get; private set; } = default!;
/// <inheritdoc />
public virtual void Initialize(string? userData)
public virtual void Initialize(string? userData, bool hideRootDir)
{
Sawmill = _logManager.GetSawmill("res");
if (userData != null)
{
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
}
else
{
@@ -379,6 +379,10 @@ namespace Robust.Shared.ContentPack
{
var rootDir = loader.GetPath(new ResPath(@"/"));
// TODO: GET RID OF THIS.
// This code shouldn't be passing OS disk paths through ResPath.
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
yield return new ResPath(rootDir);
}
}

View File

@@ -10,17 +10,22 @@ namespace Robust.Shared.ContentPack
/// <inheritdoc />
internal sealed class WritableDirProvider : IWritableDirProvider
{
/// <inheritdoc />
private readonly bool _hideRootDir;
public string RootDir { get; }
string? IWritableDirProvider.RootDir => _hideRootDir ? null : RootDir;
/// <summary>
/// Constructs an instance of <see cref="WritableDirProvider"/>.
/// </summary>
/// <param name="rootDir">Root file system directory to allow writing.</param>
public WritableDirProvider(DirectoryInfo rootDir)
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
{
// FullName does not have a trailing separator, and we MUST have a separator.
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
_hideRootDir = hideRootDir;
}
#region File Access
@@ -119,7 +124,7 @@ namespace Robust.Shared.ContentPack
throw new FileNotFoundException();
var dirInfo = new DirectoryInfo(GetFullPath(path));
return new WritableDirProvider(dirInfo);
return new WritableDirProvider(dirInfo, _hideRootDir);
}
/// <inheritdoc />
@@ -180,20 +185,7 @@ namespace Robust.Shared.ContentPack
path = path.Clean();
return GetFullPath(RootDir, path);
}
private static string GetFullPath(string root, ResPath path)
{
var relPath = path.ToRelativeSystemPath();
if (relPath.Contains("\\..") || relPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return Path.GetFullPath(Path.Combine(root, relPath));
return PathHelpers.SafeGetResourcePath(RootDir, path);
}
}
}

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
}
}
CurrentReadingEntity = null;

View File

@@ -41,31 +41,6 @@ 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.
@@ -80,17 +55,6 @@ 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);
@@ -121,7 +85,7 @@ public sealed partial class MapLoaderSystem
if (!deserializer.TryProcessData())
{
Log.Debug($"Failed to process entity data in {fileName}");
Log.Debug($"Failed to process entity data in {file}");
return false;
}
@@ -131,7 +95,7 @@ public sealed partial class MapLoaderSystem
}
catch (Exception e)
{
Log.Error($"Caught exception while creating entities for map {fileName}: {e}");
Log.Error($"Caught exception while creating entities for map {file}: {e}");
Delete(deserializer.Result);
throw;
}
@@ -139,7 +103,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 {fileName} does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
Log.Error($"Map {file} does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
Delete(deserializer.Result);
return false;
}

View File

@@ -64,13 +64,6 @@ 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, ICollection<string>? ignoredComps = null)
public bool IsDefault(EntityUid uid)
{
if (!MetaQuery.TryGetComponent(uid, out var metadata) || metadata.EntityPrototype == null)
return false;
@@ -195,9 +195,6 @@ 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 : IComponent, new()
protected T AddComp<T>(EntityUid uid) where T : Component, 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 (!TryComp(uid, out TransformComponent? transformComponent)) return;
if (!EntityManager.TryGetComponent(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 (!TryComp(uid, out PhysicsComponent? body)) return;
if (!EntityManager.TryGetComponent(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 (!HasComp(entity, component.GetType()))
AddComp(entity, component);
if (!EntityManager.HasComponent(entity, component.GetType()))
EntityManager.AddComponent(entity, component);
}
// Update entity metadata

View File

@@ -69,19 +69,19 @@ namespace Robust.Shared.GameObjects
if (!_enabled)
return;
if (!TryComp(uid, out PhysicsComponent? body))
if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? body))
{
Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(body)}");
return;
}
if (!TryComp(uid, out FixturesComponent? manager))
if (!EntityManager.TryGetComponent(uid, out FixturesComponent? manager))
{
Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(manager)}");
return;
}
if (!TryComp(uid, out TransformComponent? xform))
if (!EntityManager.TryGetComponent(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 = Comp<TransformComponent>(uid);
var xform = EntityManager.GetComponent<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;
RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key));
EntityManager.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.
RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key));
EntityManager.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 = EntityQueryEnumerator<TimerComponent>();
var timers = EntityManager.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)
{
RemComp<TimerComponent>(uid);
EntityManager.RemoveComponent<TimerComponent>(uid);
}
}
}

View File

@@ -170,8 +170,10 @@ namespace Robust.Shared.Localization
if (args.Args.Count < 1) return new LocValueString(nameof(Gender.Neuter));
ILocValue entity0 = args.Args[0];
if (entity0.Value is EntityUid entity)
if (entity0.Value != null)
{
EntityUid entity = (EntityUid)entity0.Value;
if (_entMan.TryGetComponent(entity, out GrammarComponent? grammar) && grammar.Gender.HasValue)
{
return new LocValueString(grammar.Gender.Value.ToString().ToLowerInvariant());
@@ -244,8 +246,10 @@ namespace Robust.Shared.Localization
if (args.Args.Count < 1) return new LocValueString(GetString("zzzz-counter-default"));
ILocValue entity0 = args.Args[0];
if (entity0.Value is EntityUid entity)
if (entity0.Value != null)
{
EntityUid entity = (EntityUid)entity0.Value;
if (TryGetEntityLocAttrib(entity, "counter", out var counter))
{
return new LocValueString(counter);
@@ -288,8 +292,9 @@ namespace Robust.Shared.Localization
if (args.Args.Count < 2) return new LocValueString("other");
ILocValue entity0 = args.Args[0];
if (entity0.Value is EntityUid entity)
if (entity0.Value != null)
{
EntityUid entity = (EntityUid)entity0.Value;
ILocValue attrib0 = args.Args[1];
if (TryGetEntityLocAttrib(entity, attrib0.Format(new LocContext(bundle)), out var attrib))
{
@@ -308,8 +313,10 @@ namespace Robust.Shared.Localization
if (args.Args.Count < 1) return new LocValueString("false");
ILocValue entity0 = args.Args[0];
if (entity0.Value is EntityUid entity)
if (entity0.Value != null)
{
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 (!TryComp(uid, out PhysicsComponent? physics))
if (!EntityManager.TryGetComponent(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;
RemComp<JointComponent>(joint);
EntityManager.RemoveComponent<JointComponent>(joint);
}
_dirtyJoints.Clear();
@@ -551,7 +551,7 @@ public abstract partial class SharedJointSystem : EntitySystem
_physics.WakeBody(uidA);
}
if (TryComp<PhysicsComponent>(bodyBUid, out var bodyB) &&
if (EntityManager.TryGetComponent<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(static (x, y) => InternalParallel(y).CompareTo(InternalParallel(x)));
islands.Sort((x, y) => InternalParallel(y).CompareTo(InternalParallel(x)));
var totalBodies = 0;
var actualIslands = islands.ToArray();
@@ -904,48 +904,16 @@ public abstract partial class SharedPhysicsSystem
if (options != null)
{
// 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)
const int FinaliseBodies = 32;
var batches = (int)MathF.Ceiling((float) bodyCount / FinaliseBodies);
Parallel.For(0, batches, options, i =>
{
const int FinaliseBodies = 32;
var batches = (int)MathF.Ceiling((float) bodyCount / FinaliseBodies);
var start = i * FinaliseBodies;
var end = Math.Min(bodyCount, start + FinaliseBodies);
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);
FinalisePositions(start, end, 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. Field: {field.Name} in Type: {field.DeclaringType}");
DebugTools.Assert(!field.HasCustomAttribute<DataFieldAttribute>(), "Datafields should not be static");
// Is this even a prototype id related field?
if (!TryGetFieldPrototype(field, out var proto))

View File

@@ -9,7 +9,6 @@ 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;
@@ -75,7 +74,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 (Parse.TryDouble(node.Value, out var v))
if (double.TryParse(node.Value, out var v))
{
timeSpan = TimeSpan.FromSeconds(v);
return true;
@@ -86,7 +85,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 (!Parse.TryDouble(node.Value.AsSpan()[..^1], out var number))
if (!double.TryParse(node.Value.AsSpan()[..^1], out var number))
return false;
// Check the last character of the input for time unit indicators

View File

@@ -46,9 +46,8 @@ public sealed class PipedArgumentAttribute : Attribute;
[MeansImplicitUse]
public sealed class CommandArgumentAttribute : Attribute
{
public CommandArgumentAttribute(Type? customParser = null, bool unparseable = false)
public CommandArgumentAttribute(Type? customParser = null)
{
Unparseable = unparseable;
if (customParser == null)
return;
@@ -57,13 +56,6 @@ 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,11 +1,8 @@
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;
@@ -13,10 +10,7 @@ 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)
{
@@ -26,11 +20,9 @@ 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)
{
@@ -40,38 +32,9 @@ 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)
{
@@ -81,6 +44,5 @@ 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,11 +11,8 @@ internal sealed class TpCommand : ToolshedCommand
{
private SharedTransformSystem? _xform;
// TODO TOOLSHED
// add EntityCoordinates parser
[CommandImplementation("coords")]
public EntityUid TpCoords([PipedArgument] EntityUid teleporter, [CommandArgument(unparseable:true)] EntityCoordinates target)
public EntityUid TpCoords([PipedArgument] EntityUid teleporter, EntityCoordinates target)
{
_xform ??= GetSys<SharedTransformSystem>();
_xform.SetCoordinates(teleporter, target);
@@ -23,7 +20,7 @@ internal sealed class TpCommand : ToolshedCommand
}
[CommandImplementation("coords")]
public IEnumerable<EntityUid> TpCoords([PipedArgument] IEnumerable<EntityUid> teleporters, [CommandArgument(unparseable:true)] EntityCoordinates target)
public IEnumerable<EntityUid> TpCoords([PipedArgument] IEnumerable<EntityUid> teleporters, 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.GetCustomAttribute<CommandArgumentAttribute>() is {} cmdAttr)
if (param.HasCustomAttribute<CommandArgumentAttribute>())
{
if (param.Name == null || !argNames.Add(param.Name))
throw new InvalidCommandImplementation($"Command arguments must have a unique name");
hasAnyAttribute = true;
ValidateArg(param, cmdAttr);
ValidateArg(param);
}
if (param.HasCustomAttribute<PipedArgumentAttribute>())
@@ -232,18 +232,8 @@ public abstract partial class ToolshedCommand
}
}
private void ValidateArg(ParameterInfo arg, CommandArgumentAttribute? cmdAttr = null)
private void ValidateArg(ParameterInfo arg)
{
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,10 +231,7 @@ public sealed partial class ToolshedManager
/// <returns>Success.</returns>
public bool TryParse<T>(ParserContext parserContext, [NotNullWhen(true)] out T? parsed)
{
var t = typeof(T);
t = Nullable.GetUnderlyingType(t) ?? t;
var res = TryParse(parserContext, t, out var p);
var res = TryParse(parserContext, typeof(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(GetArgHint(arg));
return CompletionResult.FromHint(typeof(T).PrettyName());
}
}

View File

@@ -28,7 +28,6 @@ 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;
@@ -142,7 +141,6 @@ namespace Robust.UnitTesting
systems.LoadExtraSystemType<RecursiveMoveSystem>();
systems.LoadExtraSystemType<SpriteSystem>();
systems.LoadExtraSystemType<SpriteTreeSystem>();
systems.LoadExtraSystemType<AppearanceSystem>();
systems.LoadExtraSystemType<GridChunkBoundsDebugSystem>();
}
else

View File

@@ -136,39 +136,35 @@ 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 client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), coords));
await RunTicks();
AssertEnt(paused: true, detached: false, clientPaused: true);
}
}
}

View File

@@ -1,134 +0,0 @@
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));
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir), hideRootDir: false);
}
[OneTimeTearDown]

View File

@@ -1,7 +1,6 @@
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;
@@ -44,7 +43,6 @@ 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,7 +14,6 @@ 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.
@@ -28,4 +27,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)