Compare commits

...

34 Commits

Author SHA1 Message Date
PJB3005
ea9d3e9ed0 Version: 227.0.6 2025-12-01 16:07:24 +01:00
PJB3005
e97596ef51 Backport BitArray .NET 10 serializer fix
83ad6042a7 & b267cd6fb4

Does not include test code to avoid risking merge conflicts.

(cherry picked from commit 415585a30d74fcae61f581808220a7aaeca3eaf5)
(cherry picked from commit e36628a6d436ea08d6d31441c101a88a5504c515)
2025-12-01 16:07:24 +01:00
PJB3005
d70ea7f78e Version: 227.0.5 2025-09-26 13:40:55 +02:00
PJB3005
95eeffb7eb Validate that content assemblies have a limited list of names.
Also, only read assemblies once from disk

(cherry picked from commit 443a8dfca65be7d60c4bd46181b4c749b4756114)
2025-09-26 13:40:54 +02:00
PJB3005
0123cbcc57 Version: 227.0.4 2025-09-19 09:17:41 +02:00
Skye
588543d3c0 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:41 +02:00
PJB3005
3a86430ce0 Version: 227.0.3 2025-09-14 14:58:29 +02:00
PJB3005
ba0cbffdc3 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)
(cherry picked from commit 4413695c77fb705054c2f81fa18ec0a189b685dd)
2025-09-14 14:58:29 +02:00
Pieter-Jan Briers
c7f7030b89 Version: 227.0.2 2024-08-11 19:54:40 +02:00
Pieter-Jan Briers
54218fb40b Use absolute path for explorer.exe
frick me

(cherry picked from commit 0284eb0430)
2024-08-11 19:54:40 +02:00
Pieter-Jan Briers
7b2c1701f3 Version: 227.0.1 2024-08-11 17:54:18 +02:00
Pieter-Jan Briers
0e3930d7d7 Security updates (#5353)
* Fix security bug in WritableDirProvider.OpenOsWindow()

Reported by @NarryG and @nyeogmi

* Sandbox updates

* Update ImageSharp again

(cherry picked from commit 7d778248ee)
(cherry picked from commit f66cda74e95619ddba2221bda644bf4394619805)
2024-08-11 17:54:18 +02:00
metalgearsloth
a9aea7027f Version: 227.0.0 2024-07-01 15:54:45 +10:00
metalgearsloth
2a49c2d9b8 Add loop support for SpriteSystem.GetFrame (#5265)
For Ftl I just want it played once.
2024-07-01 15:50:49 +10:00
metalgearsloth
a0c069f1ea Add LocalTilesIntersecting for circles (#5262)
* Add LocalTilesIntersecting for circles

* Update Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
2024-06-29 14:57:06 +10:00
Pieter-Jan Briers
2c6fb95e53 Add EntityManager dependency to base LocalizedEntityCommands 2024-06-28 17:00:14 +02:00
Nemanja
afe337644e Make spin box controls disable buttons that can't be pressed (#5221)
* spin box changes

* make SpinboxButton private

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-06-28 17:53:10 +10:00
Plykiya
b8924f3ddf Removes obsolete visibility system functions (#5209)
* Removes obsolete visibility system functions

* guh, forgot to add the test

---------

Co-authored-by: plykiya <plykiya@protonmail.com>
2024-06-28 17:30:02 +10:00
Pieter-Jan Briers
08970e745b Entity console commands system. (#5267)
* Entity console commands system.

This adds a new base type, LocalizedEntityCommands, which is able to import entity systems as dependencies. This is done by only registering these while the entity system is active.

Handling registration separately like this required a bit of changes around ConsoleHost to make it more suitable for this purpose:

You can now directly register command instances, and also have a system to suppress `UpdateAvailableCommands` on the client so there's no bad O(N*M) behavior.

* Convert TeleportCommands.cs to new entity commands.

Removes some obsoletion warnings without pain from having to manually import transform system.

* Fix RobustServerSimulation dependency issue.

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2024-06-28 17:29:24 +10:00
Leon Friedrich
0ba4a66787 Always process networked events via the priority queue (#5205) 2024-06-28 17:02:14 +10:00
Pieter-Jan Briers
75b3431ee6 New "must call base" analyzer. (#5266)
* New "must call base" analyzer.

This enforces that you actually call base when overriding stuff. This is intended for base methods like entity system's, where server/client systems overriding shared ones SHOULD call Initialize() and such.

* Add MustCallBase to entity system methods
2024-06-28 14:44:49 +10:00
geraeumig
c0ef976588 Make PvsSystem consider offset and zoom from EyeComponent (#5228)
* Make PvsSystem consider offset and zoom from EyeComponent

* Just use PvsScale float

* float.IsFinite

---------

Co-authored-by: geraeumig <alfenos@proton.me>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2024-06-28 14:24:22 +10:00
Pieter-Jan Briers
fe5cdf9e3c Fix loading of replays if string package is compressed in zip.
This happened when I had to re-compress a recovered replay from a server crash, and then loaded it up in a dev environment.
2024-06-27 16:07:49 +02:00
Morb
450349188b Dispose memory stream after deserialization exception (#4840)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-06-26 23:16:56 +02:00
metalgearsloth
897ad998d9 Add option for left or right-aligned checkboxes (#4739)
* Add option for left or right-aligned checkboxes

I think right-aligned is better but this is less of a breaking change.

* Cleanup
2024-06-26 23:11:17 +02:00
metalgearsloth
635ae3c353 Remove IGameTiming from TransformComponent (#5273) 2024-06-26 17:10:34 +02:00
Pieter-Jan Briers
a4ea5a4620 Add AnimationCompletedEvent "Finished" boolean.
Content has multiple cases where AnimationCompletedEvent is used to loop an animation. #5238 broke some of these by making this event raised even when manually removed.

Luckily most cases in content tie the animation looping to the presence of a component, so the component getting removed means there's nothing to refresh the loop. LightBehavior is not as fortunate however, causing bugs like https://github.com/space-wizards/space-station-14/issues/29144

This boolean allows looping code to properly distinguish the event, so it won't try to restart an animation after removing it directly.
2024-06-25 15:35:53 +02:00
Pieter-Jan Briers
90e87526d0 Quote tab completions containing spaces. 2024-06-24 16:05:00 +02:00
metalgearsloth
cd6576ddf9 Mark EntityCoordinates.Offset as pure (#5264)
Doesn't do anything just being called and sometimes I forget.
2024-06-24 10:53:41 +02:00
Leon Friedrich
e2cf4ee3db SIMD Colour multiplication (#5251)
* color simd

* removed wrong one

* A

* Use Unsafe.BitCast

* Color4 -> Color

* remove constructor

* remove `in`
2024-06-22 16:42:40 +02:00
metalgearsloth
860c9af2bf Version: 226.3.0 2024-06-22 14:10:43 +10:00
Pieter-Jan Briers
87bb29408a Try to report method source of sandboxing issues. 2024-06-21 00:31:47 +02:00
Pieter-Jan Briers
738cfbe992 Add non-generic IList and ICollection to sandbox.
Used by collection expressions in some cases.
2024-06-21 00:31:47 +02:00
wixoa
90edc02259 Add style property overrides to ContainerButton and TabContainer (#5222)
* Add style box override properties to ContainerButton and TabContainer

* Add background panel to TabContainer, and add text color overrides

* Undo background panel
You can achieve the same by instead putting the TabContainer in a PanelContainer

* Add BackgroundColor property to StyleBoxTexture

* Remove BackgroundColor from StyleBoxTexture
2024-06-20 20:50:51 +02:00
62 changed files with 1603 additions and 290 deletions

View File

@@ -55,9 +55,9 @@
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<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="System.Numerics.Vectors" Version="4.5.0" />

View File

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

View File

@@ -54,6 +54,59 @@ END TEMPLATE-->
*None yet*
## 227.0.6
## 227.0.5
## 227.0.4
## 227.0.3
## 227.0.2
## 227.0.1
## 227.0.0
### Breaking changes
* Add a `loop` arg to SpriteSystem.GetFrame in case you don't want to get a looping animation.
* Remove obsolete VisibileSystem methods.
### New features
* Added `LocalizedEntityCommands`, which are console commands that have the ability to take entity system dependencies.
* Added `BeginRegistrationRegion` to `IConsoleHost` to allow efficient bulk-registration of console commands.
* Added `IConsoleHost.RegisterCommand` overload that takes an `IConsoleCommand`.
* Added a `Finished` boolean to `AnimationCompletedEvent` which allows distinguishing if an animation was removed prematurely or completed naturally.
* Add GetLocalTilesIntersecting for MapSystem.
* Add an analyzer for methods that should call the base implementation and use it for EntitySystems.
### Bugfixes
* Fix loading replays if string package is compressed inside a zip.
### Other
* Tab completions containing spaces are now properly quoted, so the command will actually work properly once entered.
* Mark EntityCoordinates.Offset as Pure so it shows as warnings if the variable is unused.
* Networked events will always be processed in order even if late.
## 226.3.0
### New features
* `System.Collections.IList` and `System.Collections.ICollection` are now sandbox safe, this fixes some collection expression cases.
* The sandboxing system will now report the methods responsible for references to illegal items.
## 226.2.0
### New features

View File

@@ -0,0 +1,92 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.MustCallBaseAnalyzer>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class MustCallBaseAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<MustCallBaseAnalyzer, NUnitVerifier>()
{
TestState =
{
Sources = { code }
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.IoC.MustCallBaseAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task Test()
{
const string code = """
using Robust.Shared.Analyzers;
public class Foo
{
[MustCallBase]
public virtual void Function()
{
}
[MustCallBase(true)]
public virtual void Function2()
{
}
}
public class Bar : Foo
{
public override void Function()
{
}
public override void Function2()
{
}
}
public class Baz : Foo
{
public override void Function()
{
base.Function();
}
}
public class Bal : Bar
{
public override void Function2()
{
}
}
""";
await Verifier(code,
// /0/Test0.cs(20,26): warning RA0028: Overriders of this function must always call the base function
VerifyCS.Diagnostic().WithSpan(20, 26, 20, 34),
// /0/Test0.cs(41,26): warning RA0028: Overriders of this function must always call the base function
VerifyCS.Diagnostic().WithSpan(41, 26, 41, 35));
}
}

View File

@@ -10,6 +10,7 @@
<ItemGroup>
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" LogicalName="Robust.Shared.Analyzers.AccessAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" LogicalName="Robust.Shared.Analyzers.AccessPermissions.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\MustCallBaseAttribute.cs" LogicalName="Robust.Shared.IoC.MustCallBaseAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
</ItemGroup>

View File

@@ -0,0 +1,111 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
#nullable enable
/// <summary>
/// Enforces <c>MustCallBaseAttribute</c>.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MustCallBaseAnalyzer : DiagnosticAnalyzer
{
private const string Attribute = "Robust.Shared.Analyzers.MustCallBaseAttribute";
private static readonly DiagnosticDescriptor Rule = new(
Diagnostics.IdMustCallBase,
"No base call in overriden function",
"Overriders of this function must always call the base function",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);
}
private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
if (context.Symbol is not IMethodSymbol { IsOverride: true } method)
return;
var attrSymbol = context.Compilation.GetTypeByMetadataName(Attribute);
if (attrSymbol == null)
return;
if (DoesMethodOverriderHaveAttribute(method, attrSymbol) is not { } data)
return;
if (data is { onlyOverrides: true, depth: < 2 })
return;
var syntax = (MethodDeclarationSyntax) method.DeclaringSyntaxReferences[0].GetSyntax();
if (HasBaseCall(syntax))
return;
var diag = Diagnostic.Create(Rule, syntax.Identifier.GetLocation());
context.ReportDiagnostic(diag);
}
private static (int depth, bool onlyOverrides)? DoesMethodOverriderHaveAttribute(
IMethodSymbol method,
INamedTypeSymbol attributeSymbol)
{
var depth = 0;
while (method.OverriddenMethod != null)
{
depth += 1;
method = method.OverriddenMethod;
if (GetAttribute(method, attributeSymbol) is not { } attribute)
continue;
var onlyOverrides = attribute.ConstructorArguments is [{Kind: TypedConstantKind.Primitive, Value: true}];
return (depth, onlyOverrides);
}
return null;
}
private static bool HasBaseCall(MethodDeclarationSyntax syntax)
{
return syntax.Accept(new BaseCallLocator());
}
private static AttributeData? GetAttribute(ISymbol namedTypeSymbol, INamedTypeSymbol attrSymbol)
{
return namedTypeSymbol.GetAttributes()
.SingleOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol));
}
private sealed class BaseCallLocator : CSharpSyntaxVisitor<bool>
{
public override bool VisitBaseExpression(BaseExpressionSyntax node)
{
return true;
}
public override bool DefaultVisit(SyntaxNode node)
{
foreach (var childNode in node.ChildNodes())
{
if (childNode is not CSharpSyntaxNode cSharpSyntax)
continue;
if (cSharpSyntax.Accept(this))
return true;
}
return false;
}
}
}

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

@@ -11,7 +11,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var type = Type.GetType(args[0]);
var type = GetType(args[0]);
if (type == null)
{
@@ -25,6 +25,17 @@ namespace Robust.Client.Console.Commands
shell.WriteLine(sig);
}
}
private Type? GetType(string name)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.GetType(name) is { } type)
return type;
}
return null;
}
}
#endif
}

View File

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

@@ -76,7 +76,8 @@ namespace Robust.Client.GameObjects
foreach (var key in remie)
{
component.PlayingAnimations.Remove(key);
EntityManager.EventBus.RaiseLocalEvent(uid, new AnimationCompletedEvent {Uid = uid, Key = key}, true);
var completedEvent = new AnimationCompletedEvent {Uid = uid, Key = key, Finished = true};
EntityManager.EventBus.RaiseLocalEvent(uid, completedEvent, true);
}
return false;
@@ -187,7 +188,8 @@ namespace Robust.Client.GameObjects
return;
}
EntityManager.EventBus.RaiseLocalEvent(entity.Owner, new AnimationCompletedEvent {Uid = entity.Owner, Key = key}, true);
var completedEvent = new AnimationCompletedEvent {Uid = entity.Owner, Key = key, Finished = false};
EntityManager.EventBus.RaiseLocalEvent(entity.Owner, completedEvent, true);
}
public void Stop(EntityUid uid, AnimationPlayerComponent? component, string key)
@@ -203,5 +205,11 @@ namespace Robust.Client.GameObjects
{
public EntityUid Uid { get; init; }
public string Key { get; init; } = string.Empty;
/// <summary>
/// If true, the animation finished by getting to its natural end.
/// If false, it was removed prematurely via <see cref="AnimationPlayerSystem.Stop(Robust.Client.GameObjects.AnimationPlayerComponent,string)"/> or similar overloads.
/// </summary>
public bool Finished { get; init; }
}
}

View File

@@ -184,7 +184,8 @@ namespace Robust.Client.GameObjects
/// <summary>
/// Gets the specified frame for this sprite at the specified time.
/// </summary>
public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime)
/// <param name="loop">Should we clamp on the last frame and not loop</param>
public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime, bool loop = true)
{
Texture? sprite = null;
@@ -196,19 +197,29 @@ namespace Robust.Client.GameObjects
var frames = state!.GetFrames(RsiDirection.South);
var delays = state.GetDelays();
var totalDelay = delays.Sum();
var time = curTime.TotalSeconds % totalDelay;
var delaySum = 0f;
for (var i = 0; i < delays.Length; i++)
// No looping
if (!loop && curTime.TotalSeconds >= totalDelay)
{
var delay = delays[i];
delaySum += delay;
sprite = frames[^1];
}
// Loopable
else
{
var time = curTime.TotalSeconds % totalDelay;
var delaySum = 0f;
if (time > delaySum)
continue;
for (var i = 0; i < delays.Length; i++)
{
var delay = delays[i];
delaySum += delay;
sprite = frames[i];
break;
if (time > delaySum)
continue;
sprite = frames[i];
break;
}
}
sprite ??= Frame0(spriteSpec);

View File

@@ -162,9 +162,7 @@ public sealed partial class ReplayLoadManager
}
using var stringFile = fileReader.Open(FileStrings);
var stringData = new byte[stringFile.Length];
stringFile.ReadExactly(stringData);
_serializer.SetStringSerializerPackage(stringHash, stringData);
_serializer.SetStringSerializerPackage(stringHash, stringFile.CopyToArray());
using var cvarsFile = fileReader.Open(FileCvars);
// Note, this does not invoke the received-initial-cvars event. But at least currently, that doesn't matter

