Compare commits

...

40 Commits

Author SHA1 Message Date
PJB3005
bb8fab82d7 Version: 253.0.2 2025-09-19 09:17:31 +02:00
Skye
4930c97819 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:31 +02:00
PJB3005
7122cf90e3 Version: 253.0.1 2025-09-14 14:55:56 +02:00
PJB3005
84d0110477 Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

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

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

    Move CEF cache out of data directory

    Don't want content messing with this...

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

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

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

    Update SpaceWizards.NFluidSynth to 0.2.2

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

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
2025-09-14 14:55:55 +02:00
metalgearsloth
6f28c396cf Version: 253.0.0 2025-04-14 14:12:32 +10:00
metalgearsloth
b631f408f2 ItemList optimisation (#5796)
- VV AddComponent window no longer takes 300ms every time you press a key.
- Significantly optimise ItemList internally.
2025-04-14 13:55:48 +10:00
Leon Friedrich
34f4cf9452 More map validator fixes (#5820) 2025-04-14 13:55:02 +10:00
B_Kirill
6a4e4cf3b4 Cleanup warnings: Commands (#5825) 2025-04-14 13:54:42 +10:00
metalgearsloth
8aefa5c53e Make TestPoint use generics (#5828) 2025-04-14 01:49:07 +10:00
DrSmugleaf
2cfc981aa3 Fix popup text overflowing the sides of the screen (#5788)
* Fix popup text overflowing the sides of the screen

* Fix not escaping text
2025-04-13 12:59:23 +02:00
Leon Friedrich
4987c324d9 Fix bad debug assert (#5826) 2025-04-13 20:50:46 +10:00
DrSmugleaf
5450ddd0ba Fix SharedJointSystem.CreateDistanceJoint taking in an int for minimumDistance instead of a float (#5824) 2025-04-13 16:22:22 +10:00
Tayrtahn
378a10678c Improve location reporting for non-writeable DataFields (#5715)
* Better location reporting for readonly DataField errors

* Better location reporting for readonly DataField property errors

* Use SyntaxKind instead of string for TryGetModifierLocation
2025-04-13 16:22:05 +10:00
Leon Friedrich
2e0735b92f Fix NRE in screen-space overlays (#5823)
* Fix pre-init screen-space overlays

* Debug asserts
2025-04-13 16:21:02 +10:00
Leon Friedrich
5756d15333 Make BoundUserInterfaceMessageAttempt broadcast again (#5821) 2025-04-12 18:22:13 +10:00
Leon Friedrich
b6f74b8dea Fix logMissing in EntitySystem.Resolve() (#5822) 2025-04-12 17:40:08 +10:00
Leon Friedrich
3800c5707e Add new SerializationManager.PushComposition overload (#5785) 2025-04-12 12:58:40 +10:00
Leon Friedrich
8f49785b4e Fix RemCompDeferred not always setting lifestage (#5786)
* Fix RemCompDeferred not setting lifestage

* spaelling
2025-04-12 12:57:11 +10:00
metalgearsloth
f274de0f10 Version: 252.0.0 2025-04-12 01:43:29 +10:00
Milon
e128338f9d hi (#5811) 2025-04-12 01:29:41 +10:00
metalgearsloth
588c46273e Version: 251.0.0 2025-04-10 20:53:09 +10:00
Whatstone
919de8ce0e SharedPhysicsSystem.Island: set position after velocity (#5801) 2025-04-09 01:10:17 +10:00
Errant
af27d2d872 equatable FormattedMessage, take 2 (#5780)
* improved Equals and getHashCode

* less jank
2025-04-08 16:50:08 +02:00
Tayrtahn
45bb8740a0 Add ForbidLiteralAttribute and analyzer (#5808)
* Add ForbidLiteral attribute, analyzer, and test

* Removed unused code

* Switch order of methods. It's better this way.
2025-04-08 16:39:38 +02:00
Tobias Berger
4a24539629 Don't implement GetMassData twice (#5816)
The removed body didn't calculate mass for circles
2025-04-09 00:14:45 +10:00
PJB3005
7536c4ec68 Log late MsgEntity again
Yay :)
2025-04-07 15:50:40 +02:00
metalgearsloth
9268c8629d Refactor TileEdgeOverlay (#5295)
* Refactor TileEdgeOverlay

* weh

* Updates

* exweh

* Stupid weh noises

* I am le stupid

* Add logging about tile atlas build time

Seems fine, but wanted to check.

* Don't over-allocate edge tile region array

* Add DirectionExtensions.AllDirections

* Clean up iteration in ClydeTileDefinitionManager

* Don't stackalloc large chunk edge buffers, other code cleanup.

* More release notes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-04-03 07:52:28 +02:00
PJB3005
0bc0cafe64 Show entity name in "physics shapeinfo" output 2025-04-03 02:59:09 +02:00
PJB3005
8891f3fa0a Make EntitySystem.Subscriptions.SubscribeLocalEvent not require EntityEventArgs
This means it can be used with struct events.
2025-04-03 02:58:20 +02:00
Tornado Tech
4f96c2d233 Added separate localization & clean up (#5227)
* Added separate localization & clean up

* Added new methods docs

* Added GetFoundCultures method

* Clean up code

* Removed some formating shit

* Do better CultureInfo comparison

* Oops

* Review

* Command fixes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-04-02 16:41:02 +02:00
PJB3005
ab55d5b2f2 Revert "Add Loc property to LocalizedCommands"
This reverts commit 806c5b694b.
2025-04-02 05:23:43 +02:00
PJB3005
806c5b694b Add Loc property to LocalizedCommands
Avoids some ~300 usages of static Loc in SS14 and RT
2025-04-02 05:21:43 +02:00
metalgearsloth
6898053dbd Add autocomplete to tp command (#5795) 2025-04-02 03:48:16 +02:00
metalgearsloth
ae625ebad8 Inline manifold points (#5794)
* Inline manifold points

Max is 2 so no reason to use an array over a fixedarray.

* this
2025-04-02 00:04:51 +11:00
metalgearsloth
3c754a4f49 Don't disable contacting collisionwake ents (#5798)
Good for some content stuff I don't think it caused issues.
2025-04-01 15:04:05 +11:00
Tayrtahn
d84cb6327c Fix SharedTransformSystem methods erroring on failed Resolves (#5787)
* Don't error when GetGrid fails

* Fix other Resolves in SharedTransformSystem
2025-04-01 04:59:47 +11:00
Ciarán Walsh
4bfd92dbc5 Add button to jump to live chat when scrolled up (#5750)
* Add button to jump to live chat when scrolled up

* Expose scroll button class name as a public constant

* Add localisation string to engine

* Make enabling the OutputPanel scroll button opt-in

* Enable scroll button for the debug console

* De-duplicate visibility logic

* Update scroll button visibility when the enabling property is changed
2025-03-30 03:07:54 +02:00
metalgearsloth
c7d228c223 savemap / savegrid autocomplete (#5784)
How mappers coping without this.
2025-03-27 18:54:13 +11:00
Tayrtahn
f244c94905 Switch from checking comp.Owner == ent to GetComp(ent) == comp (#5776) 2025-03-27 15:21:05 +11:00
Tayrtahn
01cac6465b Cleanup warnings in EntityCoordinates_Tests (#5771)
* Fix warnings

* Use transform refs to simplify WithEntityId
2025-03-27 15:20:20 +11:00
81 changed files with 1826 additions and 594 deletions

View File

@@ -57,7 +57,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

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

View File

@@ -54,6 +54,77 @@ END TEMPLATE-->
*None yet*
## 253.0.2
## 253.0.1
## 253.0.0
### New features
* Add a new `SerializationManager.PushComposition()` overload that takes in a single parent instead of an array of parents.
* `BoundUserInterfaceMessageAttempt` once again gets raised as a broadcast event, in addition to being directed.
* This effectively reverts the breaking part of the changes made in v252.0.0
* Fix CreateDistanceJoint using an int instead of a float for minimum distance.
### Bugfixes
* Fix deferred component removal not setting the component's life stage to `ComponentLifeStage.Stopped` if the component has not yet been initialised.
* Fix some `EntitySystem.Resolve()` overloads not respecting the optional `logMissing` argument.
* Fix screen-space overlays not being useable without first initializing/starting entity manager & systems
* ItemList is now significantly optimized. VV's `AddComponent` window in particular should be much faster.
* Fix some more MapValidator fields.
* Fix popup text overflowing the sides of the screen.
* Improve location reporting for non-writeable datafields via analyzer.
### Other
* TestPoint now uses generics rather than IPhysShape directly.
## 252.0.0
### Breaking changes
* BoundUserInterfaceMessageAttempt is raised directed against entities and no longer broadcast.
## 251.0.0
### Breaking changes
* Localization is now separate between client and server and is handled via cvar.
* Contacting entities no longer can be disabled for CollisionWake to avoid destroying the contacts unnecessarily.
### New features
* Added `DirectionExtensions.AllDirections`, which contains a list of all `Direction`s for easy enumeration.
* Add ForbidLiteralAttribute.
* Log late MsgEntity again.
* Show entity name in `physics shapeinfo` output.
* Make SubscribeLocalEvent not require EntityEventArgs.
* Add autocomplete to `tp` command.
* Add button to jump to live chat when scrolled up.
* Add autocomplete to `savemap` and `savegrid`.
### Bugfixes
* Fix velocity not re-applying correctly on re-parenting.
* Fix Equatable on FormattedMessage.
* Fix SharedTransformSystem methods logging errors on resolves.
### Other
* Significantly optimized tile edge rendering.
### Internal
* Remove duplicate GetMassData method.
* Inline manifold points for physics.
## 250.0.0
### Breaking changes

View File

@@ -11,6 +11,7 @@ cmd-parse-failure-uid = {$arg} is not a valid entity UID.
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
cmd-parse-failure-enum = {$arg} is not a {$enum} Enum.
cmd-parse-failure-grid = {$arg} is not a valid grid.
cmd-parse-failure-cultureinfo = "{$arg}" is not valid CultureInfo.
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
cmd-parse-failure-session = There is no session with username: {$username}
@@ -572,3 +573,8 @@ cmd-pvs-override-info-desc = Prints information about any PVS overrides associat
cmd-pvs-override-info-empty = Entity {$nuid} has no PVS overrides.
cmd-pvs-override-info-global = Entity {$nuid} has a global override.
cmd-pvs-override-info-clients = Entity {$nuid} has a session override for {$clients}.
cmd-localization_set_culture-desc = Set DefaultCulture for the client LocalizationManager
cmd-localization_set_culture-help = Usage: localization_set_culture <cultureName>
cmd-localization_set_culture-culture-name = <cultureName>
cmd-localization_set_culture-changed = Localization changed to { $code } ({ $nativeName } / { $englishName })

View File

@@ -14,6 +14,10 @@ tile-spawn-window-title = Place Tiles
console-line-edit-placeholder = Command Here
## OutputPanel
output-panel-scroll-down-button-text = Scroll Down
## Common Used
window-erase-button-text = Erase Mode

View File

@@ -87,4 +87,66 @@ public sealed class DataDefinitionAnalyzerTest
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(35, 17, 35, 50).WithArguments("Bad", "Foo")
);
}
[Test]
public async Task ReadOnlyFieldTest()
{
const string code = """
using System;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
}
[DataDefinition]
public sealed partial class Foo
{
[DataField]
public readonly int Bad;
[DataField]
public int Good;
}
""";
await Verifier(code,
// /0/Test0.cs(15,12): error RA0019: Data field Bad in data definition Foo is readonly
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(15, 12, 15, 20).WithArguments("Bad", "Foo")
);
}
[Test]
public async Task ReadOnlyPropertyTest()
{
const string code = """
using System;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
}
[DataDefinition]
public sealed partial class Foo
{
[DataField]
public int Bad { get; }
[DataField]
public int Good { get; private set; }
}
""";
await Verifier(code,
// /0/Test0.cs(15,20): error RA0020: Data field property Bad in data definition Foo does not have a setter
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldPropertyWritableRule).WithSpan(15, 20, 15, 28).WithArguments("Bad", "Foo")
);
}
}

View File

@@ -0,0 +1,189 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ForbidLiteralAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class ForbidLiteralAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<ForbidLiteralAnalyzer, DefaultVerifier>()
{
TestState =
{
Sources = { code },
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.ForbidLiteralAttribute.cs"
);
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
private const string TestTypeDefs = """
using System.Collections.Generic;
using Robust.Shared.Analyzers;
public sealed class TestClass
{
public static void OneParameterForbidden([ForbidLiteral] string value) { }
public static void TwoParametersFirstForbidden([ForbidLiteral] string first, string second) { }
public static void TwoParametersBothForbidden([ForbidLiteral] string first, [ForbidLiteral] string second) { }
public static void ListParameterForbidden([ForbidLiteral] List<string> values) { }
public static void ParamsListParameterForbidden([ForbidLiteral] params List<string> values) { }
}
public record struct StringWrapper(string value)
{
private readonly string _value = value;
public static implicit operator string(StringWrapper wrapper)
{
return wrapper._value;
}
}
""";
[Test]
public async Task TestOneParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.OneParameterForbidden(_constValue);
TestClass.OneParameterForbidden(StaticValue);
TestClass.OneParameterForbidden(WrappedValue);
TestClass.OneParameterForbidden("baz");
}
}
""";
await Verifier(code,
// /0/Test0.cs(12,41): error RA0033: The "value" parameter of OneParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(12, 41, 12, 46).WithArguments("value", "OneParameterForbidden")
);
}
[Test]
public async Task TestTwoParametersFirstForbidden()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
public void Test()
{
TestClass.TwoParametersFirstForbidden(_constValue, "whatever");
TestClass.TwoParametersFirstForbidden(_constValue, _constValue);
TestClass.TwoParametersFirstForbidden("foo", "whatever");
}
}
""";
await Verifier(code,
// /0/Test0.cs(9,47): error RA0033: The "first" parameter of TwoParametersFirstForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(9, 47, 9, 52).WithArguments("first", "TwoParametersFirstForbidden")
);
}
[Test]
public async Task TestTwoParametersBothForbidden()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
public void Test()
{
TestClass.TwoParametersBothForbidden(_constValue, _constValue);
TestClass.TwoParametersBothForbidden(_constValue, StaticValue);
TestClass.TwoParametersBothForbidden(_constValue, "whatever");
TestClass.TwoParametersBothForbidden("whatever", _constValue);
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,59): error RA0033: The "second" parameter of TwoParametersBothForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 59, 10, 69).WithArguments("second", "TwoParametersBothForbidden"),
// /0/Test0.cs(11,46): error RA0033: The "first" parameter of TwoParametersBothForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(11, 46, 11, 56).WithArguments("first", "TwoParametersBothForbidden")
);
}
[Test]
public async Task TestListParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.ListParameterForbidden([_constValue, StaticValue, WrappedValue]);
TestClass.ListParameterForbidden(["foo", _constValue, "bar"]);
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,43): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 43, 10, 48).WithArguments("values", "ListParameterForbidden"),
// /0/Test0.cs(10,63): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 63, 10, 68).WithArguments("values", "ListParameterForbidden")
);
}
[Test]
public async Task TestParamsListParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.ParamsListParameterForbidden(_constValue, StaticValue, WrappedValue);
TestClass.ParamsListParameterForbidden("foo", _constValue, "bar");
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,48): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 48, 10, 53).WithArguments("values", "ParamsListParameterForbidden"),
// /0/Test0.cs(10,68): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 68, 10, 73).WithArguments("values", "ParamsListParameterForbidden")
);
}
}

View File

@@ -13,6 +13,7 @@
<EmbeddedResource Include="..\Robust.Shared\Analyzers\MustCallBaseAttribute.cs" LogicalName="Robust.Shared.IoC.MustCallBaseAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferOtherTypeAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ForbidLiteralAttribute.cs" LogicalName="Robust.Shared.Analyzers.ForbidLiteralAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\GameObjects\EventBusAttributes.cs" LogicalName="Robust.Shared.GameObjects.EventBusAttributes.cs" LinkBase="Implementations" />
</ItemGroup>

View File

@@ -41,7 +41,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to mark any type containing a nested data definition as partial."
);
private static readonly DiagnosticDescriptor DataFieldWritableRule = new(
public static readonly DiagnosticDescriptor DataFieldWritableRule = new(
Diagnostics.IdDataFieldWritable,
"Data field must not be readonly",
"Data field {0} in data definition {1} is readonly",
@@ -51,7 +51,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to remove the readonly modifier."
);
private static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
public static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
Diagnostics.IdDataFieldPropertyWritable,
"Data field property must have a setter",
"Data field property {0} in data definition {1} does not have a setter",
@@ -149,7 +149,8 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (IsReadOnlyDataField(type, fieldSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, context.Node.GetLocation(), fieldSymbol.Name, type.Name));
TryGetModifierLocation(field, SyntaxKind.ReadOnlyKeyword, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, location, fieldSymbol.Name, type.Name));
}
if (HasRedundantTag(fieldSymbol))
@@ -185,7 +186,8 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (IsReadOnlyDataField(type, propertySymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, context.Node.GetLocation(), propertySymbol.Name, type.Name));
var location = property.AccessorList != null ? property.AccessorList.GetLocation() : property.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, location, propertySymbol.Name, type.Name));
}
if (HasRedundantTag(propertySymbol))
@@ -285,6 +287,20 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return false;
}
private static bool TryGetModifierLocation(MemberDeclarationSyntax syntax, SyntaxKind modifierKind, out Location location)
{
foreach (var modifier in syntax.Modifiers)
{
if (modifier.IsKind(modifierKind))
{
location = modifier.GetLocation();
return true;
}
}
location = syntax.GetLocation();
return false;
}
private static bool IsReadOnlyMember(ITypeSymbol type, ISymbol member)
{
if (member is IFieldSymbol field)

View File

@@ -0,0 +1,101 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ForbidLiteralAnalyzer : DiagnosticAnalyzer
{
private const string ForbidLiteralType = "Robust.Shared.Analyzers.ForbidLiteralAttribute";
public static DiagnosticDescriptor ForbidLiteralRule = new(
Diagnostics.IdForbidLiteral,
"Parameter forbids literal values",
"The {0} parameter of {1} forbids literal values",
"Usage",
DiagnosticSeverity.Warning,
true,
"Pass in a validated wrapper type like ProtoId, or a const or static value."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [ForbidLiteralRule];
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation);
}
private void AnalyzeOperation(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation invocationOperation)
return;
// Check each parameter of the method invocation
foreach (var argumentOperation in invocationOperation.Arguments)
{
// Check for our attribute on the parameter
if (!AttributeHelper.HasAttribute(argumentOperation.Parameter, ForbidLiteralType, out _))
continue;
// Handle parameters using the params keyword
if (argumentOperation.Syntax is InvocationExpressionSyntax subExpressionSyntax)
{
// Check each param value
foreach (var subArgument in subExpressionSyntax.ArgumentList.Arguments)
{
CheckArgumentSyntax(context, argumentOperation, subArgument);
}
continue;
}
// Not params, so just check the single parameter
if (argumentOperation.Syntax is not ArgumentSyntax argumentSyntax)
continue;
CheckArgumentSyntax(context, argumentOperation, argumentSyntax);
}
}
private void CheckArgumentSyntax(OperationAnalysisContext context, IArgumentOperation operation, ArgumentSyntax argumentSyntax)
{
// Handle collection types
if (argumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax)
{
// Check each value of the collection
foreach (var elementSyntax in collectionExpressionSyntax.Elements)
{
if (elementSyntax is not ExpressionElementSyntax expressionSyntax)
continue;
// Check if a literal was passed in
if (expressionSyntax.Expression is not LiteralExpressionSyntax)
continue;
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
expressionSyntax.GetLocation(),
operation.Parameter.Name,
(context.Operation as IInvocationOperation).TargetMethod.Name
));
}
return;
}
// Not a collection, just a single value to check
// Check if it's a literal
if (argumentSyntax.Expression is not LiteralExpressionSyntax)
return;
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
argumentSyntax.GetLocation(),
operation.Parameter.Name,
(context.Operation as IInvocationOperation).TargetMethod.Name
));
}
}

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

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.Localization;
namespace Robust.Client.Console.Commands;
[UsedImplicitly]
internal sealed class LocalizationSetCulture : LocalizedCommands
{
private const string Name = "localization_set_culture";
private const int ArgumentCount = 1;
public override string Command => Name;
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != ArgumentCount)
{
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
return;
}
CultureInfo culture;
try
{
culture = CultureInfo.GetCultureInfo(args[0], predefinedOnly: false);
}
catch (CultureNotFoundException)
{
shell.WriteError(Loc.GetString("cmd-parse-failure-cultureinfo", ("arg", args[0])));
return;
}
LocalizationManager.SetCulture(culture);
shell.WriteLine(LocalizationManager.GetString("cmd-localization_set_culture-changed",
("code", culture.Name),
("nativeName", culture.NativeName),
("englishName", culture.EnglishName)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHintOptions(GetCultureNames(),
LocalizationManager.GetString("cmd-localization_set_culture-culture-name")),
_ => CompletionResult.Empty
};
}
private static HashSet<string> GetCultureNames()
{
var cultureInfos = CultureInfo.GetCultures(CultureTypes.AllCultures)
.Where(x => !string.IsNullOrEmpty(x.Name))
.ToArray();
var allNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
allNames.UnionWith(cultureInfos.Select(x => x.TwoLetterISOLanguageName));
allNames.UnionWith(cultureInfos.Select(x => x.Name));
return allNames;
}
}

