Compare commits

...

33 Commits

Author SHA1 Message Date
metalgearsloth
1901059755 Version: 218.2.0 2024-04-17 19:06:59 +10:00
metalgearsloth
8cdec92be6 Add slider locking (#5044)
* Add slider locking

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

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

Lets server pause or stop audio or whatever.

* Better API and state fix

* Bunch of fixes

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

* Clamps and despawn fixes

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

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

This reverts commit af36d24892.

* Fix tests

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

* weh

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

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

* Actually fix the Roslyn tests.

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

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

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

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

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

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

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

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

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

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

* A

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

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

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

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

* Remove reference to EntityEventBus.OrderedRegistration

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-03-31 17:01:03 +11:00
metalgearsloth
fdc1de2430 Fix LineEdit tests (#5021) 2024-03-31 16:58:15 +11:00
59 changed files with 1180 additions and 131 deletions

View File

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

View File

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

View File

@@ -54,11 +54,85 @@ END TEMPLATE-->
*None yet*
## 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.
* Added `AddComponents` and `RemoveComponents` methods to EntityManager that handle EntityPrototype / ComponentRegistry bulk component changes.
* Add double-clicking to LineEdit.
### Bugfixes

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1445,7 +1445,7 @@ namespace Robust.Shared
/// Comma-separated list of URLs of hub servers to advertise to.
/// </summary>
public static readonly CVarDef<string> HubUrls =
CVarDef.Create("hub.hub_urls", "https://central.spacestation14.io/hub/", CVar.SERVERONLY);
CVarDef.Create("hub.hub_urls", "https://hub.spacestation14.com/", CVar.SERVERONLY);
/// <summary>
/// URL of this server to advertise.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -951,7 +951,9 @@ Types:
IComparable: { All: True }
IComparable`1: { All: True }
IDisposable: { All: True }
IEquatable`1: { }
IEquatable`1:
Methods:
- "bool Equals(!0)"
IFormatProvider: { All: True }
IFormattable: { All: True }
Index: { All: True }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -993,7 +993,7 @@ public abstract partial class SharedTransformSystem
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWorldPosition(Entity<TransformComponent> entity, Vector2 worldPos)
{
SetWorldPositionRotation(entity.Owner, worldPos, entity.Comp.LocalRotation, entity.Comp);
SetWorldPositionRotationInternal(entity.Owner, worldPos, null, entity.Comp);
}
#endregion
@@ -1079,10 +1079,17 @@ 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 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?");
@@ -1453,8 +1460,7 @@ public abstract partial class SharedTransformSystem
while (targetXform.ParentUid.IsValid())
{
if (_container.IsEntityInContainer(targetUid)
&& _container.TryGetContainingContainer(targetXform.ParentUid, targetUid, out var container,
skipExistCheck: true)
&& _container.TryGetContainingContainer(targetXform.ParentUid, targetUid, out var container)
&& _container.Insert((entity, xform, null, null), container))
{
return;

View File

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

View File

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

View File

@@ -1082,6 +1082,34 @@ namespace Robust.Shared.Prototypes
_prototypeDataCache[prototype.ID] = data;
return data;
}
public bool TryGetRandom<T>(IRobustRandom random, [NotNullWhen(true)] out IPrototype? prototype) where T : class, IPrototype
{
var count = Count<T>();
if (count == 0)
{
prototype = null;
return false;
}
var index = 0;
var picked = random.Next(count);
foreach (var proto in EnumeratePrototypes<T>())
{
if (index == picked)
{
prototype = proto;
return true;
}
index++;
}
throw new ArgumentOutOfRangeException($"Unable to pick valid prototype for {typeof(T)}?");
}
}
public sealed class InvalidPrototypeNameException : Exception

View File

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

View File

@@ -1,34 +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.Shared.Timing;
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 timing = new Mock<IGameTiming>();
var deps = IoCManager.InitThread();
deps.Clear();
deps.RegisterInstance<IUserInterfaceManagerInternal>(uiMgr.Object);
deps.RegisterInstance<IUserInterfaceManager>(uiMgr.Object);
deps.RegisterInstance<IClyde>(clyde);
deps.RegisterInstance<IGameTiming>(timing.Object);
deps.BuildGraph();
}
public override UnitTestProject Project => UnitTestProject.Client;
[Test]
public void TestRuneBackspace()

View File

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