View File

@@ -15,6 +15,36 @@ namespace Robust.Client.UserInterface.Controls
public Label Label { get; }
public TextureRect TextureRect { get; }
/// <summary>
/// Should the checkbox be to the left or the right of the label.
/// </summary>
public bool LeftAlign
{
get => _leftAlign;
set
{
if (_leftAlign == value)
return;
_leftAlign = value;
if (value)
{
Label.HorizontalExpand = false;
TextureRect.SetPositionFirst();
Label.SetPositionInParent(1);
}
else
{
Label.HorizontalExpand = true;
Label.SetPositionFirst();
TextureRect.SetPositionInParent(1);
}
}
}
private bool _leftAlign = true;
public CheckBox()
{
ToggleMode = true;
@@ -31,10 +61,21 @@ namespace Robust.Client.UserInterface.Controls
StyleClasses = { StyleClassCheckBox },
VerticalAlignment = VAlignment.Center,
};
hBox.AddChild(TextureRect);
Label = new Label();
hBox.AddChild(Label);
if (LeftAlign)
{
Label.HorizontalExpand = false;
hBox.AddChild(TextureRect);
hBox.AddChild(Label);
}
else
{
Label.HorizontalExpand = true;
hBox.AddChild(Label);
hBox.AddChild(TextureRect);
}
}
protected override void DrawModeChanged()

View File

@@ -15,6 +15,8 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePseudoClassHover = "hover";
public const string StylePseudoClassDisabled = "disabled";
public StyleBox? StyleBoxOverride { get; set; }
public ContainerButton()
{
DrawModeChanged();
@@ -24,6 +26,11 @@ namespace Robust.Client.UserInterface.Controls
{
get
{
if (StyleBoxOverride != null)
{
return StyleBoxOverride;
}
if (TryGetStyleProperty<StyleBox>(StylePropertyStyleBox, out var box))
{
return box;

View File

@@ -1,4 +1,3 @@
using Robust.Shared.Maths;
using System;
using System.Collections.Generic;
using System.Numerics;
@@ -15,9 +14,10 @@ namespace Robust.Client.UserInterface.Controls
public const string RightButtonStyle = "spinbox-right";
public const string MiddleButtonStyle = "spinbox-middle";
public LineEdit LineEditControl { get; }
private List<Button> _leftButtons = new();
private List<Button> _rightButtons = new();
private List<SpinBoxButton> _leftButtons = new();
private List<SpinBoxButton> _rightButtons = new();
private int _stepSize = 1;
private bool _buttonsDisabled;
/// <summary>
/// Determines whether the SpinBox value gets changed by the input text.
@@ -30,12 +30,7 @@ namespace Robust.Client.UserInterface.Controls
get => _value;
set
{
if (IsValid != null && !IsValid(value))
{
return;
}
_value = value;
LineEditControl.Text = value.ToString();
OverrideValue(value);
ValueChanged?.Invoke(new ValueChangedEventArgs(value));
}
}
@@ -52,6 +47,7 @@ namespace Robust.Client.UserInterface.Controls
return;
}
_value = value;
UpdateButtonCanPress();
LineEditControl.Text = value.ToString();
}
@@ -87,6 +83,7 @@ namespace Robust.Client.UserInterface.Controls
ClearButtons();
AddLeftButton(-1, "-");
AddRightButton(1, "+");
UpdateButtonCanPress();
}
/// <summary>
@@ -94,8 +91,8 @@ namespace Robust.Client.UserInterface.Controls
/// </summary>
public void AddRightButton(int num, string text)
{
var button = new Button { Text = text };
button.OnPressed += (args) => Value += num;
var button = new SpinBoxButton(num) { Text = text };
button.OnPressed += _ => Value += num;
AddChild(button);
button.AddStyleClass(RightButtonStyle);
if (_rightButtons.Count > 0)
@@ -111,8 +108,8 @@ namespace Robust.Client.UserInterface.Controls
/// </summary>
public void AddLeftButton(int num, string text)
{
var button = new Button { Text = text };
button.OnPressed += (args) => Value += num;
var button = new SpinBoxButton(num) { Text = text };
button.OnPressed += _ => Value += num;
AddChild(button);
button.SetPositionInParent(_leftButtons.Count);
button.AddStyleClass(_leftButtons.Count == 0 ? LeftButtonStyle : MiddleButtonStyle);
@@ -162,6 +159,24 @@ namespace Robust.Client.UserInterface.Controls
{
rightButton.Disabled = disabled;
}
_buttonsDisabled = disabled;
}
private void UpdateButtonCanPress()
{
if (IsValid == null)
return;
foreach (var button in _leftButtons)
{
button.Disabled = !IsValid(_value + button.Value) || _buttonsDisabled;
}
foreach (var button in _rightButtons)
{
button.Disabled = !IsValid(_value + button.Value) || _buttonsDisabled;
}
}
/// <summary>
@@ -195,6 +210,16 @@ namespace Robust.Client.UserInterface.Controls
else if (args.Delta.Y < 0)
Value -= _stepSize;
}
private sealed class SpinBoxButton : Button
{
public readonly int Value;
public SpinBoxButton(int value)
{
Value = value;
}
}
}
public sealed class ValueChangedEventArgs : EventArgs

View File

@@ -65,6 +65,10 @@ namespace Robust.Client.UserInterface.Controls
}
}
public StyleBox? PanelStyleBoxOverride { get; set; }
public Color? TabFontColorOverride { get; set; }
public Color? TabFontColorInactiveOverride { get; set; }
public event Action<int>? OnTabChanged;
public TabContainer()
@@ -361,6 +365,9 @@ namespace Robust.Client.UserInterface.Controls
[System.Diagnostics.Contracts.Pure]
private Color _getTabFontColorActive()
{
if (TabFontColorOverride != null)
return TabFontColorOverride.Value;
if (TryGetStyleProperty(stylePropertyTabFontColor, out Color color))
{
return color;
@@ -371,6 +378,9 @@ namespace Robust.Client.UserInterface.Controls
[System.Diagnostics.Contracts.Pure]
private Color _getTabFontColorInactive()
{
if (TabFontColorInactiveOverride != null)
return TabFontColorInactiveOverride.Value;
if (TryGetStyleProperty(StylePropertyTabFontColorInactive, out Color color))
{
return color;
@@ -381,6 +391,9 @@ namespace Robust.Client.UserInterface.Controls
[System.Diagnostics.Contracts.Pure]
private StyleBox? _getPanel()
{
if (PanelStyleBoxOverride != null)
return PanelStyleBoxOverride;
TryGetStyleProperty<StyleBox>(StylePropertyPanelStyleBox, out var box);
return box;
}

View File

@@ -277,8 +277,21 @@ public sealed partial class DebugConsole
CommandBar.CursorPosition = lastRange.end;
CommandBar.SelectionStart = lastRange.start;
var insertValue = CommandParsing.Escape(completion);
// If the replacement contains a space, we must quote it to treat it as a single argument.
var mustQuote = insertValue.Contains(' ');
if ((completionFlags & CompletionOptionFlags.PartialCompletion) == 0)
{
if (mustQuote)
insertValue = $"\"{insertValue}\"";
insertValue += " ";
}
else if (mustQuote)
{
// If it's a partial completion, only quote the start.
insertValue = '"' + insertValue;
}
CommandBar.InsertAtCursor(insertValue);

View File

@@ -31,6 +31,7 @@ public static class Diagnostics
public const string IdDependencyFieldAssigned = "RA0025";
public const string IdUncachedRegex = "RA0026";
public const string IdDataFieldRedundantTag = "RA0027";
public const string IdMustCallBase = "RA0028";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

View File

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

@@ -40,12 +40,6 @@ namespace Robust.Server.GameObjects
EntityManager.EntityInitialized -= OnEntityInit;
}
[Obsolete("Use Entity<T> variant")]
public void AddLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
AddLayer((uid, component), (ushort)layer, refresh);
}
public void AddLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);
@@ -59,13 +53,6 @@ namespace Robust.Server.GameObjects
RefreshVisibility(ent);
}
[Obsolete("Use Entity<T> variant")]
public void RemoveLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
RemoveLayer((uid, component), (ushort)layer, refresh);
}
public void RemoveLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
if (!_visibilityQuery.Resolve(ent.Owner, ref ent.Comp, false))
@@ -80,12 +67,6 @@ namespace Robust.Server.GameObjects
RefreshVisibility(ent);
}
[Obsolete("Use Entity<T> variant")]
public void SetLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
SetLayer((uid, component), (ushort)layer, refresh);
}
public void SetLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);