View File

@@ -413,8 +413,9 @@ namespace Robust.Client.Debugging
}
var body = bodyEnt.Comp;
var meta = _entityManager.GetComponent<MetaDataComponent>(bodyEnt);
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner}");
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner} ({meta.EntityName})");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;

View File

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

@@ -9,10 +9,6 @@ namespace Robust.Client.GameObjects;
public sealed class MapSystem : SharedMapSystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
protected override MapId GetNextMapId()
{
// Client-side map entities use negative map Ids to avoid conflict with server-side maps.
@@ -23,16 +19,4 @@ public sealed class MapSystem : SharedMapSystem
}
return id;
}
public override void Initialize()
{
base.Initialize();
_overlayManager.AddOverlay(new TileEdgeOverlay(EntityManager, _resource, _tileDefinitionManager));
}
public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
@@ -22,6 +23,9 @@ namespace Robust.Client.Graphics.Clyde
/// </summary>
private HashSet<Type> _erroredGridOverlays = new();
private Vertex2D[]? _chunkMeshBuilderVertexBuffer;
private ushort[]? _chunkMeshBuilderIndexBuffer;
private int _verticesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * 4;
private int _indicesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
@@ -63,29 +67,78 @@ namespace Robust.Client.Graphics.Clyde
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
}
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid);
gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(transform));
gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(mapGrid));
var enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
// Handle base texture updates.
while (enumerator.MoveNext(out var chunk))
{
DebugTools.Assert(chunk.FilledTiles > 0);
if (!data.TryGetValue(chunk.Indices, out MapChunkData? datum))
data[chunk.Indices] = datum = _initChunkBuffers(mapGrid, chunk);
var datum = EnsureChunkInitialized(data, chunk, mapGrid);
if (datum.Dirty)
_updateChunkMesh(mapGrid, chunk, datum);
DebugTools.Assert(datum.TileCount > 0);
if (datum.TileCount == 0)
if (!datum.Dirty)
continue;
BindVertexArray(datum.VAO);
CheckGlError();
_updateChunkMesh(mapGrid, chunk, datum);
// Dirty edge tiles for next step.
datum.EdgeDirty = true;
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
var neighbor = chunk.Indices + new Vector2i(x, y);
if (!mapGrid.Comp.Chunks.TryGetValue(neighbor, out var neighborChunk))
continue;
var neighborDatum = EnsureChunkInitialized(data, neighborChunk, mapGrid);
neighborDatum.EdgeDirty = true;
}
}
}
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
// Handle edge sprites.
while (enumerator.MoveNext(out var chunk))
{
var datum = data[chunk.Indices];
if (!datum.EdgeDirty)
continue;
_updateChunkEdges(mapGrid, chunk, datum);
}
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
// Draw chunks
while (enumerator.MoveNext(out var chunk))
{
var datum = data[chunk.Indices];
DebugTools.Assert(datum.TileCount > 0);
if (datum.TileCount > 0)
{
BindVertexArray(datum.VAO);
CheckGlError();
_debugStats.LastGLDrawCalls += 1;
GL.DrawElements(GetQuadGLPrimitiveType(), datum.TileCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
}
if (datum.EdgeCount > 0)
{
BindVertexArray(datum.EdgeVAO);
CheckGlError();
_debugStats.LastGLDrawCalls += 1;
GL.DrawElements(GetQuadGLPrimitiveType(), datum.EdgeCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
}
_debugStats.LastGLDrawCalls += 1;
GL.DrawElements(GetQuadGLPrimitiveType(), datum.TileCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
}
requiresFlush = false;
@@ -117,6 +170,17 @@ namespace Robust.Client.Graphics.Clyde
CullEmptyChunks();
}
private MapChunkData EnsureChunkInitialized(Dictionary<Vector2i, MapChunkData> data, MapChunk chunk, Entity<MapGridComponent> mapGrid)
{
if (!data.TryGetValue(chunk.Indices, out var datum))
{
data[chunk.Indices] = datum = new MapChunkData();
_initChunkBuffers(mapGrid, chunk, datum);
}
return datum;
}
private void CullEmptyChunks()
{
foreach (var (grid, chunks) in _mapChunkData)
@@ -138,66 +202,141 @@ namespace Robust.Client.Graphics.Clyde
private void _updateChunkMesh(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
Span<ushort> indexBuffer = stackalloc ushort[_indicesPerChunk(chunk)];
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
Span<ushort> indexBuffer = EnsureSize(ref _chunkMeshBuilderIndexBuffer, _indicesPerChunk(chunk));
Span<Vertex2D> vertexBuffer = EnsureSize(ref _chunkMeshBuilderVertexBuffer, _verticesPerChunk(chunk));
var i = 0;
var cSz = grid.Comp.ChunkSize;
var cScaled = chunk.Indices * cSz;
for (ushort x = 0; x < cSz; x++)
var chunkSize = grid.Comp.ChunkSize;
var chunkOriginScaled = chunk.Indices * chunkSize;
for (ushort x = 0; x < chunkSize; x++)
{
for (ushort y = 0; y < cSz; y++)
for (ushort y = 0; y < chunkSize; y++)
{
var gridX = x + chunkOriginScaled.X;
var gridY = y + chunkOriginScaled.Y;
var tile = chunk.GetTile(x, y);
if (tile.IsEmpty)
continue;
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile);
Box2 region;
if (regionMaybe == null || regionMaybe.Length <= tile.Variant)
// Tile render
if (x != chunkSize && y != chunkSize)
{
region = _tileDefinitionManager.ErrorTileRegion;
}
else
{
region = regionMaybe[tile.Variant];
}
// ReSharper disable once IntVariableOverflowInUncheckedContext
if (tile.IsEmpty)
continue;
var gx = x + cScaled.X;
var gy = y + cScaled.Y;
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile);
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(gx, gy, region.Left, region.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gx + 1, gy, region.Right, region.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gx + 1, gy + 1, region.Right, region.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gx, gy + 1, region.Left, region.Top, Color.White);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort)(i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
i += 1;
Box2 region;
if (regionMaybe == null || regionMaybe.Length <= tile.Variant)
{
region = _tileDefinitionManager.ErrorTileRegion;
}
else
{
region = regionMaybe[tile.Variant];
}
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
i += 1;
}
}
}
var indexSlice = indexBuffer[..(i * GetQuadBatchIndexCount())];
var vertSlice = vertexBuffer[..(i * 4)];
GL.BindVertexArray(datum.VAO);
CheckGlError();
datum.EBO.Use();
datum.VBO.Use();
datum.EBO.Reallocate(indexBuffer[..(i * GetQuadBatchIndexCount())]);
datum.VBO.Reallocate(vertexBuffer[..(i * 4)]);
datum.Dirty = false;
datum.EBO.Reallocate(indexSlice);
datum.VBO.Reallocate(vertSlice);
datum.TileCount = i;
datum.Dirty = false;
}
private unsafe MapChunkData _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk)
private void _updateChunkEdges(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
// Need a buffer that can potentially store all neighbor tiles
Span<ushort> indexBuffer = EnsureSize(ref _chunkMeshBuilderIndexBuffer, _indicesPerChunk(chunk) * 8);
Span<Vertex2D> vertexBuffer = EnsureSize(ref _chunkMeshBuilderVertexBuffer, _verticesPerChunk(chunk) * 8);
var i = 0;
var chunkSize = grid.Comp.ChunkSize;
var chunkOriginScaled = chunk.Indices * chunkSize;
var maps = _entityManager.System<SharedMapSystem>();
for (ushort x = 0; x < chunkSize; x++)
{
for (ushort y = 0; y < chunkSize; y++)
{
var gridX = x + chunkOriginScaled.X;
var gridY = y + chunkOriginScaled.Y;
var tile = chunk.GetTile(x, y);
var tileDef = _tileDefinitionManager[tile.TypeId];
// Edge render
for (var nx = -1; nx <= 1; nx++)
{
for (var ny = -1; ny <= 1; ny++)
{
if (nx == 0 && ny == 0)
continue;
var neighborIndices = new Vector2i(gridX + nx, gridY + ny);
if (!maps.TryGetTile(grid.Comp, neighborIndices, out var neighborTile))
continue;
var neighborDef = _tileDefinitionManager[neighborTile.TypeId];
// If it's the same tile then no edge to be drawn.
if (tile.TypeId == neighborTile.TypeId || neighborDef.EdgeSprites.Count == 0)
continue;
// If neighbor is a lower priority then us then don't draw on our tile.
if (neighborDef.EdgeSpritePriority < tileDef.EdgeSpritePriority)
continue;
var direction = new Vector2i(nx, ny).AsDirection().GetOpposite();
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(neighborTile.TypeId, direction);
if (regionMaybe == null)
continue;
var region = regionMaybe[0];
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
i += 1;
}
}
}
}
// We don't save the edge buffers back because we might need to re-use it if a neighbor chunk updates.
var indexSlice = indexBuffer[..(i * GetQuadBatchIndexCount())];
var vertSlice = vertexBuffer[..(i * 4)];
GL.BindVertexArray(datum.EdgeVAO);
CheckGlError();
datum.EdgeEBO.Use();
datum.EdgeVBO.Use();
datum.EdgeEBO.Reallocate(indexSlice);
datum.EdgeVBO.Reallocate(vertSlice);
datum.EdgeCount = i;
datum.EdgeDirty = false;
}
private unsafe void _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
var vboSize = _verticesPerChunk(chunk) * sizeof(Vertex2D);
var eboSize = _indicesPerChunk(chunk) * sizeof(ushort);
// Base VAO
var vao = GenVertexArray();
BindVertexArray(vao);
CheckGlError();
var vboSize = _verticesPerChunk(chunk) * sizeof(Vertex2D);
var eboSize = _indicesPerChunk(chunk) * sizeof(ushort);
var vbo = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
vboSize, $"Grid {grid.Owner} chunk {chunk.Indices} VBO");
var ebo = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw,
@@ -212,12 +351,30 @@ namespace Robust.Client.Graphics.Clyde
vbo.Use();
ebo.Use();
var datum = new MapChunkData(vao, vbo, ebo)
{
Dirty = true
};
datum.EBO = ebo;
datum.VBO = vbo;
datum.VAO = vao;
return datum;
// EdgeVAO
var edgeVao = GenVertexArray();
BindVertexArray(edgeVao);
CheckGlError();
var edgeVbo = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
vboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeVBO");
var edgeEbo = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw,
eboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeEBO");
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeVAO");
SetupVAOLayout();
CheckGlError();
edgeVbo.Use();
edgeEbo.Use();
datum.EdgeEBO = edgeEbo;
datum.EdgeVBO = edgeVbo;
datum.EdgeVAO = edgeVao;
}
private void DeleteChunk(MapChunkData data)
@@ -254,19 +411,49 @@ namespace Robust.Client.Graphics.Clyde
_mapChunkData.Remove(gridId);
}
private static T[] EnsureSize<T>(ref T[]? field, int size)
{
if (field == null || field.Length < size)
field = new T[size];
return field;
}
private void WriteTileToBuffers(
int i,
int gridX,
int gridY,
Span<Vertex2D> vertexBuffer,
Span<ushort> indexBuffer,
Box2 region)
{
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(gridX, gridY, region.Left, region.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gridX + 1, gridY, region.Right, region.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gridX + 1, gridY + 1, region.Right, region.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gridX, gridY + 1, region.Left, region.Top, Color.White);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort)(i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
}
private sealed class MapChunkData
{
public bool Dirty;
public readonly uint VAO;
public readonly GLBuffer VBO;
public readonly GLBuffer EBO;
public bool EdgeDirty = true;
public bool Dirty = true;
public uint VAO;
public GLBuffer VBO = default!;
public GLBuffer EBO = default!;
public int TileCount;
public MapChunkData(uint vao, GLBuffer vbo, GLBuffer ebo)
public uint EdgeVAO;
public GLBuffer EdgeVBO = default!;
public GLBuffer EdgeEBO = default!;
public int EdgeCount;
public MapChunkData()
{
VAO = vao;
VBO = vbo;
EBO = ebo;
}
}
}

