Compare commits

...

52 Commits

Author SHA1 Message Date
Pieter-Jan Briers
6cd442d209 Version: 218.2.2 2024-08-11 19:32:35 +02:00
Pieter-Jan Briers
62167dfc94 Compile compat fixes
(cherry picked from commit 025d90d281)
(cherry picked from commit 799702b814)
(cherry picked from commit 4600ee8e5788891f1b610e2d5141fb4e1228d323)
2024-08-11 19:32:35 +02:00
Pieter-Jan Briers
472903a0af Version: 218.2.1 2024-08-11 17:56:09 +02:00
Pieter-Jan Briers
b5fa1c84fc Security updates (#5353)
* Fix security bug in WritableDirProvider.OpenOsWindow()

Reported by @NarryG and @nyeogmi

* Sandbox updates

* Update ImageSharp again

(cherry picked from commit 7d778248ee)
(cherry picked from commit f66cda74e95619ddba2221bda644bf4394619805)
(cherry picked from commit db8ba83866c523e08e4fba0b80cd954f4f190613)
2024-08-11 17:56:09 +02:00
metalgearsloth
1901059755 Version: 218.2.0 2024-04-17 19:06:59 +10:00
metalgearsloth
8cdec92be6 Add slider locking (#5044)
* Add slider locking

Useful if you just want like a playback slider without the client intefering.

* Name alignment
2024-04-17 19:04:52 +10:00
metalgearsloth
0a00e7ec29 Network audio states (#5024)
* Network audio states

Lets server pause or stop audio or whatever.

* Better API and state fix

* Bunch of fixes

- TimedDespawn proccing too early.
- PlaybackPosition setting
- SetState setting.

* Clamps and despawn fixes

* fix
2024-04-17 17:55:22 +10:00
DrSmugleaf
8c25a83066 Expose worldPosition in SpriteComponent.Render (#5043)
* Expose worldPosition in SpriteComponent.Render

* Set default value to zero
2024-04-17 16:17:12 +10:00
Pieter-Jan Briers
0fadfc2d9b Allow control layout properties to be set via style sheet. (take 2) (#5037)
* Reapply "Allow control layout properties to be set via style sheet." (#5035)

This reverts commit af36d24892.

* Fix tests

MaxSize properties had wrong default. Oops.
2024-04-17 00:42:12 +02:00
metalgearsloth
a6e7224672 Version: 218.1.0 2024-04-16 22:39:23 +10:00
metalgearsloth
37796f4806 Add a Vertical tab container (#4948)
* Vertical tab container

* weh

* Tab review
2024-04-16 22:33:43 +10:00
Pieter-Jan Briers
a5494d1df2 Change default hub URL 2024-04-15 22:27:25 +02:00
Errant
e2525a2103 Fix division remainder issue in color.cs (#5040) 2024-04-15 19:13:43 +02:00
Pieter-Jan Briers
b50f68866f Enable roslyn extension tests in CI (#5038)
* Enable roslyn extension tests in CI

* I'll be real I kinda just hoped that last one would work. dotnet test's --help documentation is useless garbage so I couldn't tell if that was supported or not. Guess not.

* Actually fix the Roslyn tests.

As far as I can tell, Roslyn tests haven't worked since #2976.

The tests used a pretty awful technique of linking the test code against the analyzer, so that the analyzer's copy of the relevant attributes got included into the test. This then broke when the namespace got changed by the linked PR.

Now the tests get an EmbeddedResource for the necessary test files compiled instead.

Also applied this to DependencyAssignAnalyzerTest because why not.
2024-04-14 09:26:07 +02:00
Pieter-Jan Briers
03a4d3e0a0 Add IEquatable`1.Equals to sandbox
Why wasn't this in here wtf.
2024-04-14 08:36:25 +02:00
metalgearsloth
af8fb52a4f Version: 218.0.0 2024-04-14 15:00:07 +10:00
metalgearsloth
fd60dc2887 Add EntManager + ProtoManager helpers for random picks (#4869)
* Add EntManager + ProtoManager helpers for random picks

Lets us cleanup content a bit from these being re-implemented.

* weh
2024-04-14 14:55:55 +10:00
metalgearsloth
cd24fd46b6 Default worldpos to null (#5036)
I think this is slightly more robust than defaulting to 0 actually while still fixing the issue.
2024-04-14 14:47:30 +10:00
metalgearsloth
44cc7127fa Default SetWorldPos to 0 rotation (#5034)
More closely aligns with the old default.
2024-04-14 14:28:16 +10:00
metalgearsloth
af36d24892 Revert "Allow control layout properties to be set via style sheet." (#5035)
This reverts commit 8c4deb2067.

# Conflicts:
#	RELEASE-NOTES.md
2024-04-14 14:14:44 +10:00
Leon Friedrich
caa8ff0f2d Modify container/spawn helper methods (#5030)
* Modify container/spawn helper methods

* A
2024-04-14 13:47:21 +10:00
Pieter-Jan Briers
cd67c67a5c Add analyzer to warn for assignment to dependency fields. 2024-04-14 05:14:12 +02:00
deltanedas
57b328e8c2 add CopyData to appearance system (#5022)
Co-authored-by: deltanedas <@deltanedas:kde.org>
2024-04-14 02:24:31 +02:00
Pieter-Jan Briers
4874b1db68 Update UI themes on prototype reload. 2024-04-14 02:15:53 +02:00
Pieter-Jan Briers
814ad08884 Allow scaling the line height of a RichTextLabel 2024-04-14 02:10:01 +02:00
Pieter-Jan Briers
8c4deb2067 Allow control layout properties to be set via style sheet.
This works by setting the stylesheet values into the regular control property fields when updated. This means 0 performance overhead except when updating styles, and even then it's probably negligible. A bitfield is used to track which properties are set and how.

This code is all done manual for now. I wanted to make a source gen for this but couldn't be arsed at the moment. The code manually written here is basically what a future source gen would generate optimally.
2024-04-14 02:09:00 +02:00
Pieter-Jan Briers
f6a5120e56 Fix exception when inspecting element in some cases.
Happened when I was trying to develop item status stuff.
2024-04-14 02:05:40 +02:00
Pieter-Jan Briers
c1b8bf8e52 Make StatusHost request headers case insensitive
Woops that's how HTTP is supposed to work.
2024-04-11 02:28:46 +02:00
Pieter-Jan Briers
4b193bad26 Add non-generic GetCVar.
Surprised we didn't have this.
2024-04-11 02:25:40 +02:00
Tayrtahn
8db3da4852 Add Type tracking to FieldNotFoundErrorNode (#5032)
* Add Type tracking to FieldNotFoundErrorNode

* Suggested changes, plus xmldoc and primary constructor conversion.
2024-04-08 19:27:02 +02:00
Leon Friedrich
0c271fc2f8 Remove uneccesary Exists() checks in container system (#5031)
* Remove `Exists()` checks in container system

* A

* A
2024-04-08 01:24:09 +02:00
Pieter-Jan Briers
ed406c06b7 Fix HTTP errors on watchdog ping not being reported 2024-04-05 23:23:46 +02:00
Pieter-Jan Briers
b31940b489 Improve logging for watchdog pinsg 2024-04-05 02:22:06 +02:00
Pieter-Jan Briers
84360c653d Add better environment variable config system
ROBUST_CVARS had multiple issues:

* Not composable, i.e. two independent systems can't easily layer CVars to set as they all have to go into one var
* Not sanitary, there's no way to store things that have a ";" in them because it'd always get used as separator.

This adds a new ROBUST_CVAR_* system. For example I can set ROBUST_CVAR_game__hostname=foobar to set a CVar via a single env var. A double underscore ("__") is replaced with a period to make the CVar names safe for environment variables.

Also made Robust.Shared.Configuration.EnvironmentVariables internal because wtf that should not be public no.
2024-04-04 02:24:57 +02:00
metalgearsloth
6764ed56b0 Version: 217.2.1 2024-03-31 17:02:37 +11:00
Leon Friedrich
2b55d39e51 Make various ValueList enumerators use spans (#5019)
* Make various ValueList enumerators use spans

* Remove reference to EntityEventBus.OrderedRegistration

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-03-31 17:01:03 +11:00
metalgearsloth
fdc1de2430 Fix LineEdit tests (#5021) 2024-03-31 16:58:15 +11:00
metalgearsloth
99c5b0ad08 Version: 217.2.0 2024-03-31 15:29:26 +11:00
faint
9e2ab2a917 Double-clicking in LineEdit (#4831)
* Double-clicking in LineEdit

epic

* Fix IGameTiming dependency

* remove iocmanager.resolves

* test fix

* Update Robust.Client/UserInterface/Controls/LineEdit.cs

Co-authored-by: ShadowCommander <shadowjjt@gmail.com>

* review

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: ShadowCommander <shadowjjt@gmail.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-03-31 15:25:31 +11:00
metalgearsloth
36eb857b55 Fix TimeSpan addition (#5018)
Rider thinks nullable timespan += is fine but it no fine.
2024-03-31 15:02:18 +11:00
metalgearsloth
92d7f2723a Add ComponentRegistry helpers to EntityManager (#4934)
* Add ComponentRegistry helpers to EntityManager

* Metadata here too

* Remove object cast

* knock

* Plural
2024-03-31 14:54:01 +11:00
Leon Friedrich
91d3f67a94 Make IntersectRayWithPredicate ignore non-hard fixtures (#5017) 2024-03-31 14:48:37 +11:00
ElectroJr
b28b5ed09b Version: 217.1.0 2024-03-30 21:16:44 -04:00
Leon Friedrich
30eed7957f Add an EffectiveCurTime for physics subticks (#5014)
* Add an `EffectiveCurTime` for physics subticks

* Release notes
2024-03-31 12:15:30 +11:00
Fildrance
73c1449811 Add GetItems() extension for IRobustRandom (#4975)
* refactor: RobustRandom and RandomExtensions namespace change to file-scoped

* refactor: IRobustRandom xml-doc methods rearranged to be more structured.

* feat: GetItems methods added to RandomExtensions, tests for new methods added.

* fix: GetItems will not request count-1 from next random, as System.Random.Next have upper bound excluded.

* fix: enforced standard deviation on picking next items in RandomExtensions.GetItems + fixed hashet initial capacity +removed mandatory hashset allocation

* refactor: specified border values interaction in IRobustRandom xml-doc

* refactor: updated relese-notes

* refactor: changed release-notes PROPERLY

* fix: order by which unique random items are picked in RandomExtensions.GetItems were fixed to ACTUALLY follow normal distribution

* refractor: added comment for devious RandomExtensions.GetItems only-unique logic

* reduce code duplication

* Cleanup code a bit

Rename variables, and make it a bit more compact.
Also, IMO the description is unnecessary

* Remove obsolete extension

* Remove incorrect O(n) comments.

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2024-03-30 10:42:39 +11:00
Pieter-Jan Briers
958b5dd06d Fix MapComponent.LightingEnabled funky FOV.
There's two separate bool checks, one wasn't turned off, meaning the FOV kept being rendered but not updated.
2024-03-29 16:54:27 +01:00
metalgearsloth
4002cbddb9 Version: 217.0.0 2024-03-29 17:04:51 +11:00
metalgearsloth
c38a14e78f WorldPos review fix (#5011)
Forgot I merged ghub and tried pushing and didn't check it failed.
2024-03-29 16:56:25 +11:00
metalgearsloth
5164d99996 Implement VV for AudioParams + dump obsolete params (#4994)
* Implement sound VV

No params yet because I'm lazy but paths and collections work.

* Fixes

* rn

* review

* pitch

* VV sound params + dump unneeded params

BusName never used and per-source Attenuation got dropped.

* weh?
2024-03-29 16:54:14 +11:00
metalgearsloth
c79217ab66 Rework SetWorldPosition (#4915)
* Remove ents from containers for worldpos updates

Avoids bugs from content not checking for this every time. Physics and anything else important manually uses localposition updates so shouldn't adversely impact performance that much.

* Fixes

* Fix grid pos

* Also fix the joint setter

* Add both

* Update Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs

Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>

---------

Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
2024-03-29 16:49:26 +11:00
deltanedas
9ef7f7cb37 add AddUi to shared ui system (#4984)
Co-authored-by: deltanedas <@deltanedas:kde.org>
2024-03-27 14:36:11 +11:00
ShadowCommander
02ac314b1a Fix first measure of ScrollContainer scroll bars (#5005)
The first measure would include scroll bar size even if the available size fit all the contents. This would make the returned size larger than it should be.
2024-03-27 14:33:59 +11:00
85 changed files with 2718 additions and 449 deletions

View File

@@ -27,7 +27,8 @@ jobs:
run: dotnet restore
- name: Build
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Test Engine
- name: Robust.UnitTesting
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0
- name: Robust.Analyzers.Tests
run: dotnet test --no-build Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj -- NUnit.ConsoleOut=0

View File

@@ -33,10 +33,10 @@ jobs:
mkdir "release/${{ steps.parse_version.outputs.version }}"
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
- name: Upload files to centcomm
- name: Upload files to Suns
uses: appleboy/scp-action@master
with:
host: centcomm.spacestation14.io
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
source: "release/${{ steps.parse_version.outputs.version }}"
@@ -46,7 +46,7 @@ jobs:
- name: Update manifest JSON
uses: appleboy/ssh-action@master
with:
host: centcomm.spacestation14.io
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}

View File

@@ -1,4 +1,14 @@
<Project>
<PropertyGroup>
<!--
We actually set ManagePackageVersionsCentrally manually in another import file.
Since .NET SDK 8.0.300, ManagePackageVersionsCentrally is automatically set if Directory.Packages.props exists.
https://github.com/NuGet/NuGet.Client/pull/5572
We actively negate this here, as we have some packages in tree we don't want such automatic behavior for.
We use Directory.Build.props to get copy the state *after* our MSBuild config but before Nuget's config.
-->
<ManagePackageVersionsCentrally />
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
@@ -45,7 +55,7 @@
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />

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

@@ -1,4 +1,4 @@
# Release notes for RobustToolbox.
# Release notes for RobustToolbox.
<!--
NOTE: automatically updated sometimes by version.py.
@@ -54,6 +54,131 @@ END TEMPLATE-->
*None yet*
## 218.2.2
## 218.2.1
## 218.2.0
### New features
* Control layout properties such as `Margin` can now be set via style sheets.
* Expose worldposition in SpriteComponent.Render
* Network audio entity Play/Pause/Stop states and playback position.
* Add `Disabled` functionality to `Slider` control.
## 218.1.0
### New features
* Add IEquatable.Equals to the sandbox.
* Enable roslyn extensions tests in CI.
* Add a VerticalTabContainer control to match the horizontal one.
### Bugfixes
* Fix divison remainder issue for Colors, fixing purples.
### Other
* Default hub address (`hub.hub_urls`) has been changed to `https://hub.spacestation14.com/`.
## 218.0.0
### Breaking changes
* `Robust.Shared.Configuration.EnvironmentVariables` is now internal and no longer usable by content.
### New features
* Add TryGetRandom to EntityManager to get a random entity with the specified component and TryGetRandom to IPrototypeManager to return a random prototype of the specified type.
* Add CopyData to AppearanceSystem.
* Update UI themes on prototype reloads.
* Allow scaling the line height of a RichTextLabel.
* You can now specify CVar overrides via environment variable with the `ROBUST_CVAR_*` prefix. For example `ROBUST_CVAR_game__hostname=foobar` would set the appropriate CVar. Double underscores in the environment variable name are replaced with ".".
* Added non-generic variant of `GetCVar` to `IConfigurationManager`.
* Add type tracking to FieldNotFoundErrorNode for serialization.
* Distance between lines of a `RichTextLabel` can now be modified with `LineHeightScale`.
* UI theme prototypes are now updated when reloaded.
* New `RA0025` analyzer diagnostic warns for manual assignment to `[Dependency]` fields.
### Bugfixes
* Request headers in `IStatusHandlerContext` are now case-insensitive.
* SetWorldPosition rotation now more closely aligns with prior behavior.
* Fix exception when inspecting elements in some cases.
* Fix HTTP errors on watchdog ping not being reported.
### Other
* Add an analyzer for redundantly assigning to dependency fields.
### Internal
* Remove redundant Exists checks in ContainerSystem.
* Improve logging on watchdog pings.
## 217.2.1
### Bugfixes
* Fix LineEdit tests on engine.
### Internal
* Make various ValueList enumerators access the span directly for performance.
## 217.2.0
### New features
* Added `AddComponents` and `RemoveComponents` methods to EntityManager that handle EntityPrototype / ComponentRegistry bulk component changes.
* Add double-clicking to LineEdit.
### Bugfixes
* Properly ignore non-hard fixtures for IntersectRayWithPredicate.
* Fix nullable TimeSpan addition on some platforms.
## 217.1.0
### New features
* Added `IRobustRandom.GetItems` extension methods for randomly picking multiple items from a collections.
* Added `SharedPhysicsSystem.EffectiveCurTime`. This is effectively a variation of `IGameTiming.CurTime` that takes into account the current physics sub-step.
### Bugfixes
* Fix `MapComponent.LightingEnabled` not leaving FOV rendering in a broken state.
### Internal
* `Shuffle<T>(Span<T>, System.Random)` has been removed, just use the builtin method.
## 217.0.0
### Breaking changes
* TransformSystem.SetWorldPosition and SetWorldPositionRotation will now also perform parent updates as necessary. Previously it would just set the entity's LocalPosition which may break if they were inside of a container. Now they will be removed from their container and TryFindGridAt will run to correctly parent them to the new position. If the old functionality is desired then you can use GetInvWorldMatrix to update the LocalPosition (bearing in mind containers may prevent this).
### New features
* Implement VV for AudioParams on SoundSpecifiers.
* Add AddUi to the shared UI system.
### Bugfixes
* Fix the first measure of ScrollContainer bars.
## 216.0.0
### Breaking changes

View File

@@ -10,3 +10,18 @@ view-variable-instance-entity-client-components-search-bar-placeholder = Search
view-variable-instance-entity-server-components-search-bar-placeholder = Search
view-variable-instance-entity-add-window-server-components = Add Component [S]
view-variable-instance-entity-add-window-client-components = Add Component [C]
## SoundSpecifier
vv-sound-none = None
vv-sound-path = Path
vv-sound-collection = Collection
vv-sound-volume = volume
vv-sound-pitch = Pitch
vv-sound-max-distance = Max Distance
vv-sound-rolloff-factor = Rolloff Factor
vv-sound-reference-distance = Reference Distance
vv-sound-loop = Loop
vv-sound-play-offset = Play Offset (s)
vv-sound-variation = Pitch variation

View File

@@ -1,3 +0,0 @@
vv-sound-none = None
vv-sound-path = Path
vv-sound-collection = Collection

View File

@@ -20,11 +20,16 @@ public sealed class AccessAnalyzer_Test
{
TestState =
{
AdditionalReferences = { typeof(AccessAnalyzer).Assembly },
Sources = { code }
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.AccessAttribute.cs",
"Robust.Shared.Analyzers.AccessPermissions.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);

View File

@@ -0,0 +1,58 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.DependencyAssignAnalyzer>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class DependencyAssignAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<DependencyAssignAnalyzer, NUnitVerifier>()
{
TestState =
{
Sources = { code }
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.IoC.DependencyAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task Test()
{
const string code = """
using Robust.Shared.IoC;
public sealed class Foo
{
[Dependency]
private object? Field;
public Foo()
{
Field = "A";
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,9): warning RA0025: Tried to assign to [Dependency] field 'Field'. Remove [Dependency] or inject it via field injection instead.
VerifyCS.Diagnostic().WithSpan(10, 9, 10, 20).WithArguments("Field"));
}
}

View File

@@ -6,6 +6,13 @@
<Import Project="..\MSBuild\Robust.Properties.targets"/>
<Import Project="..\MSBuild\Robust.Engine.props"/>
<!-- Engine source files needed to make the tests work -->
<ItemGroup>
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" LogicalName="Robust.Shared.Analyzers.AccessAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" LogicalName="Robust.Shared.Analyzers.AccessPermissions.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
</ItemGroup>
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
namespace Robust.Analyzers.Tests;
public static class TestHelper
{
public static void AddEmbeddedSources(SolutionState state, params string[] embeddedFiles)
{
AddEmbeddedSources(state, (IEnumerable<string>) embeddedFiles);
}
public static void AddEmbeddedSources(SolutionState state, IEnumerable<string> embeddedFiles)
{
foreach (var fileName in embeddedFiles)
{
using var stream = typeof(AccessAnalyzer_Test).Assembly.GetManifestResourceStream(fileName)!;
state.Sources.Add((fileName, SourceText.From(stream)));
}
}
}

View File

@@ -0,0 +1,61 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DependencyAssignAnalyzer : DiagnosticAnalyzer
{
private const string DependencyAttributeType = "Robust.Shared.IoC.DependencyAttribute";
private static readonly DiagnosticDescriptor Rule = new (
Diagnostics.IdDependencyFieldAssigned,
"Assignment to dependency field",
"Tried to assign to [Dependency] field '{0}'. Remove [Dependency] or inject it via field injection instead.",
"Usage",
DiagnosticSeverity.Warning,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterOperationAction(CheckAssignment, OperationKind.SimpleAssignment);
}
private static void CheckAssignment(OperationAnalysisContext context)
{
if (context.Operation is not ISimpleAssignmentOperation assignment)
return;
if (assignment.Target is not IFieldReferenceOperation fieldRef)
return;
var field = fieldRef.Field;
var attributes = field.GetAttributes();
if (attributes.Length == 0)
return;
var depAttribute = context.Compilation.GetTypeByMetadataName(DependencyAttributeType);
if (!HasAttribute(attributes, depAttribute))
return;
context.ReportDiagnostic(Diagnostic.Create(Rule, assignment.Syntax.GetLocation(), field.Name));
}
private static bool HasAttribute(ImmutableArray<AttributeData> attributes, ISymbol symbol)
{
foreach (var attribute in attributes)
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, symbol))
return true;
}
return false;
}
}

View File

@@ -2,24 +2,29 @@
<ItemGroup>
<!-- Needed for NotNullableFlagAnalyzer. -->
<Compile Include="..\Robust.Shared\Analyzers\NotNullableFlagAttribute.cs" />
<Compile Include="..\Robust.Shared\Analyzers\NotNullableFlagAttribute.cs" LinkBase="Implementations" />
</ItemGroup>
<ItemGroup>
<!-- Needed for FriendAnalyzer. -->
<Compile Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" />
<Compile Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" />
<Compile Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" LinkBase="Implementations" />
<Compile Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" LinkBase="Implementations" />
</ItemGroup>
<ItemGroup>
<!-- Needed for PreferGenericVariantAnalyzer. -->
<Compile Include="..\Robust.Shared\Analyzers\PreferGenericVariantAttribute.cs" />
<Compile Include="..\Robust.Shared\Analyzers\PreferGenericVariantAttribute.cs" LinkBase="Implementations" />
</ItemGroup>
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
<PropertyGroup>
<Nullable>disable</Nullable>
<!--
Rider seems to get really confused with hot reload if we directly compile in the above-linked classes.
As such, they have an #if to change their namespace in this project.
-->
<DefineConstants>$(DefineConstants);ROBUST_ANALYZERS_IMPL</DefineConstants>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using Robust.Shared.Analyzers;
using Robust.Shared.Collections;
namespace Robust.Benchmarks.Collections;
[Virtual]
public class ValueListEnumerationBenchmarks
{
[Params(4, 16, 64)]
public int N { get; set; }
private sealed class Data(int i)
{
public readonly int I = i;
}
private ValueList<Data> _valueList;
private Data[] _array = default!;
[GlobalSetup]
public void Setup()
{
var list = new List<Data>(N);
for (var i = 0; i < N; i++)
{
list.Add(new(i));
}
_array = list.ToArray();
_valueList = new(list.ToArray());
}
[Benchmark]
public int ValueList()
{
var total = 0;
foreach (var ev in _valueList)
{
total += ev.I;
}
return total;
}
[Benchmark]
public int ValueListSpan()
{
var total = 0;
foreach (var ev in _valueList.Span)
{
total += ev.I;
}
return total;
}
[Benchmark]
public int Array()
{
var total = 0;
foreach (var ev in _array)
{
total += ev.I;
}
return total;
}
[Benchmark]
public int Span()
{
var total = 0;
foreach (var ev in _array.AsSpan())
{
total += ev.I;
}
return total;
}
}

View File

@@ -126,6 +126,33 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
component.Source.SetAuxiliary(null);
}
switch (component.State)
{
case AudioState.Playing:
component.StartPlaying();
break;
case AudioState.Paused:
component.Pause();
break;
case AudioState.Stopped:
component.StopPlaying();
component.PlaybackPosition = 0f;
break;
}
// If playback position changed then update it.
if (!string.IsNullOrEmpty(component.FileName))
{
var position = (float) ((component.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
var currentPosition = component.Source.PlaybackPosition;
var diff = Math.Abs(position - currentPosition);
if (diff > 0.1f)
{
component.PlaybackPosition = position;
}
}
}
/// <summary>

View File

@@ -314,6 +314,8 @@ public abstract class BaseAudioSource : IAudioSource
set
{
_checkDisposed();
value = MathF.Max(value, 0f);
AL.Source(SourceHandle, ALSourcef.SecOffset, value);
Master._checkAlError($"Tried to set invalid playback position of {value:0.00}");
}

View File

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

View File

@@ -8,11 +8,9 @@ using System.Text;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Animations;
using Robust.Shared.ComponentTrees;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -1237,9 +1235,9 @@ namespace Robust.Client.GameObjects
public IEnumerable<ISpriteLayer> AllLayers => Layers;
// Lobby SpriteView rendering path
public void Render(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Direction? overrideDirection = null)
public void Render(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Direction? overrideDirection = null, Vector2 position = default)
{
RenderInternal(drawingHandle, eyeRotation, worldRotation, Vector2.Zero, overrideDirection);
RenderInternal(drawingHandle, eyeRotation, worldRotation, position, overrideDirection);
}
[DataField("noRot")] private bool _screenLock = false;

View File

@@ -107,7 +107,7 @@ namespace Robust.Client.GameObjects
toDelete.Add(id);
}
foreach (var dead in toDelete)
foreach (var dead in toDelete.Span)
{
component.Containers.Remove(dead);
}
@@ -142,7 +142,7 @@ namespace Robust.Client.GameObjects
toRemove.Add(entity);
}
foreach (var entity in toRemove)
foreach (var entity in toRemove.Span)
{
Remove(
(entity, TransformQuery.GetComponent(entity), MetaQuery.GetComponent(entity)),
@@ -162,7 +162,7 @@ namespace Robust.Client.GameObjects
removedExpected.Add(netEntity);
}
foreach (var entityUid in removedExpected)
foreach (var entityUid in removedExpected.Span)
{
RemoveExpectedEntity(entityUid, out _);
}

View File

@@ -1138,7 +1138,7 @@ namespace Robust.Client.GameStates
if ((meta.Flags & MetaDataFlags.InContainer) != 0 &&
metas.TryGetComponent(xform.ParentUid, out var containerMeta) &&
(containerMeta.Flags & MetaDataFlags.Detached) == 0 &&
containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container, null, true))
containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container))
{
containerSys.Remove((ent.Value, xform, meta), container, false, true);
}
@@ -1414,7 +1414,7 @@ namespace Robust.Client.GameStates
_entities.TryGetComponent(xform.ParentUid, out MetaDataComponent? containerMeta) &&
(containerMeta.Flags & MetaDataFlags.Detached) == 0)
{
containerSys.TryGetContainingContainer(xform.ParentUid, uid, out container, null, true);
containerSys.TryGetContainingContainer(xform.ParentUid, uid, out container);
}
_entities.EntitySysManager.GetEntitySystem<TransformSystem>().DetachParentToNull(uid, xform);

View File

@@ -11,6 +11,7 @@ using Robust.Shared;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Utility;
@@ -514,7 +515,9 @@ namespace Robust.Client.Graphics.Clyde
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawLight && eye.DrawFov)
{
ApplyFovToBuffer(viewport, eye);
var mapUid = _mapManager.GetMapEntityId(eye.Position.MapId);
if (_entityManager.GetComponent<MapComponent>(mapUid).LightingEnabled)
ApplyFovToBuffer(viewport, eye);
}
}

View File

@@ -57,7 +57,7 @@ namespace Robust.Client.UserInterface
toRemove.Add(key);
}
foreach (var key in toRemove)
foreach (var key in toRemove.Span)
{
_playingAnimations.Remove(key);
AnimationCompleted?.Invoke(key);

View File

@@ -0,0 +1,132 @@
using System;
using System.Runtime.CompilerServices;
namespace Robust.Client.UserInterface;
public partial class Control
{
private LayoutStyleProperties _layoutStyleOverride;
private LayoutStyleProperties _layoutStyleSheet;
private void UpdateLayoutStyleProperties()
{
var propertiesSet = LayoutStyleProperties.None;
// Assumed most controls will have little or no style properties,
// so iterating once is less expensive overall then checking 10+ properties.
// C# switch statements are compiled efficiently anyways.
foreach (var (key, value) in _styleProperties)
{
switch (key)
{
case nameof(SizeFlagsStretchRatio):
UpdateField(ref _sizeFlagsStretchRatio, value, LayoutStyleProperties.StretchRatio);
break;
case nameof(MinWidth):
UpdateField(ref _minWidth, value, LayoutStyleProperties.MinWidth);
break;
case nameof(MinHeight):
UpdateField(ref _minHeight, value, LayoutStyleProperties.MinHeight);
break;
case nameof(SetWidth):
UpdateField(ref _setWidth, value, LayoutStyleProperties.SetWidth);
break;
case nameof(SetHeight):
UpdateField(ref _setHeight, value, LayoutStyleProperties.SetHeight);
break;
case nameof(MaxWidth):
UpdateField(ref _maxWidth, value, LayoutStyleProperties.MaxWidth);
break;
case nameof(MaxHeight):
UpdateField(ref _maxHeight, value, LayoutStyleProperties.MaxHeight);
break;
case nameof(HorizontalExpand):
UpdateField(ref _horizontalExpand, value, LayoutStyleProperties.HorizontalExpand);
break;
case nameof(VerticalExpand):
UpdateField(ref _verticalExpand, value, LayoutStyleProperties.VerticalExpand);
break;
case nameof(HorizontalAlignment):
UpdateField(ref _horizontalAlignment, value, LayoutStyleProperties.HorizontalAlignment);
break;
case nameof(VerticalAlignment):
UpdateField(ref _verticalAlignment, value, LayoutStyleProperties.VerticalAlignment);
break;
case nameof(Margin):
UpdateField(ref _margin, value, LayoutStyleProperties.Margin);
break;
}
}
// Reset cleared properties back to defaults.
var toClear = _layoutStyleSheet & ~propertiesSet;
if (toClear != 0)
{
ClearField(ref _sizeFlagsStretchRatio, DefaultStretchRatio, LayoutStyleProperties.StretchRatio);
ClearField(ref _minWidth, 0, LayoutStyleProperties.MinWidth);
ClearField(ref _minHeight, 0, LayoutStyleProperties.MinHeight);
ClearField(ref _setWidth, DefaultSetSize, LayoutStyleProperties.SetWidth);
ClearField(ref _setHeight, DefaultSetSize, LayoutStyleProperties.SetHeight);
ClearField(ref _maxWidth, DefaultMaxSize, LayoutStyleProperties.MaxWidth);
ClearField(ref _maxHeight, DefaultMaxSize, LayoutStyleProperties.MaxHeight);
ClearField(ref _horizontalExpand, false, LayoutStyleProperties.HorizontalExpand);
ClearField(ref _verticalExpand, false, LayoutStyleProperties.VerticalExpand);
ClearField(ref _horizontalAlignment, DefaultHAlignment, LayoutStyleProperties.HorizontalAlignment);
ClearField(ref _verticalAlignment, DefaultVAlignment, LayoutStyleProperties.VerticalAlignment);
ClearField(ref _margin, default, LayoutStyleProperties.Margin);
}
_layoutStyleSheet = propertiesSet;
return;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void UpdateField<T>(ref T field, object value, LayoutStyleProperties flag)
{
if ((_layoutStyleOverride & flag) != 0)
return;
// TODO: Probably need better error handling...
if (value is not T valueCast)
return;
field = valueCast;
propertiesSet |= flag;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void ClearField<T>(ref T field, T defaultValue, LayoutStyleProperties flag)
{
if ((toClear & flag) == 0)
return;
field = defaultValue;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetLayoutStyleProp(LayoutStyleProperties flag)
{
_layoutStyleOverride |= flag;
}
[Flags]
private enum LayoutStyleProperties : short
{
// @formatter:off
None = 0,
Margin = 1 << 0,
MinWidth = 1 << 1,
MinHeight = 1 << 2,
SetWidth = 1 << 3,
SetHeight = 1 << 4,
MaxWidth = 1 << 5,
MaxHeight = 1 << 6,
StretchRatio = 1 << 7,
HorizontalExpand = 1 << 8,
VerticalExpand = 1 << 9,
HorizontalAlignment = 1 << 10,
VerticalAlignment = 1 << 11,
// @formatter:on
}
}

View File

@@ -12,24 +12,30 @@ namespace Robust.Client.UserInterface
public partial class Control
{
private const float DefaultStretchRatio = 1;
private const float DefaultSetSize = float.NaN;
private const float DefaultMaxSize = float.PositiveInfinity;
private const HAlignment DefaultHAlignment = HAlignment.Stretch;
private const VAlignment DefaultVAlignment = VAlignment.Stretch;
private Vector2 _size;
[ViewVariables] internal Vector2? PreviousMeasure;
[ViewVariables] internal UIBox2? PreviousArrange;
private float _sizeFlagsStretchRatio = 1;
private float _sizeFlagsStretchRatio = DefaultStretchRatio;
private float _minWidth;
private float _minHeight;
private float _setWidth = float.NaN;
private float _setHeight = float.NaN;
private float _maxWidth = float.PositiveInfinity;
private float _maxHeight = float.PositiveInfinity;
private float _setWidth = DefaultSetSize;
private float _setHeight = DefaultSetSize;
private float _maxWidth = DefaultMaxSize;
private float _maxHeight = DefaultMaxSize;
private bool _horizontalExpand;
private bool _verticalExpand;
private HAlignment _horizontalAlignment = HAlignment.Stretch;
private VAlignment _verticalAlignment = VAlignment.Stretch;
private HAlignment _horizontalAlignment = DefaultHAlignment;
private VAlignment _verticalAlignment = DefaultVAlignment;
private Thickness _margin;
private bool _measuring;
private bool _arranging;
@@ -53,6 +59,7 @@ namespace Robust.Client.UserInterface
set
{
_margin = value;
SetLayoutStyleProp(LayoutStyleProperties.Margin);
InvalidateMeasure();
}
}
@@ -242,6 +249,7 @@ namespace Robust.Client.UserInterface
set
{
_horizontalAlignment = value;
SetLayoutStyleProp(LayoutStyleProperties.HorizontalAlignment);
InvalidateArrange();
}
}
@@ -258,6 +266,7 @@ namespace Robust.Client.UserInterface
set
{
_verticalAlignment = value;
SetLayoutStyleProp(LayoutStyleProperties.VerticalAlignment);
InvalidateArrange();
}
}
@@ -276,6 +285,7 @@ namespace Robust.Client.UserInterface
set
{
_horizontalExpand = value;
SetLayoutStyleProp(LayoutStyleProperties.HorizontalExpand);
Parent?.InvalidateMeasure();
}
}
@@ -294,6 +304,7 @@ namespace Robust.Client.UserInterface
set
{
_verticalExpand = value;
SetLayoutStyleProp(LayoutStyleProperties.VerticalExpand);
Parent?.InvalidateArrange();
}
}
@@ -318,6 +329,7 @@ namespace Robust.Client.UserInterface
_sizeFlagsStretchRatio = value;
SetLayoutStyleProp(LayoutStyleProperties.StretchRatio);
Parent?.InvalidateArrange();
}
}
@@ -394,6 +406,7 @@ namespace Robust.Client.UserInterface
set
{
_minWidth = value;
SetLayoutStyleProp(LayoutStyleProperties.MinWidth);
InvalidateMeasure();
}
}
@@ -408,6 +421,7 @@ namespace Robust.Client.UserInterface
set
{
_minHeight = value;
SetLayoutStyleProp(LayoutStyleProperties.MinHeight);
InvalidateMeasure();
}
}
@@ -422,6 +436,7 @@ namespace Robust.Client.UserInterface
set
{
_setWidth = value;
SetLayoutStyleProp(LayoutStyleProperties.SetWidth);
InvalidateMeasure();
}
}
@@ -436,6 +451,7 @@ namespace Robust.Client.UserInterface
set
{
_setHeight = value;
SetLayoutStyleProp(LayoutStyleProperties.SetHeight);
InvalidateMeasure();
}
}
@@ -450,6 +466,7 @@ namespace Robust.Client.UserInterface
set
{
_maxWidth = value;
SetLayoutStyleProp(LayoutStyleProperties.MaxWidth);
InvalidateMeasure();
}
}
@@ -464,6 +481,7 @@ namespace Robust.Client.UserInterface
set
{
_maxHeight = value;
SetLayoutStyleProp(LayoutStyleProperties.MaxHeight);
InvalidateMeasure();
}
}

View File

@@ -239,6 +239,7 @@ namespace Robust.Client.UserInterface
protected virtual void StylePropertiesChanged()
{
UpdateLayoutStyleProperties();
InvalidateMeasure();
}

View File

@@ -641,7 +641,11 @@ namespace Robust.Client.UserInterface
foreach (var child in Children.ToArray())
{
RemoveChild(child);
// This checks fails in some obscure cases like using the element inspector in the dev window.
// Why? Well I could probably spend 15 minutes in a debugger to find out,
// but I'd probably still end up with this fix.
if (child.Parent == this)
RemoveChild(child);
}
}

View File

@@ -4,6 +4,8 @@ using System.Numerics;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -20,6 +22,8 @@ namespace Robust.Client.UserInterface.Controls
public class LineEdit : Control
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private const float MouseScrollDelay = 0.001f;
@@ -46,6 +50,9 @@ namespace Robust.Client.UserInterface.Controls
private bool _mouseSelectingText;
private float _lastMousePosition;
private TimeSpan? _lastClickTime;
private Vector2? _lastClickPosition;
private bool IsPlaceHolderVisible => string.IsNullOrEmpty(_text) && _placeHolder != null;
public event Action<LineEditEventArgs>? OnTextChanged;
@@ -685,8 +692,26 @@ namespace Robust.Client.UserInterface.Controls
args.Handle();
}
}
// Double-clicking. Clicks delay should be <= 250ms and the distance < 10 pixels.
else if (args.Function == EngineKeyFunctions.UIClick && _lastClickPosition != null && _lastClickTime != null
&& _timing.RealTime - _lastClickTime <= TimeSpan.FromMilliseconds(_cfgManager.GetCVar(CVars.DoubleClickDelay))
&& (_lastClickPosition.Value - args.PointerLocation.Position).IsShorterThan(_cfgManager.GetCVar(CVars.DoubleClickRange)))
{
_lastClickTime = _timing.RealTime;
_lastClickPosition = args.PointerLocation.Position;
_lastMousePosition = args.RelativePosition.X;
_selectionStart = TextEditShared.PrevWordPosition(_text, GetIndexAtPos(args.RelativePosition.X));
_cursorPosition = TextEditShared.EndWordPosition(_text, GetIndexAtPos(args.RelativePosition.X));
args.Handle();
}
else
{
_lastClickTime = _timing.RealTime;
_lastClickPosition = args.PointerLocation.Position;
_mouseSelectingText = true;
_lastMousePosition = args.RelativePosition.X;

View File

@@ -6,6 +6,7 @@ using Robust.Client.UserInterface.RichText;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface.Controls
{
@@ -16,6 +17,26 @@ namespace Robust.Client.UserInterface.Controls
private FormattedMessage? _message;
private RichTextEntry _entry;
private float _lineHeightScale = 1;
private bool _lineHeightOverride;
[ViewVariables(VVAccess.ReadWrite)]
public float LineHeightScale
{
get
{
if (!_lineHeightOverride && TryGetStyleProperty(nameof(LineHeightScale), out float value))
return value;
return _lineHeightScale;
}
set
{
_lineHeightScale = value;
_lineHeightOverride = true;
InvalidateMeasure();
}
}
public RichTextLabel()
{
@@ -47,7 +68,7 @@ namespace Robust.Client.UserInterface.Controls
}
var font = _getFont();
_entry.Update(font, availableSize.X * UIScale, UIScale);
_entry.Update(font, availableSize.X * UIScale, UIScale, LineHeightScale);
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
}
@@ -61,7 +82,7 @@ namespace Robust.Client.UserInterface.Controls
return;
}
_entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale);
_entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
}
[Pure]

View File

@@ -123,10 +123,10 @@ namespace Robust.Client.UserInterface.Controls
if (!ReturnMeasure)
return Vector2.Zero;
if (_vScrollEnabled)
if (_vScrollEnabled && size.Y >= availableSize.Y)
size.X += _vScrollBar.DesiredSize.X;
if (_hScrollEnabled)
if (_hScrollEnabled && size.X >= availableSize.X)
size.Y += _hScrollBar.DesiredSize.Y;
return size;

View File

@@ -2,6 +2,7 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using static Robust.Client.UserInterface.Controls.LayoutContainer;
@@ -32,6 +33,11 @@ namespace Robust.Client.UserInterface.Controls
public bool Grabbed => _grabbed;
/// <summary>
/// Whether the slider can be adjusted.
/// </summary>
public bool Disabled { get; set; }
public StyleBox? ForegroundStyleBoxOverride
{
get => _foregroundStyleBoxOverride;
@@ -132,7 +138,7 @@ namespace Robust.Client.UserInterface.Controls
{
base.KeyBindDown(args);
if (args.Function != EngineKeyFunctions.UIClick)
if (args.Function != EngineKeyFunctions.UIClick || Disabled)
{
return;
}
@@ -146,7 +152,7 @@ namespace Robust.Client.UserInterface.Controls
{
base.KeyBindUp(args);
if (args.Function != EngineKeyFunctions.UIClick) return;
if (args.Function != EngineKeyFunctions.UIClick || !_grabbed) return;
_grabbed = false;
OnReleased?.Invoke(this);

View File

@@ -0,0 +1,34 @@
<!-- Organised with tabs in a vertical list to the left and the contents to the right -->
<BoxContainer Name="Container" xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:graphics="clr-namespace:Robust.Client.Graphics"
Orientation="Horizontal"
MouseFilter="Pass">
<ScrollContainer VerticalExpand="True"
HScrollEnabled="False"
ReturnMeasure="True"
Margin="5 5 0 5">
<PanelContainer Margin="5" VerticalExpand="False"
VerticalAlignment="Top">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<BoxContainer Name="TabContainer" Margin="5" Orientation="Vertical"/>
</PanelContainer>
</ScrollContainer>
<ScrollContainer VerticalExpand="True"
HScrollEnabled="False"
HorizontalAlignment="Stretch"
HorizontalExpand="True"
ReturnMeasure="True"
Margin="0 5 5 5">
<PanelContainer Margin="5" HorizontalExpand="True" HorizontalAlignment="Stretch">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<BoxContainer Margin="5"
Orientation="Vertical"
Name="ContentsContainer"/>
</PanelContainer>
</ScrollContainer>
</BoxContainer>

View File

@@ -0,0 +1,97 @@
using System.Collections.Generic;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.Controls;
[GenerateTypedNameReferences]
public sealed partial class VerticalTabContainer : BoxContainer
{
private readonly Dictionary<Control, BaseButton> _tabs = new();
// Just used to order controls in case one gets removed.
private readonly List<Control> _controls = new();
private readonly ButtonGroup _tabGroup = new(false);
private Control? _currentControl;
public VerticalTabContainer()
{
RobustXamlLoader.Load(this);
}
public int AddTab(Control control, string title)
{
var button = new Button()
{
Text = title,
Group = _tabGroup,
};
TabContainer.AddChild(button);
ContentsContainer.AddChild(control);
var index = ChildCount - 1;
button.OnPressed += args =>
{
SelectTab(control);
};
_controls.Add(control);
_tabs.Add(control, button);
// Existing tabs
if (ContentsContainer.ChildCount > 1)
{
control.Visible = false;
}
// First tab
else
{
SelectTab(control);
}
return index;
}
protected override void ChildRemoved(Control child)
{
if (_tabs.Remove(child, out var button))
{
button.Dispose();
}
// Set the current tab to a different control
if (_currentControl == child)
{
var previous = _controls.IndexOf(child) - 1;
if (previous > -1)
{
var setControl = _controls[previous];
SelectTab(setControl);
}
else
{
_currentControl = null;
}
}
_controls.Remove(child);
base.ChildRemoved(child);
}
private void SelectTab(Control control)
{
if (_currentControl != null)
{
_currentControl.Visible = false;
}
var button = _tabs[control];
button.Pressed = true;
control.Visible = true;
_currentControl = control;
}
}

View File

@@ -71,7 +71,8 @@ namespace Robust.Client.UserInterface
/// <param name="defaultFont">The font being used for display.</param>
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
/// <param name="uiScale"></param>
public void Update(Font defaultFont, float maxSizeX, float uiScale)
/// <param name="lineHeightScale"></param>
public void Update(Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
{
// This method is gonna suck due to complexity.
// Bear with me here.
@@ -159,7 +160,7 @@ namespace Robust.Client.UserInterface
if (!context.Font.TryPeek(out var font))
font = defaultFont;
src.Height += font.GetLineHeight(uiScale);
src.Height += GetLineHeight(font, uiScale, lineHeightScale);
}
}
}
@@ -170,7 +171,8 @@ namespace Robust.Client.UserInterface
UIBox2 drawBox,
float verticalOffset,
MarkupDrawingContext context,
float uiScale)
float uiScale,
float lineHeightScale = 1)
{
context.Clear();
context.Color.Push(_defaultColor);
@@ -197,7 +199,7 @@ namespace Robust.Client.UserInterface
if (lineBreakIndex < LineBreaks.Count &&
LineBreaks[lineBreakIndex] == globalBreakCounter)
{
baseLine = new Vector2(drawBox.Left, baseLine.Y + font.GetLineHeight(uiScale) + controlYAdvance);
baseLine = new Vector2(drawBox.Left, baseLine.Y + GetLineHeight(font, uiScale, lineHeightScale) + controlYAdvance);
controlYAdvance = 0;
lineBreakIndex += 1;
}
@@ -216,7 +218,7 @@ namespace Robust.Client.UserInterface
control.Position = new Vector2(baseLine.X * invertedScale, (baseLine.Y - defaultFont.GetAscent(uiScale)) * invertedScale);
control.Measure(new Vector2(Width, Height));
var advanceX = control.DesiredPixelSize.X;
controlYAdvance = Math.Max(0f, (control.DesiredPixelSize.Y - font.GetLineHeight(uiScale)) * invertedScale);
controlYAdvance = Math.Max(0f, (control.DesiredPixelSize.Y - GetLineHeight(font, uiScale, lineHeightScale)) * invertedScale);
baseLine += new Vector2(advanceX, 0);
}
}
@@ -242,5 +244,11 @@ namespace Robust.Client.UserInterface
tag.PopDrawContext(node, context);
return tag.TextAfter(node);
}
private static int GetLineHeight(Font font, float uiScale, float lineHeightScale)
{
var height = font.GetLineHeight(uiScale);
return (int)(height * lineHeightScale);
}
}
}

View File

@@ -2,6 +2,7 @@
using Robust.Client.UserInterface.Themes;
using Robust.Shared;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
namespace Robust.Client.UserInterface;
@@ -18,11 +19,29 @@ internal partial class UserInterfaceManager
{
DefaultTheme = _protoManager.Index<UITheme>(UITheme.DefaultName);
CurrentTheme = DefaultTheme;
ReloadThemes();
_configurationManager.OnValueChanged(CVars.InterfaceTheme, SetThemeOrPrevious, true);
_protoManager.PrototypesReloaded += OnPrototypesReloaded;
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs eventArgs)
{
if (eventArgs.WasModified<UITheme>())
{
_sawmillUI.Debug("Reloading UI themes due to prototype reload");
ReloadThemes();
}
}
private void ReloadThemes()
{
_themes.Clear();
foreach (var proto in _protoManager.EnumeratePrototypes<UITheme>())
{
_themes.Add(proto.ID, proto);
}
_configurationManager.OnValueChanged(CVars.InterfaceTheme, SetThemeOrPrevious, true);
SetThemeOrPrevious(CurrentTheme.ID);
}
//Try to set the current theme, if the theme is not found do nothing

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Numerics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -16,6 +17,10 @@ public sealed class VVPropEditorSoundSpecifier : VVPropEditor
private readonly IPrototypeManager _protoManager;
private readonly IResourceManager _resManager;
// Need to cache to some level just to make sure each edit doesn't reset the specifier to the default.
private SoundSpecifier? _specifier;
public VVPropEditorSoundSpecifier(IPrototypeManager protoManager, IResourceManager resManager)
{
_protoManager = protoManager;
@@ -39,7 +44,7 @@ public sealed class VVPropEditorSoundSpecifier : VVPropEditor
Editable = !ReadOnly,
};
var controls = new BoxContainer()
var pathControls = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
@@ -57,10 +62,15 @@ public sealed class VVPropEditorSoundSpecifier : VVPropEditor
case SoundCollectionSpecifier collection:
typeButton.SelectId(1);
editBox.Text = collection.Collection ?? string.Empty;
_specifier = collection;
break;
case SoundPathSpecifier path:
typeButton.SelectId(2);
editBox.Text = path.Path.ToString();
_specifier = path;
break;
default:
_specifier = null;
break;
}
}
@@ -90,7 +100,11 @@ public sealed class VVPropEditorSoundSpecifier : VVPropEditor
if (!_protoManager.HasIndex<SoundCollectionPrototype>(args.Text))
return;
ValueChanged(new SoundCollectionSpecifier(args.Text));
_specifier = new SoundCollectionSpecifier(args.Text)
{
Params = _specifier?.Params ?? AudioParams.Default,
};
ValueChanged(_specifier);
break;
case 2:
var path = new ResPath(args.Text);
@@ -98,13 +112,282 @@ public sealed class VVPropEditorSoundSpecifier : VVPropEditor
if (!_resManager.ContentFileExists(path))
return;
ValueChanged(new SoundPathSpecifier(args.Text));
_specifier = new SoundPathSpecifier(args.Text)
{
Params = _specifier?.Params ?? AudioParams.Default,
};
ValueChanged(_specifier);
break;
default:
return;
}
};
// Audio params
/* Volume */
var volumeEdit = new LineEdit()
{
Text = _specifier?.Params.Volume.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
HorizontalExpand = true,
Editable = !ReadOnly && _specifier != null,
};
volumeEdit.OnTextEntered += args =>
{
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
return;
_specifier.Params = _specifier.Params.WithVolume(floatValue);
ValueChanged(_specifier);
};
var volumeContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
new Label()
{
Text = Loc.GetString("vv-sound-volume"),
},
volumeEdit,
}
};
/* Pitch */
var pitchEdit = new LineEdit()
{
Text = _specifier?.Params.Pitch.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
HorizontalExpand = true,
Editable = !ReadOnly && _specifier != null,
};
pitchEdit.OnTextEntered += args =>
{
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
return;
_specifier.Params = _specifier.Params.WithPitchScale(floatValue);
ValueChanged(_specifier);
};
var pitchContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
new Label()
{
Text = Loc.GetString("vv-sound-pitch"),
},
pitchEdit,
}
};
/* MaxDistance */
var maxDistanceEdit = new LineEdit()
{
Text = _specifier?.Params.MaxDistance.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
HorizontalExpand = true,
Editable = !ReadOnly && _specifier != null,
};
maxDistanceEdit.OnTextEntered += args =>
{
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
return;
_specifier.Params = _specifier.Params.WithMaxDistance(floatValue);
ValueChanged(_specifier);
};
var maxDistanceContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
new Label()
{
Text = Loc.GetString("vv-sound-max-distance"),
},
maxDistanceEdit,
}
};
/* RolloffFactor */
var rolloffFactorEdit = new LineEdit()
{
Text = _specifier?.Params.RolloffFactor.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
HorizontalExpand = true,
Editable = !ReadOnly && _specifier != null,
};
rolloffFactorEdit.OnTextEntered += args =>
{
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
return;
_specifier.Params = _specifier.Params.WithRolloffFactor(floatValue);
ValueChanged(_specifier);
};
var rolloffFactorContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
new Label()
{
Text = Loc.GetString("vv-sound-rolloff-factor"),
},
rolloffFactorEdit,
}
};
/* ReferenceDistance */
var referenceDistanceEdit = new LineEdit()
{
Text = _specifier?.Params.ReferenceDistance.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
HorizontalExpand = true,
Editable = !ReadOnly && _specifier != null,
};
referenceDistanceEdit.OnTextEntered += args =>
{
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
return;
_specifier.Params = _specifier.Params.WithReferenceDistance(floatValue);
ValueChanged(_specifier);
};
var referenceDistanceContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
new Label()
{
Text = Loc.GetString("vv-sound-reference-distance"),
},
referenceDistanceEdit,
}
};
/* Loop */
var loopButton = new Button()
{
Text = Loc.GetString("vv-sound-loop"),
Pressed = _specifier?.Params.Loop ?? false,
ToggleMode = true,
Disabled = ReadOnly || _specifier == null,
};
loopButton.OnPressed += args =>
{
if (_specifier == null)
return;
_specifier.Params = _specifier.Params.WithLoop(args.Button.Pressed);
ValueChanged(_specifier);
};
/* PlayOffsetSeconds */
var playOffsetEdit = new LineEdit()
{
Text = _specifier?.Params.PlayOffsetSeconds.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
HorizontalExpand = true,
Editable = !ReadOnly && _specifier != null,
};
playOffsetEdit.OnTextEntered += args =>
{
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
return;
_specifier.Params = _specifier.Params.WithPlayOffset(floatValue);
ValueChanged(_specifier);
};
var playOffsetContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
new Label()
{
Text = Loc.GetString("vv-sound-play-offset"),
},
playOffsetEdit,
}
};
/* Variation */
var variationEdit = new LineEdit()
{
Text = _specifier?.Params.Variation.ToString() ?? string.Empty,
HorizontalExpand = true,
Editable = !ReadOnly && _specifier != null,
};
variationEdit.OnTextEntered += args =>
{
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
return;
_specifier.Params = _specifier.Params.WithVariation(floatValue);
ValueChanged(_specifier);
};
var variationContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
new Label()
{
Text = Loc.GetString("vv-sound-variation"),
},
variationEdit,
}
};
var audioParamsControls = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Children =
{
volumeContainer,
pitchContainer,
maxDistanceContainer,
rolloffFactorContainer,
referenceDistanceContainer,
loopButton,
playOffsetContainer,
variationContainer,
}
};
var controls = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Children =
{
pathControls,
audioParamsControls,
}
};
return controls;
}
}

View File

@@ -28,6 +28,7 @@ public static class Diagnostics
public const string IdComponentPauseNoFields = "RA0022";
public const string IdComponentPauseNoParentAttribute = "RA0023";
public const string IdComponentPauseWrongTypeAttribute = "RA0024";
public const string IdDependencyFieldAssigned = "RA0025";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

View File

@@ -256,7 +256,7 @@ namespace Robust.Server.ServerStatus
_context = context;
RequestMethod = new HttpMethod(context.Request.HttpMethod!);
var headers = new Dictionary<string, StringValues>();
var headers = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
foreach (string? key in context.Request.Headers.Keys)
{
if (key == null)

View File

@@ -157,11 +157,14 @@ namespace Robust.Server.ServerStatus
try
{
// Passing null as content works so...
await _httpClient.PostAsync(new Uri(_baseUri, $"server_api/{_watchdogKey}/ping"), null!);
_sawmill.Debug("Sending ping to watchdog...");
using var resp = await _httpClient.PostAsync(new Uri(_baseUri, $"server_api/{_watchdogKey}/ping"), null!);
resp.EnsureSuccessStatusCode();
_sawmill.Debug("Succeeded in sending ping to watchdog");
}
catch (HttpRequestException e)
{
_sawmill.Warning("Failed to send ping to watchdog:\n{0}", e);
_sawmill.Error("Failed to send ping to watchdog:\n{0}", e);
}
}

View File

@@ -592,7 +592,11 @@ namespace Robust.Shared.Maths
if (c != 0)
{
if (max == rgb.R)
{
h = (rgb.G - rgb.B) / c % 6.0f;
if (h < 0f)
h += 6.0f;
}
else if (max == rgb.G)
h = (rgb.B - rgb.R) / c + 2.0f;
else if (max == rgb.B)
@@ -774,7 +778,11 @@ namespace Robust.Shared.Maths
var h = 0.0f;
if (max == rgb.R)
{
h = (rgb.G - rgb.B) / c % 6.0f;
if (h < 0f)
h += 6.0f;
}
else if (max == rgb.G)
h = (rgb.B - rgb.R) / c + 2.0f;
else if (max == rgb.B)

View File

@@ -1,6 +1,6 @@
using System;
#if NETSTANDARD2_0
#if ROBUST_ANALYZERS_IMPL
namespace Robust.Shared.Analyzers.Implementation;
#else
namespace Robust.Shared.Analyzers;

View File

@@ -1,6 +1,6 @@
using System;
#if NETSTANDARD2_0
#if ROBUST_ANALYZERS_IMPL
namespace Robust.Shared.Analyzers.Implementation;
#else
namespace Robust.Shared.Analyzers;

View File

@@ -1,6 +1,6 @@
using System;
#if NETSTANDARD2_0
#if ROBUST_ANALYZERS_IMPL
namespace Robust.Shared.Analyzers.Implementation;
#else
namespace Robust.Shared.Analyzers;

View File

@@ -1,6 +1,6 @@
using System;
#if NETSTANDARD2_0
#if ROBUST_ANALYZERS_IMPL
namespace Robust.Shared.Analyzers.Implementation;
#else
namespace Robust.Shared.Analyzers;

View File

@@ -29,12 +29,6 @@ namespace Robust.Shared.Audio
[DataDefinition]
public partial struct AudioParams
{
/// <summary>
/// The DistanceModel to use for this specific source.
/// </summary>
[DataField]
public Attenuation Attenuation { get; set; } = Attenuation.LinearDistanceClamped;
/// <summary>
/// Base volume to play the audio at, in dB.
/// </summary>
@@ -45,13 +39,13 @@ namespace Robust.Shared.Audio
/// Scale for the audio pitch.
/// </summary>
[DataField]
public float Pitch { get; set; } = Default.Pitch;
public float Pitch
{
get => _pitch;
set => _pitch = MathF.Max(0f, value);
}
/// <summary>
/// Audio bus to play on.
/// </summary>
[DataField]
public string BusName { get; set; } = Default.BusName;
private float _pitch = Default.Pitch;
/// <summary>
/// Only applies to positional audio.
@@ -89,7 +83,7 @@ namespace Robust.Shared.Audio
/// <summary>
/// The "default" audio configuration.
/// </summary>
public static readonly AudioParams Default = new(0, 1, "Master", SharedAudioSystem.DefaultSoundRange, 1, 1, false, 0f);
public static readonly AudioParams Default = new(0, 1, SharedAudioSystem.DefaultSoundRange, 1, 1, false, 0f);
// explicit parameterless constructor required so that default values get set properly.
public AudioParams() { }
@@ -97,21 +91,19 @@ namespace Robust.Shared.Audio
public AudioParams(
float volume,
float pitch,
string busName,
float maxDistance,
float refDistance,
bool loop,
float playOffsetSeconds,
float? variation = null)
: this(volume, pitch, busName, maxDistance, 1, refDistance, loop, playOffsetSeconds, variation)
: this(volume, pitch, maxDistance, 1, refDistance, loop, playOffsetSeconds, variation)
{
}
public AudioParams(float volume, float pitch, string busName, float maxDistance,float rolloffFactor, float refDistance, bool loop, float playOffsetSeconds, float? variation = null) : this()
public AudioParams(float volume, float pitch, float maxDistance,float rolloffFactor, float refDistance, bool loop, float playOffsetSeconds, float? variation = null) : this()
{
Volume = volume;
Pitch = pitch;
BusName = busName;
MaxDistance = maxDistance;
RolloffFactor = rolloffFactor;
ReferenceDistance = refDistance;
@@ -167,18 +159,6 @@ namespace Robust.Shared.Audio
return me;
}
/// <summary>
/// Returns a copy of this instance with a new bus name set, for easy chaining.
/// </summary>
/// <param name="bus">The new bus name.</param>
[Pure]
public readonly AudioParams WithBusName(string bus)
{
var me = this;
me.BusName = bus;
return me;
}
/// <summary>
/// Returns a copy of this instance with a new max distance set, for easy chaining.
/// </summary>
@@ -227,18 +207,6 @@ namespace Robust.Shared.Audio
return me;
}
/// <summary>
/// Returns a copy of this instance with attenuation set, for easy chaining.
/// </summary>
/// <param name="attenuation">The new attenuation.</param>
[Pure]
public readonly AudioParams WithAttenuation(Attenuation attenuation)
{
var me = this;
me.Attenuation = attenuation;
return me;
}
[Pure]
public readonly AudioParams WithPlayOffset(float offset)
{

View File

@@ -6,6 +6,7 @@ using Robust.Shared.Audio.Sources;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.ViewVariables;
@@ -90,11 +91,24 @@ public sealed partial class AudioComponent : Component, IAudioSource
public void StartPlaying() => Source.StartPlaying();
/// <inheritdoc />
public void StopPlaying() => Source.StopPlaying();
public void StopPlaying()
{
PlaybackPosition = 0f;
Source.StopPlaying();
}
/// <inheritdoc />
public void Restart() => Source.Restart();
[DataField, AutoNetworkedField]
public AudioState State = AudioState.Playing;
/// <summary>
/// Time when the audio was paused so we can offset it later if relevant.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan? PauseTime;
/// <summary>
/// <see cref="IAudioSource.Playing"/>
/// </summary>
@@ -208,6 +222,7 @@ public sealed partial class AudioComponent : Component, IAudioSource
/// <see cref="IAudioSource.PlaybackPosition"/>
/// </summary>
[ViewVariables]
[Access(Other = AccessPermissions.ReadWriteExecute)]
public float PlaybackPosition
{
get => Source.PlaybackPosition;
@@ -240,6 +255,14 @@ public sealed partial class AudioComponent : Component, IAudioSource
}
}
[Serializable, NetSerializable]
public enum AudioState : byte
{
Stopped,
Playing,
Paused,
}
[Flags]
public enum AudioFlags : byte
{

View File

@@ -55,6 +55,141 @@ public abstract partial class SharedAudioSystem : EntitySystem
SubscribeLocalEvent<AudioComponent, EntityUnpausedEvent>(OnAudioUnpaused);
}
/// <summary>
/// Sets the playback position of audio to the specified spot.
/// </summary>
public void SetPlaybackPosition(Entity<AudioComponent?>? nullEntity, float position)
{
if (nullEntity == null)
return;
var entity = nullEntity.Value;
if (!Resolve(entity.Owner, ref entity.Comp, false))
return;
var audioLength = GetAudioLength(entity.Comp.FileName);
if (audioLength.TotalSeconds < position)
{
// Just stop it and return
if (!_netManager.IsClient)
QueueDel(nullEntity.Value);
entity.Comp.StopPlaying();
return;
}
if (position < 0f)
{
Log.Error($"Tried to set playback position for {ToPrettyString(entity.Owner)} / {entity.Comp.FileName} outside of bounds");
return;
}
// If we're paused then the current position is <pause time - start time>, else it's <cur time - start time>
var currentPos = (entity.Comp.PauseTime ?? Timing.CurTime) - entity.Comp.AudioStart;
var timeOffset = TimeSpan.FromSeconds(position - currentPos.TotalSeconds);
DebugTools.Assert(currentPos > TimeSpan.Zero);
// Rounding.
if (Math.Abs(timeOffset.TotalSeconds) <= 0.01)
{
return;
}
if (entity.Comp.PauseTime != null)
{
entity.Comp.PauseTime = entity.Comp.PauseTime.Value + timeOffset;
// Paused audio doesn't have TimedDespawn so.
}
else
{
// Bump it back so the actual playback positions moves forward
entity.Comp.AudioStart -= timeOffset;
// need to ensure it doesn't despawn too early.
if (TryComp(entity.Owner, out TimedDespawnComponent? despawn))
{
despawn.Lifetime -= (float) timeOffset.TotalSeconds;
}
}
entity.Comp.PlaybackPosition = position;
// Network the new playback position.
Dirty(entity);
}
/// <summary>
/// Calculates playback position considering length paused.
/// </summary>
/// <param name="component"></param>
/// <returns></returns>
private float GetPlaybackPosition(AudioComponent component)
{
return (float) (Timing.CurTime - (component.PauseTime ?? TimeSpan.Zero) - component.AudioStart).TotalSeconds;
}
/// <summary>
/// Sets the shared state for an audio entity.
/// </summary>
public void SetState(EntityUid? entity, AudioState state, bool force = false, AudioComponent? component = null)
{
if (entity == null || !Resolve(entity.Value, ref component, false))
return;
if (component.State == state && !force)
return;
// Unpause it
if (component.State == AudioState.Paused && state == AudioState.Playing)
{
var pauseOffset = Timing.CurTime - component.PauseTime;
component.AudioStart += pauseOffset ?? TimeSpan.Zero;
component.PlaybackPosition = (float) (Timing.CurTime - component.AudioStart).TotalSeconds;
}
// If we were stopped then played then restart audiostart to now.
if (component.State == AudioState.Stopped && state == AudioState.Playing)
{
component.AudioStart = Timing.CurTime;
component.PauseTime = null;
}
switch (state)
{
case AudioState.Stopped:
component.AudioStart = Timing.CurTime;
component.PauseTime = null;
component.StopPlaying();
RemComp<TimedDespawnComponent>(entity.Value);
break;
case AudioState.Paused:
// Set it to current time so we can easily unpause it later.
component.PauseTime = Timing.CurTime;
component.Pause();
RemComp<TimedDespawnComponent>(entity.Value);
break;
case AudioState.Playing:
component.PauseTime = null;
component.StartPlaying();
// Reset TimedDespawn so the audio still gets cleaned up.
if (!component.Looping)
{
var timed = EnsureComp<TimedDespawnComponent>(entity.Value);
var audioLength = GetAudioLength(component.FileName);
timed.Lifetime = (float) audioLength.TotalSeconds + 0.01f;
}
break;
}
component.State = state;
Dirty(entity.Value, component);
}
protected void SetZOffset(float value)
{
ZOffset = value;
@@ -505,4 +640,12 @@ public abstract partial class SharedAudioSystem : EntitySystem
{
public NetEntity NetEntity;
}
public bool IsPlaying(EntityUid? stream, AudioComponent? component = null)
{
if (stream == null || !Resolve(stream.Value, ref component, false))
return false;
return component.State == AudioState.Playing;
}
}

View File

@@ -886,6 +886,22 @@ namespace Robust.Shared
public static readonly CVarDef<string> RenderFOVColor =
CVarDef.Create("render.fov_color", Color.Black.ToHex(), CVar.ARCHIVE | CVar.CLIENTONLY);
/*
* CONTROLS
*/
/// <summary>
/// Milliseconds to wait to consider double-click delays.
/// </summary>
public static readonly CVarDef<int> DoubleClickDelay =
CVarDef.Create("controls.double_click_delay", 250, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// Range in pixels for double-clicks
/// </summary>
public static readonly CVarDef<int> DoubleClickRange =
CVarDef.Create("controls.double_click_range", 10, CVar.ARCHIVE | CVar.CLIENTONLY);
/*
* DISPLAY
*/
@@ -1429,7 +1445,7 @@ namespace Robust.Shared
/// Comma-separated list of URLs of hub servers to advertise to.
/// </summary>
public static readonly CVarDef<string> HubUrls =
CVarDef.Create("hub.hub_urls", "https://central.spacestation14.io/hub/", CVar.SERVERONLY);
CVarDef.Create("hub.hub_urls", "https://hub.spacestation14.com/", CVar.SERVERONLY);
/// <summary>
/// URL of this server to advertise.

View File

@@ -558,17 +558,21 @@ namespace Robust.Shared.Configuration
OverrideDefault(def.Name, value);
}
/// <inheritdoc />
public T GetCVar<T>(string name)
public object GetCVar(string name)
{
using var _ = Lock.ReadGuard();
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
//TODO: Make flags work, required non-derpy net system.
return (T)(GetConfigVarValue(cVar))!;
return GetConfigVarValue(cVar);
throw new InvalidConfigurationException($"Trying to get unregistered variable '{name}'");
}
/// <inheritdoc />
public T GetCVar<T>(string name)
{
return (T)GetCVar(name);
}
public T GetCVar<T>(CVarDef<T> def) where T : notnull
{
return GetCVar<T>(def.Name);

View File

@@ -1,9 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Robust.Shared.Configuration
{
public static class EnvironmentVariables
internal static class EnvironmentVariables
{
/// <summary>
/// The environment variable for configuring CVar overrides. The value
@@ -12,23 +13,38 @@ namespace Robust.Shared.Configuration
/// </summary>
public const string ConfigVarEnvironmentVariable = "ROBUST_CVARS";
public const string SingleVarPrefix = "ROBUST_CVAR_";
/// <summary>
/// Get the CVar overrides defined in the relevant environment variable.
/// </summary>
public static IEnumerable<(string, string)> GetEnvironmentCVars()
internal static IEnumerable<(string, string)> GetEnvironmentCVars()
{
var eVarString = Environment.GetEnvironmentVariable(ConfigVarEnvironmentVariable);
if (eVarString == null)
{
yield break;
}
// Handle ROBUST_CVARS.
var eVarString = Environment.GetEnvironmentVariable(ConfigVarEnvironmentVariable) ?? "";
foreach (var cVarPair in eVarString.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
var pairParts = cVarPair.Split('=', 2);
yield return (pairParts[0], pairParts[1]);
}
// Handle ROBUST_CVAR_*
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
{
var key = (string)entry.Key;
var value = (string?)entry.Value;
if (value == null)
continue;
if (!key.StartsWith(SingleVarPrefix))
continue;
var varName = key[SingleVarPrefix.Length..].Replace("__", ".");
yield return (varName, value);
}
}
}
}

View File

@@ -118,6 +118,13 @@ namespace Robust.Shared.Configuration
/// <param name="value">The new default value of the CVar.</param>
void OverrideDefault<T>(CVarDef<T> def, T value) where T : notnull;
/// <summary>
/// Get the value of a CVar.
/// </summary>
/// <param name="name">The name of the CVar.</param>
/// <returns></returns>
object GetCVar(string name);
/// <summary>
/// Get the value of a CVar.
/// </summary>

View File

@@ -135,6 +135,25 @@ public abstract partial class SharedContainerSystem
return true;
}
/// <summary>
/// Attempts to insert an entity into a container. If it fails, it will instead drop the entity next to the
/// container entity.
/// </summary>
/// <returns>Whether or not the entity was successfully inserted</returns>
public bool InsertOrDrop(Entity<TransformComponent?, MetaDataComponent?, PhysicsComponent?> toInsert,
BaseContainer container,
TransformComponent? containerXform = null)
{
if (!Resolve(toInsert.Owner, ref toInsert.Comp1) || !Resolve(container.Owner, ref containerXform))
return false;
if (Insert(toInsert, container, containerXform))
return true;
_transform.DropNextTo(toInsert, (container.Owner, containerXform));
return false;
}
/// <summary>
/// Checks if the entity can be inserted into the given container.
/// </summary>

View File

@@ -168,9 +168,16 @@ namespace Robust.Shared.Containers
return containerManager.Containers.TryGetValue(id, out container);
}
public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [NotNullWhen(true)] out BaseContainer? container, ContainerManagerComponent? containerManager = null, bool skipExistCheck = false)
[Obsolete("Use variant without skipExistCheck argument")]
public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [NotNullWhen(true)] out BaseContainer? container, bool skipExistCheck)
{
if (!Resolve(uid, ref containerManager, false) || !(skipExistCheck || Exists(containedUid)))
return TryGetContainingContainer(uid, containedUid, out container);
}
public bool TryGetContainingContainer(EntityUid uid, EntityUid containedUid, [NotNullWhen(true)] out BaseContainer? container, ContainerManagerComponent? containerManager = null)
{
DebugTools.Assert(Exists(containedUid));
if (!Resolve(uid, ref containerManager, false))
{
container = null;
return false;
@@ -191,7 +198,8 @@ namespace Robust.Shared.Containers
public bool ContainsEntity(EntityUid uid, EntityUid containedUid, ContainerManagerComponent? containerManager = null)
{
if (!Resolve(uid, ref containerManager, false) || !Exists(containedUid))
DebugTools.Assert(Exists(containedUid));
if (!Resolve(uid, ref containerManager, false))
return false;
foreach (var container in containerManager.Containers.Values)
@@ -251,7 +259,7 @@ namespace Robust.Shared.Containers
if (!Resolve(uid, ref transform, false))
return false;
return TryGetContainingContainer(transform.ParentUid, uid, out container, skipExistCheck: true);
return TryGetContainingContainer(transform.ParentUid, uid, out container);
}
/// <summary>

View File

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

View File

@@ -84,12 +84,146 @@ Types:
- "bool get_HasContents()"
Lidgren.Network:
NetBuffer:
All: True
Methods:
- "byte[] get_Data()"
- "void set_Data(byte[])"
- "int get_LengthBytes()"
- "void set_LengthBytes(int)"
- "int get_LengthBits()"
- "void set_LengthBits(int)"
- "long get_Position()"
- "void set_Position(long)"
- "int get_PositionInBytes()"
- "byte[] PeekDataBuffer()"
- "bool PeekBoolean()"
- "byte PeekByte()"
- "sbyte PeekSByte()"
- "byte PeekByte(int)"
- "System.Span`1<byte> PeekBytes(System.Span`1<byte>)"
- "byte[] PeekBytes(int)"
- "void PeekBytes(byte[], int, int)"
- "short PeekInt16()"
- "ushort PeekUInt16()"
- "int PeekInt32()"
- "int PeekInt32(int)"
- "uint PeekUInt32()"
- "uint PeekUInt32(int)"
- "ulong PeekUInt64()"
- "long PeekInt64()"
- "ulong PeekUInt64(int)"
- "long PeekInt64(int)"
- "float PeekFloat()"
- "System.Half PeekHalf()"
- "float PeekSingle()"
- "double PeekDouble()"
- "string PeekString()"
- "int PeekStringSize()"
- "bool ReadBoolean()"
- "byte ReadByte()"
- "bool ReadByte(ref byte)"
- "sbyte ReadSByte()"
- "byte ReadByte(int)"
- "System.Span`1<byte> ReadBytes(System.Span`1<byte>)"
- "byte[] ReadBytes(int)"
- "bool ReadBytes(int, ref byte[])"
- "bool TryReadBytes(System.Span`1<byte>)"
- "void ReadBytes(byte[], int, int)"
- "void ReadBits(System.Span`1<byte>, int)"
- "void ReadBits(byte[], int, int)"
- "short ReadInt16()"
- "ushort ReadUInt16()"
- "int ReadInt32()"
- "bool ReadInt32(ref int)"
- "int ReadInt32(int)"
- "uint ReadUInt32()"
- "bool ReadUInt32(ref uint)"
- "uint ReadUInt32(int)"
- "ulong ReadUInt64()"
- "long ReadInt64()"
- "ulong ReadUInt64(int)"
- "long ReadInt64(int)"
- "float ReadFloat()"
- "System.Half ReadHalf()"
- "float ReadSingle()"
- "bool ReadSingle(ref float)"
- "double ReadDouble()"
- "uint ReadVariableUInt32()"
- "bool ReadVariableUInt32(ref uint)"
- "int ReadVariableInt32()"
- "long ReadVariableInt64()"
- "ulong ReadVariableUInt64()"
- "float ReadSignedSingle(int)"
- "float ReadUnitSingle(int)"
- "float ReadRangedSingle(float, float, int)"
- "int ReadRangedInteger(int, int)"
- "long ReadRangedInteger(long, long)"
- "string ReadString()"
- "bool ReadString(ref string)"
- "double ReadTime(Lidgren.Network.NetConnection, bool)"
- "System.Net.IPEndPoint ReadIPEndPoint()"
- "void SkipPadBits()"
- "void ReadPadBits()"
- "void SkipPadBits(int)"
- "void EnsureBufferSize(int)"
- "void Write(bool)"
- "void Write(byte)"
- "void WriteAt(int, byte)"
- "void Write(sbyte)"
- "void Write(byte, int)"
- "void Write(byte[])"
- "void Write(System.ReadOnlySpan`1<byte>)"
- "void Write(byte[], int, int)"
- "void Write(ushort)"
- "void WriteAt(int, ushort)"
- "void Write(ushort, int)"
- "void Write(short)"
- "void WriteAt(int, short)"
- "void Write(int)"
- "void WriteAt(int, int)"
- "void Write(uint)"
- "void WriteAt(int, uint)"
- "void Write(uint, int)"
- "void Write(int, int)"
- "void Write(ulong)"
- "void WriteAt(int, ulong)"
- "void Write(ulong, int)"
- "void Write(long)"
- "void Write(long, int)"
- "void Write(System.Half)"
- "void Write(float)"
- "void Write(double)"
- "int WriteVariableUInt32(uint)"
- "int WriteVariableInt32(int)"
- "int WriteVariableInt64(long)"
- "int WriteVariableUInt64(ulong)"
- "void WriteSignedSingle(float, int)"
- "void WriteUnitSingle(float, int)"
- "void WriteRangedSingle(float, float, float, int)"
- "int WriteRangedInteger(int, int, int)"
- "int WriteRangedInteger(long, long, long)"
- "void Write(string)"
- "void Write(System.Net.IPEndPoint)"
- "void WriteTime(bool)"
- "void WriteTime(double, bool)"
- "void WritePadBits()"
- "void WritePadBits(int)"
- "void Write(Lidgren.Network.NetBuffer)"
- "void Zero(int)"
- "void .ctor()"
NetDeliveryMethod: { }
NetIncomingMessage:
All: True
Methods:
- "Lidgren.Network.NetIncomingMessageType get_MessageType()"
- "Lidgren.Network.NetDeliveryMethod get_DeliveryMethod()"
- "int get_SequenceChannel()"
- "System.Net.IPEndPoint get_SenderEndPoint()"
- "Lidgren.Network.NetConnection get_SenderConnection()"
- "double get_ReceiveTime()"
- "double ReadTime(bool)"
- "string ToString()"
NetOutgoingMessage:
All: True
Methods:
- "string ToString()"
Nett:
CommentLocation: { } # Enum
Toml:
@@ -951,7 +1085,9 @@ Types:
IComparable: { All: True }
IComparable`1: { All: True }
IDisposable: { All: True }
IEquatable`1: { }
IEquatable`1:
Methods:
- "bool Equals(!0)"
IFormatProvider: { All: True }
IFormattable: { All: True }
Index: { All: True }

View File

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

View File

@@ -329,7 +329,7 @@ namespace Robust.Shared.GameObjects
ref Unit unitRef,
EventData subs)
{
foreach (var handler in subs.BroadcastRegistrations)
foreach (var handler in subs.BroadcastRegistrations.Span)
{
if ((handler.Mask & source) != 0)
handler.Handler(ref unitRef);

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.GameObjects
EventData sub,
ref ValueList<OrderedEventDispatch> found)
{
foreach (var handler in sub.BroadcastRegistrations)
foreach (var handler in sub.BroadcastRegistrations.Span)
{
if ((handler.Mask & source) != 0)
found.Add(new OrderedEventDispatch(handler.Handler, handler.Order));
@@ -44,7 +44,7 @@ namespace Robust.Shared.GameObjects
{
found.Sort(OrderedEventDispatchComparer.Instance);
foreach (var (handler, _) in found)
foreach (var (handler, _) in found.Span)
{
handler(ref eventArgs);
}

View File

@@ -11,6 +11,7 @@ using Robust.Shared.GameStates;
using Robust.Shared.Log;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
#if EXCEPTION_TOLERANCE
@@ -176,6 +177,60 @@ namespace Robust.Shared.GameObjects
}
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponents(EntityUid target, EntityPrototype prototype, bool removeExisting = true)
{
AddComponents(target, prototype.Components, removeExisting);
}
/// <inheritdoc />
public void AddComponents(EntityUid target, ComponentRegistry registry, bool removeExisting = true)
{
if (registry.Count == 0)
return;
var metadata = MetaQuery.GetComponent(target);
foreach (var (name, entry) in registry)
{
var reg = _componentFactory.GetRegistration(name);
if (HasComponent(target, reg.Type))
{
if (!removeExisting)
continue;
RemoveComponent(target, reg.Type, metadata);
}
var comp = _componentFactory.GetComponent(reg);
_serManager.CopyTo(entry.Component, ref comp, notNullableOverride: true);
AddComponent(target, comp, metadata: metadata);
}
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponents(EntityUid target, EntityPrototype prototype)
{
RemoveComponents(target, prototype.Components);
}
/// <inheritdoc />
public void RemoveComponents(EntityUid target, ComponentRegistry registry)
{
if (registry.Count == 0)
return;
var metadata = MetaQuery.GetComponent(target);
foreach (var entry in registry.Values)
{
RemoveComponent(target, entry.Component.GetType(), metadata);
}
}
public IComponent AddComponent(EntityUid uid, ushort netId, MetaDataComponent? meta = null)
{
var newComponent = _componentFactory.GetComponent(netId);

View File

@@ -111,32 +111,18 @@ public partial class EntityManager
if (!xform.ParentUid.IsValid())
return false;
if (!MetaQuery.TryGetComponent(target, out var meta))
return false;
if ((meta.Flags & MetaDataFlags.InContainer) == 0)
if (!_containers.TryGetContainingContainer(target, out var container))
{
uid = SpawnAttachedTo(protoName, xform.Coordinates, overrides);
uid = SpawnNextToOrDrop(protoName, target, xform, overrides);
return true;
}
if (!TryGetComponent(xform.ParentUid, out ContainerManagerComponent? containerComp))
return false;
foreach (var container in containerComp.Containers.Values)
{
if (!container.Contains(target))
continue;
uid = Spawn(protoName, overrides);
if (_containers.Insert(uid.Value, container))
return true;
DeleteEntity(uid.Value);
uid = null;
return false;
}
uid = Spawn(protoName, overrides);
if (_containers.Insert(uid.Value, container))
return true;
DeleteEntity(uid.Value);
uid = null;
return false;
}
@@ -183,14 +169,28 @@ public partial class EntityManager
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
return SpawnInContainerOrDrop(protoName, containerUid, containerId, out _, xform, containerComp, overrides);
}
public EntityUid SpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
out bool inserted,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
var uid = Spawn(protoName, overrides);
inserted = true;
if ((containerComp == null && !TryGetComponent(containerUid, out containerComp))
|| !containerComp.Containers.TryGetValue(containerId, out var container)
|| !_containers.Insert(uid, container))
{
inserted = false;
xform ??= TransformQuery.GetComponent(containerUid);
if (xform.ParentUid.IsValid())
_xforms.DropNextTo(uid, (containerUid, xform));

View File

@@ -1,4 +1,7 @@
namespace Robust.Shared.GameObjects
using Robust.Shared.Collections;
using Robust.Shared.Random;
namespace Robust.Shared.GameObjects
{
public static class EntityManagerExt
{
@@ -19,5 +22,79 @@
return default;
}
/// <summary>
/// Picks an entity at random with the supplied component.
/// </summary>
public static bool TryGetRandom<TComp1>(this IEntityManager entManager, IRobustRandom random, out EntityUid entity, bool includePaused = false) where TComp1 : IComponent
{
var entities = new ValueList<EntityUid>();
if (includePaused)
{
var query = entManager.AllEntityQueryEnumerator<TComp1>();
while (query.MoveNext(out var uid, out _))
{
entities.Add(uid);
}
}
else
{
var query = entManager.EntityQueryEnumerator<TComp1>();
while (query.MoveNext(out var uid, out _))
{
entities.Add(uid);
}
}
if (entities.Count == 0)
{
entity = EntityUid.Invalid;
return false;
}
entity = random.Pick(entities);
return true;
}
/// <summary>
/// Picks an entity at random with the supplied components.
/// </summary>
public static bool TryGetRandom<TComp1, TComp2>(this IEntityManager entManager, IRobustRandom random, out EntityUid entity, bool includePaused = false)
where TComp1 : IComponent
where TComp2 : IComponent
{
var entities = new ValueList<EntityUid>();
if (includePaused)
{
var query = entManager.AllEntityQueryEnumerator<TComp1, TComp2>();
while (query.MoveNext(out var uid, out _, out _))
{
entities.Add(uid);
}
}
else
{
var query = entManager.EntityQueryEnumerator<TComp1, TComp2>();
while (query.MoveNext(out var uid, out _, out _))
{
entities.Add(uid);
}
}
if (entities.Count == 0)
{
entity = EntityUid.Invalid;
return false;
}
entity = random.Pick(entities);
return true;
}
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Robust.Shared.GameObjects
@@ -38,6 +39,26 @@ namespace Robust.Shared.GameObjects
/// </summary>
int Count(Type component);
/// <summary>
/// Adds the specified components from the <see cref="EntityPrototype"/>
/// </summary>
void AddComponents(EntityUid target, EntityPrototype prototype, bool removeExisting = true);
/// <summary>
/// Adds the specified registry components to the target entity.
/// </summary>
void AddComponents(EntityUid target, ComponentRegistry registry, bool removeExisting = true);
/// <summary>
/// Removes the specified entity prototype components from the target entity.
/// </summary>
void RemoveComponents(EntityUid target, EntityPrototype prototype);
/// <summary>
/// Removes the specified registry components from the target entity.
/// </summary>
void RemoveComponents(EntityUid target, ComponentRegistry registry);
/// <summary>
/// Adds a Component type to an entity. If the entity is already Initialized, the component will
/// automatically be Initialized and Started.

View File

@@ -46,7 +46,7 @@ public partial interface IEntityManager
EntityUid SpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null);
/// <summary>
/// Attempts to spawn an entity inside of a container.
/// Attempt to spawn an entity and insert it into a container. If the insertion fails, the entity gets deleted.
/// </summary>
bool TrySpawnInContainer(
string? protoName,
@@ -58,9 +58,9 @@ public partial interface IEntityManager
/// <summary>
/// Attempts to spawn an entity inside of a container. If it fails to insert into the container, it will
/// instead attempt to spawn the entity next to the target.
/// instead drop the entity next to the target (see <see cref="SpawnNextToOrDrop"/>).
/// </summary>
public EntityUid SpawnInContainerOrDrop(
EntityUid SpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
@@ -68,9 +68,20 @@ public partial interface IEntityManager
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null);
/// <inheritdoc cref="SpawnInContainerOrDrop(string?,Robust.Shared.GameObjects.EntityUid,string,Robust.Shared.GameObjects.TransformComponent?,Robust.Shared.Containers.ContainerManagerComponent?,Robust.Shared.Prototypes.ComponentRegistry?)"/>
EntityUid SpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
out bool inserted,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null);
/// <summary>
/// Attempts to spawn an entity adjacent to some other entity. If the other entity is in a container, this will
/// attempt to insert the new entity into the same container.
/// Attempts to spawn an entity adjacent to some other target entity. If the target entity is in
/// a container, this will attempt to insert the spawned entity into the same container. If the insertion fails,
/// the entity is deleted. If the entity is not in a container, this behaves like <see cref="SpawnNextToOrDrop"/>.
/// </summary>
bool TrySpawnNextTo(
string? protoName,

View File

@@ -67,7 +67,7 @@ public sealed partial class EntityLookupSystem
}
}
foreach (var uid in toAdd)
foreach (var uid in toAdd.Span)
{
intersecting.Add(uid);
}

View File

@@ -58,7 +58,7 @@ public sealed partial class EntityLookupSystem
}
}
foreach (var uid in toAdd)
foreach (var uid in toAdd.Span)
{
intersecting.Add(uid);
}

View File

@@ -74,6 +74,28 @@ public abstract class SharedAppearanceSystem : EntitySystem
return component.AppearanceData.TryGetValue(key, out value);
}
/// <summary>
/// Copies appearance data from <c>src</c> to <c>dest</c>.
/// If <c>src</c> has no <see cref="AppearanceComponent"/> nothing is done.
/// If <c>dest</c> has no <c>AppearanceComponent</c> then it is created.
/// </summary>
public void CopyData(Entity<AppearanceComponent?> src, Entity<AppearanceComponent?> dest)
{
if (!Resolve(src, ref src.Comp, false))
return;
dest.Comp ??= EnsureComp<AppearanceComponent>(dest);
dest.Comp.AppearanceData.Clear();
foreach (var (key, value) in src.Comp.AppearanceData)
{
dest.Comp.AppearanceData[key] = value;
}
Dirty(dest, dest.Comp);
QueueUpdate(dest, dest.Comp);
}
}
[Serializable, NetSerializable]

View File

@@ -180,7 +180,7 @@ namespace Robust.Shared.GameObjects
toRemove.Add((oldId, oldFixture));
}
foreach (var (id, fixture) in toRemove)
foreach (var (id, fixture) in toRemove.Span)
{
// TODO add a DestroyFixture() override that takes in a list.
// reduced broadphase lookups
@@ -194,7 +194,7 @@ namespace Robust.Shared.GameObjects
}
// Anything remaining is a new fixture (or at least, may have not serialized onto the chunk yet).
foreach (var (id, fixture) in newFixtures)
foreach (var (id, fixture) in newFixtures.Span)
{
var existingFixture = _fixtures.GetFixtureOrNull(uid, id, manager: manager);
// Check if it's the same (otherwise remove anyway).

View File

@@ -975,36 +975,25 @@ public abstract partial class SharedTransformSystem
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPosition(EntityUid uid, Vector2 worldPos)
{
var xform = Transform(uid);
var xform = XformQuery.GetComponent(uid);
SetWorldPosition(xform, worldPos);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPosition(EntityUid uid, Vector2 worldPos, EntityQuery<TransformComponent> xformQuery)
{
var component = xformQuery.GetComponent(uid);
SetWorldPosition(component, worldPos, xformQuery);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPosition(TransformComponent component, Vector2 worldPos)
{
SetWorldPosition(component, worldPos, XformQuery);
SetWorldPosition((component.Owner, component), worldPos);
}
/// <summary>
/// Sets the position of the entity in world-terms to the specified position.
/// May also de-parent the entity.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPosition(TransformComponent component, Vector2 worldPos, EntityQuery<TransformComponent> xformQuery)
public void SetWorldPosition(Entity<TransformComponent> entity, Vector2 worldPos)
{
if (!component._parent.IsValid())
{
DebugTools.Assert("Parent is invalid while attempting to set WorldPosition - did you try to move root node?");
return;
}
var (curWorldPos, curWorldRot) = GetWorldPositionRotation(component, xformQuery);
var negativeParentWorldRot = component._localRotation - curWorldRot;
var newLocalPos = component._localPosition + negativeParentWorldRot.RotateVec(worldPos - curWorldPos);
SetLocalPosition(component, newLocalPos);
SetWorldPositionRotationInternal(entity.Owner, worldPos, null, entity.Comp);
}
#endregion
@@ -1090,24 +1079,35 @@ public abstract partial class SharedTransformSystem
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPositionRotation(EntityUid uid, Vector2 worldPos, Angle worldRot, TransformComponent? component = null)
{
SetWorldPositionRotationInternal(uid, worldPos, worldRot, component);
}
private void SetWorldPositionRotationInternal(EntityUid uid, Vector2 worldPos, Angle? worldRot = null, TransformComponent? component = null)
{
if (!XformQuery.Resolve(uid, ref component))
return;
if (!component._parent.IsValid())
// If no worldRot supplied then default the new rotation to 0.
if (!component._parent.IsValid() || component.MapUid == null)
{
DebugTools.Assert("Parent is invalid while attempting to set WorldPosition - did you try to move root node?");
return;
}
var (curWorldPos, curWorldRot) = GetWorldPositionRotation(component);
var negativeParentWorldRot = component.LocalRotation - curWorldRot;
var newLocalPos = component.LocalPosition + negativeParentWorldRot.RotateVec(worldPos - curWorldPos);
var newLocalRot = component.LocalRotation + worldRot - curWorldRot;
SetLocalPositionRotation(uid, newLocalPos, newLocalRot, component);
if (component.GridUid != uid && _mapManager.TryFindGridAt(component.MapUid.Value, worldPos, out var targetGrid, out _))
{
var targetGridXform = XformQuery.GetComponent(targetGrid);
var invLocalMatrix = targetGridXform.InvLocalMatrix;
var gridRot = targetGridXform.LocalRotation;
var localRot = worldRot - gridRot;
SetCoordinates(uid, component, new EntityCoordinates(targetGrid, invLocalMatrix.Transform(worldPos)), rotation: localRot);
}
else
{
SetCoordinates(uid, component, new EntityCoordinates(component.MapUid.Value, worldPos), rotation: worldRot);
}
}
[Obsolete("Use override with EntityUid")]
@@ -1460,8 +1460,7 @@ public abstract partial class SharedTransformSystem
while (targetXform.ParentUid.IsValid())
{
if (_container.IsEntityInContainer(targetUid)
&& _container.TryGetContainingContainer(targetXform.ParentUid, targetUid, out var container,
skipExistCheck: true)
&& _container.TryGetContainingContainer(targetXform.ParentUid, targetUid, out var container)
&& _container.Insert((entity, xform, null, null), container))
{
return;

View File

@@ -25,8 +25,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
foreach (var prototypeData in component.InterfaceData)
{
component.Interfaces[prototypeData.UiKey] = new PlayerBoundUserInterface(prototypeData, uid);
component.MappedInterfaceData[prototypeData.UiKey] = prototypeData;
AddUi((uid, component), prototypeData);
}
}
@@ -105,6 +104,19 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
{
}
/// <summary>
/// Add a UI after an entity has been created.
/// It cannot be added already.
/// </summary>
public void AddUi(Entity<UserInterfaceComponent?> ent, PrototypeData data)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.Interfaces[data.UiKey] = new PlayerBoundUserInterface(data, ent);
ent.Comp.MappedInterfaceData[data.UiKey] = data;
}
public bool TryGetUi(EntityUid uid, Enum uiKey, [NotNullWhen(true)] out PlayerBoundUserInterface? bui, UserInterfaceComponent? ui = null)
{
bui = null;

View File

@@ -289,7 +289,7 @@ namespace Robust.Shared.Physics.Systems
// TODO add a DestroyFixture() override that takes in a list.
// reduced broadphase lookups
foreach (var (id, fixture) in toRemoveFixtures)
foreach (var (id, fixture) in toRemoveFixtures.Span)
{
computeProperties = true;
DestroyFixture(uid, id, fixture, false, physics, component);
@@ -298,7 +298,7 @@ namespace Robust.Shared.Physics.Systems
// TODO: We also still need event listeners for shapes (Probably need C# events)
// Or we could just make it so shapes can only be updated via fixturesystem which handles it
// automagically (friends or something?)
foreach (var (id, fixture) in toAddFixtures)
foreach (var (id, fixture) in toAddFixtures.Span)
{
computeProperties = true;
CreateFixture(uid, id, fixture, false, component, physics, xform);

View File

@@ -737,7 +737,7 @@ public partial class SharedPhysicsSystem
public (int Layer, int Mask) GetHardCollision(EntityUid uid, FixturesComponent? manager = null)
{
if (!Resolve(uid, ref manager))
if (!_fixturesQuery.Resolve(uid, ref manager, false))
{
return (0, 0);
}

View File

@@ -291,7 +291,7 @@ namespace Robust.Shared.Physics.Systems
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
if (!proxy.Body.Hard)
if (!proxy.Fixture.Hard)
return true;
if (predicate.Invoke(proxy.Entity, state) == true)
@@ -319,7 +319,7 @@ namespace Robust.Shared.Physics.Systems
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
if (!proxy.Body.Hard)
if (!proxy.Fixture.Hard)
return true;
if (predicate.Invoke(proxy.Entity, state) == true)
@@ -567,6 +567,7 @@ namespace Robust.Shared.Physics.Systems
// No requirement on collision being enabled so chainshapes will fail
foreach (var fixtureA in manager.Fixtures.Values)
{
// We ignore non-hard fixtures if there is at least one hard fixture (i.e., if the body is hard)
if (body.Hard && !fixtureA.Hard)
continue;

View File

@@ -13,6 +13,7 @@ using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
@@ -60,6 +61,12 @@ namespace Robust.Shared.Physics.Systems
private int _substeps;
/// <summary>
/// A variation of <see cref="IGameTiming.CurTime"/> that takes into account the current physics sub-step.
/// Useful for some entities that need to interpolate their positions during sub-steps.
/// </summary>
public TimeSpan? EffectiveCurTime;
public bool MetricsEnabled { get; protected set; }
private EntityQuery<FixturesComponent> _fixturesQuery;
@@ -284,6 +291,7 @@ namespace Robust.Shared.Physics.Systems
{
var frameTime = deltaTime / _substeps;
EffectiveCurTime = _gameTiming.CurTime;
for (int i = 0; i < _substeps; i++)
{
var updateBeforeSolve = new PhysicsUpdateBeforeSolveEvent(prediction, frameTime);
@@ -323,7 +331,11 @@ namespace Robust.Shared.Physics.Systems
FinalStep(comp);
}
}
EffectiveCurTime = EffectiveCurTime.Value + TimeSpan.FromSeconds(frameTime);
}
EffectiveCurTime = null;
}
protected virtual void FinalStep(PhysicsMapComponent component)

View File

@@ -3,6 +3,7 @@ using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown;
@@ -386,6 +387,11 @@ public interface IPrototypeManager
/// For example: /Prototypes/Guidebook
/// </param>
void AbstractDirectory(ResPath path);
/// <summary>
/// Tries to get a random prototype.
/// </summary>
bool TryGetRandom<T>(IRobustRandom random, [NotNullWhen(true)] out IPrototype? prototype) where T : class, IPrototype;
}
internal interface IPrototypeManagerInternal : IPrototypeManager

View File

@@ -36,7 +36,7 @@ public partial class PrototypeManager
.ToArray();
// Shuffle to avoid input data patterns causing uneven thread workloads.
RandomExtensions.Shuffle(streams.AsSpan(), new System.Random());
(new System.Random()).Shuffle(streams.AsSpan());
var sawmill = _logManager.GetSawmill("eng");

View File

@@ -432,7 +432,7 @@ namespace Robust.Shared.Prototypes
}).ToArray();
// Randomize to remove any patterns that could cause uneven load.
RandomExtensions.Shuffle(allResults.AsSpan(), rand);
rand.Shuffle(allResults.AsSpan());
// Create channel that all AfterDeserialization hooks in this group will be sent into.
var hooksChannelOptions = new UnboundedChannelOptions
@@ -1082,6 +1082,34 @@ namespace Robust.Shared.Prototypes
_prototypeDataCache[prototype.ID] = data;
return data;
}
public bool TryGetRandom<T>(IRobustRandom random, [NotNullWhen(true)] out IPrototype? prototype) where T : class, IPrototype
{
var count = Count<T>();
if (count == 0)
{
prototype = null;
return false;
}
var index = 0;
var picked = random.Next(count);
foreach (var proto in EnumeratePrototypes<T>())
{
if (index == picked)
{
prototype = proto;
return true;
}
index++;
}
throw new ArgumentOutOfRangeException($"Unable to pick valid prototype for {typeof(T)}?");
}
}
public sealed class InvalidPrototypeNameException : Exception

View File

@@ -1,56 +1,141 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.Collections;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed.Commands.Generic;
namespace Robust.Shared.Random;
/// <summary>
/// Wrapper around random number generator helping methods.
/// </summary>
public interface IRobustRandom
{
/// <summary>
/// Get the underlying System.Random
/// </summary>
/// <returns></returns>
/// <summary> Get the underlying <see cref="Random"/>.</summary>
System.Random GetRandom();
/// <summary> Set seed for underlying <see cref="Random"/>. </summary>
void SetSeed(int seed);
/// <summary> Get random <see cref="float"/> value between 0 (included) and 1 (excluded). </summary>
float NextFloat();
/// <summary> Get random <see cref="float"/> value in range of <paramref name="minValue"/> (included) and <paramref name="maxValue"/> (excluded). </summary>
/// <param name="minValue">Random value should be greater or equal to this value.</param>
/// <param name="maxValue">Random value should be less then this value.</param>
public float NextFloat(float minValue, float maxValue)
=> NextFloat() * (maxValue - minValue) + minValue;
/// <summary> Get random <see cref="float"/> value in range of 0 (included) and <paramref name="maxValue"/> (excluded). </summary>
/// <param name="maxValue">Random value should be less then this value.</param>
public float NextFloat(float maxValue) => NextFloat() * maxValue;
/// <summary> Get random <see cref="int"/> value. </summary>
int Next();
int Next(int minValue, int maxValue);
TimeSpan Next(TimeSpan minTime, TimeSpan maxTime);
TimeSpan Next(TimeSpan maxTime);
/// <summary> Get random <see cref="int"/> value in range of 0 (included) and <paramref name="maxValue"/> (excluded). </summary>
/// <param name="maxValue">Random value should be less then this value.</param>
int Next(int maxValue);
/// <summary> Get random <see cref="int"/> value in range of <paramref name="minValue"/> (included) and <paramref name="maxValue"/> (excluded). </summary>
/// <param name="minValue">Random value should be greater or equal to this value.</param>
/// <param name="maxValue">Random value should be less then this value.</param>
int Next(int minValue, int maxValue);
/// <summary> Get random <see cref="byte"/> value between 0 (included) and <see cref="byte.MaxValue"/> (excluded). </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte NextByte()
=> NextByte(byte.MaxValue);
/// <summary> Get random <see cref="byte"/> value in range of 0 (included) and <paramref name="maxValue"/> (excluded). </summary>
/// <param name="maxValue">Random value should be less then this value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte NextByte(byte maxValue)
=> NextByte(0, maxValue);
/// <summary> Get random <see cref="byte"/> value in range of <paramref name="minValue"/> (included) and <paramref name="maxValue"/> (excluded). </summary>
/// <param name="minValue">Random value should be greater or equal to this value.</param>
/// <param name="maxValue">Random value should be less then this value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte NextByte(byte minValue, byte maxValue)
=> (byte)Next(minValue, maxValue);
/// <summary> Get random <see cref="double"/> value between 0 (included) and 1 (excluded). </summary>
double NextDouble();
double NextDouble(double minValue, double maxValue) => NextDouble() * (maxValue - minValue) + minValue;
/// <summary> Get random <see cref="double"/> value in range of 0 (included) and <paramref name="maxValue"/> (excluded). </summary>
/// <param name="maxValue">Random value should be less then this value.</param>
double Next(double maxValue)
=> NextDouble() * maxValue;
/// <summary> Get random <see cref="double"/> value in range of <paramref name="minValue"/> (included) and <paramref name="maxValue"/> (excluded). </summary>
/// <param name="minValue">Random value should be greater or equal to this value.</param>
/// <param name="maxValue">Random value should be less then this value.</param>
double NextDouble(double minValue, double maxValue)
=> NextDouble() * (maxValue - minValue) + minValue;
/// <summary> Get random <see cref="TimeSpan"/> value in range of <see cref="TimeSpan.Zero"/> (included) and <paramref name="maxTime"/> (excluded). </summary>
/// <param name="maxTime">Random value should be less then this value.</param>
TimeSpan Next(TimeSpan maxTime);
/// <summary> Get random <see cref="TimeSpan"/> value in range of <paramref name="minTime"/> (included) and <paramref name="maxTime"/> (excluded). </summary>
/// <param name="minTime">Random value should be greater or equal to this value.</param>
/// <param name="maxTime">Random value should be less then this value.</param>
TimeSpan Next(TimeSpan minTime, TimeSpan maxTime);
/// <summary> Fill buffer with random bytes (values). </summary>
void NextBytes(byte[] buffer);
public Angle NextAngle() => NextFloat() * MathF.Tau;
public Angle NextAngle(Angle minValue, Angle maxValue) => NextFloat() * (maxValue - minValue) + minValue;
public Angle NextAngle(Angle maxValue) => NextFloat() * maxValue;
/// <summary> Get random <see cref="Angle"/> value in range of 0 (included) and <see cref="MathF.Tau"/> (excluded). </summary>
public Angle NextAngle()
=> NextFloat() * MathF.Tau;
/// <summary> Get random <see cref="Angle"/> value in range of 0 (included) and <paramref name="maxValue"/> (excluded). </summary>
/// <param name="maxValue">Random value should be less then this value.</param>
public Angle NextAngle(Angle maxValue)
=> NextFloat() * maxValue;
/// <summary> Get random <see cref="Angle"/> value in range of <paramref name="minValue"/> (included) and <paramref name="maxValue"/> (excluded). </summary>
/// <param name="minValue">Random value should be greater or equal to this value.</param>
/// <param name="maxValue">Random value should be less then this value.</param>
public Angle NextAngle(Angle minValue, Angle maxValue)
=> NextFloat() * (maxValue - minValue) + minValue;
/// <summary>
/// Random vector, created from a uniform distribution of magnitudes and angles.
/// </summary>
/// <param name="maxMagnitude">Max value for randomized vector magnitude (excluded).</param>
public Vector2 NextVector2(float maxMagnitude = 1)
=> NextVector2(0, maxMagnitude);
/// <summary>
/// Random vector, created from a uniform distribution of magnitudes and angles.
/// </summary>
/// <param name="minMagnitude">Min value for randomized vector magnitude (included).</param>
/// <param name="maxMagnitude">Max value for randomized vector magnitude (excluded).</param>
/// <remarks>
/// In general, NextVector2(1) will tend to result in vectors with smaller magnitudes than
/// NextVector2Box(1,1), even if you ignored any vectors with a magnitude larger than one.
/// </remarks>
public Vector2 NextVector2(float minMagnitude, float maxMagnitude) => NextAngle().RotateVec(new Vector2(NextFloat(minMagnitude, maxMagnitude), 0));
public Vector2 NextVector2(float maxMagnitude = 1) => NextVector2(0, maxMagnitude);
public Vector2 NextVector2(float minMagnitude, float maxMagnitude)
=> NextAngle().RotateVec(new Vector2(NextFloat(minMagnitude, maxMagnitude), 0));
/// <summary>
/// Random vector, created from a uniform distribution of x and y coordinates lying inside some box.
/// </summary>
public Vector2 NextVector2Box(float minX, float minY, float maxX, float maxY) => new Vector2(NextFloat(minX, maxX), NextFloat(minY, maxY));
public Vector2 NextVector2Box(float maxAbsX = 1, float maxAbsY = 1) => NextVector2Box(-maxAbsX, -maxAbsY, maxAbsX, maxAbsY);
public Vector2 NextVector2Box(float minX, float minY, float maxX, float maxY)
=> new Vector2(NextFloat(minX, maxX), NextFloat(minY, maxY));
/// <summary>
/// Random vector, created from a uniform distribution of x and y coordinates lying inside some box.
/// Box will have coordinates starting at [-<paramref name="maxAbsX"/> , -<paramref name="maxAbsY"/>]
/// and ending in [<paramref name="maxAbsX"/> , <paramref name="maxAbsY"/>]
/// </summary>
public Vector2 NextVector2Box(float maxAbsX = 1, float maxAbsY = 1)
=> NextVector2Box(-maxAbsX, -maxAbsY, maxAbsX, maxAbsY);
/// <summary> Randomly switches positions in collection. </summary>
void Shuffle<T>(IList<T> list)
{
var n = list.Count;
@@ -62,6 +147,7 @@ public interface IRobustRandom
}
}
/// <summary> Randomly switches positions in collection. </summary>
void Shuffle<T>(Span<T> list)
{
var n = list.Length;
@@ -73,6 +159,7 @@ public interface IRobustRandom
}
}
/// <summary> Randomly switches positions in collection. </summary>
void Shuffle<T>(ValueList<T> list)
{
var n = list.Count;
@@ -83,24 +170,6 @@ public interface IRobustRandom
(list[k], list[n]) = (list[n], list[k]);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte NextByte(byte maxValue)
{
return NextByte(0, maxValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte NextByte()
{
return NextByte(byte.MaxValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte NextByte(byte minValue, byte maxValue)
{
return (byte) Next(minValue, maxValue);
}
}
public static class RandomHelpers
@@ -136,6 +205,6 @@ public static class RandomHelpers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte NextByte(this System.Random random, byte minValue, byte maxValue)
{
return (byte) random.Next(minValue, maxValue);
return (byte)random.Next(minValue, maxValue);
}
}

View File

@@ -1,160 +1,263 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Robust.Shared.Collections;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Shared.Random
namespace Robust.Shared.Random;
public static class RandomExtensions
{
public static class RandomExtensions
/// <summary>
/// Generate a random number from a normal (gaussian) distribution.
/// </summary>
/// <param name="random">The random object to generate the number from.</param>
/// <param name="μ">The average or "center" of the normal distribution.</param>
/// <param name="σ">The standard deviation of the normal distribution.</param>
public static double NextGaussian(this IRobustRandom random, double μ = 0, double σ = 1)
{
/// <summary>
/// Generate a random number from a normal (gaussian) distribution.
/// </summary>
/// <param name="random">The random object to generate the number from.</param>
/// <param name="μ">The average or "center" of the normal distribution.</param>
/// <param name="σ">The standard deviation of the normal distribution.</param>
public static double NextGaussian(this IRobustRandom random, double μ = 0, double σ = 1)
{
return random.GetRandom().NextGaussian(μ, σ);
}
return random.GetRandom().NextGaussian(μ, σ);
}
public static T Pick<T>(this IRobustRandom random, IReadOnlyList<T> list)
{
var index = random.Next(list.Count);
return list[index];
}
/// <summary>Picks a random element from a collection.</summary>
public static T Pick<T>(this IRobustRandom random, IReadOnlyList<T> list)
{
var index = random.Next(list.Count);
return list[index];
}
public static ref T Pick<T>(this IRobustRandom random, ValueList<T> list)
{
var index = random.Next(list.Count);
return ref list[index];
}
/// <summary>Picks a random element from a collection.</summary>
public static ref T Pick<T>(this IRobustRandom random, ValueList<T> list)
{
var index = random.Next(list.Count);
return ref list[index];
}
public static ref T Pick<T>(this System.Random random, ValueList<T> list)
{
var index = random.Next(list.Count);
return ref list[index];
}
/// <summary>Picks a random element from a collection.</summary>
public static ref T Pick<T>(this System.Random random, ValueList<T> list)
{
var index = random.Next(list.Count);
return ref list[index];
}
/// <summary>Picks a random element from a collection.</summary>
/// <remarks>
/// This is O(n).
/// </remarks>
public static T Pick<T>(this IRobustRandom random, IReadOnlyCollection<T> collection)
/// <summary>Picks a random element from a collection.</summary>
/// <remarks>
/// This is O(n).
/// </remarks>
public static T Pick<T>(this IRobustRandom random, IReadOnlyCollection<T> collection)
{
var index = random.Next(collection.Count);
var i = 0;
foreach (var t in collection)
{
var index = random.Next(collection.Count);
var i = 0;
foreach (var t in collection)
if (i++ == index)
{
if (i++ == index)
{
return t;
}
}
throw new UnreachableException("This should be unreachable!");
}
public static T PickAndTake<T>(this IRobustRandom random, IList<T> list)
{
var index = random.Next(list.Count);
var element = list[index];
list.RemoveAt(index);
return element;
}
/// <summary>
/// Picks a random element from a set and returns it.
/// This is O(n) as it has to iterate the collection until the target index.
/// </summary>
public static T Pick<T>(this System.Random random, ICollection<T> collection)
{
var index = random.Next(collection.Count);
var i = 0;
foreach (var t in collection)
{
if (i++ == index)
{
return t;
}
}
throw new UnreachableException("This should be unreachable!");
}
/// <summary>
/// Picks a random from a collection then removes it and returns it.
/// This is O(n) as it has to iterate the collection until the target index.
/// </summary>
public static T PickAndTake<T>(this System.Random random, ICollection<T> set)
{
var tile = Pick(random, set);
set.Remove(tile);
return tile;
}
/// <summary>
/// Generate a random number from a normal (gaussian) distribution.
/// </summary>
/// <param name="random">The random object to generate the number from.</param>
/// <param name="μ">The average or "center" of the normal distribution.</param>
/// <param name="σ">The standard deviation of the normal distribution.</param>
public static double NextGaussian(this System.Random random, double μ = 0, double σ = 1)
{
// https://stackoverflow.com/a/218600
var α = random.NextDouble();
var β = random.NextDouble();
var randStdNormal = Math.Sqrt(-2.0 * Math.Log(α)) * Math.Sin(2.0 * Math.PI * β);
return μ + σ * randStdNormal;
}
public static Angle NextAngle(this System.Random random) => NextFloat(random) * MathF.Tau;
public static Angle NextAngle(this System.Random random, Angle minAngle, Angle maxAngle)
{
DebugTools.Assert(minAngle < maxAngle);
return minAngle + (maxAngle - minAngle) * random.NextDouble();
}
public static float NextFloat(this IRobustRandom random)
{
// This is pretty much the CoreFX implementation.
// So credits to that.
// Except using float instead of double.
return random.Next() * 4.6566128752458E-10f;
}
public static float NextFloat(this System.Random random)
{
return random.Next() * 4.6566128752458E-10f;
}
/// <summary>
/// Have a certain chance to return a boolean.
/// </summary>
/// <param name="random">The random instance to run on.</param>
/// <param name="chance">The chance to pass, from 0 to 1.</param>
public static bool Prob(this IRobustRandom random, float chance)
{
DebugTools.Assert(chance <= 1 && chance >= 0, $"Chance must be in the range 0-1. It was {chance}.");
return random.NextDouble() < chance;
}
internal static void Shuffle<T>(Span<T> array, System.Random random)
{
var n = array.Length;
while (n > 1)
{
n--;
var k = random.Next(n + 1);
(array[k], array[n]) =
(array[n], array[k]);
return t;
}
}
throw new UnreachableException("This should be unreachable!");
}
/// <summary>
/// Picks a random element from a list, removes it from list and returns it.
/// This is O(n) as it preserves the order of other items in the list.
/// </summary>
public static T PickAndTake<T>(this IRobustRandom random, IList<T> list)
{
var index = random.Next(list.Count);
var element = list[index];
list.RemoveAt(index);
return element;
}
/// <summary>
/// Picks a random element from a set and returns it.
/// This is O(n) as it has to iterate the collection until the target index.
/// </summary>
public static T Pick<T>(this System.Random random, ICollection<T> collection)
{
var index = random.Next(collection.Count);
var i = 0;
foreach (var t in collection)
{
if (i++ == index)
{
return t;
}
}
throw new UnreachableException("This should be unreachable!");
}
/// <summary>
/// Picks a random from a collection then removes it and returns it.
/// This is O(n) as it has to iterate the collection until the target index.
/// </summary>
public static T PickAndTake<T>(this System.Random random, ICollection<T> set)
{
var tile = Pick(random, set);
set.Remove(tile);
return tile;
}
/// <summary>
/// Generate a random number from a normal (gaussian) distribution.
/// </summary>
/// <param name="random">The random object to generate the number from.</param>
/// <param name="μ">The average or "center" of the normal distribution.</param>
/// <param name="σ">The standard deviation of the normal distribution.</param>
public static double NextGaussian(this System.Random random, double μ = 0, double σ = 1)
{
// https://stackoverflow.com/a/218600
var α = random.NextDouble();
var β = random.NextDouble();
var randStdNormal = Math.Sqrt(-2.0 * Math.Log(α)) * Math.Sin(2.0 * Math.PI * β);
return μ + σ * randStdNormal;
}
public static Angle NextAngle(this System.Random random) => NextFloat(random) * MathF.Tau;
public static Angle NextAngle(this System.Random random, Angle minAngle, Angle maxAngle)
{
DebugTools.Assert(minAngle < maxAngle);
return minAngle + (maxAngle - minAngle) * random.NextDouble();
}
public static float NextFloat(this IRobustRandom random)
{
// This is pretty much the CoreFX implementation.
// So credits to that.
// Except using float instead of double.
return random.Next() * 4.6566128752458E-10f;
}
public static float NextFloat(this System.Random random)
{
return random.Next() * 4.6566128752458E-10f;
}
/// <summary>
/// Have a certain chance to return a boolean.
/// </summary>
/// <param name="random">The random instance to run on.</param>
/// <param name="chance">The chance to pass, from 0 to 1.</param>
public static bool Prob(this IRobustRandom random, float chance)
{
DebugTools.Assert(chance <= 1 && chance >= 0, $"Chance must be in the range 0-1. It was {chance}.");
return random.NextDouble() < chance;
}
/// <summary>
/// Get set amount of random items from a collection.
/// If <paramref name="allowDuplicates"/> is false and <paramref name="source"/>
/// is smaller then <paramref name="count"/> - returns shuffled <paramref name="source"/> clone.
/// If <paramref name="source"/> is empty, and/or <paramref name="count"/> is 0, returns empty.
/// </summary>
/// <param name="random">Instance of random to invoke upon.</param>
/// <param name="source">Collection from which items should be picked.</param>
/// <param name="count">Number of random items to be picked.</param>
/// <param name="allowDuplicates">If true, items are allowed to be picked more than once.</param>
public static T[] GetItems<T>(this IRobustRandom random, IList<T> source, int count, bool allowDuplicates = true)
{
if (source.Count == 0 || count <= 0)
return Array.Empty<T>();
if (allowDuplicates == false && count >= source.Count)
{
var arr = source.ToArray();
random.Shuffle(arr);
return arr;
}
var sourceCount = source.Count;
var result = new T[count];
if (allowDuplicates)
{
for (var i = 0; i < count; i++)
{
result[i] = source[random.Next(sourceCount)];
}
return result;
}
var indices = sourceCount <= 1024 ? stackalloc int[sourceCount] : new int[sourceCount];
for (var i = 0; i < sourceCount; i++)
{
indices[i] = i;
}
for (var i = 0; i < count; i++)
{
var j = random.Next(sourceCount - i);
result[i] = source[indices[j]];
indices[j] = indices[sourceCount - i - 1];
}
return result;
}
/// <inheritdoc cref="GetItems{T}(Robust.Shared.Random.IRobustRandom,System.Collections.Generic.IList{T},int,bool)"/>
public static T[] GetItems<T>(this IRobustRandom random, ValueList<T> source, int count, bool allowDuplicates = true)
{
return GetItems(random, source.Span, count, allowDuplicates);
}
/// <inheritdoc cref="GetItems{T}(Robust.Shared.Random.IRobustRandom,System.Collections.Generic.IList{T},int,bool)"/>
public static T[] GetItems<T>(this IRobustRandom random, T[] source, int count, bool allowDuplicates = true)
{
return GetItems(random, source.AsSpan(), count, allowDuplicates);
}
/// <inheritdoc cref="GetItems{T}(Robust.Shared.Random.IRobustRandom,System.Collections.Generic.IList{T},int,bool)"/>
public static T[] GetItems<T>(this IRobustRandom random, Span<T> source, int count, bool allowDuplicates = true)
{
if (source.Length == 0 || count <= 0)
return Array.Empty<T>();
if (allowDuplicates == false && count >= source.Length)
{
var arr = source.ToArray();
random.Shuffle(arr);
return arr;
}
var sourceCount = source.Length;
var result = new T[count];
if (allowDuplicates)
{
// TODO RANDOM consider just using System.Random.GetItems()
// However, the different implementations might mean that lists & arrays shuffled using the same seed
// generate different results, which might be undesirable?
for (var i = 0; i < count; i++)
{
result[i] = source[random.Next(sourceCount)];
}
return result;
}
var indices = sourceCount <= 1024 ? stackalloc int[sourceCount] : new int[sourceCount];
for (var i = 0; i < sourceCount; i++)
{
indices[i] = i;
}
for (var i = 0; i < count; i++)
{
var j = random.Next(sourceCount - i);
result[i] = source[indices[j]];
indices[j] = indices[sourceCount - i - 1];
}
return result;
}
}

View File

@@ -1,58 +1,65 @@
using System;
using Robust.Shared.Utility;
namespace Robust.Shared.Random
namespace Robust.Shared.Random;
/// <summary>
/// Wrapper for <see cref="Random"/>.
/// </summary>
/// <remarks>
/// This should not contain any logic, not directly related to calling specific methods of <see cref="Random"/>.
/// To write additional logic, attached to random roll, please create interface-implemented methods on <see cref="IRobustRandom"/>
/// or add it to <see cref="RandomExtensions"/>.
/// </remarks>
public sealed class RobustRandom : IRobustRandom
{
public sealed class RobustRandom : IRobustRandom
private System.Random _random = new();
public System.Random GetRandom() => _random;
public void SetSeed(int seed)
{
private System.Random _random = new();
_random = new(seed);
}
public System.Random GetRandom() => _random;
public float NextFloat()
{
return _random.NextFloat();
}
public void SetSeed(int seed)
{
_random = new(seed);
}
public int Next()
{
return _random.Next();
}
public float NextFloat()
{
return _random.NextFloat();
}
public int Next(int minValue, int maxValue)
{
return _random.Next(minValue, maxValue);
}
public int Next()
{
return _random.Next();
}
public TimeSpan Next(TimeSpan minTime, TimeSpan maxTime)
{
DebugTools.Assert(minTime < maxTime);
return minTime + (maxTime - minTime) * _random.NextDouble();
}
public int Next(int minValue, int maxValue)
{
return _random.Next(minValue, maxValue);
}
public TimeSpan Next(TimeSpan maxTime)
{
return Next(TimeSpan.Zero, maxTime);
}
public TimeSpan Next(TimeSpan minTime, TimeSpan maxTime)
{
DebugTools.Assert(minTime < maxTime);
return minTime + (maxTime - minTime) * _random.NextDouble();
}
public int Next(int maxValue)
{
return _random.Next(maxValue);
}
public TimeSpan Next(TimeSpan maxTime)
{
return Next(TimeSpan.Zero, maxTime);
}
public double NextDouble()
{
return _random.NextDouble();
}
public int Next(int maxValue)
{
return _random.Next(maxValue);
}
public double NextDouble()
{
return _random.NextDouble();
}
public void NextBytes(byte[] buffer)
{
_random.NextBytes(buffer);
}
public void NextBytes(byte[] buffer)
{
_random.NextBytes(buffer);
}
}

View File

@@ -3,9 +3,10 @@ using Robust.Shared.Serialization.Markdown.Value;
namespace Robust.Shared.Serialization.Markdown.Validation;
public sealed class FieldNotFoundErrorNode : ErrorNode
public sealed class FieldNotFoundErrorNode(ValueDataNode key, Type type) : ErrorNode(key, $"Field \"{key.Value}\" not found in \"{type}\".", false)
{
public FieldNotFoundErrorNode(ValueDataNode key, Type type) : base(key, $"Field \"{key.Value}\" not found in \"{type}\".", false)
{
}
/// <summary>
/// The Type in which the field was not found.
/// </summary>
public Type FieldType { get; } = type;
}

View File

@@ -1,31 +1,16 @@
using Moq;
using NUnit.Framework;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Client.UserInterface.Controls
{
[TestFixture]
[TestOf(typeof(LineEdit))]
public sealed class LineEditTest
public sealed class LineEditTest : RobustUnitTest
{
[OneTimeSetUp]
public void Setup()
{
var uiMgr = new Mock<IUserInterfaceManagerInternal>();
var clyde = new ClydeHeadless();
var deps = IoCManager.InitThread();
deps.Clear();
deps.RegisterInstance<IUserInterfaceManagerInternal>(uiMgr.Object);
deps.RegisterInstance<IUserInterfaceManager>(uiMgr.Object);
deps.RegisterInstance<IClyde>(clyde);
deps.BuildGraph();
}
public override UnitTestProject Project => UnitTestProject.Client;
[Test]
public void TestRuneBackspace()

View File

@@ -0,0 +1,60 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Robust.UnitTesting.Server.GameObjects.Components;
[TestFixture]
public sealed class TransformIntegration_Test
{
/// <summary>
/// Asserts that calling SetWorldPosition while in a container correctly removes the entity from its container.
/// </summary>
[Test]
public void WorldPositionContainerSet()
{
var factory = RobustServerSimulation.NewSimulation();
var sim = factory.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var containerSystem = entManager.System<SharedContainerSystem>();
var xformSystem = entManager.System<SharedTransformSystem>();
var map1Id = mapManager.CreateMap();
var map1 = mapManager.GetMapEntityId(map1Id);
var ent1 = entManager.SpawnEntity(null, new EntityCoordinates(map1, Vector2.Zero));
var ent2 = entManager.SpawnEntity(null, new EntityCoordinates(map1, Vector2.Zero));
var ent3 = entManager.SpawnEntity(null, new EntityCoordinates(map1, Vector2.Zero));
var container = containerSystem.EnsureContainer<ContainerSlot>(ent1, "a");
// Assert that setting worldpos updates parent correctly.
containerSystem.Insert(ent2, container, force: true);
Assert.That(containerSystem.IsEntityInContainer(ent2));
xformSystem.SetWorldPosition(ent2, Vector2.One);
Assert.That(!containerSystem.IsEntityInContainer(ent2));
Assert.That(xformSystem.GetWorldPosition(ent2), Is.EqualTo(Vector2.One));
// Assert that you can set recursively contained (but not directly contained) entities correctly.
containerSystem.Insert(ent2, container);
xformSystem.SetParent(ent3, ent2);
Assert.That(xformSystem.GetParentUid(ent3), Is.EqualTo(ent2));
xformSystem.SetWorldPosition(ent3, Vector2.One);
Assert.That(xformSystem.GetParentUid(ent3), Is.EqualTo(map1));
Assert.That(xformSystem.GetWorldPosition(ent3).Equals(Vector2.One));
// Cleanup
entManager.DeleteEntity(map1);
}
}

View File

@@ -1,11 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects
@@ -15,6 +18,64 @@ namespace Robust.UnitTesting.Shared.GameObjects
{
private static readonly EntityCoordinates DefaultCoords = new(EntityUid.FirstUid, Vector2.Zero);
private const string DummyLoad = @"
- type: entity
id: DummyLoad
name: weh
components:
- type: Joint
- type: Physics
";
[Test]
public void AddRegistryComponentTest()
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterPrototypes(fac => fac.LoadString(DummyLoad))
.InitializeInstance();
sim.AddMap(1);
var entMan = sim.Resolve<IEntityManager>();
var protoManager = sim.Resolve<IPrototypeManager>();
var entity = entMan.SpawnEntity(null, DefaultCoords);
Assert.That(!entMan.HasComponent<PhysicsComponent>(entity));
var proto = protoManager.Index<EntityPrototype>("DummyLoad");
entMan.AddComponents(entity, proto);
Assert.Multiple(() =>
{
Assert.That(entMan.HasComponent<JointComponent>(entity));
Assert.That(entMan.HasComponent<PhysicsComponent>(entity));
});
}
[Test]
public void RemoveRegistryComponentTest()
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterPrototypes(fac => fac.LoadString(DummyLoad))
.InitializeInstance();
sim.AddMap(1);
var entMan = sim.Resolve<IEntityManager>();
var protoManager = sim.Resolve<IPrototypeManager>();
var entity = entMan.SpawnEntity("DummyLoad", DefaultCoords);
var proto = protoManager.Index<EntityPrototype>("DummyLoad");
entMan.RemoveComponents(entity, proto);
Assert.Multiple(() =>
{
Assert.That(!entMan.HasComponent<JointComponent>(entity));
Assert.That(!entMan.HasComponent<PhysicsComponent>(entity));
});
}
[Test]
public void AddComponentTest()
{

View File

@@ -117,7 +117,6 @@ public sealed class GridSplit_Tests
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = mapManager.CreateMap();
var gridEnt = mapManager.CreateGridEntity(mapId);
var grid = gridEnt.Comp;
for (var x = 0; x < 3; x++)
{

View File

@@ -0,0 +1,284 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using Robust.Shared.Collections;
using Robust.Shared.Random;
// ReSharper disable AccessToStaticMemberViaDerivedType
namespace Robust.UnitTesting.Shared.Random;
/// <summary> Instantiable tests for <see cref="RandomExtensions.GetItems{T}(IRobustRandom,IList{T},int,bool)"/>. </summary>
[TestFixture]
public sealed class RandomExtensionsGetItemsWithListTests : RandomExtensionsTests<IList<string>>
{
/// <inheritdoc />
protected override IList<string> CreateCollection()
=> new List<string>(CollectionForTests);
/// <inheritdoc />
protected override IReadOnlyCollection<string> Invoke(IList<string> collection, int count, bool allowDuplicates)
=> _underlyingRandom.GetItems(collection, count, allowDuplicates);
}
/// <summary> Instantiable tests for <see cref="RandomExtensions.GetItems{T}(IRobustRandom,Span{T},int,bool)"/>. </summary>
[TestFixture]
public sealed class RandomExtensionsGetItemsWithSpanTests : RandomExtensionsTests<string[]>
{
/// <inheritdoc />
protected override string[] CreateCollection()
=> CollectionForTests;
/// <inheritdoc />
protected override IReadOnlyCollection<string> Invoke(string[] collection, int count, bool allowDuplicates)
{
var span = new Span<string>(collection);
return _underlyingRandom.GetItems(span, count, allowDuplicates)
.ToArray();
}
}
/// <summary> Instantiable tests for <see cref="RandomExtensions.GetItems{T}(IRobustRandom,ValueList{T},int,bool)"/>. </summary>
[TestFixture]
public sealed class RandomExtensionsGetItemsWithValueListTests : RandomExtensionsTests<ValueList<string>>
{
/// <inheritdoc />
protected override ValueList<string> CreateCollection()
=> new ValueList<string>(CollectionForTests);
/// <inheritdoc />
protected override IReadOnlyCollection<string> Invoke(ValueList<string> collection, int count, bool allowDuplicates)
=> _underlyingRandom.GetItems(collection, count, allowDuplicates)
.ToArray();
}
[TestFixture]
public abstract class RandomExtensionsTests<T>
{
protected IRobustRandom _underlyingRandom = default!;
protected readonly string[] CollectionForTests = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
private T _collection = default!;
private int Count => CollectionForTests.Length;
[SetUp]
public void Setup()
{
_underlyingRandom = Mock.Of<IRobustRandom>();
_collection = CreateCollection();
}
[Test]
public void GetItems_PickOneFromList_ReturnOfRandomizedIndex()
{
// Arrange
Mock.Get(_underlyingRandom)
.Setup(x => x.Next(Count))
.Returns(8);
// Act
var result = Invoke(_collection, 1, true);
// Assert
Assert.That(result.Count, Is.EqualTo(1));
Assert.That(result.Single(), Is.EqualTo("8"));
}
[Test]
public void GetItems_PickOneFromListWithoutDuplicates_ReturnOfRandomizedIndex()
{
// Arrange
Mock.Get(_underlyingRandom)
.Setup(x => x.Next(Count))
.Returns(8);
// Act
var result = Invoke(_collection, 1, allowDuplicates: false);
// Assert
Assert.That(result.Count, Is.EqualTo(1));
Assert.That(result.Single(), Is.EqualTo("8"));
}
[Test]
public void GetItems_PickSomeFromList_ReturnOfRandomizedIndex()
{
// Arrange
Mock.Get(_underlyingRandom)
.SetupSequence(x => x.Next(Count))
.Returns(8)
.Returns(3)
.Returns(2);
// Act
var result = Invoke(_collection, 3, true);
// Assert
Assert.That(result, Is.EqualTo(new[] { "8", "3", "2" }));
}
[Test]
public void GetItems_PickSomeFromListWhileRollingDuplicates_ReturnWithDuplicates()
{
// Arrange
Mock.Get(_underlyingRandom)
.SetupSequence(x => x.Next(Count))
.Returns(8)
.Returns(2)
.Returns(2)
.Returns(2);
// Act
var result = Invoke(_collection, 4, allowDuplicates: true);
// Assert
Assert.That(result, Is.EqualTo(new[] { "8", "2", "2", "2" }));
}
[Test]
public void GetItems_PickSameAmountAsOriginalCollection_ReturnWithDuplicates()
{
// Arrange
Mock.Get(_underlyingRandom)
.SetupSequence(x => x.Next(Count))
.Returns(0)
.Returns(2)
.Returns(2)
.Returns(4)
.Returns(6)
.Returns(5)
.Returns(4)
.Returns(3)
.Returns(2)
.Returns(1)
.Returns(0);
// Act
var result = Invoke(_collection, 11, allowDuplicates: true);
// Assert
Assert.That(result, Is.EqualTo(new[] { "0", "2", "2", "4", "6", "5", "4", "3", "2", "1", "0" }));
}
[Test]
public void GetItems_PickMoreItemsThenOriginalCollectionHave_ReturnWithDuplicates()
{
// Arrange
Mock.Get(_underlyingRandom)
.SetupSequence(x => x.Next(Count))
.Returns(0)
.Returns(2)
.Returns(2)
.Returns(4)
.Returns(6)
.Returns(5)
.Returns(4)
.Returns(3)
.Returns(2)
.Returns(1)
.Returns(9)
.Returns(9);
// Act
var result = Invoke(_collection, 12, allowDuplicates: true);
// Assert
Assert.That(result, Is.EqualTo(new[] { "0", "2", "2", "4", "6", "5", "4", "3", "2", "1", "9", "9" }));
}
[Test]
public void GetItems_PickSomeItemsWithoutDuplicates_ReturnWithoutDuplicates()
{
// Arrange
var mock = Mock.Get(_underlyingRandom);
mock.Setup(x => x.Next(Count)).Returns(1);
mock.Setup(x => x.Next(Count - 1)).Returns(1);
mock.Setup(x => x.Next(Count - 2)).Returns(6);
// Act
var result = Invoke(_collection, 3, allowDuplicates: false);
// Assert
Assert.That(result, Is.EqualTo(new[] { "1", "10", "6" }));
}
[Test]
public void GetItems_PickOneLessItemsThenOriginalCollectionHaveWithoutDuplicates_ReturnWithoutDuplicates()
{
// Arrange
var mock = Mock.Get(_underlyingRandom);
mock.Setup(x => x.Next(Count)).Returns(1);
mock.Setup(x => x.Next(Count - 1)).Returns(1);
mock.Setup(x => x.Next(Count - 2)).Returns(6);
mock.Setup(x => x.Next(Count - 3)).Returns(6);
mock.Setup(x => x.Next(Count - 4)).Returns(3);
mock.Setup(x => x.Next(Count - 5)).Returns(4);
mock.Setup(x => x.Next(Count - 6)).Returns(4);
mock.Setup(x => x.Next(Count - 7)).Returns(3);
mock.Setup(x => x.Next(Count - 8)).Returns(1);
mock.Setup(x => x.Next(Count - 9)).Returns(1);
// Act
var result = Invoke(_collection, 10, allowDuplicates: false);
// Assert
Assert.That(result, Is.EqualTo(new[] { "1", "10", "6", "8", "3", "4", "5", "7", "9", "2" }));
}
[Test]
public void GetItems_PickAllItemsWithoutDuplicates_ReturnOriginalCollectionShuffledWithoutDuplicates()
{
// Arrange
var shuffled = new[] { "9", "0", "4", "2", "3", "7", "5", "8", "6", "10", "1" };
Mock.Get(_underlyingRandom)
.Setup(x => x.Shuffle(It.IsAny<IList<string>>()))
.Callback<IList<string>>(x =>
{
for (int i = 0; i < shuffled.Length; i++)
{
x[i] = shuffled[i];
}
});
// Act
var result = Invoke(_collection, 11, allowDuplicates: false);
// Assert
Assert.That(result, Is.EqualTo(shuffled));
Mock.Get(_underlyingRandom).Verify(x => x.Next(It.IsAny<int>()), Times.Never);
}
[Test]
public void GetItems_PickMoreItemsThenOriginalHaveWithoutDuplicates_ReturnOriginalShuffledOriginalCollectionWithoutDuplicates()
{
// Arrange
var shuffled = new[] { "9", "0", "4", "2", "3", "7", "5", "8", "6", "10", "1" };
Mock.Get(_underlyingRandom)
.Setup(x => x.Shuffle(It.IsAny<IList<string>>()))
.Callback<IList<string>>(x =>
{
for (int i = 0; i < shuffled.Length; i++)
{
x[i] = shuffled[i];
}
});
// Act
var result = Invoke(_collection, 30, allowDuplicates: false);
// Assert
Assert.That(result, Is.EqualTo(shuffled));
Mock.Get(_underlyingRandom).Verify(x => x.Next(It.IsAny<int>()), Times.Never);
}
/// <summary> Create concrete collection for tests. </summary>
protected abstract T CreateCollection();
/// <summary> Invoke method under test. Separate implementation types will have different overrides to be tested. </summary>
protected abstract IReadOnlyCollection<string> Invoke(T collection, int count, bool allowDuplicates);
}

View File

@@ -31,15 +31,14 @@ public sealed class TrySpawnNextToTest : EntitySpawnHelpersTest
Assert.That(EntMan.EntityExists(uid), Is.False);
});
// Spawning next to an entity that is not in a container will simply spawn it in the same position
// Spawning next to an entity that is not in a container will drop it
await Server.WaitPost(() =>
{
Assert.That(EntMan.TrySpawnNextTo(null, GrandChildB, out var uid));
Assert.That(EntMan.EntityExists(uid));
Assert.That(Xforms.GetParentUid(uid!.Value), Is.EqualTo(ChildB));
Assert.That(Container.IsEntityInContainer(uid.Value), Is.False);
Assert.That(Xforms.GetParentUid(uid!.Value), Is.EqualTo(Parent));
Assert.That(Container.IsEntityInContainer(uid.Value));
Assert.That(Container.IsEntityOrParentInContainer(uid.Value));
Assert.That(EntMan.GetComponent<TransformComponent>(uid.Value).Coordinates, Is.EqualTo(GrandChildBPos));
});
// Spawning "next to" a nullspace entity will fail.