View File

@@ -229,21 +229,6 @@ namespace Robust.Server.GameObjects
private void HandleEntityNetworkMessage(MsgEntity message)
{
var msgT = message.SourceTick;
var cT = _gameTiming.CurTick;
if (msgT <= cT)
{
if (msgT < cT && _logLateMsgs)
{
_netEntSawmill.Warning("Got late MsgEntity! Diff: {0}, msgT: {2}, cT: {3}, player: {1}",
(int) msgT.Value - (int) cT.Value, message.MsgChannel.UserName, msgT, cT);
}
DispatchEntityNetworkMessage(message);
return;
}
_queue.Add(message);
}

View File

@@ -80,14 +80,17 @@ internal sealed partial class PvsSystem
// Update visibility masks & viewer positions
// TODO PVS do this before sending state.
// I,e, we already enumerate over all eyes when computing visible chunks.
Span<MapCoordinates> positions = stackalloc MapCoordinates[session.Viewers.Length];
Span<(MapCoordinates pos, float scale)> positions = stackalloc (MapCoordinates, float)[session.Viewers.Length];
int i = 0;
foreach (var viewer in session.Viewers)
{
if (viewer.Comp2 != null)
session.VisMask |= viewer.Comp2.VisibilityMask;
positions[i++] = _transform.GetMapCoordinates(viewer.Owner, viewer.Comp1);
var mapCoordinates = _transform.GetMapCoordinates(viewer.Owner, viewer.Comp1);
mapCoordinates = mapCoordinates.Offset(viewer.Comp2?.Offset ?? Vector2.Zero);
var scale = MathF.Max((viewer.Comp2?.PvsScale ?? 1), 0.1f);
positions[i++] = (mapCoordinates, scale);
}
if (!CullingEnabled || session.DisableCulling)
@@ -112,7 +115,7 @@ internal sealed partial class PvsSystem
DebugTools.Assert(!chunk.UpdateQueued);
DebugTools.Assert(!chunk.Dirty);
foreach (var pos in positions)
foreach (var (pos, scale) in positions)
{
if (pos.MapId != chunk.Position.MapId)
continue;
@@ -120,8 +123,9 @@ internal sealed partial class PvsSystem
dist = Math.Min(dist, (pos.Position - chunk.Position.Position).LengthSquared());
var relative = Vector2.Transform(pos.Position, chunk.InvWorldMatrix) - chunk.Centre;
relative = Vector2.Abs(relative);
chebDist = Math.Min(chebDist, Math.Max(relative.X, relative.Y));
chebDist = Math.Min(chebDist, Math.Max(relative.X, relative.Y) / scale);
}
distances.Add(dist);

View File

@@ -362,8 +362,19 @@ internal sealed partial class PvsSystem : EntitySystem
private (Vector2 worldPos, float range, EntityUid? map) CalcViewBounds(Entity<TransformComponent, EyeComponent?> eye)
{
var size = Math.Max(eye.Comp2?.PvsSize ?? _priorityViewSize, 1);
return (_transform.GetWorldPosition(eye.Comp1), size / 2f, eye.Comp1.MapUid);
var size = _priorityViewSize;
var worldPos = _transform.GetWorldPosition(eye.Comp1);
if (eye.Comp2 is not null)
{
// not using EyeComponent.Eye.Position, because it's updated only on the client's side
worldPos += eye.Comp2.Offset;
size *= eye.Comp2.PvsScale;
}
size = Math.Max(size, 1);
return (worldPos, size / 2f, eye.Comp1.MapUid);
}
private void CullDeletionHistoryUntil(GameTick tick)

View File