View File

@@ -123,9 +123,13 @@ namespace Robust.Client.Graphics.Clyde
private void RenderSingleWorldOverlay(Overlay overlay, Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
{
// Check that entity manager has started.
// This is required for us to be able to use MapSystem.
DebugTools.Assert(_entityManager.Started, "Entity manager should be started/initialized before rendering world-space overlays");
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
var mapId = vp.Eye!.Position.MapId;
var mapId = vp.Eye?.Position.MapId ?? MapId.Nullspace;
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapSystem.GetMapOrInvalid(mapId), mapId, worldBox, worldBounds);
if (!overlay.BeforeDraw(args))
@@ -152,6 +156,7 @@ namespace Robust.Client.Graphics.Clyde
private void RenderOverlays(Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
{
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
using (DebugGroup($"Overlays: {space}"))
{
foreach (var overlay in GetOverlaysForSpace(space))
@@ -176,9 +181,18 @@ namespace Robust.Client.Graphics.Clyde
var worldBounds = CalcWorldBounds(vp);
var worldAABB = worldBounds.CalcBoundingBox();
var mapId = vp.Eye!.Position.MapId;
var mapId = vp.Eye?.Position.MapId ?? MapId.Nullspace;
var mapUid = EntityUid.Invalid;
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, _mapSystem.GetMapOrInvalid(mapId), mapId, worldAABB, worldBounds);
// Screen space overlays may be getting used before entity manager & entity systems have been initialized.
// This might mean that _mapSystem is currently null.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (_entityManager.Started && _mapSystem != null)
mapUid = _mapSystem.GetMapOrInvalid(mapId);
DebugTools.Assert(_mapSystem != null || !_entityManager.Started);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, mapUid, mapId, worldAABB, worldBounds);
foreach (var overlay in list)
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.Graphics;
@@ -10,9 +11,11 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace Robust.Client.Map
{
@@ -29,7 +32,7 @@ namespace Robust.Client.Map
public Texture TileTextureAtlas => _tileTextureAtlas ?? Texture.Transparent;
private readonly Dictionary<int, Box2[]> _tileRegions = new();
private FrozenDictionary<(int Id, Direction Direction), Box2[]> _tileRegions = FrozenDictionary<(int Id, Direction Direction), Box2[]>.Empty;
public Box2 ErrorTileRegion { get; private set; }
@@ -42,7 +45,14 @@ namespace Robust.Client.Map
/// <inheritdoc />
public Box2[]? TileAtlasRegion(int tileType)
{
if (_tileRegions.TryGetValue(tileType, out var region))
return TileAtlasRegion(tileType, Direction.Invalid);
}
/// <inheritdoc />
public Box2[]? TileAtlasRegion(int tileType, Direction direction)
{
// ReSharper disable once CanSimplifyDictionaryTryGetValueWithGetValueOrDefault
if (_tileRegions.TryGetValue((tileType, direction), out var region))
{
return region;
}
@@ -83,7 +93,8 @@ namespace Robust.Client.Map
internal void _genTextureAtlas()
{
_tileRegions.Clear();
var sw = RStopwatch.StartNew();
var tileRegs = new Dictionary<(int Id, Direction Direction), Box2[]>();
_tileTextureAtlas = null;
var defList = TileDefs.Where(t => t.Sprite != null).ToList();
@@ -94,7 +105,7 @@ namespace Robust.Client.Map
const int tileSize = EyeManager.PixelsPerMeter;
var tileCount = defList.Select(x => (int)x.Variants).Sum() + 1;
var tileCount = defList.Select(x => x.Variants + x.EdgeSprites.Count).Sum() + 1;
var dimensionX = (int) Math.Ceiling(Math.Sqrt(tileCount));
var dimensionY = (int) Math.Ceiling((float) tileCount / dimensionX);
@@ -102,11 +113,11 @@ namespace Robust.Client.Map
var imgWidth = dimensionX * tileSize;
var imgHeight = dimensionY * tileSize;
var sheet = new Image<Rgba32>(imgWidth, imgHeight);
var w = (float) sheet.Width;
var h = (float) sheet.Height;
// Add in the missing tile texture sprite as tile texture 0.
{
var w = (float) sheet.Width;
var h = (float) sheet.Height;
ErrorTileRegion = Box2.FromDimensions(
0, (h - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
@@ -154,25 +165,98 @@ namespace Robust.Client.Map
var box = new UIBox2i(0, 0, tileSize, tileSize).Translated(new Vector2i(j * tileSize, 0));
image.Blit(box, sheet, point);
var w = (float) sheet.Width;
var h = (float) sheet.Height;
regionList[j] = Box2.FromDimensions(
point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
column++;
if (column >= dimensionX)
{
column = 0;
row++;
}
BumpColumn(ref row, ref column, dimensionX);
}
_tileRegions.Add(def.TileId, regionList);
tileRegs.Add((def.TileId, Direction.Invalid), regionList);
// Edges
if (def.EdgeSprites.Count <= 0)
continue;
foreach (var direction in DirectionExtensions.AllDirections)
{
if (!def.EdgeSprites.TryGetValue(direction, out var edge))
continue;
using (var stream = _manager.ContentFileRead(edge))
{
image = Image.Load<Rgba32>(stream);
}
if (image.Width != tileSize || image.Height != tileSize)
{
throw new NotSupportedException(
$"Unable to load {path}, due to being unable to use tile textures with a dimension other than {tileSize}x{tileSize}.");
}
Angle angle = Angle.Zero;
switch (direction)
{
// Corner sprites
case Direction.SouthEast:
break;
case Direction.NorthEast:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.NorthWest:
angle = new Angle(MathF.PI);
break;
case Direction.SouthWest:
angle = new Angle(MathF.PI * 1.5f);
break;
// Edge sprites
case Direction.South:
break;
case Direction.East:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.North:
angle = new Angle(MathF.PI);
break;
case Direction.West:
angle = new Angle(MathF.PI * 1.5f);
break;
}
if (angle != Angle.Zero)
{
image.Mutate(o => o.Rotate((float)-angle.Degrees));
}
var point = new Vector2i(column * tileSize, row * tileSize);
var box = new UIBox2i(0, 0, tileSize, tileSize);
image.Blit(box, sheet, point);
// If you ever need edge variants then you could just bump this.
var edgeList = new Box2[1];
edgeList[0] = Box2.FromDimensions(
point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
tileRegs.Add((def.TileId, direction), edgeList);
BumpColumn(ref row, ref column, dimensionX);
}
}
_tileRegions = tileRegs.ToFrozenDictionary();
_tileTextureAtlas = Texture.LoadFromImage(sheet, "Tile Atlas");
_sawmill.Debug($"Tile atlas took {sw.Elapsed} to build");
}
private void BumpColumn(ref int row, ref int column, int dimensionX)
{
column++;
if (column >= dimensionX)
{
column = 0;
row++;
}
}
void IPostInjectInit.PostInject()

View File

@@ -29,5 +29,12 @@ namespace Robust.Client.Map
/// </summary>
/// <returns>If null, do not draw the tile at all.</returns>
Box2[]? TileAtlasRegion(int tileType);
/// <summary>
/// Gets the region inside the texture atlas to use to draw a tile type.
/// Also handles edge sprites.
/// </summary>
/// <returns>If null, do not draw the tile at all.</returns>
public Box2[]? TileAtlasRegion(int tileType, Direction direction);
}
}

View File

@@ -1,125 +0,0 @@
using System;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Map;
/// <summary>
/// Draws border sprites for tiles that support them.
/// </summary>
public sealed class TileEdgeOverlay : GridOverlay
{
private readonly IEntityManager _entManager;
private readonly IResourceCache _resource;
private readonly ITileDefinitionManager _tileDefManager;
public TileEdgeOverlay(IEntityManager entManager, IResourceCache resource, ITileDefinitionManager tileDefManager)
{
_entManager = entManager;
_resource = resource;
_tileDefManager = tileDefManager;
ZIndex = -1;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return;
var mapSystem = _entManager.System<SharedMapSystem>();
var xformSystem = _entManager.System<SharedTransformSystem>();
var tileSize = Grid.Comp.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(Grid.Owner);
args.WorldHandle.SetTransform(worldMatrix);
var bounds = args.WorldBounds;
bounds = new Box2Rotated(bounds.Box.Enlarged(1), bounds.Rotation, bounds.Origin);
var localAABB = invMatrix.TransformBox(bounds);
var enumerator = mapSystem.GetLocalTilesEnumerator(Grid.Owner, Grid, localAABB, false);
while (enumerator.MoveNext(out var tileRef))
{
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
if (tileDef.EdgeSprites.Count == 0)
continue;
// Get what tiles border us to determine what sprites we need to draw.
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x == 0 && y == 0)
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
var neighborTile = mapSystem.GetTileRef(Grid.Owner, Grid, neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.
if (tileRef.Tile.TypeId == neighborTile.Tile.TypeId)
continue;
// Don't draw if the the neighbor tile edges should draw over us (or if we have the same priority)
if (neighborDef.EdgeSprites.Count != 0 && neighborDef.EdgeSpritePriority >= tileDef.EdgeSpritePriority)
continue;
var direction = new Vector2i(x, y).AsDirection();
// No edge tile
if (!tileDef.EdgeSprites.TryGetValue(direction, out var edgePath))
continue;
var texture = _resource.GetResource<TextureResource>(edgePath);
var box = Box2.FromDimensions(neighborIndices, tileDimensions);
var angle = Angle.Zero;
// If we ever need one for both cardinals and corners then update this.
switch (direction)
{
// Corner sprites
case Direction.SouthEast:
break;
case Direction.NorthEast:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.NorthWest:
angle = new Angle(MathF.PI);
break;
case Direction.SouthWest:
angle = new Angle(MathF.PI * 1.5f);
break;
// Edge sprites
case Direction.South:
break;
case Direction.East:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.North:
angle = new Angle(MathF.PI);
break;
case Direction.West:
angle = new Angle(MathF.PI * 1.5f);
break;
}
if (angle == Angle.Zero)
args.WorldHandle.DrawTextureRect(texture.Texture, box);
else
args.WorldHandle.DrawTextureRect(texture.Texture, new Box2Rotated(box, angle, box.Center));
RequiresFlush = true;
}
}
}
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
}

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Collections;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Maths;
@@ -83,9 +84,23 @@ namespace Robust.Client.UserInterface.Controls
_updateScrollbarVisibility();
}
public void Add(IEnumerable<Item> items)
{
foreach (var item in items)
{
if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");
_itemList.Add(item);
item.OnSelected += Select;
item.OnDeselected += Deselect;
}
Recalculate();
}
public void Add(Item item)
{
if (item == null) return;
if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");
_itemList.Add(item);
@@ -93,9 +108,19 @@ namespace Robust.Client.UserInterface.Controls
item.OnSelected += Select;
item.OnDeselected += Deselect;
RecalculateContentHeight();
if (_isAtBottom && ScrollFollowing)
_scrollBar.MoveToEnd();
Recalculate();
}
public void AddItems(IEnumerable<string> texts, Texture? icon = null, bool selectable = true, object? metadata = null)
{
var items = new ValueList<Item>();
foreach (var text in texts)
{
items.Add(new Item(this) {Text = text, Icon = icon, Selectable = selectable, Metadata = metadata});
}
Add(items);
}
public Item AddItem(string text, Texture? icon = null, bool selectable = true, object? metadata = null)
@@ -107,11 +132,15 @@ namespace Robust.Client.UserInterface.Controls
public void Clear()
{
foreach (var item in _itemList.ToArray())
// Handle this manually so we can just clear all at once.
foreach (var item in _itemList)
{
Remove(item);
item.OnSelected -= Select;
item.OnDeselected -= Deselect;
}
_itemList.Clear();
Recalculate();
_totalContentHeight = 0;
}
@@ -125,25 +154,35 @@ namespace Robust.Client.UserInterface.Controls
_itemList.CopyTo(array, arrayIndex);
}
private void InternalRemoveAt(int index)
{
if (_itemList.Count <= index)
return;
// If you modify this then also make sure to update Clear!
var item = _itemList[index];
_itemList.RemoveAt(index);
item.OnSelected -= Select;
item.OnDeselected -= Deselect;
}
public bool Remove(Item item)
{
if (item == null) return false;
var value = _itemList.Remove(item);
item.OnSelected -= Select;
item.OnDeselected -= Deselect;
RecalculateContentHeight();
if (_isAtBottom && ScrollFollowing)
_scrollBar.MoveToEnd();
Recalculate();
return value;
}
public void RemoveAt(int index)
{
Remove(this[index]);
InternalRemoveAt(index);
Recalculate();
}
public IEnumerator<Item> GetEnumerator()
@@ -161,16 +200,24 @@ namespace Robust.Client.UserInterface.Controls
return _itemList.IndexOf(item);
}
public void Insert(int index, Item item)
private void InternalInsert(int index, Item item)
{
if (item == null) return;
if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");
_itemList.Insert(index, item);
item.OnSelected += Select;
item.OnDeselected += Deselect;
}
public void Insert(int index, Item item)
{
InternalInsert(index, item);
Recalculate();
}
private void Recalculate()
{
RecalculateContentHeight();
if (_isAtBottom && ScrollFollowing)
_scrollBar.MoveToEnd();
@@ -191,7 +238,6 @@ namespace Robust.Client.UserInterface.Controls
SetItems(newItems, (a,b) => string.Compare(a.Text, b.Text));
}
/// <inheritdoc />
/// <summary>
/// This variant allows for a custom equality operator to compare items, when
/// comparing the Item text is not desired.
@@ -215,13 +261,13 @@ namespace Robust.Client.UserInterface.Controls
else if (cmpResult > 0)
{
// Item exists in our list, but not in `newItems`. Remove it.
RemoveAt(i);
InternalRemoveAt(i);
i--;
}
else if (cmpResult < 0)
{
// A new entry which doesn't exist in our list. Insert it.
Insert(i + 1, newItems[j]);
InternalInsert(i + 1, newItems[j]);
j--;
}
}
@@ -229,16 +275,18 @@ namespace Robust.Client.UserInterface.Controls
// Any remaining items in our list don't exist in `newItems` so remove them
while (i >= 0)
{
RemoveAt(i);
InternalRemoveAt(i);
i--;
}
// And finally, any remaining items in `newItems` don't exist in our list. Create them.
while (j >= 0)
{
Insert(0, newItems[j]);
InternalInsert(0, newItems[j]);
j--;
}
Recalculate();
}
// Without this attribute, this would compile into a property called "Item", causing problems with the Item class.
@@ -290,9 +338,12 @@ namespace Robust.Client.UserInterface.Controls
public void ClearSelected(int? except = null)
{
foreach (var item in GetSelected())
for (var i = 0; i < _itemList.Count; i++)
{
if(IndexOf(item) == except) continue;
if (i == except)
continue;
var item = _itemList[i];
item.Selected = false;
}
}

View File

