mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7fa0cbb6c | ||
|
|
8feebd2a87 | ||
|
|
6cd442d209 | ||
|
|
62167dfc94 | ||
|
|
472903a0af | ||
|
|
b5fa1c84fc | ||
|
|
1901059755 | ||
|
|
8cdec92be6 | ||
|
|
0a00e7ec29 | ||
|
|
8c25a83066 | ||
|
|
0fadfc2d9b | ||
|
|
a6e7224672 | ||
|
|
37796f4806 | ||
|
|
a5494d1df2 | ||
|
|
e2525a2103 | ||
|
|
b50f68866f | ||
|
|
03a4d3e0a0 | ||
|
|
af8fb52a4f | ||
|
|
fd60dc2887 | ||
|
|
cd24fd46b6 | ||
|
|
44cc7127fa | ||
|
|
af36d24892 | ||
|
|
caa8ff0f2d | ||
|
|
cd67c67a5c | ||
|
|
57b328e8c2 | ||
|
|
4874b1db68 | ||
|
|
814ad08884 | ||
|
|
8c4deb2067 | ||
|
|
f6a5120e56 | ||
|
|
c1b8bf8e52 | ||
|
|
4b193bad26 | ||
|
|
8db3da4852 | ||
|
|
0c271fc2f8 | ||
|
|
ed406c06b7 | ||
|
|
b31940b489 | ||
|
|
84360c653d | ||
|
|
6764ed56b0 | ||
|
|
2b55d39e51 | ||
|
|
fdc1de2430 | ||
|
|
99c5b0ad08 | ||
|
|
9e2ab2a917 | ||
|
|
36eb857b55 | ||
|
|
92d7f2723a | ||
|
|
91d3f67a94 | ||
|
|
b28b5ed09b | ||
|
|
30eed7957f | ||
|
|
73c1449811 | ||
|
|
958b5dd06d | ||
|
|
4002cbddb9 | ||
|
|
c38a14e78f | ||
|
|
5164d99996 | ||
|
|
c79217ab66 | ||
|
|
9ef7f7cb37 | ||
|
|
02ac314b1a |
5
.github/workflows/build-test.yml
vendored
5
.github/workflows/build-test.yml
vendored
@@ -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
|
||||
|
||||
|
||||
6
.github/workflows/publish-client.yml
vendored
6
.github/workflows/publish-client.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
130
RELEASE-NOTES.md
130
RELEASE-NOTES.md
@@ -1,4 +1,4 @@
|
||||
# Release notes for RobustToolbox.
|
||||
# Release notes for RobustToolbox.
|
||||
|
||||
<!--
|
||||
NOTE: automatically updated sometimes by version.py.
|
||||
@@ -54,6 +54,134 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 218.2.3
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
vv-sound-none = None
|
||||
vv-sound-path = Path
|
||||
vv-sound-collection = Collection
|
||||
@@ -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);
|
||||
|
||||
|
||||
58
Robust.Analyzers.Tests/DependencyAssignAnalyzerTest.cs
Normal file
58
Robust.Analyzers.Tests/DependencyAssignAnalyzerTest.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
22
Robust.Analyzers.Tests/TestHelper.cs
Normal file
22
Robust.Analyzers.Tests/TestHelper.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Robust.Analyzers/DependencyAssignAnalyzer.cs
Normal file
61
Robust.Analyzers/DependencyAssignAnalyzer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 _);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
132
Robust.Client/UserInterface/Control.Layout.Styling.cs
Normal file
132
Robust.Client/UserInterface/Control.Layout.Styling.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
protected virtual void StylePropertiesChanged()
|
||||
{
|
||||
UpdateLayoutStyleProperties();
|
||||
InvalidateMeasure();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
#if ROBUST_ANALYZERS_IMPL
|
||||
namespace Robust.Shared.Analyzers.Implementation;
|
||||
#else
|
||||
namespace Robust.Shared.Analyzers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
#if ROBUST_ANALYZERS_IMPL
|
||||
namespace Robust.Shared.Analyzers.Implementation;
|
||||
#else
|
||||
namespace Robust.Shared.Analyzers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
#if ROBUST_ANALYZERS_IMPL
|
||||
namespace Robust.Shared.Analyzers.Implementation;
|
||||
#else
|
||||
namespace Robust.Shared.Analyzers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
#if ROBUST_ANALYZERS_IMPL
|
||||
namespace Robust.Shared.Analyzers.Implementation;
|
||||
#else
|
||||
namespace Robust.Shared.Analyzers;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 = $"{Environment.GetEnvironmentVariable("SystemRoot")}\\explorer.exe",
|
||||
Arguments = ".",
|
||||
WorkingDirectory = fullPath,
|
||||
});
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "open",
|
||||
Arguments = ".",
|
||||
WorkingDirectory = fullPath,
|
||||
});
|
||||
}
|
||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD())
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "xdg-open",
|
||||
Arguments = ".",
|
||||
WorkingDirectory = fullPath,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Opening OS windows not supported on this OS");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -67,7 +67,7 @@ public sealed partial class EntityLookupSystem
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var uid in toAdd)
|
||||
foreach (var uid in toAdd.Span)
|
||||
{
|
||||
intersecting.Add(uid);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public sealed partial class EntityLookupSystem
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var uid in toAdd)
|
||||
foreach (var uid in toAdd.Span)
|
||||
{
|
||||
intersecting.Add(uid);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
284
Robust.UnitTesting/Shared/Random/RandomExtensionsTests.cs
Normal file
284
Robust.UnitTesting/Shared/Random/RandomExtensionsTests.cs
Normal 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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user