@@ -26,6 +26,7 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -35,11 +36,6 @@ using Robust.Shared.Utility;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
#if NETCOREAPP
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
namespace Robust.Shared.Maths
{
/// <summary>
@@ -50,37 +46,43 @@ namespace Robust.Shared.Maths
public struct Color : IEquatable<Color>, ISpanFormattable
{
/// <summary>
/// The red component of this Color4 structure.
/// The red component of this Color structure.
/// </summary>
public float R;
/// <summary>
/// The green component of this Color4 structure.
/// The green component of this Color structure.
/// </summary>
public float G;
/// <summary>
/// The blue component of this Color4 structure.
/// The blue component of this Color structure.
/// </summary>
public float B;
/// <summary>
/// The alpha component of this Color4 structure.
/// The alpha component of this Color structure.
/// </summary>
public float A;
/// <summary>
/// Vector representation, for easy SIMD operations.
/// </summary>
// ReSharper disable once InconsistentNaming
public readonly SysVector4 RGBA => Unsafe.BitCast<Color, SysVector4>(this);
public readonly byte RByte => (byte) (R * byte.MaxValue);
public readonly byte GByte => (byte) (G * byte.MaxValue);
public readonly byte BByte => (byte) (B * byte.MaxValue);
public readonly byte AByte => (byte) (A * byte.MaxValue);
/// <summary>
/// Constructs a new Color4 structure from the specified components.
/// Constructs a new <see cref="Color"/> structure from the specified components.
/// </summary>
/// <param name="r">The red component of the new Color4 structure.</param>
/// <param name="g">The green component of the new Color4 structure.</param>
/// <param name="b">The blue component of the new Color4 structure.</param>
/// <param name="a">The alpha component of the new Color4 structure.</param>
/// <param name="r">The red component of the new Color structure.</param>
/// <param name="g">The green component of the new Color structure.</param>
/// <param name="b">The blue component of the new Color structure.</param>
/// <param name="a">The alpha component of the new Color structure.</param>
public Color(float r, float g, float b, float a = 1)
{
R = r;
@@ -90,14 +92,23 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Constructs a new Color4 structure from the specified components.
/// Constructs a new Color structure from the components in a <see cref="SysVector4"/>.
/// </summary>
/// <param name="r">The red component of the new Color4 structure.</param>
/// <param name="g">The green component of the new Color4 structure.</param>
/// <param name="b">The blue component of the new Color4 structure.</param>
/// <param name="a">The alpha component of the new Color4 structure.</param>
public Color(in SysVector4 vec)
{
this = Unsafe.BitCast<SysVector4, Color>(vec);
}
/// <summary>
/// Constructs a new Color structure from the specified components.
/// </summary>
/// <param name="r">The red component of the new Color structure.</param>
/// <param name="g">The green component of the new Color structure.</param>
/// <param name="b">The blue component of the new Color structure.</param>
/// <param name="a">The alpha component of the new Color structure.</param>
public Color(byte r, byte g, byte b, byte a = 255)
{
Unsafe.SkipInit(out this);
R = r / (float) byte.MaxValue;
G = g / (float) byte.MaxValue;
B = b / (float) byte.MaxValue;
@@ -124,7 +135,7 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Compares the specified Color4 structures for equality.
/// Compares the specified Color structures for equality.
/// </summary>
/// <param name="left">The left-hand side of the comparison.</param>
/// <param name="right">The right-hand side of the comparison.</param>
@@ -135,7 +146,7 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Compares the specified Color4 structures for inequality.
/// Compares the specified Color structures for inequality.
/// </summary>
/// <param name="left">The left-hand side of the comparison.</param>
/// <param name="right">The right-hand side of the comparison.</param>
@@ -146,10 +157,10 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Converts the specified System.Drawing.Color to a Color4 structure.
/// Converts the specified System.Drawing.Color to a Color structure.
/// </summary>
/// <param name="color">The System.Drawing.Color to convert.</param>
/// <returns>A new Color4 structure containing the converted components.</returns>
/// <returns>A new Color structure containing the converted components.</returns>
public static implicit operator Color(System.Drawing.Color color)
{
return new(color.R, color.G, color.B, color.A);
@@ -181,9 +192,9 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Converts the specified Color4 to a System.Drawing.Color structure.
/// Converts the specified Color to a System.Drawing.Color structure.
/// </summary>
/// <param name="color">The Color4 to convert.</param>
/// <param name="color">The Color to convert.</param>
/// <returns>A new System.Drawing.Color structure containing the converted components.</returns>
public static explicit operator System.Drawing.Color(Color color)
{
@@ -210,11 +221,11 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Compares whether this Color4 structure is equal to the specified object.
/// Compares whether this Color structure is equal to the specified object.
/// </summary>
/// <param name="obj">An object to compare to.</param>
/// <returns>True obj is a Color4 structure with the same components as this Color4; false otherwise.</returns>
public override readonly bool Equals(object? obj)
/// <returns>True obj is a Color structure with the same components as this Color; false otherwise.</returns>
public readonly override bool Equals(object? obj)
{
if (!(obj is Color))
return false;
@@ -223,19 +234,19 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Calculates the hash code for this Color4 structure.
/// Calculates the hash code for this Color structure.
/// </summary>
/// <returns>A System.Int32 containing the hash code of this Color4 structure.</returns>
public override readonly int GetHashCode()
/// <returns>A System.Int32 containing the hash code of this Color structure.</returns>
public readonly override int GetHashCode()
{
return ToArgb();
}
/// <summary>
/// Creates a System.String that describes this Color4 structure.
/// Creates a System.String that describes this Color structure.
/// </summary>
/// <returns>A System.String that describes this Color4 structure.</returns>
public override readonly string ToString()
/// <returns>A System.String that describes this Color structure.</returns>
public readonly override string ToString()
{
return $"{{(R, G, B, A) = ({R}, {G}, {B}, {A})}}";
}
@@ -309,7 +320,6 @@ namespace Robust.Shared.Maths
public static Color FromSrgb(Color srgb)
{
float r, g, b;
#if NETCOREAPP
if (srgb.R <= 0.04045f)
r = srgb.R / 12.92f;
else
@@ -324,22 +334,6 @@ namespace Robust.Shared.Maths
b = srgb.B / 12.92f;
else
b = MathF.Pow((srgb.B + 0.055f) / (1.0f + 0.055f), 2.4f);
#else
if (srgb.R <= 0.04045f)
r = srgb.R / 12.92f;
else
r = (float) Math.Pow((srgb.R + 0.055f) / (1.0f + 0.055f), 2.4f);
if (srgb.G <= 0.04045f)
g = srgb.G / 12.92f;
else
g = (float) Math.Pow((srgb.G + 0.055f) / (1.0f + 0.055f), 2.4f);
if (srgb.B <= 0.04045f)
b = srgb.B / 12.92f;
else
b = (float) Math.Pow((srgb.B + 0.055f) / (1.0f + 0.055f), 2.4f);
#endif
return new Color(r, g, b, srgb.A);
}
@@ -355,7 +349,6 @@ namespace Robust.Shared.Maths
{
float r, g, b;
#if NETCOREAPP
if (rgb.R <= 0.0031308)
r = 12.92f * rgb.R;
else
@@ -370,22 +363,6 @@ namespace Robust.Shared.Maths
b = 12.92f * rgb.B;
else
b = (1.0f + 0.055f) * MathF.Pow(rgb.B, 1.0f / 2.4f) - 0.055f;
#else
if (rgb.R <= 0.0031308)
r = 12.92f * rgb.R;
else
r = (1.0f + 0.055f) * (float) Math.Pow(rgb.R, 1.0f / 2.4f) - 0.055f;
if (rgb.G <= 0.0031308)
g = 12.92f * rgb.G;
else
g = (1.0f + 0.055f) * (float) Math.Pow(rgb.G, 1.0f / 2.4f) - 0.055f;
if (rgb.B <= 0.0031308)
b = 12.92f * rgb.B;
else
b = (1.0f + 0.055f) * (float) Math.Pow(rgb.B, 1.0f / 2.4f) - 0.055f;
#endif
return new Color(r, g, b, rgb.A);
}
@@ -471,6 +448,7 @@ namespace Robust.Shared.Maths
/// Each has a range of 0.0 to 1.0.
/// </returns>
/// <param name="rgb">Color value to convert.</param>
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
public static Vector4 ToHsl(Color rgb)
{
var max = MathF.Max(rgb.R, MathF.Max(rgb.G, rgb.B));
@@ -582,6 +560,7 @@ namespace Robust.Shared.Maths
/// Each has a range of 0.0 to 1.0.
/// </returns>
/// <param name="rgb">Color value to convert.</param>
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
public static Vector4 ToHsv(Color rgb)
{
var max = MathF.Max(rgb.R, MathF.Max(rgb.G, rgb.B));
@@ -770,6 +749,7 @@ namespace Robust.Shared.Maths
/// Each has a range of 0.0 to 1.0.
/// </returns>
/// <param name="rgb">Color value to convert.</param>
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
public static Vector4 ToHcy(Color rgb)
{
var max = MathF.Max(rgb.R, MathF.Max(rgb.G, rgb.B));
@@ -828,23 +808,10 @@ namespace Robust.Shared.Maths
/// with 0.5 being 50% of both colors, 0.25 being 25% of <paramref name="β" /> and 75%
/// <paramref name="α" />.
/// </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color InterpolateBetween(Color α, Color β, float λ)
{
if (Sse.IsSupported && Fma.IsSupported)
{
var vecA = Unsafe.As<Color, Vector128<float>>(ref α);
var vecB = Unsafe.As<Color, Vector128<float>>(ref β);
vecB = Fma.MultiplyAdd(Sse.Subtract(vecB, vecA), Vector128.Create(λ), vecA);
return Unsafe.As<Vector128<float>, Color>(ref vecB);
}
ref var svA = ref Unsafe.As<Color, SysVector4>(ref α);
ref var svB = ref Unsafe.As<Color, SysVector4>(ref β);
var res = SysVector4.Lerp(svA, svB, λ);
return Unsafe.As<SysVector4, Color>(ref res);
return new(SysVector4.Lerp(α.RGBA, β.RGBA, λ));
}
public static Color? TryFromHex(ReadOnlySpan<char> hexColor)
@@ -1000,13 +967,8 @@ namespace Robust.Shared.Maths
/// <summary>
/// Component wise multiplication of two colors.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static Color operator *(Color a, Color b)
{
return new(a.R * b.R, a.G * b.G, a.B * b.B, a.A * b.A);
}
public static Color operator *(in Color a, in Color b)
=> new(a.RGBA * b.RGBA);
public readonly string ToHex()
{
@@ -1030,17 +992,16 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Compares whether this Color4 structure is equal to the specified Color4.
/// Compares whether this Color structure is equal to the specified Color.
/// </summary>
/// <param name="other">The Color4 structure to compare to.</param>
/// <returns>True if both Color4 structures contain the same components; false otherwise.</returns>
/// <param name="other">The Color structure to compare to.</param>
/// <returns>True if both Color structures contain the same components; false otherwise.</returns>
public readonly bool Equals(Color other)
{
return
MathHelper.CloseToPercent(R, other.R) &&
MathHelper.CloseToPercent(G, other.G) &&
MathHelper.CloseToPercent(B, other.B) &&
MathHelper.CloseToPercent(A, other.A);
// TODO COLOR why is this approximate
// This method literally doesn't do what its docstring says it does.
// If people wanted approximate equality, they can check that manually.
return MathHelper.CloseToPercent(this, other);
}
[PublicAPI]
@@ -1942,7 +1903,7 @@ namespace Robust.Shared.Maths
public readonly string? Name()
{
return DefaultColorsInverted.TryGetValue(this, out var name) ? name : null;
return DefaultColorsInverted.GetValueOrDefault(this);
}
public static bool TryParse(string input, out Color color)

View File

@@ -15,6 +15,7 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using Vec4 = System.Numerics.Vector4;
namespace Robust.Shared.Maths
{
@@ -525,6 +526,27 @@ namespace Robust.Shared.Maths
return Math.Abs(a - b) <= epsilon;
}
/// <summary>
/// Returns whether two vectors are within <paramref name="percentage"/> of each other
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool CloseToPercent(Vec4 a, Vec4 b, float percentage = .00001f)
{
a = Vec4.Abs(a);
b = Vec4.Abs(b);
var p = new Vec4(percentage);
var epsilon = Vec4.Max(Vec4.Max(a, b) * p, p);
var delta = Vec4.Abs(a - b);
return delta.X <= epsilon.X && delta.Y <= epsilon.Y && delta.Z <= epsilon.Z && delta.W <= epsilon.W;
}
/// <summary>
/// Returns whether two colours are within <paramref name="percentage"/> of each other
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool CloseToPercent(Color a, Color b, float percentage = .00001f)
=> CloseToPercent(a.RGBA, b.RGBA, percentage);
/// <summary>
/// Returns whether two floating point numbers are within <paramref name="percentage"/> of eachother
/// </summary>

View File

@@ -6,4 +6,3 @@
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("Content.Benchmarks")]

View File

@@ -0,0 +1,17 @@
using System;
namespace Robust.Shared.Analyzers;
/// <summary>
/// Indicates that overriders of this method must always call the base function.
/// </summary>
/// <param name="onlyOverrides">
/// If true, only base calls to *overrides* are necessary.
/// This is intended for base classes where the base function is always empty,
/// so a base call from the first override may be ommitted.
/// </param>
[AttributeUsage(AttributeTargets.Method)]
public sealed class MustCallBaseAttribute(bool onlyOverrides = false) : Attribute
{
public bool OnlyOverrides { get; } = onlyOverrides;
}

View File

@@ -9,18 +9,17 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Shared.Console.Commands;
internal sealed class TeleportCommand : LocalizedCommands
internal sealed class TeleportCommand : LocalizedEntityCommands
{
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override string Command => "tp";
public override bool RequireServerOrSingleplayer => true;
@@ -36,11 +35,10 @@ internal sealed class TeleportCommand : LocalizedCommands
return;
}
var xformSystem = _entitySystem.GetEntitySystem<SharedTransformSystem>();
var transform = _entityManager.GetComponent<TransformComponent>(entity);
var position = new Vector2(posX, posY);
xformSystem.AttachToGridOrMap(entity, transform);
_transform.AttachToGridOrMap(entity, transform);
MapId mapId;
if (args.Length == 3 && int.TryParse(args[2], out var intMapId))
@@ -56,25 +54,26 @@ internal sealed class TeleportCommand : LocalizedCommands
if (_map.TryFindGridAt(mapId, position, out var gridUid, out var grid))
{
var gridPos = Vector2.Transform(position, xformSystem.GetInvWorldMatrix(gridUid));
var gridPos = Vector2.Transform(position, _transform.GetInvWorldMatrix(gridUid));
xformSystem.SetCoordinates(entity, transform, new EntityCoordinates(gridUid, gridPos));
_transform.SetCoordinates(entity, transform, new EntityCoordinates(gridUid, gridPos));
}
else
{
var mapEnt = _map.GetMapEntityIdOrThrow(mapId);
xformSystem.SetWorldPosition(transform, position);
xformSystem.SetParent(entity, transform, mapEnt);
_transform.SetWorldPosition(transform, position);
_transform.SetParent(entity, transform, mapEnt);
}
shell.WriteLine($"Teleported {shell.Player} to {mapId}:{posX},{posY}.");
}
}
public sealed class TeleportToCommand : LocalizedCommands
public sealed class TeleportToCommand : LocalizedEntityCommands
{
[Dependency] private readonly ISharedPlayerManager _players = default!;
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override string Command => "tpto";
public override bool RequireServerOrSingleplayer => true;
@@ -89,7 +88,6 @@ public sealed class TeleportToCommand : LocalizedCommands
if (!TryGetTransformFromUidOrUsername(target, shell, out var targetUid, out _))
return;
var transformSystem = _entities.System<SharedTransformSystem>();
var targetCoords = new EntityCoordinates(targetUid.Value, Vector2.Zero);
if (_entities.TryGetComponent(targetUid, out PhysicsComponent? targetPhysics))
@@ -127,8 +125,8 @@ public sealed class TeleportToCommand : LocalizedCommands
foreach (var victim in victims)
{
transformSystem.SetCoordinates(victim.Entity, targetCoords);
transformSystem.AttachToGridOrMap(victim.Entity, victim.Transform);
_transform.SetCoordinates(victim.Entity, targetCoords);
_transform.AttachToGridOrMap(victim.Entity, victim.Transform);
}
}
@@ -178,9 +176,10 @@ public sealed class TeleportToCommand : LocalizedCommands
}
}
sealed class LocationCommand : LocalizedCommands
sealed class LocationCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override string Command => "loc";
@@ -192,18 +191,19 @@ sealed class LocationCommand : LocalizedCommands
var pt = _ent.GetComponent<TransformComponent>(entity);
var pos = pt.Coordinates;
shell.WriteLine($"MapID:{pos.GetMapId(_ent)} GridUid:{pos.GetGridUid(_ent)} X:{pos.X:N2} Y:{pos.Y:N2}");
var mapId = _transform.GetMapId(pos);
var gridUid = _transform.GetGrid(pos);
shell.WriteLine($"MapID:{mapId} GridUid:{gridUid} X:{pos.X:N2} Y:{pos.Y:N2}");
}
}
sealed class TpGridCommand : LocalizedCommands
sealed class TpGridCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
public override string Command => "tpgrid";
public override string Description => Loc.GetString("cmd-tpgrid-desc");
public override string Help => Loc.GetString("cmd-tpgrid-help");
public override bool RequireServerOrSingleplayer => true;
public override void Execute(IConsoleShell shell, string argStr, string[] args)
@@ -246,14 +246,14 @@ sealed class TpGridCommand : LocalizedCommands
mapId = new MapId(map);
}
var id = _map.GetMapEntityId(mapId);
var id = _map.GetMap(mapId);
if (id == EntityUid.Invalid)
{
shell.WriteError(Loc.GetString("cmd-parse-failure-mapid", ("arg", mapId.Value)));
return;
}
var pos = new EntityCoordinates(_map.GetMapEntityId(mapId), new Vector2(xPos, yPos));
var pos = new EntityCoordinates(id, new Vector2(xPos, yPos));
_ent.System<SharedTransformSystem>().SetCoordinates(uid.Value, pos);
}

View File

@@ -29,6 +29,9 @@ namespace Robust.Shared.Console
[Dependency] protected readonly ILocalizationManager LocalizationManager = default!;
[ViewVariables] protected readonly Dictionary<string, IConsoleCommand> RegisteredCommands = new();
[ViewVariables] private readonly HashSet<string> _autoRegisteredCommands = [];
private bool _isInRegistrationRegion;
private readonly CommandBuffer _commandBuffer = new CommandBuffer();
@@ -61,6 +64,11 @@ namespace Robust.Shared.Console
// search for all client commands in all assemblies, and register them
foreach (var type in ReflectionManager.GetAllChildren<IConsoleCommand>())
{
// This sucks but I can't come up with anything better
// that won't just be 10x worse complexity for no gain.
if (type.IsAssignableTo(typeof(IEntityConsoleCommand)))
continue;
var instance = (IConsoleCommand)_typeFactory.CreateInstanceUnchecked(type, true);
if (AvailableCommands.TryGetValue(instance.Command, out var duplicate))
{
@@ -69,6 +77,7 @@ namespace Robust.Shared.Console
}
RegisteredCommands[instance.Command] = instance;
_autoRegisteredCommands.Add(instance.Command);
}
}
@@ -76,6 +85,23 @@ namespace Robust.Shared.Console
{
}
public void BeginRegistrationRegion()
{
if (_isInRegistrationRegion)
throw new InvalidOperationException("Cannot enter registration region twice!");
_isInRegistrationRegion = true;
}
public void EndRegistrationRegion()
{
if (!_isInRegistrationRegion)
throw new InvalidOperationException("Was not in registration region.");
_isInRegistrationRegion = false;
UpdateAvailableCommands();
}
#region RegisterCommand
public void RegisterCommand(
string command,
@@ -88,8 +114,7 @@ namespace Robust.Shared.Console
throw new InvalidOperationException($"Command already registered: {command}");
var newCmd = new RegisteredCommand(command, description, help, callback, requireServerOrSingleplayer);
RegisteredCommands.Add(command, newCmd);
UpdateAvailableCommands();
RegisterCommand(newCmd);
}
public void RegisterCommand(
@@ -104,8 +129,7 @@ namespace Robust.Shared.Console
throw new InvalidOperationException($"Command already registered: {command}");
var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer);
RegisteredCommands.Add(command, newCmd);
UpdateAvailableCommands();
RegisterCommand(newCmd);
}
public void RegisterCommand(
@@ -120,8 +144,7 @@ namespace Robust.Shared.Console
throw new InvalidOperationException($"Command already registered: {command}");
var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer);
RegisteredCommands.Add(command, newCmd);
UpdateAvailableCommands();
RegisterCommand(newCmd);
}
public void RegisterCommand(string command, ConCommandCallback callback,
@@ -153,6 +176,15 @@ namespace Robust.Shared.Console
var help = LocalizationManager.TryGetString($"cmd-{command}-help", out var val) ? val : "";
RegisterCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer);
}
public void RegisterCommand(IConsoleCommand command)
{
RegisteredCommands.Add(command.Command, command);
if (!_isInRegistrationRegion)
UpdateAvailableCommands();
}
#endregion
/// <inheritdoc />
@@ -161,12 +193,14 @@ namespace Robust.Shared.Console
if (!RegisteredCommands.TryGetValue(command, out var cmd))
throw new KeyNotFoundException($"Command {command} is not registered.");
if (cmd is not RegisteredCommand)
if (_autoRegisteredCommands.Contains(command))
throw new InvalidOperationException(
"You cannot unregister commands that have been registered automatically.");
RegisteredCommands.Remove(command);
UpdateAvailableCommands();
if (!_isInRegistrationRegion)
UpdateAvailableCommands();
}
//TODO: Pull up

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
namespace Robust.Shared.Console;
/// <summary>
/// Manages registration for "entity" console commands.
/// </summary>
/// <remarks>
/// See <see cref="LocalizedEntityCommands"/> for details on what "entity" console commands are.
/// </remarks>
internal sealed class EntityConsoleHost
{
[Dependency] private readonly IConsoleHost _consoleHost = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
private readonly HashSet<string> _entityCommands = [];
public void Startup()
{
DebugTools.Assert(_entityCommands.Count == 0);
var deps = ((EntitySystemManager)_entitySystemManager).SystemDependencyCollection;
_consoleHost.BeginRegistrationRegion();
// search for all client commands in all assemblies, and register them
foreach (var type in _reflectionManager.GetAllChildren<IEntityConsoleCommand>())
{
var instance = (IConsoleCommand)Activator.CreateInstance(type)!;
deps.InjectDependencies(instance, oneOff: true);
_entityCommands.Add(instance.Command);
_consoleHost.RegisterCommand(instance);
}
_consoleHost.EndRegistrationRegion();
}
public void Shutdown()
{
foreach (var command in _entityCommands)
{
_consoleHost.UnregisterCommand(command);
}
_entityCommands.Clear();
}
}