@@ -4,6 +4,7 @@ using Robust.Client.Graphics;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Collections;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -15,10 +16,23 @@ namespace Robust.Client.UserInterface.Controls
[Virtual]
public class OutputPanel : Control
{
public const string StyleClassOutputPanelScrollDownButton = "outputPanelScrollDownButton";
[Dependency] private readonly MarkupTagManager _tagManager = default!;
public const string StylePropertyStyleBox = "stylebox";
public bool ShowScrollDownButton
{
get => _showScrollDownButton;
set
{
_showScrollDownButton = value;
_updateScrollButtonVisibility();
}
}
private bool _showScrollDownButton;
private readonly RingBufferList<RichTextEntry> _entries = new();
private bool _isAtBottom = true;
@@ -26,6 +40,7 @@ namespace Robust.Client.UserInterface.Controls
private bool _firstLine = true;
private StyleBox? _styleBoxOverride;
private VScrollBar _scrollBar;
private Button _scrollDownButton;
public bool ScrollFollowing { get; set; } = true;
@@ -43,7 +58,25 @@ namespace Robust.Client.UserInterface.Controls
HorizontalAlignment = HAlignment.Right
};
AddChild(_scrollBar);
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
AddChild(_scrollDownButton = new Button()
{
Name = "scrollLiveBtn",
StyleClasses = { StyleClassOutputPanelScrollDownButton },
VerticalAlignment = VAlignment.Bottom,
HorizontalAlignment = HAlignment.Center,
Text = String.Format("⬇ {0} ⬇", Loc.GetString("output-panel-scroll-down-button-text")),
MaxWidth = 300,
Visible = false,
});
_scrollDownButton.OnPressed += _ => ScrollToBottom();
_scrollBar.OnValueChanged += _ =>
{
_isAtBottom = _scrollBar.IsAtEnd;
_updateScrollButtonVisibility();
};
}
public int EntryCount => _entries.Count;
@@ -184,6 +217,7 @@ namespace Robust.Client.UserInterface.Controls
var styleBoxSize = _getStyleBox()?.MinimumSize.Y ?? 0;
_scrollBar.Page = UIScale * (Height - styleBoxSize);
_updateScrollButtonVisibility();
_invalidateEntries();
}
@@ -284,5 +318,10 @@ namespace Robust.Client.UserInterface.Controls
_invalidOnVisible = false;
}
}
private void _updateScrollButtonVisibility()
{
_scrollDownButton.Visible = ShowScrollDownButton && !_isAtBottom;
}
}
}

View File

@@ -1,7 +1,7 @@
<Control xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics">
<BoxContainer Orientation="Vertical">
<OutputPanel Name="Output" VerticalExpand="True" StyleClasses="monospace">
<OutputPanel Name="Output" VerticalExpand="True" StyleClasses="monospace" ShowScrollDownButton="True">
<OutputPanel.StyleBoxOverride>
<gfx:StyleBoxFlat BackgroundColor="#25252add"
ContentMarginLeftOverride="3" ContentMarginRightOverride="3"

View File

@@ -5,9 +5,9 @@ using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface;
internal sealed partial class UserInterfaceManager
@@ -61,7 +61,7 @@ internal sealed partial class UserInterfaceManager
Title = string.IsNullOrEmpty(title) ? Loc.GetString("popup-title") : title,
};
var label = new Label { Text = contents };
var label = new RichTextLabel { Text = $"[color=white]{FormattedMessage.EscapeText(contents)}[/color]" };
var vBox = new BoxContainer
{

View File

@@ -2,11 +2,15 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Collections;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.ViewVariables
{
@@ -51,15 +55,19 @@ namespace Robust.Client.ViewVariables
_lastSearch = search;
EntryItemList.ClearSelected();
EntryItemList.Clear();
AddButton.Disabled = true;
var items = new ValueList<string>();
foreach (var component in _entries)
{
if(!string.IsNullOrEmpty(search) && !component.Contains(search, StringComparison.InvariantCultureIgnoreCase))
continue;
EntryItemList.AddItem(component);
items.Add(component);
}
EntryItemList.AddItems(items);
}
private void OnSearchTextChanged(LineEdit.LineEditEventArgs obj)

View File

@@ -36,6 +36,7 @@ public static class Diagnostics
public const string IdUseNonGenericVariant = "RA0030";
public const string IdPreferOtherType = "RA0031";
public const string IdDuplicateDependency = "RA0032";
public const string IdForbidLiteral = "RA0033";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

View File

@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;

View File

@@ -8,6 +8,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -46,11 +47,11 @@ namespace Robust.Server.Console.Commands
bool saveSuccess = _ent.System<MapLoaderSystem>().TrySaveGrid(uid, new ResPath(args[1]));
if(saveSuccess)
{
shell.WriteLine("Save successful. Look in the user data directory.");
shell.WriteLine("Save successful. Look in the user data directory.");
}
else
{
shell.WriteError("Save unsuccessful!");
shell.WriteError("Save unsuccessful!");
}
}
@@ -59,7 +60,7 @@ namespace Robust.Server.Console.Commands
switch (args.Length)
{
case 1:
return CompletionResult.FromHint(Loc.GetString("cmd-hint-savebp-id"));
return CompletionResult.FromHintOptions(CompletionHelper.Components<MapGridComponent>(args[0], _ent), Loc.GetString("cmd-hint-savebp-id"));
case 2:
var opts = CompletionHelper.UserFilePath(args[1], _resource.UserData);
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));
@@ -159,6 +160,7 @@ namespace Robust.Server.Console.Commands
public sealed class SaveMap : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly IResourceManager _resource = default!;
@@ -169,7 +171,7 @@ namespace Robust.Server.Console.Commands
switch (args.Length)
{
case 1:
return CompletionResult.FromHint(Loc.GetString("cmd-hint-savemap-id"));
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(_entManager), Loc.GetString("cmd-hint-savemap-id"));
case 2:
var opts = CompletionHelper.UserFilePath(args[1], _resource.UserData);
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));

View File

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

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics.Contracts;
using System.Numerics;
@@ -37,6 +38,21 @@ namespace Robust.Shared.Maths
/// </summary>
public static class DirectionExtensions
{
/// <summary>
/// A list of all cardinal and diagonal <see cref="Direction"/>s.
/// </summary>
public static readonly ImmutableArray<Direction> AllDirections =
[
Direction.South,
Direction.SouthEast,
Direction.East,
Direction.NorthEast,
Direction.North,
Direction.NorthWest,
Direction.West,
Direction.SouthWest
];
private const double Segment = 2 * Math.PI / 8.0; // Cut the circle into 8 pieces
public static Direction AsDir(this DirectionFlag directionFlag)

View File

@@ -0,0 +1,11 @@
using System;
namespace Robust.Shared.Analyzers;
/// <summary>
/// Marks that values used for this parameter should not be literal values.
/// This helps prevent magic numbers/strings/etc, by indicating that values
/// should either be wrapped (for validation) or defined as constants or readonly statics.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class ForbidLiteralAttribute : Attribute;

View File

@@ -1874,5 +1874,12 @@ namespace Robust.Shared
/// </summary>
public static readonly CVarDef<int> ToolshedNearbyEntitiesLimit =
CVarDef.Create("toolshed.nearby_entities_limit", 5, CVar.SERVER | CVar.REPLICATED);
/*
* Localization
*/
public static readonly CVarDef<string> LocCultureName =
CVarDef.Create("loc.culture_name", "en-US", CVar.ARCHIVE);
}
}

View File

@@ -37,7 +37,7 @@ sealed class AddMapCommand : LocalizedEntityCommands
sealed class RemoveMapCommand : LocalizedCommands
{
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;
public override string Command => "rmmap";
public override bool RequireServerOrSingleplayer => true;
@@ -51,14 +51,15 @@ sealed class RemoveMapCommand : LocalizedCommands
}
var mapId = new MapId(int.Parse(args[0]));
var mapSystem = _systems.GetEntitySystem<SharedMapSystem>();
if (!_map.MapExists(mapId))
if (!mapSystem.MapExists(mapId))
{
shell.WriteError($"Map {mapId.Value} does not exist.");
return;
}
_map.DeleteMap(mapId);
mapSystem.DeleteMap(mapId);
shell.WriteLine($"Map {mapId.Value} was removed.");
}
}

View File

@@ -20,6 +20,7 @@ internal sealed class TeleportCommand : LocalizedEntityCommands
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
public override string Command => "tp";
public override bool RequireServerOrSingleplayer => true;
@@ -46,7 +47,7 @@ internal sealed class TeleportCommand : LocalizedEntityCommands
else
mapId = transform.MapID;
if (!_map.MapExists(mapId))
if (!_mapSystem.MapExists(mapId))
{
shell.WriteError($"Map {mapId} doesn't exist!");
return;
@@ -60,13 +61,26 @@ internal sealed class TeleportCommand : LocalizedEntityCommands
}
else
{
var mapEnt = _map.GetMapEntityIdOrThrow(mapId);
_transform.SetWorldPosition((entity, transform), position);
_transform.SetParent(entity, transform, mapEnt);
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
{
_transform.SetWorldPosition((entity, transform), position);
_transform.SetParent(entity, transform, mapEnt.Value);
}
}
shell.WriteLine($"Teleported {shell.Player} to {mapId}:{posX},{posY}.");
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHint("<x>"),
2 => CompletionResult.FromHint("<y>"),
3 => CompletionResult.FromHintOptions(CompletionHelper.MapIds(_entityManager), "[MapId]"),
_ => CompletionResult.Empty
};
}
}
public sealed class TeleportToCommand : LocalizedEntityCommands

View File

@@ -60,9 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
// Sanitise platform-specific path and standardize it for engine use.
.Replace(Path.DirectorySeparatorChar, '/');
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
}
/// <inheritdoc />

View File

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

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
{
/// <summary>
/// The root path of this provider.
/// Can be null if it's a virtual provider.
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
/// </summary>
string? RootDir { get; }

View File

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

View File

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

View File

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

View File

@@ -155,9 +155,9 @@ public sealed class EntitySerializer : ISerializationContext,
public event IsSerializableDelegate? OnIsSerializeable;
public delegate void IsSerializableDelegate(Entity<MetaDataComponent> ent, ref bool serializable);
public EntitySerializer(IDependencyCollection _dependency, SerializationOptions options)
public EntitySerializer(IDependencyCollection dependency, SerializationOptions options)
{
_dependency.InjectDependencies(this);
dependency.InjectDependencies(this);
_log = _logMan.GetSawmill("entity_serializer");
SerializerProvider.RegisterSerializer(this);

View File

@@ -595,12 +595,23 @@ namespace Robust.Shared.GameObjects
if (!_deleteSet.Add(component))
{
// already deferred deletion
// Already deferring deletion
DebugTools.Assert(component.LifeStage >= ComponentLifeStage.Stopped);
return;
}
if (component.LifeStage >= ComponentLifeStage.Initialized && component.LifeStage <= ComponentLifeStage.Running)
DebugTools.Assert(component.LifeStage >= ComponentLifeStage.Added);
if (component.LifeStage is >= ComponentLifeStage.Initialized and < ComponentLifeStage.Stopping)
LifeShutdown(uid, component, _componentFactory.GetIndex(component.GetType()));
else if (component.LifeStage == ComponentLifeStage.Added)
{
// The component was added, but never initialized or started. It's kinda weird to add and then
// immediately defer-remove a component, but oh well. Let's just set the life stage directly and not
// raise shutdown events? The removal events will still get called later.
// This is also what LifeShutdown() would also do, albeit behind a DebugAssert.
component.LifeStage = ComponentLifeStage.Stopped;
}
#if EXCEPTION_TOLERANCE
}
catch (Exception e)

View File

@@ -16,6 +16,7 @@ public partial class EntityManager
/// </summary>
internal void LifeAddToEntity(EntityUid uid, IComponent component, CompIdx idx)
{
DebugTools.Assert(!_deleteSet.Contains(component));
DebugTools.Assert(component.LifeStage == ComponentLifeStage.PreAdd);
#pragma warning disable CS0618 // Type or member is obsolete
@@ -34,6 +35,7 @@ public partial class EntityManager
/// </summary>
internal void LifeInitialize(EntityUid uid, IComponent component, CompIdx idx)
{
DebugTools.Assert(!_deleteSet.Contains(component));
DebugTools.Assert(component.LifeStage == ComponentLifeStage.Added);
component.LifeStage = ComponentLifeStage.Initializing;
@@ -47,6 +49,7 @@ public partial class EntityManager
/// </summary>
internal void LifeStartup(EntityUid uid, IComponent component, CompIdx idx)
{
DebugTools.Assert(!_deleteSet.Contains(component));
DebugTools.Assert(component.LifeStage == ComponentLifeStage.Initialized);
component.LifeStage = ComponentLifeStage.Starting;

View File

@@ -37,18 +37,22 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc cref="Resolve{TComp}(Robust.Shared.GameObjects.EntityUid,ref TComp?,bool)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool Resolve(EntityUid uid, [NotNullWhen(true)] ref MetaDataComponent? component,
protected bool Resolve(
EntityUid uid,
[NotNullWhen(true)] ref MetaDataComponent? component,
bool logMissing = true)
{
return EntityManager.MetaQuery.Resolve(uid, ref component);
return EntityManager.MetaQuery.Resolve(uid, ref component, logMissing);
}
/// <inheritdoc cref="Resolve{TComp}(Robust.Shared.GameObjects.EntityUid,ref TComp?,bool)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TransformComponent? component,
protected bool Resolve(
EntityUid uid,
[NotNullWhen(true)] ref TransformComponent? component,
bool logMissing = true)
{
return EntityManager.TransformQuery.Resolve(uid, ref component);
return EntityManager.TransformQuery.Resolve(uid, ref component, logMissing);
}
/// <summary>

View File

@@ -193,7 +193,7 @@ namespace Robust.Shared.GameObjects
ComponentEventHandler<TComp, TEvent> handler,
Type[]? before = null, Type[]? after = null)
where TComp : IComponent
where TEvent : EntityEventArgs
where TEvent : notnull
{
System.SubscribeLocalEvent(handler, before, after);
}
@@ -206,7 +206,7 @@ namespace Robust.Shared.GameObjects
ComponentEventRefHandler<TComp, TEvent> handler,
Type[]? before = null, Type[]? after = null)
where TComp : IComponent
where TEvent : EntityEventArgs
where TEvent : notnull
{
System.SubscribeLocalEvent(handler, before, after);
}
@@ -219,7 +219,7 @@ namespace Robust.Shared.GameObjects
EntityEventRefHandler<TComp, TEvent> handler,
Type[]? before = null, Type[]? after = null)
where TComp : IComponent
where TEvent : EntityEventArgs
where TEvent : notnull
{
System.SubscribeLocalEvent(handler, before, after);
}
@@ -229,7 +229,7 @@ namespace Robust.Shared.GameObjects
/// </summary>
/// <remarks>
/// This can be used by extension methods for <see cref="Subscriptions"/>
/// to unsubscribe from from external sources such as CVars.
/// to unsubscribe from external sources such as CVars.
/// </remarks>
/// <param name="action">An action to be ran when the entity system is shut down.</param>
public void RegisterUnsubscription(Action action)

View File

@@ -96,6 +96,7 @@ namespace Robust.Shared.GameObjects
// If we're attached to the map we'll also just never disable collision due to how grid movement works.
var canCollide = body.Awake ||
body.ContactCount > 0 ||
(TryComp(uid, out JointComponent? jointComponent) && jointComponent.JointCount > 0) ||
xform.GridUid == null;

View File

@@ -111,7 +111,7 @@ public abstract partial class SharedTransformSystem
public EntityUid? GetGrid(Entity<TransformComponent?> entity)
{
return !Resolve(entity, ref entity.Comp) ? null : entity.Comp.GridUid;
return !Resolve(entity, ref entity.Comp, logMissing:false) ? null : entity.Comp.GridUid;
}
/// <summary>
@@ -124,7 +124,7 @@ public abstract partial class SharedTransformSystem
public MapId GetMapId(Entity<TransformComponent?> entity)
{
return !Resolve(entity, ref entity.Comp) ? MapId.Nullspace : entity.Comp.MapID;
return !Resolve(entity, ref entity.Comp, logMissing: false) ? MapId.Nullspace : entity.Comp.MapID;
}
/// <summary>
@@ -137,7 +137,7 @@ public abstract partial class SharedTransformSystem
public EntityUid? GetMap(Entity<TransformComponent?> entity)
{
return !Resolve(entity, ref entity.Comp) ? null : entity.Comp.MapUid;
return !Resolve(entity, ref entity.Comp, logMissing: false) ? null : entity.Comp.MapUid;
}
/// <summary>
@@ -167,10 +167,10 @@ public abstract partial class SharedTransformSystem
/// </summary>
public bool InRange(Entity<TransformComponent?> entA, Entity<TransformComponent?> entB, float range)
{
if (!Resolve(entA, ref entA.Comp))
if (!Resolve(entA, ref entA.Comp, logMissing: false))
return false;
if (!Resolve(entB, ref entB.Comp))
if (!Resolve(entB, ref entB.Comp, logMissing: false))
return false;
if (!entA.Comp.ParentUid.IsValid() || !entB.Comp.ParentUid.IsValid())

View File

@@ -121,9 +121,14 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (msg.Message is not CloseBoundInterfaceMessage && ui.RequireInputValidation)
{
var attempt = new BoundUserInterfaceMessageAttempt(sender, uid, msg.UiKey, msg.Message);
RaiseLocalEvent(attempt);
if (attempt.Cancelled)
return;
RaiseLocalEvent(uid, attempt);
if (attempt.Cancelled)
return;
}
// get the wrapped message and populate it with the sender & UI key information.

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using JetBrains.Annotations;
@@ -95,9 +96,15 @@ namespace Robust.Shared.Localization
CultureInfo? DefaultCulture { get; set; }
/// <summary>
/// Checks if the culture has been loaded.
/// Checks if the culture is loaded, if not,
/// loads it via <see cref="ILocalizationManager.LoadCulture"/>
/// and then set it as <see cref="ILocalizationManager.DefaultCulture"/>.
/// </summary>
void SetCulture(CultureInfo culture);
/// <summary>
/// Checks to see if the culture has been loaded.
/// </summary>
/// <param name="culture"></param>
bool HasCulture(CultureInfo culture);
/// <summary>
@@ -106,6 +113,17 @@ namespace Robust.Shared.Localization
/// <param name="culture"></param>
void LoadCulture(CultureInfo culture);
/// <summary>
/// Loads <see cref="CultureInfo"/> obtained from <see cref="CVars.LocCultureName"/>,
/// they are different for client and server, and also can be saved.
/// </summary>
CultureInfo SetDefaultCulture();
/// <summary>
/// Returns all locale directories from the game's resources.
/// </summary>
List<CultureInfo> GetFoundCultures();
/// <summary>
/// Sets culture to be used in the absence of the main one.
/// </summary>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
@@ -12,6 +13,7 @@ using Linguini.Shared.Types.Bundle;
using Linguini.Syntax.Ast;
using Linguini.Syntax.Parser;
using Linguini.Syntax.Parser.Error;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -24,6 +26,9 @@ namespace Robust.Shared.Localization
{
internal sealed partial class LocalizationManager : ILocalizationManagerInternal, IPostInjectInit
{
private static readonly ResPath LocaleDirPath = new("/Locale");
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
@@ -41,6 +46,18 @@ namespace Robust.Shared.Localization
_prototype.PrototypesReloaded += OnPrototypesReloaded;
}
public CultureInfo SetDefaultCulture()
{
var code = _configuration.GetCVar(CVars.LocCultureName);
var culture = CultureInfo.GetCultureInfo(code, predefinedOnly: false);
SetCulture(culture);
// Return the culture for further work with it,
// like of adding functions
return culture;
}
public string GetString(string messageId)
{
if (_defaultCulture == null)
@@ -337,6 +354,17 @@ namespace Robust.Shared.Localization
}
}
public void SetCulture(CultureInfo culture)
{
if (!HasCulture(culture))
LoadCulture(culture);
if (DefaultCulture?.NameEquals(culture) ?? false)
return;
DefaultCulture = culture;
}
public bool HasCulture(CultureInfo culture)
{
return _contexts.ContainsKey(culture);
@@ -344,6 +372,10 @@ namespace Robust.Shared.Localization
public void LoadCulture(CultureInfo culture)
{
// Attempting to load an already loaded culture
if (HasCulture(culture))
throw new InvalidOperationException("Culture is already loaded");
var bundle = LinguiniBuilder.Builder()
.CultureInfo(culture)
.SkipResources()
@@ -358,6 +390,20 @@ namespace Robust.Shared.Localization
DefaultCulture ??= culture;
}
public List<CultureInfo> GetFoundCultures()
{
var result = new List<CultureInfo>();
foreach (var name in _res.ContentGetDirectoryEntries(LocaleDirPath))
{
// Remove last "/" symbol
// Example "en-US/" -> "en-US"
var cultureName = name.TrimEnd('/');
result.Add(CultureInfo.GetCultureInfo(cultureName, predefinedOnly: false));
}
return result;
}
public void SetFallbackCluture(params CultureInfo[] cultures)
{
_fallbackCultures = Array.Empty<(CultureInfo, FluentBundle)>();
@@ -438,7 +484,7 @@ namespace Robust.Shared.Localization
// Load data from .ftl files.
// Data is loaded from /Locale/<language-code>/*
var root = new ResPath($"/Locale/{culture.Name}/");
var root = LocaleDirPath / culture.Name;
var files = resourceManager.ContentFindFiles(root)
.Where(c => c.Filename.EndsWith(".ftl", StringComparison.InvariantCultureIgnoreCase))

View File

@@ -11,7 +11,6 @@ namespace Robust.Shared.Map.Commands;
/// </summary>
public sealed class AmbientLightCommand : IConsoleCommand
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;
public string Command => $"setambientlight";
@@ -32,10 +31,11 @@ public sealed class AmbientLightCommand : IConsoleCommand
}
var mapId = new MapId(mapInt);
var mapSystem = _systems.GetEntitySystem<SharedMapSystem>();
if (!_mapManager.MapExists(mapId))
if (!mapSystem.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-mapid"));
shell.WriteError(Loc.GetString("cmd-parse-failure-mapid", ("arg", mapId.Value)));
return;
}
@@ -49,7 +49,6 @@ public sealed class AmbientLightCommand : IConsoleCommand
}
var color = Color.FromSrgb(new Color(r, g, b, a));
var mapSystem = _systems.GetEntitySystem<SharedMapSystem>();
mapSystem.SetAmbientLight(mapId, color);
}
}

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Robust.Shared.Map