View File

@@ -83,4 +83,11 @@ namespace Robust.Shared.Console
return ValueTask.FromResult(GetCompletion(shell, args));
}
}
/// <summary>
/// Special marker interface used to indicate "entity" commands.
/// See <see cref="LocalizedEntityCommands"/> for an overview.
/// </summary>
/// <seealso cref="EntityConsoleHost"/>
internal interface IEntityConsoleCommand : IConsoleCommand;
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
namespace Robust.Shared.Console
@@ -173,6 +174,33 @@ namespace Robust.Shared.Console
ConCommandCallback callback,
ConCommandCompletionAsyncCallback completionCallback,
bool requireServerOrSingleplayer = false);
/// <summary>
/// Register an existing console command instance directly.
/// </summary>
/// <remarks>
/// For this to be useful, the command has to be somehow excluded from automatic registration,
/// such as by using the <see cref="ReflectAttribute"/>.
/// </remarks>
/// <param name="command">The command to register.</param>
/// <seealso cref="BeginRegistrationRegion"/>
void RegisterCommand(IConsoleCommand command);
/// <summary>
/// Begin a region for registering many console commands in one go.
/// The region can be ended with <see cref="EndRegistrationRegion"/>.
/// </summary>
/// <remarks>
/// Commands registered inside this region temporarily suppress some updating
/// logic that would cause significant wasted work. This logic runs when the region is ended instead.
/// </remarks>
void BeginRegistrationRegion();
/// <summary>
/// End a registration region started with <see cref="BeginRegistrationRegion"/>.
/// </summary>
void EndRegistrationRegion();
#endregion
/// <summary>

View File

@@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
@@ -34,3 +35,21 @@ public abstract class LocalizedCommands : IConsoleCommand
return ValueTask.FromResult(GetCompletion(shell, args));
}
}
/// <summary>
/// Base class for localized console commands that run in "entity space".
/// </summary>
/// <remarks>
/// <para>
/// This type of command is registered only while the entity system is active.
/// On the client this means that the commands are only available while connected to a server or in single player.
/// </para>
/// <para>
/// These commands are allowed to take dependencies on entity systems, reducing boilerplate for many usages.
/// </para>
/// </remarks>
public abstract class LocalizedEntityCommands : LocalizedCommands, IEntityConsoleCommand
{
[Dependency]
protected readonly EntityManager EntityManager = default!;
}

View File

@@ -88,6 +88,7 @@ namespace Robust.Shared.ContentPack
public string SystemAssemblyName = default!;
public HashSet<VerifierError> AllowedVerifierErrors = default!;
public List<string> WhitelistedNamespaces = default!;
public List<string> AllowedAssemblyPrefixes = default!;
public Dictionary<string, Dictionary<string, TypeConfig>> Types = default!;
}

View File

@@ -32,7 +32,7 @@ namespace Robust.Shared.ContentPack
String("short").ThenReturn(PrimitiveTypeCode.Int16);
private static readonly Parser<char, PrimitiveTypeCode> UInt16TypeParser =
String("ushort").ThenReturn(PrimitiveTypeCode.UInt32);
String("ushort").ThenReturn(PrimitiveTypeCode.UInt16);
private static readonly Parser<char, PrimitiveTypeCode> Int32TypeParser =
String("int").ThenReturn(PrimitiveTypeCode.Int32);

View File

@@ -0,0 +1,442 @@
#if TOOLS
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
namespace Robust.Shared.ContentPack;
internal sealed partial class AssemblyTypeChecker
{
// This part of the code tries to find the originator of bad sandbox references.
private void ReportBadReferences(PEReader peReader, MetadataReader reader, IEnumerable<EntityHandle> reference)
{
_sawmill.Info("Started search for originator of bad references...");
var refs = reference.ToHashSet();
ExpandReferences(reader, refs);
foreach (var methodDefHandle in reader.MethodDefinitions)
{
var methodDef = reader.GetMethodDefinition(methodDefHandle);
if (methodDef.RelativeVirtualAddress == 0)
continue;
var methodName = reader.GetString(methodDef.Name);
var body = peReader.GetMethodBody(methodDef.RelativeVirtualAddress);
var bytes = body.GetILBytes()!;
var ilReader = new ILReader(bytes);
var prefPosition = 0;
while (ilReader.MoveNext(out var instruction))
{
if (instruction.TryGetEntityHandle(out var handle))
{
if (refs.Contains(handle))
{
var type = GetTypeFromDefinition(reader, methodDef.GetDeclaringType());
_sawmill.Error(
$"Found reference to {DisplayHandle(reader, handle)} in method {type}.{methodName} at IL 0x{prefPosition:X4}");
}
}
prefPosition = ilReader.Position;
}
}
}
private static string DisplayHandle(MetadataReader reader, EntityHandle handle)
{
switch (handle.Kind)
{
case HandleKind.MemberReference:
var memberRef = reader.GetMemberReference((MemberReferenceHandle)handle);
var name = reader.GetString(memberRef.Name);
var parent = DisplayHandle(reader, memberRef.Parent);
return $"{parent}.{name}";
case HandleKind.TypeReference:
return $"{ParseTypeReference(reader, (TypeReferenceHandle)handle)}";
case HandleKind.TypeSpecification:
var typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)handle);
var provider = new TypeProvider();
var type = typeSpec.DecodeSignature(provider, 0);
return $"{type}";
default:
return $"({handle.Kind} handle)";
}
}
private static void ExpandReferences(MetadataReader reader, HashSet<EntityHandle> handles)
{
var toAdd = new List<EntityHandle>();
foreach (var memberRefHandle in reader.MemberReferences)
{
var memberRef = reader.GetMemberReference(memberRefHandle);
if (handles.Contains(memberRef.Parent))
{
toAdd.Add(memberRefHandle);
}
}
handles.UnionWith(toAdd);
}
private readonly struct ILInstruction
{
public readonly ILOpCode OpCode;
public readonly long Argument;
public readonly int[]? SwitchTargets;
public ILInstruction(ILOpCode opCode)
{
OpCode = opCode;
}
public ILInstruction(ILOpCode opCode, long argument)
{
OpCode = opCode;
Argument = argument;
}
public ILInstruction(ILOpCode opCode, long argument, int[] switchTargets)
{
OpCode = opCode;
Argument = argument;
SwitchTargets = switchTargets;
}
public bool TryGetEntityHandle(out EntityHandle handle)
{
switch (OpCode)
{
case ILOpCode.Call:
case ILOpCode.Callvirt:
case ILOpCode.Newobj:
case ILOpCode.Jmp:
case ILOpCode.Box:
case ILOpCode.Castclass:
case ILOpCode.Cpobj:
case ILOpCode.Initobj:
case ILOpCode.Isinst:
case ILOpCode.Ldelem:
case ILOpCode.Ldelema:
case ILOpCode.Ldfld:
case ILOpCode.Ldflda:
case ILOpCode.Ldobj:
case ILOpCode.Ldstr:
case ILOpCode.Ldtoken:
case ILOpCode.Ldvirtftn:
case ILOpCode.Mkrefany:
case ILOpCode.Newarr:
case ILOpCode.Refanyval:
case ILOpCode.Sizeof:
case ILOpCode.Stelem:
case ILOpCode.Stfld:
case ILOpCode.Stobj:
case ILOpCode.Stsfld:
case ILOpCode.Throw:
case ILOpCode.Unbox_any:
handle = Unsafe.BitCast<int, EntityHandle>((int)Argument);
return true;
default:
handle = default;
return false;
}
}
}
private sealed class ILReader(byte[] body)
{
public int Position;
public bool MoveNext(out ILInstruction instruction)
{
if (Position >= body.Length)
{
instruction = default;
return false;
}
var firstByte = body[Position++];
var opCode = (ILOpCode)firstByte;
if (firstByte == 0xFE)
opCode = 0xFE00 + (ILOpCode)body[Position++];
switch (opCode)
{
// no args.
case ILOpCode.Readonly:
case ILOpCode.Tail:
case ILOpCode.Volatile:
case ILOpCode.Add:
case ILOpCode.Add_ovf:
case ILOpCode.Add_ovf_un:
case ILOpCode.And:
case ILOpCode.Arglist:
case ILOpCode.Break:
case ILOpCode.Ceq:
case ILOpCode.Cgt:
case ILOpCode.Cgt_un:
case ILOpCode.Ckfinite:
case ILOpCode.Clt:
case ILOpCode.Clt_un:
case ILOpCode.Conv_i1:
case ILOpCode.Conv_i2:
case ILOpCode.Conv_i4:
case ILOpCode.Conv_i8:
case ILOpCode.Conv_r4:
case ILOpCode.Conv_r8:
case ILOpCode.Conv_u1:
case ILOpCode.Conv_u2:
case ILOpCode.Conv_u4:
case ILOpCode.Conv_u8:
case ILOpCode.Conv_i:
case ILOpCode.Conv_u:
case ILOpCode.Conv_r_un:
case ILOpCode.Conv_ovf_i1:
case ILOpCode.Conv_ovf_i2:
case ILOpCode.Conv_ovf_i4:
case ILOpCode.Conv_ovf_i8:
case ILOpCode.Conv_ovf_u4:
case ILOpCode.Conv_ovf_u8:
case ILOpCode.Conv_ovf_i:
case ILOpCode.Conv_ovf_u:
case ILOpCode.Conv_ovf_i1_un:
case ILOpCode.Conv_ovf_i2_un:
case ILOpCode.Conv_ovf_i4_un:
case ILOpCode.Conv_ovf_i8_un:
case ILOpCode.Conv_ovf_u4_un:
case ILOpCode.Conv_ovf_u8_un:
case ILOpCode.Conv_ovf_i_un:
case ILOpCode.Conv_ovf_u_un:
case ILOpCode.Cpblk:
case ILOpCode.Div:
case ILOpCode.Div_un:
case ILOpCode.Dup:
case ILOpCode.Endfilter:
case ILOpCode.Endfinally:
case ILOpCode.Initblk:
case ILOpCode.Ldarg_0:
case ILOpCode.Ldarg_1:
case ILOpCode.Ldarg_2:
case ILOpCode.Ldarg_3:
case ILOpCode.Ldc_i4_0:
case ILOpCode.Ldc_i4_1:
case ILOpCode.Ldc_i4_2:
case ILOpCode.Ldc_i4_3:
case ILOpCode.Ldc_i4_4:
case ILOpCode.Ldc_i4_5:
case ILOpCode.Ldc_i4_6:
case ILOpCode.Ldc_i4_7:
case ILOpCode.Ldc_i4_8:
case ILOpCode.Ldc_i4_m1:
case ILOpCode.Ldind_i1:
case ILOpCode.Ldind_u1:
case ILOpCode.Ldind_i2:
case ILOpCode.Ldind_u2:
case ILOpCode.Ldind_i4:
case ILOpCode.Ldind_u4:
case ILOpCode.Ldind_i8:
case ILOpCode.Ldind_i:
case ILOpCode.Ldind_r4:
case ILOpCode.Ldind_r8:
case ILOpCode.Ldind_ref:
case ILOpCode.Ldloc_0:
case ILOpCode.Ldloc_1:
case ILOpCode.Ldloc_2:
case ILOpCode.Ldloc_3:
case ILOpCode.Ldnull:
case ILOpCode.Localloc:
case ILOpCode.Mul:
case ILOpCode.Mul_ovf:
case ILOpCode.Mul_ovf_un:
case ILOpCode.Neg:
case ILOpCode.Nop:
case ILOpCode.Not:
case ILOpCode.Or:
case ILOpCode.Pop:
case ILOpCode.Rem:
case ILOpCode.Rem_un:
case ILOpCode.Ret:
case ILOpCode.Shl:
case ILOpCode.Shr:
case ILOpCode.Shr_un:
case ILOpCode.Stind_i1:
case ILOpCode.Stind_i2:
case ILOpCode.Stind_i4:
case ILOpCode.Stind_i8:
case ILOpCode.Stind_r4:
case ILOpCode.Stind_r8:
case ILOpCode.Stind_i:
case ILOpCode.Stind_ref:
case ILOpCode.Stloc_0:
case ILOpCode.Stloc_1:
case ILOpCode.Stloc_2:
case ILOpCode.Stloc_3:
case ILOpCode.Sub:
case ILOpCode.Sub_ovf:
case ILOpCode.Sub_ovf_un:
case ILOpCode.Xor:
case ILOpCode.Ldelem_i1:
case ILOpCode.Ldelem_u1:
case ILOpCode.Ldelem_i2:
case ILOpCode.Ldelem_u2:
case ILOpCode.Ldelem_i4:
case ILOpCode.Ldelem_u4:
case ILOpCode.Ldelem_i8:
case ILOpCode.Ldelem_i:
case ILOpCode.Ldelem_r4:
case ILOpCode.Ldelem_r8:
case ILOpCode.Ldelem_ref:
case ILOpCode.Ldlen:
case ILOpCode.Refanytype:
case ILOpCode.Rethrow:
case ILOpCode.Stelem_i1:
case ILOpCode.Stelem_i2:
case ILOpCode.Stelem_i4:
case ILOpCode.Stelem_i8:
case ILOpCode.Stelem_i:
case ILOpCode.Stelem_r4:
case ILOpCode.Stelem_r8:
case ILOpCode.Stelem_ref:
case ILOpCode.Throw:
instruction = new ILInstruction(opCode);
break;
// 1-byte arg.
case ILOpCode.Unaligned:
case ILOpCode.Beq_s:
case ILOpCode.Bge_s:
case ILOpCode.Bge_un_s:
case ILOpCode.Bgt_s:
case ILOpCode.Bgt_un_s:
case ILOpCode.Ble_s:
case ILOpCode.Ble_un_s:
case ILOpCode.Blt_s:
case ILOpCode.Blt_un_s:
case ILOpCode.Bne_un_s:
case ILOpCode.Br_s:
case ILOpCode.Brfalse_s:
case ILOpCode.Brtrue_s:
case ILOpCode.Ldarg_s:
case ILOpCode.Ldarga_s:
case ILOpCode.Ldc_i4_s:
case ILOpCode.Ldloc_s:
case ILOpCode.Ldloca_s:
case ILOpCode.Leave_s:
case ILOpCode.Starg_s:
case ILOpCode.Stloc_s:
instruction = new ILInstruction(opCode, body[Position]);
Position += 1;
break;
// 2-byte value
case ILOpCode.Ldarg:
case ILOpCode.Ldarga:
case ILOpCode.Ldloc:
case ILOpCode.Ldloca:
case ILOpCode.Starg:
case ILOpCode.Stloc:
var shortValue = BinaryPrimitives.ReadInt16LittleEndian(body.AsSpan(Position, 2));
Position += 2;
instruction = new ILInstruction(opCode, shortValue);
break;
// 4-byte value
case ILOpCode.Constrained:
case ILOpCode.Beq:
case ILOpCode.Bge:
case ILOpCode.Bge_un:
case ILOpCode.Bgt:
case ILOpCode.Bgt_un:
case ILOpCode.Ble:
case ILOpCode.Ble_un:
case ILOpCode.Blt:
case ILOpCode.Blt_un:
case ILOpCode.Bne_un:
case ILOpCode.Br:
case ILOpCode.Brfalse:
case ILOpCode.Brtrue:
case ILOpCode.Call:
case ILOpCode.Calli:
case ILOpCode.Jmp:
case ILOpCode.Ldc_i4:
case ILOpCode.Ldc_r4:
case ILOpCode.Ldftn:
case ILOpCode.Leave:
case ILOpCode.Box:
case ILOpCode.Callvirt:
case ILOpCode.Castclass:
case ILOpCode.Cpobj:
case ILOpCode.Initobj:
case ILOpCode.Isinst:
case ILOpCode.Ldelem:
case ILOpCode.Ldelema:
case ILOpCode.Ldfld:
case ILOpCode.Ldflda:
case ILOpCode.Ldobj:
case ILOpCode.Ldsfld:
case ILOpCode.Ldsflda:
case ILOpCode.Ldstr:
case ILOpCode.Ldtoken:
case ILOpCode.Ldvirtftn:
case ILOpCode.Mkrefany:
case ILOpCode.Newarr:
case ILOpCode.Newobj:
case ILOpCode.Refanyval:
case ILOpCode.Sizeof:
case ILOpCode.Stelem:
case ILOpCode.Stfld:
case ILOpCode.Stobj:
case ILOpCode.Stsfld:
case ILOpCode.Unbox:
case ILOpCode.Unbox_any:
var intValue = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(Position, 4));
Position += 4;
instruction = new ILInstruction(opCode, intValue);
break;
// 8-byte value
case ILOpCode.Ldc_i8:
case ILOpCode.Ldc_r8:
var longValue = BinaryPrimitives.ReadInt64LittleEndian(body.AsSpan(Position, 8));
Position += 8;
instruction = new ILInstruction(opCode, longValue);
break;
// Switch
case ILOpCode.Switch:
var switchLength = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(Position, 4));
Position += 4;
var switchArgs = new int[switchLength];
for (var i = 0; i < switchLength; i++)
{
switchArgs[i] = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(Position, 4));
Position += 4;
}
instruction = new ILInstruction(opCode, switchLength, switchArgs);
break;
default:
throw new InvalidDataException($"Unknown opcode: {opCode}");
}
return true;
}
}
}
#endif

View File

@@ -131,6 +131,16 @@ namespace Robust.Shared.ContentPack
return false;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
if (!loadedConfig.AllowedAssemblyPrefixes.Any(allowedNamePrefix => asmName.StartsWith(allowedNamePrefix)))
{
_sawmill.Error($"Assembly name '{asmName}' is not allowed for a content assembly");
return false;
}
if (VerifyIL)
{
if (!DoVerifyIL(asmName, resolver, peReader, reader))
@@ -148,7 +158,7 @@ namespace Robust.Shared.ContentPack
if ((Dump & DumpFlags.Types) != 0)
{
foreach (var mType in types)
foreach (var (_, mType) in types)
{
_sawmill.Debug($"RefType: {mType}");
}
@@ -156,7 +166,7 @@ namespace Robust.Shared.ContentPack
if ((Dump & DumpFlags.Members) != 0)
{
foreach (var memberRef in members)
foreach (var (_, memberRef) in members)
{
_sawmill.Debug($"RefMember: {memberRef}");
}
@@ -179,18 +189,17 @@ namespace Robust.Shared.ContentPack
return true;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
var badRefs = new ConcurrentBag<EntityHandle>();
// We still do explicit type reference scanning, even though the actual whitelists work with raw members.
// This is so that we can simplify handling of generic type specifications during member checking:
// we won't have to check that any types in their type arguments are whitelisted.
foreach (var type in types)
foreach (var (handle, type) in types)
{
if (!IsTypeAccessAllowed(loadedConfig, type, out _))
{
errors.Add(new SandboxError($"Access to type not allowed: {type}"));
badRefs.Add(handle);
}
}
@@ -208,13 +217,20 @@ namespace Robust.Shared.ContentPack
_sawmill.Debug($"Type abuse... {fullStopwatch.ElapsedMilliseconds}ms");
CheckMemberReferences(loadedConfig, members, errors);
CheckMemberReferences(loadedConfig, members, errors, badRefs);
foreach (var error in errors)
{
_sawmill.Error($"Sandbox violation: {error.Message}");
}
#if TOOLS
if (!badRefs.IsEmpty)
{
ReportBadReferences(peReader, reader, badRefs);
}
#endif
_sawmill.Debug($"Checked assembly in {fullStopwatch.ElapsedMilliseconds}ms");
return errors.IsEmpty;
@@ -351,11 +367,13 @@ namespace Robust.Shared.ContentPack
private void CheckMemberReferences(
SandboxConfig sandboxConfig,
List<MMemberRef> members,
ConcurrentBag<SandboxError> errors)
List<(MemberReferenceHandle handle, MMemberRef parsed)> members,
ConcurrentBag<SandboxError> errors,
ConcurrentBag<EntityHandle> badReferences)
{
Parallel.ForEach(members, memberRef =>
Parallel.ForEach(members, entry =>
{
var (handle, memberRef) = entry;
MType baseType = memberRef.ParentType;
while (!(baseType is MTypeReferenced))
{
@@ -416,6 +434,7 @@ namespace Robust.Shared.ContentPack
}
errors.Add(new SandboxError($"Access to field not allowed: {mMemberRefField}"));
badReferences.Add(handle);
break;
}
case MMemberRefMethod mMemberRefMethod:
@@ -444,6 +463,7 @@ namespace Robust.Shared.ContentPack
}
errors.Add(new SandboxError($"Access to method not allowed: {mMemberRefMethod}"));
badReferences.Add(handle);
break;
default:
throw new ArgumentOutOfRangeException(nameof(memberRef));
@@ -458,18 +478,18 @@ namespace Robust.Shared.ContentPack
{
// This inheritance whitelisting primarily serves to avoid content doing funny stuff
// by e.g. inheriting Type.
foreach (var (_, baseType, interfaces) in inherited)
foreach (var (type, baseType, interfaces) in inherited)
{
if (!CanInherit(baseType))
{
errors.Add(new SandboxError($"Inheriting of type not allowed: {baseType}"));
errors.Add(new SandboxError($"Inheriting of type not allowed: {baseType} (by {type})"));
}
foreach (var @interface in interfaces)
{
if (!CanInherit(@interface))
{
errors.Add(new SandboxError($"Implementing of interface not allowed: {@interface}"));
errors.Add(new SandboxError($"Implementing of interface not allowed: {@interface} (by {type})"));
}
}
@@ -547,25 +567,25 @@ namespace Robust.Shared.ContentPack
return nsDict.TryGetValue(type.Name, out cfg);
}
private List<MTypeReferenced> GetReferencedTypes(MetadataReader reader, ConcurrentBag<SandboxError> errors)
private List<(TypeReferenceHandle handle, MTypeReferenced parsed)> GetReferencedTypes(MetadataReader reader, ConcurrentBag<SandboxError> errors)
{
return reader.TypeReferences.Select(typeRefHandle =>
{
try
{
return ParseTypeReference(reader, typeRefHandle);
return (typeRefHandle, ParseTypeReference(reader, typeRefHandle));
}
catch (UnsupportedMetadataException e)
{
errors.Add(new SandboxError(e));
return null;
return default;
}
})
.Where(p => p != null)
.Where(p => p.Item2 != null)
.ToList()!;
}
private List<MMemberRef> GetReferencedMembers(MetadataReader reader, ConcurrentBag<SandboxError> errors)
private List<(MemberReferenceHandle handle, MMemberRef parsed)> GetReferencedMembers(MetadataReader reader, ConcurrentBag<SandboxError> errors)
{
return reader.MemberReferences.AsParallel()
.Select(memRefHandle =>
@@ -586,7 +606,7 @@ namespace Robust.Shared.ContentPack
catch (UnsupportedMetadataException u)
{
errors.Add(new SandboxError(u));
return null;
return default;
}
break;
@@ -600,7 +620,7 @@ namespace Robust.Shared.ContentPack
catch (UnsupportedMetadataException u)
{
errors.Add(new SandboxError(u));
return null;
return default;
}
break;
@@ -616,7 +636,7 @@ namespace Robust.Shared.ContentPack
{
// Ensure this isn't a self-defined type.
// This can happen due to generics since MethodSpec needs to point to MemberRef.
return null;
return default;
}
break;
@@ -625,18 +645,18 @@ namespace Robust.Shared.ContentPack
{
errors.Add(new SandboxError(
$"Module global variables and methods are unsupported. Name: {memName}"));
return null;
return default;
}
case HandleKind.MethodDefinition:
{
errors.Add(new SandboxError($"Vararg calls are unsupported. Name: {memName}"));
return null;
return default;
}
default:
{
errors.Add(new SandboxError(
$"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}"));
return null;
return default;
}
}
@@ -667,9 +687,9 @@ namespace Robust.Shared.ContentPack
throw new ArgumentOutOfRangeException();
}
return memberRef;
return (memRefHandle, memberRef);
})
.Where(p => p != null)
.Where(p => p.memberRef != null)
.ToList()!;
}
@@ -780,7 +800,6 @@ namespace Robust.Shared.ContentPack
}
}
/// <exception href="UnsupportedMetadataException">
/// Thrown if the metadata does something funny we don't "support" like type forwarding.
/// </exception>