View File

@@ -27,7 +27,7 @@ internal sealed partial class CollisionManager
manifold.LocalNormal = Vector2.Zero;
manifold.PointCount = 1;
ref var p0 = ref manifold.Points[0];
ref var p0 = ref manifold.Points._00;
p0.LocalPoint = Vector2.Zero; // Also here
p0.Id.Key = 0;

View File

@@ -79,9 +79,9 @@ internal sealed partial class CollisionManager
manifold.Type = ManifoldType.Circles;
manifold.LocalNormal = Vector2.Zero;
manifold.LocalPoint = P;
manifold.Points[0].Id.Key = 0;
manifold.Points[0].Id.Features = cf;
manifold.Points[0].LocalPoint = circleB.Position;
manifold.Points._00.Id.Key = 0;
manifold.Points._00.Id.Features = cf;
manifold.Points._00.LocalPoint = circleB.Position;
return;
}
@@ -114,9 +114,9 @@ internal sealed partial class CollisionManager
manifold.Type = ManifoldType.Circles;
manifold.LocalNormal = Vector2.Zero;
manifold.LocalPoint = P;
manifold.Points[0].Id.Key = 0;
manifold.Points[0].Id.Features = cf;
manifold.Points[0].LocalPoint = circleB.Position;
manifold.Points._00.Id.Key = 0;
manifold.Points._00.Id.Features = cf;
manifold.Points._00.LocalPoint = circleB.Position;
return;
}
@@ -142,8 +142,8 @@ internal sealed partial class CollisionManager
manifold.Type = ManifoldType.FaceA;
manifold.LocalNormal = n;
manifold.LocalPoint = A;
manifold.Points[0].Id.Key = 0;
manifold.Points[0].Id.Features = cf;
manifold.Points[0].LocalPoint = circleB.Position;
manifold.Points._00.Id.Key = 0;
manifold.Points._00.Id.Features = cf;
manifold.Points._00.LocalPoint = circleB.Position;
}
}

View File