View File

@@ -60,7 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()));
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

@@ -93,19 +93,23 @@ namespace Robust.Shared.ContentPack
{
var sw = Stopwatch.StartNew();
Sawmill.Debug("LOADING modules");
var files = new Dictionary<string, (ResPath Path, string[] references)>();
var files = new Dictionary<string, (ResPath Path, MemoryStream data, string[] references)>();
// Find all modules we want to load.
foreach (var fullPath in paths)
{
using var asmFile = _res.ContentFileRead(fullPath);
var refData = GetAssemblyReferenceData(asmFile);
var ms = new MemoryStream();
asmFile.CopyTo(ms);
ms.Position = 0;
var refData = GetAssemblyReferenceData(ms);
if (refData == null)
continue;
var (asmRefs, asmName) = refData.Value;
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
if (!files.TryAdd(asmName, (fullPath, ms, asmRefs)))
{
Sawmill.Error("Found multiple modules with the same assembly name " +
$"'{asmName}', A: {files[asmName].Path}, B: {fullPath}.");
@@ -122,10 +126,10 @@ namespace Robust.Shared.ContentPack
Parallel.ForEach(files, pair =>
{
var (name, (path, _)) = pair;
var (name, (_, data, _)) = pair;
using var stream = _res.ContentFileRead(path);
if (!typeChecker.CheckAssembly(stream, resolver))
data.Position = 0;
if (!typeChecker.CheckAssembly(data, resolver))
{
throw new TypeCheckFailedException($"Assembly {name} failed type checks.");
}
@@ -137,14 +141,15 @@ namespace Robust.Shared.ContentPack
var nodes = TopologicalSort.FromBeforeAfter(
files,
kv => kv.Key,
kv => kv.Value.Path,
kv => kv.Value,
_ => Array.Empty<string>(),
kv => kv.Value.references,
allowMissing: true); // missing refs would be non-content assemblies so allow that.
// Actually load them in the order they depend on each other.
foreach (var path in TopologicalSort.Sort(nodes))
foreach (var item in TopologicalSort.Sort(nodes))
{
var (path, memory, _) = item;
Sawmill.Debug($"Loading module: '{path}'");
try
{
@@ -156,9 +161,9 @@ namespace Robust.Shared.ContentPack
}
else
{
using var assemblyStream = _res.ContentFileRead(path);
memory.Position = 0;
using var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
LoadGameAssembly(assemblyStream, symbolsStream, skipVerify: true);
LoadGameAssembly(memory, symbolsStream, skipVerify: true);
}
}
catch (Exception e)
@@ -174,7 +179,7 @@ namespace Robust.Shared.ContentPack
private (string[] refs, string name)? GetAssemblyReferenceData(Stream stream)
{
using var reader = ModLoader.MakePEReader(stream);
using var reader = ModLoader.MakePEReader(stream, leaveOpen: true);
var metaReader = reader.GetMetadataReader();
var name = metaReader.GetString(metaReader.GetAssemblyDefinition().Name);

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,7 +379,13 @@ namespace Robust.Shared.ContentPack
{
if (root is DirLoader loader)
{
yield return new ResPath(loader.GetPath(new ResPath(@"/")));
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

@@ -15,6 +15,10 @@ WhitelistedNamespaces:
- Content
- OpenDreamShared
AllowedAssemblyPrefixes:
- OpenDream
- Content
# The type whitelist does NOT care about which assembly types come from.
# This is because types switch assembly all the time.
# Just look up stuff like StreamReader on https://apisof.net.
@@ -84,12 +88,146 @@ Types:
- "bool get_HasContents()"
Lidgren.Network:
NetBuffer:
All: True
Methods:
- "byte[] get_Data()"
- "void set_Data(byte[])"
- "int get_LengthBytes()"
- "void set_LengthBytes(int)"
- "int get_LengthBits()"
- "void set_LengthBits(int)"
- "long get_Position()"
- "void set_Position(long)"
- "int get_PositionInBytes()"
- "byte[] PeekDataBuffer()"
- "bool PeekBoolean()"
- "byte PeekByte()"
- "sbyte PeekSByte()"
- "byte PeekByte(int)"
- "System.Span`1<byte> PeekBytes(System.Span`1<byte>)"
- "byte[] PeekBytes(int)"
- "void PeekBytes(byte[], int, int)"
- "short PeekInt16()"
- "ushort PeekUInt16()"
- "int PeekInt32()"
- "int PeekInt32(int)"
- "uint PeekUInt32()"
- "uint PeekUInt32(int)"
- "ulong PeekUInt64()"
- "long PeekInt64()"
- "ulong PeekUInt64(int)"
- "long PeekInt64(int)"
- "float PeekFloat()"
- "System.Half PeekHalf()"
- "float PeekSingle()"
- "double PeekDouble()"
- "string PeekString()"
- "int PeekStringSize()"
- "bool ReadBoolean()"
- "byte ReadByte()"
- "bool ReadByte(ref byte)"
- "sbyte ReadSByte()"
- "byte ReadByte(int)"
- "System.Span`1<byte> ReadBytes(System.Span`1<byte>)"
- "byte[] ReadBytes(int)"
- "bool ReadBytes(int, ref byte[])"
- "bool TryReadBytes(System.Span`1<byte>)"
- "void ReadBytes(byte[], int, int)"
- "void ReadBits(System.Span`1<byte>, int)"
- "void ReadBits(byte[], int, int)"
- "short ReadInt16()"
- "ushort ReadUInt16()"
- "int ReadInt32()"
- "bool ReadInt32(ref int)"
- "int ReadInt32(int)"
- "uint ReadUInt32()"
- "bool ReadUInt32(ref uint)"
- "uint ReadUInt32(int)"
- "ulong ReadUInt64()"
- "long ReadInt64()"
- "ulong ReadUInt64(int)"
- "long ReadInt64(int)"
- "float ReadFloat()"
- "System.Half ReadHalf()"
- "float ReadSingle()"
- "bool ReadSingle(ref float)"
- "double ReadDouble()"
- "uint ReadVariableUInt32()"
- "bool ReadVariableUInt32(ref uint)"
- "int ReadVariableInt32()"
- "long ReadVariableInt64()"
- "ulong ReadVariableUInt64()"
- "float ReadSignedSingle(int)"
- "float ReadUnitSingle(int)"
- "float ReadRangedSingle(float, float, int)"
- "int ReadRangedInteger(int, int)"
- "long ReadRangedInteger(long, long)"
- "string ReadString()"
- "bool ReadString(ref string)"
- "double ReadTime(Lidgren.Network.NetConnection, bool)"
- "System.Net.IPEndPoint ReadIPEndPoint()"
- "void SkipPadBits()"
- "void ReadPadBits()"
- "void SkipPadBits(int)"
- "void EnsureBufferSize(int)"
- "void Write(bool)"
- "void Write(byte)"
- "void WriteAt(int, byte)"
- "void Write(sbyte)"
- "void Write(byte, int)"
- "void Write(byte[])"
- "void Write(System.ReadOnlySpan`1<byte>)"
- "void Write(byte[], int, int)"
- "void Write(ushort)"
- "void WriteAt(int, ushort)"
- "void Write(ushort, int)"
- "void Write(short)"
- "void WriteAt(int, short)"
- "void Write(int)"
- "void WriteAt(int, int)"
- "void Write(uint)"
- "void WriteAt(int, uint)"
- "void Write(uint, int)"
- "void Write(int, int)"
- "void Write(ulong)"
- "void WriteAt(int, ulong)"
- "void Write(ulong, int)"
- "void Write(long)"
- "void Write(long, int)"
- "void Write(System.Half)"
- "void Write(float)"
- "void Write(double)"
- "int WriteVariableUInt32(uint)"
- "int WriteVariableInt32(int)"
- "int WriteVariableInt64(long)"
- "int WriteVariableUInt64(ulong)"
- "void WriteSignedSingle(float, int)"
- "void WriteUnitSingle(float, int)"
- "void WriteRangedSingle(float, float, float, int)"
- "int WriteRangedInteger(int, int, int)"
- "int WriteRangedInteger(long, long, long)"
- "void Write(string)"
- "void Write(System.Net.IPEndPoint)"
- "void WriteTime(bool)"
- "void WriteTime(double, bool)"
- "void WritePadBits()"
- "void WritePadBits(int)"
- "void Write(Lidgren.Network.NetBuffer)"
- "void Zero(int)"
- "void .ctor()"
NetDeliveryMethod: { }
NetIncomingMessage:
All: True
Methods:
- "Lidgren.Network.NetIncomingMessageType get_MessageType()"
- "Lidgren.Network.NetDeliveryMethod get_DeliveryMethod()"
- "int get_SequenceChannel()"
- "System.Net.IPEndPoint get_SenderEndPoint()"
- "Lidgren.Network.NetConnection get_SenderConnection()"
- "double get_ReceiveTime()"
- "double ReadTime(bool)"
- "string ToString()"
NetOutgoingMessage:
All: True
Methods:
- "string ToString()"
Nett:
CommentLocation: { } # Enum
Toml:
@@ -320,9 +458,10 @@ Types:
ConcurrentStack`1: { All: True }
System.Collections:
BitArray: { All: True }
ICollection: { All: True }
IEnumerable: { All: True }
IEnumerator: { All: True }
IReadOnlyList`1: { All: True }
IList: { All: True }
System.ComponentModel:
CancelEventArgs: { All: True }
PropertyDescriptor: { }

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
@@ -9,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
@@ -118,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 />
@@ -135,11 +141,37 @@ namespace Robust.Shared.ContentPack
path = path.Directory;
var fullPath = GetFullPath(path);
Process.Start(new ProcessStartInfo
if (OperatingSystem.IsWindows())
{
UseShellExecute = true,
FileName = fullPath,
});
Process.Start(new ProcessStartInfo
{
FileName = $"{Environment.GetEnvironmentVariable("SystemRoot")}\\explorer.exe",
Arguments = ".",
WorkingDirectory = fullPath,
});
}
else if (OperatingSystem.IsMacOS())
{
Process.Start(new ProcessStartInfo
{
FileName = "open",
Arguments = ".",
WorkingDirectory = fullPath,
});
}
else if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD())
{
Process.Start(new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = ".",
WorkingDirectory = fullPath,
});
}
else
{
throw new NotSupportedException("Opening OS windows not supported on this OS");
}
}
#endregion
@@ -153,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

@@ -1,7 +1,6 @@
using System.Numerics;
using Robust.Shared.GameStates;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
@@ -41,6 +40,9 @@ namespace Robust.Shared.GameObjects
[ViewVariables(VVAccess.ReadWrite), DataField("zoom")]
public Vector2 Zoom = Vector2.One;
/// <summary>
/// Eye offset, relative to the map, and not affected by <see cref="Rotation"/>
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("offset"), AutoNetworkedField]
public Vector2 Offset;
@@ -52,9 +54,13 @@ namespace Robust.Shared.GameObjects
public int VisibilityMask = DefaultVisibilityMask;
/// <summary>
/// Overrides the PVS view range of this eye, Effectively a per-eye <see cref="CVars.NetMaxUpdateRange"/> cvar.
/// Scaling factor for the PVS view range of this eye. This effectively allows the
/// <see cref="CVars.NetMaxUpdateRange"/> and <see cref="CVars.NetPvsPriorityRange"/> cvars to be configured per
/// eye.
/// </summary>
[DataField] public float? PvsSize;
[Access(typeof(SharedEyeSystem))]
[DataField]
public float PvsScale = 1;
}
/// <summary>

View File

@@ -23,7 +23,6 @@ namespace Robust.Shared.GameObjects
public sealed partial class TransformComponent : Component, IComponentDebug
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
// Currently this field just exists for VV. In future, it might become a real field
[ViewVariables, PublicAPI]

View File

@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Prometheus;
using Robust.Shared.Console;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Log;
@@ -42,6 +43,7 @@ namespace Robust.Shared.GameObjects
[IoC.Dependency] private readonly ProfManager _prof = default!;
[IoC.Dependency] private readonly INetManager _netMan = default!;
[IoC.Dependency] private readonly IReflectionManager _reflection = default!;
[IoC.Dependency] private readonly EntityConsoleHost _entityConsoleHost = default!;
// I feel like PJB might shed me for putting a system dependency here, but its required for setting entity
// positions on spawn....
@@ -216,6 +218,7 @@ namespace Robust.Shared.GameObjects
TransformQuery = GetEntityQuery<TransformComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_actorQuery = GetEntityQuery<ActorComponent>();
_entityConsoleHost.Startup();
}
public virtual void Shutdown()
@@ -227,6 +230,7 @@ namespace Robust.Shared.GameObjects
ClearComponents();
ShuttingDown = false;
Started = false;
_entityConsoleHost.Shutdown();
}
public virtual void Cleanup()

View File

@@ -74,6 +74,7 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
[MustCallBase(true)]
public virtual void Initialize() { }
/// <inheritdoc />
@@ -81,12 +82,15 @@ namespace Robust.Shared.GameObjects
/// Not ran on the client if prediction is disabled and
/// <see cref="UpdatesOutsidePrediction"/> is false (the default).
/// </remarks>
[MustCallBase(true)]
public virtual void Update(float frameTime) { }
/// <inheritdoc />
[MustCallBase(true)]
public virtual void FrameUpdate(float frameTime) { }
/// <inheritdoc />
[MustCallBase(true)]
public virtual void Shutdown()
{
ShutdownSubscriptions();

View File

@@ -1,3 +1,4 @@
using System;
using System.Numerics;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -97,6 +98,23 @@ public abstract class SharedEyeSystem : EntitySystem
eyeComponent.Eye.Zoom = value;
}
public void SetPvsScale(Entity<EyeComponent?> eye, float scale)
{
if (!Resolve(eye.Owner, ref eye.Comp, false))
return;
// Prevent a admin or some other fuck-up from causing exception spam in PVS system due to divide-by-zero or
// other such issues
if (!float.IsFinite(scale))
{
Log.Error($"Attempted to set pvs scale to invalid value: {scale}. Eye: {ToPrettyString(eye)}");
SetPvsScale(eye, 1);
return;
}
eye.Comp.PvsScale = Math.Clamp(scale, 0.1f, 100f);
}
public void SetVisibilityMask(EntityUid uid, int value, EyeComponent? eyeComponent = null)
{
if (!Resolve(uid, ref eyeComponent))

View File

@@ -909,6 +909,26 @@ public abstract partial class SharedMapSystem
}
}
public IEnumerable<TileRef> GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Circle localCircle, bool ignoreEmpty = true,
Predicate<TileRef>? predicate = null)
{
var aabb = new Box2(localCircle.Position.X - localCircle.Radius, localCircle.Position.Y - localCircle.Radius,
localCircle.Position.X + localCircle.Radius, localCircle.Position.Y + localCircle.Radius);
var tileEnumerator = GetLocalTilesEnumerator(uid, grid, aabb, ignoreEmpty, predicate);
while (tileEnumerator.MoveNext(out var tile))
{
var tileCenter = tile.GridIndices + grid.TileSizeHalfVector;
var direction = tileCenter - localCircle.Position;
if (direction.IsShorterThanOrEqualTo(localCircle.Radius))
{
yield return tile;
}
}
}
public IEnumerable<TileRef> GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Circle worldArea, bool ignoreEmpty = true,
Predicate<TileRef>? predicate = null)
{

View File

@@ -31,6 +31,9 @@ namespace Robust.Shared.Graphics
set => _coords = value;
}
/// <summary>
/// Eye offset, relative to the map, and not affected by <see cref="Rotation"/>
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Offset { get; set; }

View File

@@ -248,6 +248,7 @@ namespace Robust.Shared.Map
/// </summary>
/// <param name="position">The vector to offset by local to the entity.</param>
/// <returns>Newly offset coordinates.</returns>
[Pure]
public EntityCoordinates Offset(Vector2 position)
{
return new(EntityId, Position + position);

View File

@@ -59,9 +59,16 @@ namespace Robust.Shared.Network.Messages
buffer.ReadAlignedMemory(finalStream, uncompressedLength);
}
serializer.DeserializeDirect(finalStream, out State);
try
{
serializer.DeserializeDirect(finalStream, out State);
}
finally
{
finalStream.Dispose();
}
State.PayloadSize = uncompressedLength;
finalStream.Dispose();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)

View File

@@ -9,7 +9,6 @@
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("OpenToolkit.GraphicsLibraryFramework")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Gives access to Castle(Moq)
[assembly: InternalsVisibleTo("Content.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Client.WebView")]
[assembly: InternalsVisibleTo("Robust.Packaging")]

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using JetBrains.Annotations;
using NetSerializer;
namespace Robust.Shared.Serialization;
/// <summary>
/// Custom serializer implementation for <see cref="BitArray"/>.
/// </summary>
/// <remarks>
/// <para>
/// This type is necessary as, since .NET 10, the internal layout of <see cref="BitArray"/> was changed.
/// The type now (internally) implements <see cref="ISerializable"/> for backwards compatibility with existing
/// <c>BinaryFormatter</c> code, but NetSerializer does not support <see cref="ISerializable"/>.
/// </para>
/// <para>
/// This code is designed to be backportable &amp; network compatible with the previous behavior on .NET 9.
/// </para>
/// </remarks>
internal sealed class NetBitArraySerializer : IStaticTypeSerializer
{
// For reference, the layout of BitArray before .NET 10 was:
// private int[] m_array;
// private int m_length;
// private int _version;
// NetSerializer serialized these in the following order (sorted by name):
// _version, m_array, m_length
public bool Handles(Type type)
{
return type == typeof(BitArray);
}
public IEnumerable<Type> GetSubtypes(Type type)
{
return [typeof(int[]), typeof(int)];
}
public MethodInfo GetStaticWriter(Type type)
{
return typeof(NetBitArraySerializer).GetMethod("Write", BindingFlags.Static | BindingFlags.NonPublic)!;
}
public MethodInfo GetStaticReader(Type type)
{
return typeof(NetBitArraySerializer).GetMethod("Read", BindingFlags.Static | BindingFlags.NonPublic)!;
}
[UsedImplicitly]
private static void Write(Serializer serializer, Stream stream, BitArray value)
{
var intCount = (31 + value.Length) >> 5;
var ints = new int[intCount];
value.CopyTo(ints, 0);
serializer.SerializeDirect(stream, 0); // _version
serializer.SerializeDirect(stream, ints); // m_array
serializer.SerializeDirect(stream, value.Length); // m_length
}
[UsedImplicitly]
private static void Read(Serializer serializer, Stream stream, out BitArray value)
{
serializer.DeserializeDirect<int>(stream, out _); // _version
serializer.DeserializeDirect<int[]>(stream, out var array); // m_array
serializer.DeserializeDirect<int>(stream, out var length); // m_length
value = new BitArray(array)
{
Length = length
};
}
}

View File

@@ -91,6 +91,7 @@ namespace Robust.Shared.Serialization
MappedStringSerializer.TypeSerializer,
new Vector2Serializer(),
new Matrix3x2Serializer(),
new NetBitArraySerializer()
}
};
_serializer = new Serializer(types, settings);

View File

@@ -1,5 +1,6 @@
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
@@ -50,6 +51,7 @@ namespace Robust.Shared
deps.Register<ToolshedManager>();
deps.Register<HttpClientHolder>();
deps.Register<RobustMemoryManager>();
deps.Register<EntityConsoleHost>();
}
}
}

View File

@@ -232,6 +232,7 @@ namespace Robust.UnitTesting.Server
container.Register<IParallelManagerInternal, TestingParallelManager>();
// Needed for grid fixture debugging.
container.Register<IConGroupController, ConGroupController>();
container.Register<EntityConsoleHost>();
// I just wanted to load pvs system
container.Register<IServerEntityManager, ServerEntityManager>();

View File

@@ -44,7 +44,7 @@ public sealed partial class VisibilityTest : RobustIntegrationTest
metaComp[i] = server.EntMan.GetComponent<MetaDataComponent>(ent);
visComp[i] = server.EntMan.AddComponent<VisibilityComponent>(ent);
vis.AddLayer(ent, visComp[i], 1 << i);
vis.AddLayer((ent, visComp[i]), (ushort)(1 << i));
if (i > 0)
xforms.SetParent(ent, ents[i - 1]);
}
@@ -62,7 +62,7 @@ public sealed partial class VisibilityTest : RobustIntegrationTest
// Adding a layer to the root entity's mask will apply it to all children
var extraMask = 1 << (N + 1);
mask = RequiredMask | extraMask;
vis.AddLayer(ents[0], visComp[0], extraMask);
vis.AddLayer((ents[0], visComp[0]), (ushort)extraMask);
for (int i = 0; i < N; i++)
{
mask |= 1 << i;
@@ -71,7 +71,7 @@ public sealed partial class VisibilityTest : RobustIntegrationTest
}
// Removing the removes it from all children.
vis.RemoveLayer(ents[0], visComp[0], extraMask);
vis.RemoveLayer((ents[0], visComp[0]), (ushort)extraMask);
mask = RequiredMask;
for (int i = 0; i < N; i++)
{
@@ -101,7 +101,7 @@ public sealed partial class VisibilityTest : RobustIntegrationTest
}
// Re-attaching the entity also updates the masks.
await server.WaitPost(() => xforms.SetParent(ents[split], ents[split-1]));
await server.WaitPost(() => xforms.SetParent(ents[split], ents[split - 1]));
mask = RequiredMask;
for (int i = 0; i < N; i++)
{
@@ -111,7 +111,7 @@ public sealed partial class VisibilityTest : RobustIntegrationTest
}
// Setting a mask on a child does not propagate upwards, only downwards
vis.AddLayer(ents[split], visComp[split], extraMask);
vis.AddLayer((ents[split], visComp[split]), (ushort)extraMask);
mask = RequiredMask;
for (int i = 0; i < split; i++)
{

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]