@@ -238,15 +238,15 @@ internal sealed partial class CollisionManager
}
var pointCount = 0;
var points = manifold.Points.AsSpan;
for (var i = 0; i < 2; ++i)
{
float separation;
separation = Vector2.Dot(refFace.normal, clipPoints2[i].V - refFace.v1);
var separation = Vector2.Dot(refFace.normal, clipPoints2[i].V - refFace.v1);
if (separation <= radius)
{
ref var cp = ref manifold.Points[pointCount];
ref var cp = ref points[pointCount];
if (primaryAxis.Type == EPAxisType.EdgeA)
{

View File

@@ -64,7 +64,7 @@ internal sealed partial class CollisionManager
manifold.LocalNormal = normals[normalIndex];
manifold.LocalPoint = (v1 + v2) * 0.5f;
ref var p0 = ref manifold.Points[0];
ref var p0 = ref manifold.Points._00;
p0.LocalPoint = circleB.Position;
p0.Id.Key = 0;
@@ -88,7 +88,7 @@ internal sealed partial class CollisionManager
manifold.LocalNormal = (cLocal - v1).Normalized();
manifold.LocalPoint = v1;
ref var p0 = ref manifold.Points[0];
ref var p0 = ref manifold.Points._00;
p0.LocalPoint = circleB.Position;
p0.Id.Key = 0;
@@ -107,7 +107,7 @@ internal sealed partial class CollisionManager
manifold.LocalNormal = (cLocal - v2).Normalized();
manifold.LocalPoint = v2;
ref var p0 = ref manifold.Points[0];
ref var p0 = ref manifold.Points._00;
p0.LocalPoint = circleB.Position;
p0.Id.Key = 0;
@@ -126,7 +126,7 @@ internal sealed partial class CollisionManager
manifold.LocalNormal = normals[vertIndex1];
manifold.LocalPoint = faceCenter;
ref var p0 = ref manifold.Points[0];
ref var p0 = ref manifold.Points._00;
p0.LocalPoint = circleB.Position;
p0.Id.Key = 0;

View File

@@ -238,6 +238,8 @@ internal sealed partial class CollisionManager
manifold.LocalPoint = planePoint;
int pointCount = 0;
var points = manifold.Points.AsSpan;
for (int i = 0; i < 2; ++i)
{
Vector2 value = clipPoints2[i].V;
@@ -245,7 +247,7 @@ internal sealed partial class CollisionManager
if (separation <= totalRadius)
{
ref var cp = ref manifold.Points[pointCount];
ref var cp = ref points[pointCount];
cp.LocalPoint = Transform.MulT(xf2, clipPoints2[i].V);
cp.Id = clipPoints2[i].ID;

View File

@@ -51,15 +51,18 @@ internal sealed partial class CollisionManager : IManifoldManager
in Manifold manifold2)
{
// Detect persists and removes.
var points1 = manifold1.Points.AsSpan;
var points2 = manifold2.Points.AsSpan;
for (int i = 0; i < manifold1.PointCount; ++i)
{
ContactID id = manifold1.Points[i].Id;
var id = points1[i].Id;
state1[i] = PointState.Remove;
for (int j = 0; j < manifold2.PointCount; ++j)
{
if (manifold2.Points[j].Id.Key == id.Key)
if (points2[j].Id.Key == id.Key)
{
state1[i] = PointState.Persist;
break;
@@ -70,13 +73,13 @@ internal sealed partial class CollisionManager : IManifoldManager
// Detect persists and adds.
for (int i = 0; i < manifold2.PointCount; ++i)
{
ContactID id = manifold2.Points[i].Id;
var id = points2[i].Id;
state2[i] = PointState.Add;
for (int j = 0; j < manifold1.PointCount; ++j)
for (var j = 0; j < manifold1.PointCount; ++j)
{
if (manifold1.Points[j].Id.Key == id.Key)
if (points1[j].Id.Key == id.Key)
{
state2[i] = PointState.Persist;
break;

View File

@@ -25,6 +25,7 @@ using System.Numerics;
using System.Runtime.InteropServices;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Collision;
@@ -146,7 +147,7 @@ public struct Manifold : IEquatable<Manifold>, IApproxEquatable<Manifold>
/// <summary>
/// Points of contact, can only be 0 -> 2.
/// </summary>
internal ManifoldPoint[] Points;
internal FixedArray2<ManifoldPoint> Points;
public ManifoldType Type;
@@ -157,9 +158,12 @@ public struct Manifold : IEquatable<Manifold>, IApproxEquatable<Manifold>
LocalNormal.Equals(other.LocalNormal) &&
LocalPoint.Equals(other.LocalPoint))) return false;
var points = Points.AsSpan;
var otherPoints = other.Points.AsSpan;
for (var i = 0; i < PointCount; i++)
{
if (!Points[i].Equals(other.Points[i])) return false;
if (!points[i].Equals(otherPoints[i])) return false;
}
return true;
@@ -172,9 +176,12 @@ public struct Manifold : IEquatable<Manifold>, IApproxEquatable<Manifold>
LocalNormal.EqualsApprox(other.LocalNormal) &&
LocalPoint.EqualsApprox(other.LocalPoint))) return false;
var points = Points.AsSpan;
var otherPoints = other.Points.AsSpan;
for (var i = 0; i < PointCount; i++)
{
if (!Points[i].EqualsApprox(other.Points[i])) return false;
if (!points[i].EqualsApprox(otherPoints[i])) return false;
}
return true;
@@ -187,13 +194,26 @@ public struct Manifold : IEquatable<Manifold>, IApproxEquatable<Manifold>
LocalNormal.EqualsApprox(other.LocalNormal, tolerance) &&
LocalPoint.EqualsApprox(other.LocalPoint, tolerance))) return false;
var points = Points.AsSpan;
var otherPoints = other.Points.AsSpan;
for (var i = 0; i < PointCount; i++)
{
if (!Points[i].EqualsApprox(other.Points[i], tolerance)) return false;
if (!points[i].EqualsApprox(otherPoints[i], tolerance)) return false;
}
return true;
}
public override bool Equals(object? obj)
{
return obj is Manifold manifold && Equals(manifold);
}
public override int GetHashCode()
{
return HashCode.Combine(LocalNormal, LocalPoint, PointCount, Points, (int)Type);
}
}
public struct ManifoldPoint : IEquatable<ManifoldPoint>, IApproxEquatable<ManifoldPoint>

View File

@@ -206,16 +206,19 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
// Match old contact ids to new contact ids and copy the
// stored impulses to warm start the solver.
var points = Manifold.Points.AsSpan;
var oldPoints = oldManifold.Points.AsSpan;
for (var i = 0; i < Manifold.PointCount; ++i)
{
var mp2 = Manifold.Points[i];
var mp2 = points[i];
mp2.NormalImpulse = 0.0f;
mp2.TangentImpulse = 0.0f;
var id2 = mp2.Id;
for (var j = 0; j < oldManifold.PointCount; ++j)
{
var mp1 = oldManifold.Points[j];
var mp1 = oldPoints[j];
if (mp1.Id.Key == id2.Key)
{
@@ -225,7 +228,7 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
}
}
Manifold.Points[i] = mp2;
points[i] = mp2;
}
if (touching != wasTouching)

View File

@@ -22,6 +22,7 @@
using System.Numerics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Dynamics.Contacts
{
@@ -37,7 +38,7 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
/// </summary>
public int IndexB { get; set; }
public Vector2[] LocalPoints;
internal FixedArray2<Vector2> LocalPoints;
public Vector2 LocalNormal;

View File

@@ -21,6 +21,7 @@
*/
using System.Numerics;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Dynamics.Contacts
{
@@ -39,13 +40,13 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
public int IndexB;
// Use 2 as its the max number of manifold points.
public VelocityConstraintPoint[] Points;
public FixedArray2<VelocityConstraintPoint> Points;
public Vector2 Normal;
public System.Numerics.Vector4 NormalMass;
public Vector4 NormalMass;
public System.Numerics.Vector4 K;
public Vector4 K;
public float InvMassA;
public float InvMassB;

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.Physics.Systems
/// <summary>
/// Tests whether a particular point is contained in the shape.
/// </summary>
public bool TestPoint(IPhysShape shape, Transform xform, Vector2 worldPoint)
public bool TestPoint<T>(T shape, Transform xform, Vector2 worldPoint) where T : IPhysShape
{
switch (shape)
{
@@ -77,113 +77,7 @@ namespace Robust.Shared.Physics.Systems
{
var data = new MassData();
// Box2D just calls fixture.GetMassData which just calls the shape method anyway soooo
// we can just cut out the middle-man
switch (shape)
{
case ChainShape:
data.Mass = 0f;
data.Center = Vector2.Zero;
data.I = 0f;
break;
case EdgeShape edge:
data.Mass = 0.0f;
data.Center = (edge.Vertex1 + edge.Vertex2) * 0.5f;
data.I = 0.0f;
break;
case PhysShapeCircle circle:
// massData->mass = density * b2_pi * m_radius * m_radius;
data.Center = circle.Position;
// inertia about the local origin
data.I = data.Mass * (0.5f * circle.Radius * circle.Radius + Vector2.Dot(circle.Position, circle.Position));
break;
case PhysShapeAabb aabb:
var polygon = (PolygonShape) aabb;
GetMassData(polygon, ref data, density);
break;
case Polygon fastPoly:
return GetMassData(new PolygonShape(fastPoly), density);
case SlimPolygon slim:
return GetMassData(new PolygonShape(slim), density);
case PolygonShape poly:
// Polygon mass, centroid, and inertia.
// Let rho be the polygon density in mass per unit area.
// Then:
// mass = rho * int(dA)
// centroid.x = (1/mass) * rho * int(x * dA)
// centroid.y = (1/mass) * rho * int(y * dA)
// I = rho * int((x*x + y*y) * dA)
//
// We can compute these integrals by summing all the integrals
// for each triangle of the polygon. To evaluate the integral
// for a single triangle, we make a change of variables to
// the (u,v) coordinates of the triangle:
// x = x0 + e1x * u + e2x * v
// y = y0 + e1y * u + e2y * v
// where 0 <= u && 0 <= v && u + v <= 1.
//
// We integrate u from [0,1-v] and then v from [0,1].
// We also need to use the Jacobian of the transformation:
// D = cross(e1, e2)
//
// Simplification: triangle centroid = (1/3) * (p1 + p2 + p3)
//
// The rest of the derivation is handled by computer algebra.
var count = poly.VertexCount;
DebugTools.Assert(count >= 3);
Vector2 center = new(0.0f, 0.0f);
var area = 0.0f;
var I = 0.0f;
// Get a reference point for forming triangles.
// Use the first vertex to reduce round-off errors.
var s = poly.Vertices[0];
const float k_inv3 = 1.0f / 3.0f;
for (var i = 0; i < count; ++i)
{
// Triangle vertices.
var e1 = poly.Vertices[i] - s;
var e2 = i + 1 < count ? poly.Vertices[i+1] - s : poly.Vertices[0] - s;
var D = Vector2Helpers.Cross(e1, e2);
var triangleArea = 0.5f * D;
area += triangleArea;
// Area weighted centroid
center += (e1 + e2) * triangleArea * k_inv3;
float ex1 = e1.X, ey1 = e1.Y;
float ex2 = e2.X, ey2 = e2.Y;
var intx2 = ex1*ex1 + ex2*ex1 + ex2*ex2;
var inty2 = ey1*ey1 + ey2*ey1 + ey2*ey2;
I += (0.25f * k_inv3 * D) * (intx2 + inty2);
}
// Total mass
data.Mass = density * area;
// Center of mass
DebugTools.Assert(area > float.Epsilon);
center *= 1.0f / area;
data.Center = center + s;
// Inertia tensor relative to the local origin (point s).
data.I = density * I;
// Shift to center of mass then to original body origin.
data.I += data.Mass * (Vector2.Dot(data.Center, data.Center) - Vector2.Dot(center, center));
break;
default:
throw new NotImplementedException($"Cannot get MassData for {shape} as it's not implemented!");
}
GetMassData(shape, ref data, density);
return data;
}

View File

@@ -19,6 +19,7 @@ public abstract partial class SharedJointSystem : EntitySystem
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private EntityQuery<JointComponent> _jointsQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
@@ -237,7 +238,7 @@ public abstract partial class SharedJointSystem : EntitySystem
string? id = null,
TransformComponent? xformA = null,
TransformComponent? xformB = null,
int? minimumDistance = null)
float? minimumDistance = null)
{
if (!Resolve(bodyA, ref xformA) || !Resolve(bodyB, ref xformB))
{
@@ -247,8 +248,8 @@ public abstract partial class SharedJointSystem : EntitySystem
anchorA ??= Vector2.Zero;
anchorB ??= Vector2.Zero;
var vecA = Vector2.Transform(anchorA.Value, xformA.WorldMatrix);
var vecB = Vector2.Transform(anchorB.Value, xformB.WorldMatrix);
var vecA = Vector2.Transform(anchorA.Value, _transform.GetWorldMatrix(xformA));
var vecB = Vector2.Transform(anchorB.Value, _transform.GetWorldMatrix(xformB));
var length = (vecA - vecB).Length();
if (minimumDistance != null)
length = Math.Max(minimumDistance.Value, length);

View File

@@ -113,10 +113,7 @@ public abstract partial class SharedPhysicsSystem
#if DEBUG
contact._debugPhysics = _debugPhysicsSystem;
#endif
contact.Manifold = new Manifold
{
Points = new ManifoldPoint[2]
};
contact.Manifold = new Manifold();
return contact;
}

View File

@@ -1026,12 +1026,6 @@ public abstract partial class SharedPhysicsSystem
var angle = angles[offset + i];
var xform = xformQuery.GetComponent(uid);
// Temporary NaN guards until PVS is fixed.
if (!float.IsNaN(position.X) && !float.IsNaN(position.Y))
{
_transform.SetLocalPositionRotation(xform, xform.LocalPosition + position, xform.LocalRotation + angle);
}
var linVelocity = linearVelocities[offset + i];
var physicsDirtied = false;
@@ -1047,6 +1041,13 @@ public abstract partial class SharedPhysicsSystem
physicsDirtied |= SetAngularVelocity(uid, angVelocity, false, body: body);
}
// Temporary NaN guards until PVS is fixed.
// May reparent object and change body's velocity.
if (!float.IsNaN(position.X) && !float.IsNaN(position.Y))
{
_transform.SetLocalPositionRotation(xform, xform.LocalPosition + position, xform.LocalRotation + angle);
}
if (physicsDirtied)
Dirty(uid, body);
}

View File

@@ -67,7 +67,6 @@ public abstract partial class SharedPhysicsSystem
velocityConstraint.TangentSpeed = contact.TangentSpeed;
velocityConstraint.IndexA = bodyA.IslandIndex[island.Index];
velocityConstraint.IndexB = bodyB.IslandIndex[island.Index];
Array.Resize(ref velocityConstraint.Points, 2);
// Don't need to reset point data as it all gets set below.
var (invMassA, invMassB) = GetInvMass(bodyA, bodyB);
@@ -87,7 +86,6 @@ public abstract partial class SharedPhysicsSystem
(positionConstraint.InvMassA, positionConstraint.InvMassB) = (invMassA, invMassB);
positionConstraint.LocalCenterA = bodyA.LocalCenter;
positionConstraint.LocalCenterB = bodyB.LocalCenter;
Array.Resize(ref positionConstraint.LocalPoints, 2);
positionConstraint.InvIA = bodyA.InvI;
positionConstraint.InvIB = bodyB.InvI;
@@ -97,11 +95,14 @@ public abstract partial class SharedPhysicsSystem
positionConstraint.RadiusA = radiusA;
positionConstraint.RadiusB = radiusB;
positionConstraint.Type = manifold.Type;
var points = manifold.Points.AsSpan;
var posPoints = positionConstraint.LocalPoints.AsSpan;
var velPoints = velocityConstraint.Points.AsSpan;
for (var j = 0; j < pointCount; ++j)
{
var contactPoint = manifold.Points[j];
ref var constraintPoint = ref velocityConstraint.Points[j];
var contactPoint = points[j];
ref var constraintPoint = ref velPoints[j];
if (_warmStarting)
{
@@ -120,7 +121,7 @@ public abstract partial class SharedPhysicsSystem
constraintPoint.TangentMass = 0.0f;
constraintPoint.VelocityBias = 0.0f;
positionConstraint.LocalPoints[j] = contactPoint.LocalPoint;
posPoints[j] = contactPoint.LocalPoint;
}
}
}
@@ -220,10 +221,11 @@ public abstract partial class SharedPhysicsSystem
velocityConstraint.Normal = normal;
int pointCount = velocityConstraint.PointCount;
var velPoints = velocityConstraint.Points.AsSpan;
for (int j = 0; j < pointCount; ++j)
{
ref var vcp = ref velocityConstraint.Points[j];
ref var vcp = ref velPoints[j];
vcp.RelativeVelocityA = points[j] - centerA;
vcp.RelativeVelocityB = points[j] - centerB;
@@ -256,8 +258,8 @@ public abstract partial class SharedPhysicsSystem
// If we have two points, then prepare the block solver.
if (velocityConstraint.PointCount == 2)
{
var vcp1 = velocityConstraint.Points[0];
var vcp2 = velocityConstraint.Points[1];
var vcp1 = velocityConstraint.Points._00;
var vcp2 = velocityConstraint.Points._01;
var rn1A = Vector2Helpers.Cross(vcp1.RelativeVelocityA, velocityConstraint.Normal);
var rn1B = Vector2Helpers.Cross(vcp1.RelativeVelocityB, velocityConstraint.Normal);
@@ -299,6 +301,7 @@ public abstract partial class SharedPhysicsSystem
for (var i = 0; i < island.Contacts.Count; ++i)
{
var velocityConstraint = velocityConstraints[i];
var velPoints = velocityConstraint.Points.AsSpan;
var indexA = velocityConstraint.IndexA;
var indexB = velocityConstraint.IndexB;
@@ -318,7 +321,7 @@ public abstract partial class SharedPhysicsSystem
for (var j = 0; j < pointCount; ++j)
{
var constraintPoint = velocityConstraint.Points[j];
var constraintPoint = velPoints[j];
var P = normal * constraintPoint.NormalImpulse + tangent * constraintPoint.TangentImpulse;
angVelocityA -= invIA * Vector2Helpers.Cross(constraintPoint.RelativeVelocityA, P);
linVelocityA -= P * invMassA;
@@ -386,12 +389,13 @@ public abstract partial class SharedPhysicsSystem
var friction = velocityConstraint.Friction;
DebugTools.Assert(pointCount is 1 or 2);
var velPoints = velocityConstraint.Points.AsSpan;
// Solve tangent constraints first because non-penetration is more important
// than friction.
for (var j = 0; j < pointCount; ++j)
{
ref var velConstraintPoint = ref velocityConstraint.Points[j];
ref var velConstraintPoint = ref velPoints[j];
// Relative velocity at contact
var dv = vB + Vector2Helpers.Cross(wB, velConstraintPoint.RelativeVelocityB) - vA - Vector2Helpers.Cross(wA, velConstraintPoint.RelativeVelocityA);
@@ -419,7 +423,7 @@ public abstract partial class SharedPhysicsSystem
// Solve normal constraints
if (velocityConstraint.PointCount == 1)
{
ref var vcp = ref velocityConstraint.Points[0];
ref var vcp = ref velocityConstraint.Points._00;
// Relative velocity at contact
Vector2 dv = vB + Vector2Helpers.Cross(wB, vcp.RelativeVelocityB) - vA - Vector2Helpers.Cross(wA, vcp.RelativeVelocityA);
@@ -476,8 +480,8 @@ public abstract partial class SharedPhysicsSystem
// = A * x + b'
// b' = b - A * a;
ref var cp1 = ref velocityConstraint.Points[0];
ref var cp2 = ref velocityConstraint.Points[1];
ref var cp1 = ref velocityConstraint.Points._00;
ref var cp2 = ref velocityConstraint.Points._01;
Vector2 a = new Vector2(cp1.NormalImpulse, cp2.NormalImpulse);
DebugTools.Assert(a.X >= 0.0f && a.Y >= 0.0f);
@@ -643,14 +647,16 @@ public abstract partial class SharedPhysicsSystem
{
for (var i = 0; i < island.Contacts.Count; ++i)
{
ContactVelocityConstraint velocityConstraint = velocityConstraints[i];
var velocityConstraint = velocityConstraints[i];
ref var manifold = ref island.Contacts[velocityConstraint.ContactIndex].Manifold;
var manPoints = manifold.Points.AsSpan;
var velPoints = velocityConstraint.Points.AsSpan;
for (var j = 0; j < velocityConstraint.PointCount; ++j)
{
ref var point = ref manifold.Points[j];
point.NormalImpulse = velocityConstraint.Points[j].NormalImpulse;
point.TangentImpulse = velocityConstraint.Points[j].TangentImpulse;
ref var point = ref manPoints[j];
point.NormalImpulse = velPoints[j].NormalImpulse;
point.TangentImpulse = velPoints[j].TangentImpulse;
}
}
}
@@ -794,7 +800,7 @@ public abstract partial class SharedPhysicsSystem
{
normal = new Vector2(1.0f, 0.0f);
Vector2 pointA = Physics.Transform.Mul(xfA, manifold.LocalPoint);
Vector2 pointB = Physics.Transform.Mul(xfB, manifold.Points[0].LocalPoint);
Vector2 pointB = Physics.Transform.Mul(xfB, manifold.Points._00.LocalPoint);
if ((pointA - pointB).LengthSquared() > float.Epsilon * float.Epsilon)
{
@@ -812,10 +818,11 @@ public abstract partial class SharedPhysicsSystem
{
normal = Physics.Transform.Mul(xfA.Quaternion2D, manifold.LocalNormal);
Vector2 planePoint = Physics.Transform.Mul(xfA, manifold.LocalPoint);
var manPoints = manifold.Points.AsSpan;
for (int i = 0; i < manifold.PointCount; ++i)
{
Vector2 clipPoint = Physics.Transform.Mul(xfB, manifold.Points[i].LocalPoint);
Vector2 clipPoint = Physics.Transform.Mul(xfB, manPoints[i].LocalPoint);
Vector2 cA = clipPoint + normal * (radiusA - Vector2.Dot(clipPoint - planePoint, normal));
Vector2 cB = clipPoint - normal * radiusB;
points[i] = (cA + cB) * 0.5f;
@@ -827,10 +834,11 @@ public abstract partial class SharedPhysicsSystem
{
normal = Physics.Transform.Mul(xfB.Quaternion2D, manifold.LocalNormal);
Vector2 planePoint = Physics.Transform.Mul(xfB, manifold.LocalPoint);
var manPoints = manifold.Points.AsSpan;
for (int i = 0; i < manifold.PointCount; ++i)
{
Vector2 clipPoint = Physics.Transform.Mul(xfA, manifold.Points[i].LocalPoint);
Vector2 clipPoint = Physics.Transform.Mul(xfA, manPoints[i].LocalPoint);
Vector2 cB = clipPoint + normal * (radiusB - Vector2.Dot(clipPoint - planePoint, normal));
Vector2 cA = clipPoint - normal * radiusA;
points[i] = (cA + cB) * 0.5f;
@@ -863,7 +871,7 @@ public abstract partial class SharedPhysicsSystem
case ManifoldType.Circles:
{
Vector2 pointA = Physics.Transform.Mul(xfA, pc.LocalPoint);
Vector2 pointB = Physics.Transform.Mul(xfB, pc.LocalPoints[0]);
Vector2 pointB = Physics.Transform.Mul(xfB, pc.LocalPoints._00);
normal = pointB - pointA;
//FPE: Fix to handle zero normalization
@@ -877,10 +885,11 @@ public abstract partial class SharedPhysicsSystem
case ManifoldType.FaceA:
{
var pcPoints = pc.LocalPoints.AsSpan;
normal = Physics.Transform.Mul(xfA.Quaternion2D, pc.LocalNormal);
Vector2 planePoint = Physics.Transform.Mul(xfA, pc.LocalPoint);
Vector2 clipPoint = Physics.Transform.Mul(xfB, pc.LocalPoints[index]);
Vector2 clipPoint = Physics.Transform.Mul(xfB, pcPoints[index]);
separation = Vector2.Dot(clipPoint - planePoint, normal) - pc.RadiusA - pc.RadiusB;
point = clipPoint;
}
@@ -888,10 +897,11 @@ public abstract partial class SharedPhysicsSystem
case ManifoldType.FaceB:
{
var pcPoints = pc.LocalPoints.AsSpan;
normal = Physics.Transform.Mul(xfB.Quaternion2D, pc.LocalNormal);
Vector2 planePoint = Physics.Transform.Mul(xfB, pc.LocalPoint);
Vector2 clipPoint = Physics.Transform.Mul(xfA, pc.LocalPoints[index]);
Vector2 clipPoint = Physics.Transform.Mul(xfA, pcPoints[index]);
separation = Vector2.Dot(clipPoint - planePoint, normal) - pc.RadiusA - pc.RadiusB;
point = clipPoint;

View File

@@ -408,16 +408,26 @@ namespace Robust.Shared.Prototypes
if (nonPushedParent)
continue;
var parentMaps = new MappingDataNode[parents.Length];
for (var i = 0; i < parentMaps.Length; i++)
if (parents.Length == 1)
{
parentMaps[i] = kindData.Results[parents[i]];
kindData.Results[id] = _serializationManager.PushCompositionWithGenericNode(
kind,
kindData.Results[parents[0]],
kindData.RawResults[id]);
}
else
{
var parentMaps = new MappingDataNode[parents.Length];
for (var i = 0; i < parentMaps.Length; i++)
{
parentMaps[i] = kindData.Results[parents[i]];
}
kindData.Results[id] = _serializationManager.PushCompositionWithGenericNode(
kind,
parentMaps,
kindData.RawResults[id]);
kindData.Results[id] = _serializationManager.PushCompositionWithGenericNode(
kind,
parentMaps,
kindData.RawResults[id]);
}
}
else
{
@@ -629,16 +639,26 @@ namespace Robust.Shared.Prototypes
{
if (tree.TryGetParents(id, out var parents))
{
var parentNodes = new MappingDataNode[parents.Length];
for (var i = 0; i < parents.Length; i++)
if (parents.Length == 1)
{
parentNodes[i] = results[parents[i]].Result;
datum.Result = _serializationManager.PushCompositionWithGenericNode(
kind,
results[parents[0]].Result,
datum.Result);
}
else
{
var parentNodes = new MappingDataNode[parents.Length];
for (var i = 0; i < parents.Length; i++)
{
parentNodes[i] = results[parents[i]].Result;
}
datum.Result = _serializationManager.PushCompositionWithGenericNode(
kind,
parentNodes,
datum.Result);
datum.Result = _serializationManager.PushCompositionWithGenericNode(
kind,
parentNodes,
datum.Result);
}
}
if (tree.TryGetChildren(id, out var children))

View File

@@ -421,6 +421,7 @@ namespace Robust.Shared.Serialization.Manager
#region Composition
DataNode PushComposition(Type type, DataNode[] parents, DataNode child, ISerializationContext? context = null);
DataNode PushComposition(Type type, DataNode parent, DataNode child, ISerializationContext? context = null);
public TNode PushComposition<TType, TNode>(TNode[] parents, TNode child, ISerializationContext? context = null) where TNode : DataNode
{
@@ -428,6 +429,12 @@ namespace Robust.Shared.Serialization.Manager
return (TNode)PushComposition(typeof(TType), parents, child, context);
}
public TNode PushComposition<TType, TNode>(TNode parent, TNode child, ISerializationContext? context = null)
where TNode : DataNode
{
return (TNode) PushComposition(typeof(TType), parent, child, context);
}
TNode PushInheritance<TType, TNode>(ITypeInheritanceHandler<TType, TNode> inheritanceHandler, TNode parent, TNode child,
ISerializationContext? context = null) where TNode : DataNode;
@@ -441,6 +448,12 @@ namespace Robust.Shared.Serialization.Manager
return (TNode) PushComposition(type, parents, child, context);
}
public TNode PushCompositionWithGenericNode<TNode>(Type type, TNode parent, TNode child, ISerializationContext? context = null)
where TNode : DataNode
{
return (TNode) PushComposition(type, parent, child, context);
}
/// <summary>
/// Simple <see cref="MappingDataNode"/> inheritance pusher clones data and overrides a parent's values with
/// the child's.

View File

@@ -24,27 +24,23 @@ public partial class SerializationManager
private readonly ConcurrentDictionary<(Type value, Type node), PushCompositionDelegate> _compositionPushers = new();
public DataNode PushComposition(Type type, DataNode[] parents, DataNode child, ISerializationContext? context = null)
public DataNode PushComposition(
Type type,
DataNode[] parents,
DataNode child,
ISerializationContext? context = null)
{
// TODO SERIALIZATION
// Add variant that doesn't require a parent array.
// TODO SERIALIZATION
// Change inheritance pushing so that it modifies the passed in child. This avoids re-creating the child
// multiple times when there are multiple children.
// multiple times when there are multiple parents.
//
// I.e., change the PushCompositionDelegate signature to not have a return value, and also add an override
// of this method that modified the given child.
// of this method that modifies the given child.
if (parents.Length == 0)
return child.Copy();
DebugTools.Assert(parents.All(x => x.GetType() == child.GetType()));
// the child.Clone() statement to the beginning here, then make the delegate modify the clone.
// Currently pusing more than one parent requires multiple unnecessary clones.
var pusher = GetOrCreatePushCompositionDelegate(type, child);
var node = child;
@@ -61,6 +57,22 @@ public partial class SerializationManager
return node;
}
public DataNode PushComposition(
Type type,
DataNode parent,
DataNode child,
ISerializationContext? context = null)
{
DebugTools.AssertEqual(parent.GetType(), child.GetType());
var pusher = GetOrCreatePushCompositionDelegate(type, child);
var newNode = pusher(type, parent, child, context);
// Currently delegate pusher should be returning a new instance, and not modifying the passed in child.
DebugTools.Assert(!ReferenceEquals(newNode, child));
return newNode;
}
private PushCompositionDelegate GetOrCreatePushCompositionDelegate(Type type, DataNode node)
{
return _compositionPushers.GetOrAdd((type, node.GetType()), static (tuple, vfArgument) =>
@@ -185,7 +197,7 @@ public partial class SerializationManager
{
if (field.InheritanceBehavior == InheritanceBehavior.Always)
{
newMapping[key] = PushComposition(field.FieldType, new[] { parentValue }, childValue, context);
newMapping[key] = PushComposition(field.FieldType, parentValue, childValue, context);
}
}
else

View File

@@ -207,7 +207,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations
{
newCompReg[idx] = serializationManager.PushCompositionWithGenericNode(
reg.Type,
new[] { parent[mapping] },
parent[mapping],
newCompReg[idx],
context);

View File

@@ -0,0 +1,11 @@
using System.Globalization;
namespace Robust.Shared.Utility;
public static class CultureInfoExtension
{
public static bool NameEquals(this CultureInfo cultureInfo, CultureInfo otherCultureInfo)
{
return cultureInfo.Name == otherCultureInfo.Name;
}
}

View File

@@ -79,12 +79,34 @@ namespace Robust.Shared.Utility
}
}
internal struct FixedArray2<T>
internal struct FixedArray2<T> : IEquatable<FixedArray2<T>>
{
public T _00;
public T _01;
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 2);
internal FixedArray2(T x0, T x1)
{
_00 = x0;
_01 = x1;
}
public bool Equals(FixedArray2<T> other)
{
return EqualityComparer<T>.Default.Equals(_00, other._00) &&
EqualityComparer<T>.Default.Equals(_01, other._01);
}
public override bool Equals(object? obj)
{
return obj is FixedArray2<T> other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(_00, _01);
}
}
internal struct FixedArray4<T> : IEquatable<FixedArray4<T>>
@@ -96,12 +118,20 @@ namespace Robust.Shared.Utility
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 4);
internal FixedArray4(T x0, T x1, T x2, T x3)
{
_00 = x0;
_01 = x1;
_02 = x2;
_03 = x3;
}
public bool Equals(FixedArray4<T> other)
{
return _00?.Equals(other._00) == true &&
_01?.Equals(other._01) == true &&
_02?.Equals(other._02) == true &&
_03?.Equals(other._03) == true;
return EqualityComparer<T>.Default.Equals(_00, other._00) &&
EqualityComparer<T>.Default.Equals(_01, other._01) &&
EqualityComparer<T>.Default.Equals(_02, other._02) &&
EqualityComparer<T>.Default.Equals(_03, other._03);
}
public override bool Equals(object? obj)
@@ -128,16 +158,28 @@ namespace Robust.Shared.Utility
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 8);
internal FixedArray8(T x0, T x1, T x2, T x3, T x4, T x5, T x6, T x7)
{
_00 = x0;
_01 = x1;
_02 = x2;
_03 = x3;
_04 = x4;
_05 = x5;
_06 = x6;
_07 = x7;
}
public bool Equals(FixedArray8<T> other)
{
return _00?.Equals(other._00) == true &&
_01?.Equals(other._01) == true &&
_02?.Equals(other._02) == true &&
_03?.Equals(other._03) == true &&
_04?.Equals(other._04) == true &&
_05?.Equals(other._05) == true &&
_06?.Equals(other._06) == true &&
_07?.Equals(other._07) == true;
return EqualityComparer<T>.Default.Equals(_00, other._00) &&
EqualityComparer<T>.Default.Equals(_01, other._01) &&
EqualityComparer<T>.Default.Equals(_02, other._02) &&
EqualityComparer<T>.Default.Equals(_03, other._03) &&
EqualityComparer<T>.Default.Equals(_04, other._04) &&
EqualityComparer<T>.Default.Equals(_05, other._05) &&
EqualityComparer<T>.Default.Equals(_06, other._06) &&
EqualityComparer<T>.Default.Equals(_07, other._07);
}
public override bool Equals(object? obj)

View File

@@ -281,13 +281,28 @@ public sealed partial class FormattedMessage : IEquatable<FormattedMessage>, IRe
/// <inheritdoc />
public bool Equals(FormattedMessage? other)
{
return other?.ToMarkup() == ToMarkup();
if (_nodes.Count != other?._nodes.Count)
return false;
for (var i = 0; i < _nodes.Count; i++)
{
if (!_nodes[i].Equals(other?._nodes[i]))
return false;
}
return true;
}
/// <inheritdoc />
public override int GetHashCode()
{
return ToMarkup().GetHashCode();
var hash = 0;
foreach (var node in _nodes)
{
hash = HashCode.Combine(hash, node.GetHashCode());
}
return hash;
}
/// <returns>The string without markup tags.</returns>

View File

@@ -36,7 +36,7 @@ public sealed partial class EntityManagerCopyTests
var targetComp = entManager.CopyComponent(original, target, comp);
Assert.That(targetComp!.Owner == target);
Assert.That(entManager.GetComponent<AComponent>(target), Is.EqualTo(targetComp));
Assert.That(targetComp.Value, Is.EqualTo(comp.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
}
@@ -67,7 +67,7 @@ public sealed partial class EntityManagerCopyTests
var targetComp = entManager.CopyComponent(original, target, (IComponent) comp);
Assert.That(targetComp!.Owner == target);
Assert.That(entManager.GetComponent<AComponent>(target), Is.EqualTo(targetComp));
Assert.That(((AComponent) targetComp).Value, Is.EqualTo(comp.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
}
@@ -102,10 +102,10 @@ public sealed partial class EntityManagerCopyTests
var targetComp = entManager.GetComponent<AComponent>(target);
var targetComp2 = entManager.GetComponent<BComponent>(target);
Assert.That(targetComp!.Owner == target);
Assert.That(entManager.GetComponent<AComponent>(target), Is.EqualTo(targetComp));
Assert.That(targetComp.Value, Is.EqualTo(comp.Value));
Assert.That(targetComp2!.Owner == target);
Assert.That(entManager.GetComponent<BComponent>(target), Is.EqualTo(targetComp2));
Assert.That(targetComp2.Value, Is.EqualTo(comp2.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
@@ -142,16 +142,16 @@ public sealed partial class EntityManagerCopyTests
var targetComp = entManager.GetComponent<AComponent>(target);
var targetComp2 = entManager.GetComponent<BComponent>(target);
Assert.That(targetComp!.Owner == target);
Assert.That(entManager.GetComponent<AComponent>(target), Is.EqualTo(targetComp));
Assert.That(targetComp.Value, Is.EqualTo(comp.Value));
Assert.That(targetComp2!.Owner == target);
Assert.That(entManager.GetComponent<BComponent>(target), Is.EqualTo(targetComp2));
Assert.That(targetComp2.Value, Is.EqualTo(comp2.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
Assert.That(!ReferenceEquals(comp2, targetComp2));
}
[DataDefinition]
private sealed partial class AComponent : Component
{

View File

@@ -97,10 +97,11 @@ namespace Robust.UnitTesting.Shared.Map
public void NoParent_OffsetZero()
{
var entMan = IoCManager.Resolve<IEntityManager>();
var xformSys = entMan.System<SharedTransformSystem>();
var uid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var xform = entMan.GetComponent<TransformComponent>(uid);
Assert.That(xform.Coordinates.Position, Is.EqualTo(Vector2.Zero));
xform.LocalPosition = Vector2.One;
xformSys.SetLocalPosition(uid, Vector2.One);
Assert.That(xform.Coordinates.Position, Is.EqualTo(Vector2.Zero));
}
@@ -108,12 +109,13 @@ namespace Robust.UnitTesting.Shared.Map
public void GetGridId_Map()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var newEnt = entityManager.CreateEntityUninitialized(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(mapEnt).Coordinates.GetGridUid(entityManager), Is.Null);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.GetGridUid(entityManager), Is.Null);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.EntityId, Is.EqualTo(mapEnt));
Assert.That(xformSys.GetGrid(entityManager.GetComponent<TransformComponent>(mapEnt).Coordinates), Is.Null);
Assert.That(xformSys.GetGrid(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates), Is.Null);
Assert.That(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates.EntityId, Is.EqualTo(mapEnt));
}
[Test]
@@ -121,6 +123,7 @@ namespace Robust.UnitTesting.Shared.Map
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
@@ -128,20 +131,21 @@ namespace Robust.UnitTesting.Shared.Map
var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(gridEnt, Vector2.Zero));
// Grids aren't parented to other grids.
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(gridEnt).Coordinates.GetGridUid(entityManager), Is.Null);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.GetGridUid(entityManager), Is.EqualTo(grid.Owner));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.EntityId, Is.EqualTo(gridEnt));
Assert.That(xformSys.GetGrid(entityManager.GetComponent<TransformComponent>(gridEnt).Coordinates), Is.Null);
Assert.That(xformSys.GetGrid(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates), Is.EqualTo(grid.Owner));
Assert.That(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates.EntityId, Is.EqualTo(gridEnt));
}
[Test]
public void GetMapId_Map()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var newEnt = entityManager.CreateEntityUninitialized(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(mapEnt).Coordinates.GetMapId(entityManager), Is.EqualTo(mapId));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.GetMapId(entityManager), Is.EqualTo(mapId));
Assert.That(xformSys.GetMapId(entityManager.GetComponent<TransformComponent>(mapEnt).Coordinates), Is.EqualTo(mapId));
Assert.That(xformSys.GetMapId(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates), Is.EqualTo(mapId));
}
[Test]
@@ -149,14 +153,15 @@ namespace Robust.UnitTesting.Shared.Map
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(gridEnt, Vector2.Zero));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(gridEnt).Coordinates.GetMapId(entityManager), Is.EqualTo(mapId));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.GetMapId(entityManager), Is.EqualTo(mapId));
Assert.That(xformSys.GetMapId(entityManager.GetComponent<TransformComponent>(gridEnt).Coordinates), Is.EqualTo(mapId));
Assert.That(xformSys.GetMapId(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates), Is.EqualTo(mapId));
}
[Test]
@@ -164,6 +169,7 @@ namespace Robust.UnitTesting.Shared.Map
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
@@ -175,7 +181,7 @@ namespace Robust.UnitTesting.Shared.Map
Assert.That(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates.EntityId, Is.EqualTo(gridEnt));
// Reparenting the entity should return correct results.
entityManager.GetComponent<TransformComponent>(newEnt).AttachParent(mapEnt);
xformSys.SetParent(newEnt, mapEnt);
Assert.That(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates.EntityId, Is.EqualTo(mapEnt));
}
@@ -185,6 +191,7 @@ namespace Robust.UnitTesting.Shared.Map
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
@@ -205,7 +212,7 @@ namespace Robust.UnitTesting.Shared.Map
Assert.That(newEntCoords.EntityId, Is.EqualTo(gridEnt));
// Reparenting the entity should return correct results.
newEntTransform.AttachParent(mapEnt);
xformSys.SetParent(newEnt, mapEnt);
var newEntCoords2 = newEntTransform.Coordinates;
Assert.That(newEntCoords2.IsValid(entityManager), Is.EqualTo(true));
@@ -235,19 +242,18 @@ namespace Robust.UnitTesting.Shared.Map
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var transformSystem = entityManager.System<SharedTransformSystem>();
var xformSys = entityManager.System<SharedTransformSystem>();
entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, entPos));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.ToMap(entityManager, transformSystem), Is.EqualTo(new MapCoordinates(entPos, mapId)));
Assert.That(xformSys.ToMapCoordinates(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates), Is.EqualTo(new MapCoordinates(entPos, mapId)));
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(gridEnt).LocalPosition += gridPos;
xformSys.SetLocalPosition(gridEnt, entityManager.GetComponent<TransformComponent>(gridEnt).LocalPosition + gridPos);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.ToMap(entityManager, transformSystem), Is.EqualTo(new MapCoordinates(entPos + gridPos, mapId)));
Assert.That(xformSys.ToMapCoordinates(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates), Is.EqualTo(new MapCoordinates(entPos + gridPos, mapId)));
}
[Test]
@@ -255,35 +261,38 @@ namespace Robust.UnitTesting.Shared.Map
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, Vector2.Zero));
var newEntXform = entityManager.GetComponent<TransformComponent>(newEnt);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.WithEntityId(mapEnt).Position, Is.EqualTo(Vector2.Zero));
Assert.That(xformSys.WithEntityId(newEntXform.Coordinates, mapEnt).Position, Is.EqualTo(Vector2.Zero));
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).LocalPosition = Vector2.One;
xformSys.SetLocalPosition(newEnt, Vector2.One);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.Position, Is.EqualTo(Vector2.One));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.WithEntityId(mapEnt).Position, Is.EqualTo(Vector2.One));
Assert.That(newEntXform.Coordinates.Position, Is.EqualTo(Vector2.One));
Assert.That(xformSys.WithEntityId(newEntXform.Coordinates, mapEnt).Position, Is.EqualTo(Vector2.One));
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(gridEnt).LocalPosition = Vector2.One;
xformSys.SetLocalPosition(gridEnt, Vector2.One);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.Position, Is.EqualTo(Vector2.One));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.WithEntityId(mapEnt).Position, Is.EqualTo(new Vector2(2, 2)));
Assert.That(newEntXform.Coordinates.Position, Is.EqualTo(Vector2.One));
Assert.That(xformSys.WithEntityId(newEntXform.Coordinates, mapEnt).Position, Is.EqualTo(new Vector2(2, 2)));
var newEntTwo = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(newEnt, Vector2.Zero));
var newEntTwoXform = entityManager.GetComponent<TransformComponent>(newEntTwo);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEntTwo).Coordinates.Position, Is.EqualTo(Vector2.Zero));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEntTwo).Coordinates.WithEntityId(mapEnt).Position, Is.EqualTo(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.WithEntityId(mapEnt).Position));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEntTwo).Coordinates.WithEntityId(gridEnt).Position, Is.EqualTo(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates.Position));
Assert.That(newEntTwoXform.Coordinates.Position, Is.EqualTo(Vector2.Zero));
Assert.That(xformSys.WithEntityId(newEntTwoXform.Coordinates, mapEnt).Position, Is.EqualTo(xformSys.WithEntityId(newEntXform.Coordinates, mapEnt).Position));
Assert.That(xformSys.WithEntityId(newEntTwoXform.Coordinates, gridEnt).Position, Is.EqualTo(newEntXform.Coordinates.Position));
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEntTwo).LocalPosition = -Vector2.One;
xformSys.SetLocalPosition(newEntTwo, -Vector2.One);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEntTwo).Coordinates.Position, Is.EqualTo(-Vector2.One));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEntTwo).Coordinates.WithEntityId(mapEnt).Position, Is.EqualTo(Vector2.One));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEntTwo).Coordinates.WithEntityId(gridEnt).Position, Is.EqualTo(Vector2.Zero));
Assert.That(newEntTwoXform.Coordinates.Position, Is.EqualTo(-Vector2.One));
Assert.That(xformSys.WithEntityId(newEntTwoXform.Coordinates, mapEnt).Position, Is.EqualTo(Vector2.One));
Assert.That(xformSys.WithEntityId(newEntTwoXform.Coordinates, gridEnt).Position, Is.EqualTo(Vector2.Zero));
}
}
}

View File

@@ -0,0 +1,223 @@
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture, TestOf(typeof(SharedPhysicsSystem))]
public sealed class GridReparentVelocity_Test
{
private ISimulation _sim = default!;
private IEntitySystemManager _systems = default!;
private IEntityManager _entManager = default!;
private IMapManager _mapManager = default!;
private FixtureSystem _fixtureSystem = default!;
private SharedMapSystem _mapSystem = default!;
private SharedPhysicsSystem _physSystem = default!;
// Test objects.
private EntityUid _mapUid = default!;
private MapId _mapId = default!;
private EntityUid _gridUid = default!;
private EntityUid _objUid = default!;
[OneTimeSetUp]
public void FixtureSetup()
{
_sim = RobustServerSimulation.NewSimulation()
.InitializeInstance();
_systems = _sim.Resolve<IEntitySystemManager>();
_entManager = _sim.Resolve<IEntityManager>();
_mapManager = _sim.Resolve<IMapManager>();
_fixtureSystem = _systems.GetEntitySystem<FixtureSystem>();
_mapSystem = _systems.GetEntitySystem<SharedMapSystem>();
_physSystem = _systems.GetEntitySystem<SharedPhysicsSystem>();
}
[SetUp]
public void Setup()
{
_mapUid = _mapSystem.CreateMap(out _mapId);
// Spawn a 1x1 grid centered at (0.5, 0.5), ensure it's movable and its velocity has no damping.
var gridEnt = _mapManager.CreateGridEntity(_mapId);
var gridPhys = _entManager.GetComponent<PhysicsComponent>(gridEnt);
_physSystem.SetSleepingAllowed(gridEnt, gridPhys, false);
_physSystem.SetBodyType(gridEnt, BodyType.Dynamic, body: gridPhys);
_physSystem.SetLinearDamping(gridEnt, gridPhys, 0.0f);
_physSystem.SetAngularDamping(gridEnt, gridPhys, 0.0f);
_mapSystem.SetTile(gridEnt, Vector2i.Zero, new Tile(1));
_physSystem.WakeBody(gridEnt, body: gridPhys);
_gridUid = gridEnt.Owner;
}
// Spawn a bullet-like test object at the given position.
public EntityUid SetupTestObject(EntityCoordinates coords)
{
var obj = _entManager.SpawnEntity(null, coords);
var objPhys = _entManager.EnsureComponent<PhysicsComponent>(obj);
var objFix = _entManager.EnsureComponent<FixturesComponent>(obj);
// Set up physics (no velocity damping, dynamic body, physics enabled)
_entManager.GetComponent<PhysicsComponent>(obj);
_physSystem.SetSleepingAllowed(obj, objPhys, false);
_physSystem.SetBodyType(obj, BodyType.Dynamic, body: objPhys);
_physSystem.SetLinearDamping(obj, objPhys, 0.0f);
_physSystem.SetAngularDamping(obj, objPhys, 0.0f);
// Set up fixture.
var poly = new PolygonShape();
poly.SetAsBox(0.1f, 0.1f);
_fixtureSystem.CreateFixture(obj, "fix1", new Fixture(poly, 0, 0, false), manager: objFix, body: objPhys);
_physSystem.WakeBody(obj, body: objPhys);
return obj;
}
[TearDown]
public void Teardown()
{
_entManager.DeleteEntity(_gridUid);
_gridUid = default!;
_entManager.DeleteEntity(_objUid);
_objUid = default!;
_mapSystem.DeleteMap(_mapId);
_mapId = default!;
_entManager.DeleteEntity(_mapUid);
}
// Moves an object off of a moving grid, checks for conservation of linear velocity.
[Test]
public void TestLinearVelocityOnlyMoveOffGrid()
{
// Spawn our test object in the middle of the grid, ensure it has no damping.
_objUid = SetupTestObject(new EntityCoordinates(_gridUid, 0.5f, 0.5f));
Assert.Multiple(() =>
{
// Our object should start on the grid.
Assert.That(_entManager.GetComponent<TransformComponent>(_objUid).ParentUid, Is.EqualTo(_gridUid));
// Set the velocity of the grid and our object.
Assert.That(_physSystem.SetLinearVelocity(_objUid, new Vector2(3.5f, 4.75f)), Is.True);
Assert.That(_physSystem.SetLinearVelocity(_gridUid, new Vector2(1.0f, 2.0f)), Is.True);
// Wait a second to clear the grid
_physSystem.Update(1.0f);
// The object should be parented to the map and maintain its map velocity, the grid should be unchanged.
var objXform = _entManager.GetComponent<TransformComponent>(_objUid);
var gridXform = _entManager.GetComponent<TransformComponent>(_gridUid);
Assert.That(objXform.ParentUid, Is.EqualTo(_mapUid), $"Object is not on map - actual position: {objXform.ParentUid} {objXform.LocalPosition}, grid position: {gridXform.ParentUid} {gridXform.LocalPosition}");
Assert.That(_entManager.GetComponent<PhysicsComponent>(_objUid).LinearVelocity, Is.EqualTo(new Vector2(4.5f, 6.75f)));
Assert.That(_entManager.GetComponent<PhysicsComponent>(_gridUid).LinearVelocity, Is.EqualTo(new Vector2(1.0f, 2.0f)));
});
}
[Test]
// Moves an object onto a moving grid, checks for conservation of linear velocity.
public void TestLinearVelocityOnlyMoveOntoGrid()
{
// Spawn our test object 1 m off of the middle of the grid in both directions.
_objUid = SetupTestObject(new EntityCoordinates(_mapUid, 1.5f, 1.5f));
Assert.Multiple(() =>
{
// Assert that we start off the grid.
Assert.That(_entManager.GetComponent<TransformComponent>(_objUid).ParentUid, Is.EqualTo(_mapUid));
// Set the velocity of the grid and our object.
Assert.That(_physSystem.SetLinearVelocity(_objUid, new Vector2(-2.0f, -3.0f)), Is.True);
Assert.That(_physSystem.SetLinearVelocity(_gridUid, new Vector2(-1.0f, -2.0f)), Is.True);
// Wait a second to move onto the middle of the grid
_physSystem.Update(1.0f);
// The object should be parented to the grid and maintain its map velocity (slowing down), the grid should be unchanged.
var objXform = _entManager.GetComponent<TransformComponent>(_objUid);
var gridXform = _entManager.GetComponent<TransformComponent>(_gridUid);
Assert.That(objXform.ParentUid, Is.EqualTo(_gridUid), $"Object is not on grid - actual position: {objXform.ParentUid} {objXform.LocalPosition}, grid position: {gridXform.ParentUid} {gridXform.LocalPosition}");
Assert.That(_entManager.GetComponent<PhysicsComponent>(_objUid).LinearVelocity, Is.EqualTo(new Vector2(-1.0f, -1.0f)));
Assert.That(_entManager.GetComponent<PhysicsComponent>(_gridUid).LinearVelocity, Is.EqualTo(new Vector2(-1.0f, -2.0f)));
});
}
[Test]
// Moves a rotating object off of a rotating grid, checks for conservation of angular velocity.
public void TestLinearAndAngularVelocityMoveOffGrid()
{
// Spawn our test object in the middle of the grid.
_objUid = SetupTestObject(new EntityCoordinates(_gridUid, 0.5f, 0.5f));
Assert.Multiple(() =>
{
// Our object should start on the grid.
Assert.That(_entManager.GetComponent<TransformComponent>(_objUid).ParentUid, Is.EqualTo(_gridUid));
// Set the velocity of the grid and our object.
Assert.That(_physSystem.SetLinearVelocity(_objUid, new Vector2(3.5f, 4.75f)), Is.True);
Assert.That(_physSystem.SetAngularVelocity(_objUid, 1.0f), Is.True);
Assert.That(_physSystem.SetLinearVelocity(_gridUid, new Vector2(1.0f, 2.0f)), Is.True);
Assert.That(_physSystem.SetAngularVelocity(_gridUid, 2.0f), Is.True);
// Wait a second to clear the grid
_physSystem.Update(1.0f);
// The object should be parented to the map and maintain its map velocity, the grid should be unchanged.
var objXform = _entManager.GetComponent<TransformComponent>(_objUid);
var gridXform = _entManager.GetComponent<TransformComponent>(_gridUid);
Assert.That(objXform.ParentUid, Is.EqualTo(_mapUid), $"Object is not on map - actual position: {objXform.ParentUid} {objXform.LocalPosition}, grid position: {gridXform.ParentUid} {gridXform.LocalPosition}");
// Not checking object's linear velocity in this case, non-zero contribution from grid angular velocity.
Assert.That(_entManager.GetComponent<PhysicsComponent>(_objUid).AngularVelocity, Is.EqualTo(3.0f));
var gridPhys = _entManager.GetComponent<PhysicsComponent>(_gridUid);
Assert.That(gridPhys.LinearVelocity, Is.EqualTo(new Vector2(1.0f, 2.0f)));
Assert.That(gridPhys.AngularVelocity, Is.EqualTo(2.0f));
});
}
[Test]
// Moves a rotating object onto a rotating grid, checks for conservation of angular velocity.
public void TestLinearAndAngularVelocityMoveOntoGrid()
{
// Spawn our test object 1 m off of the middle of the grid in both directions.
_objUid = SetupTestObject(new EntityCoordinates(_mapUid, 1.5f, 1.5f));
Assert.Multiple(() =>
{
// Assert that we start off the grid.
Assert.That(_entManager.GetComponent<TransformComponent>(_objUid).ParentUid, Is.EqualTo(_mapUid));
// Set the velocity of the grid and our object.
Assert.That(_physSystem.SetLinearVelocity(_objUid, new Vector2(-2.0f, -3.0f)), Is.True);
Assert.That(_physSystem.SetAngularVelocity(_objUid, 1.0f), Is.True);
Assert.That(_physSystem.SetLinearVelocity(_gridUid, new Vector2(-1.0f, -2.0f)), Is.True);
Assert.That(_physSystem.SetAngularVelocity(_gridUid, 2.0f), Is.True);
// Wait a second to move onto the middle of the grid
_physSystem.Update(1.0f);
// The object should be parented to the grid and maintain its map velocity (slowing down), the grid should be unchanged.
var objXform = _entManager.GetComponent<TransformComponent>(_objUid);
var gridXform = _entManager.GetComponent<TransformComponent>(_gridUid);
Assert.That(objXform.ParentUid, Is.EqualTo(_gridUid), $"Object is not on grid - actual position: {objXform.ParentUid} {objXform.LocalPosition}, grid position: {gridXform.ParentUid} {gridXform.LocalPosition}");
// Not checking object's linear velocity in this case, non-zero contribution from grid angular velocity.
Assert.That(_entManager.GetComponent<PhysicsComponent>(_objUid).AngularVelocity, Is.EqualTo(-1.0f));
var gridPhys = _entManager.GetComponent<PhysicsComponent>(_gridUid);
Assert.That(gridPhys.LinearVelocity, Is.EqualTo(new Vector2(-1.0f, -2.0f)));
Assert.That(gridPhys.AngularVelocity, Is.EqualTo(2.0f));
});
}
}

View File

@@ -5,6 +5,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.Physics
{
@@ -76,10 +77,7 @@ namespace Robust.UnitTesting.Shared.Physics
{
var transformB = new Transform(Vector2.One, 0f);
var transformA = new Transform(transformB.Position + new Vector2(0.5f, 0.0f), 0f);
var manifold = new Manifold()
{
Points = new ManifoldPoint[2]
};
var manifold = new Manifold();
var expectedManifold = new Manifold
{
@@ -87,17 +85,24 @@ namespace Robust.UnitTesting.Shared.Physics
LocalNormal = new Vector2(-1, 0),
LocalPoint = new Vector2(-0.5f, 0),
PointCount = 2,
Points = new ManifoldPoint[]
{
new() {LocalPoint = new Vector2(0.5f, -0.5f), Id = new ContactID {Key = 65795}},
new() {LocalPoint = new Vector2(0.5f, 0.5f), Id = new ContactID {Key = 66051}}
}
Points = new FixedArray2<ManifoldPoint>(
new ManifoldPoint
{
LocalPoint = new Vector2(0.5f, -0.5f),
Id = new ContactID {Key = 65795}
},
new ManifoldPoint
{
LocalPoint = new Vector2(0.5f, 0.5f),
Id = new ContactID {Key = 66051}
}
)
};
_manifoldManager.CollidePolygons(ref manifold, _polyA, transformA, _polyB, transformB);
for (var i = 0; i < manifold.Points.Length; i++)
for (var i = 0; i < manifold.PointCount; i++)
{
Assert.That(manifold.Points[i], Is.EqualTo(expectedManifold.Points[i]));
Assert.That(manifold.Points.AsSpan[i], Is.EqualTo(expectedManifold.Points.AsSpan[i]));
}
Assert.That(manifold, Is.EqualTo(expectedManifold));

View File

@@ -24,7 +24,7 @@ namespace Robust.UnitTesting.Shared.Resources
_testDir = Directory.CreateDirectory(_testDirPath);
var subDir = Path.Combine(_testDirPath, "writable");
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir));
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir), hideRootDir: false);
}
[OneTimeTearDown]

View File

@@ -21,6 +21,8 @@ proto:
---
entity:
uid: int()
paused: bool(required=False)
mapInit: bool(required=False)
components: list(comp())
missingComponents: list(str(), required=False)