Merge remote-tracking branch 'upstream/master' into dont-skip-leg-day

This commit is contained in:
PJB3005
2025-08-07 21:27:41 +02:00
57 changed files with 1324 additions and 352 deletions

View File

@@ -55,9 +55,9 @@
<PackageVersion Include="Serilog" Version="4.2.0" />
<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.7" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.0" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

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

View File

@@ -16,7 +16,10 @@
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.NameGenerator\Robust.Client.NameGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\Robust.Client.Injectors.csproj" ReferenceOutputAssembly="false"/>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\Robust.Client.Injectors.csproj" ReferenceOutputAssembly="false">
<SetConfiguration Condition="'$(Configuration)' == 'DebugOpt'">Configuration=Debug</SetConfiguration>
<SetConfiguration Condition="'$(Configuration)' == 'Tools'">Configuration=Release</SetConfiguration>
</ProjectReference>
</ItemGroup>
<!-- XamlIL does not make use of special Robust configurations like DebugOpt. Convert these down. -->

View File

@@ -35,7 +35,7 @@ END TEMPLATE-->
### Breaking changes
*None yet*
* When a player disconnects, the relevant callbacks are now fired *after* removing the channel from `INetManager`.
### New features
@@ -54,6 +54,60 @@ END TEMPLATE-->
*None yet*
## 266.0.0
### Breaking changes
* A new analyzer has been added that will error if you attempt to subscribe to `AfterAutoHandleStateEvent` on a
component that doesn't have the `AutoGenerateComponentState` attribute, or doesn't have the first argument of that
attribute set to `true`. In most cases you will want to set said argument to `true`.
* The fields on `AutoGenerateComponentStateAttribute` are now `readonly`. Setting these directly (instead of using the constructor arguments) never worked in the first place, so this change only catches existing programming errors.
* When a player disconnects, `ISharedPlayerManager.PlayerStatusChanged` is now fired *after* removing the session from the `Sessions` list.
* `.rsi` files are now compacted into individual `.rsic` files on packaging. This should significantly reduce file count & improve performance all over release builds, but breaks the ability to access `.png` files into RSIs directly. To avoid this, `"rsic": false` can be specified in the RSI's JSON metadata.
* The `scale` command has been removed, with the intent of it being moved to content instead.
### New features
* ViewVariables editors for `ProtoId` fields now have a Select button which opens a window listing all available prototypes of the appropriate type.
* added **IConfigurationManager**.*SubscribeMultiple* ext. method to provide simpler way to unsubscribe from multiple cvar at once
* Added `SharedMapSystem.QueueDeleteMap`, which deletes a map with the specified MapId in the next tick.
* Added generic version of `ComponentRegistry.TryGetComponent`.
* `AttributeHelper.HasAttribute` has had an overload's type signature loosened from `INamedTypeSymbol` to `ITypeSymbol`.
* Errors are now logged when sending messages to disconnected `INetChannel`s.
* Warnings are now logged if sending a message via Lidgren failed for some reason.
* `.yml` and `.ftl` files in the same directory are now concatenated onto each other, to reduce file count in packaged builds. This is done through the new `AssetPassMergeTextDirectories` pass.
* Added `System.Linq.ImmutableArrayExtensions` to sandbox.
* `ImmutableDictionary<TKey, TValue>` and `ImmutableHashSet<T>` can now be network serialized.
* `[AutoPausedField]` now works on fields of type `Dictionary<TKey, TimeSpan>`.
* `[NotYamlSerializable]` analyzer now detects nullable fields of the not-serializable type.
* `ItemList` items can now have a scale applied for the icon.
* Added new OS mouse cursor shapes for the SDL3 backend. These are not available on the GLFW backend.
* Added `IMidiRenderer.MinVolume` to scale the volume of MIDI notes.
* Added `SharedPhysicsSystem.ScaleFixtures`, to apply the physics-only changes of the prior `scale` command.
### Bugfixes
* `LayoutContainer.SetMarginsPreset` and `SetAnchorAndMarginPreset` now correctly use the provided control's top anchor when calculating the margins for its presets; it previously used the bottom anchor instead. This may result in a few UI differences, by a few pixels at most.
* `IConfigurationManager` no longer logs a warning when saving configuration in an integration test.
* Fixed impossible-to-source `ChannelClosedException`s when sending some net messages to disconnected `INetChannel`s.
* Fixed an edge case causing some color values to throw an error in `ColorNaming`.
* Fresh builds from specific projects should no longer cause errors related to `Robust.Client.Injectors` not being found.
* Stopped errors getting logged about `NoteOff` and `NoteOn` operations failing in MIDI.
* Fixed MIDI players not resuming properly when re-entering PVS range.
### Other
* Updated ImageSharp to 3.1.11 to stop the warning about a DoS vulnerability.
* Prototype YAML documents that are completely empty are now skipped by the prototype loader. Previously they would cause a load error for the whole file.
* `TileSpawnWindow` can now be localized.
* `BaseWindow` uses the new mouse cursor shapes for diagonal resizing.
* `NFluidsynth` has been updated to 0.2.0
### Internal
* Added `uitest` tab for standard mouse cursor shapes.
## 265.0.0
### Breaking changes

View File

@@ -21,6 +21,7 @@ color-brown = brown
color-white = white
color-gray = gray
color-black = black
color-unknown = unknown color, you should not see this
color-pink-color-red = pinkish red
color-red-color-orange = reddish orange

View File

@@ -411,9 +411,6 @@ cmd-spawn-help = spawn <prototype> OR spawn <prototype> <relative entity ID> OR
cmd-cspawn-desc = Spawns a client-side entity with specific type at your feet.
cmd-cspawn-help = cspawn <entity type>
cmd-scale-desc = Increases or decreases an entity's size naively.
cmd-scale-help = scale <entityUid> <float>
cmd-dumpentities-desc = Dump entity list.
cmd-dumpentities-help = Dumps entity list of UIDs and prototype.

View File

@@ -1,8 +1,6 @@
## EntitySpawnWindow
entity-spawn-window-title = Entity Spawn Panel
entity-spawn-window-search-bar-placeholder = search
entity-spawn-window-clear-button = Clear
entity-spawn-window-replace-button-text = Replace
entity-spawn-window-override-menu-tooltip = Override placement
@@ -22,3 +20,5 @@ output-panel-scroll-down-button-text = Scroll Down
## Common Used
window-erase-button-text = Erase Mode
window-search-bar-placeholder = Search
window-clear-button = Clear

View File

@@ -25,3 +25,9 @@ vv-sound-reference-distance = Reference Distance
vv-sound-loop = Loop
vv-sound-play-offset = Play Offset (s)
vv-sound-variation = Pitch variation
## ProtoId
vv-protoid-id-placeholder = Prototype ID
vv-protoid-select-button-label = Select
vv-protoid-addwindow-title = Set Prototype

View File

@@ -0,0 +1,110 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.AfterAutoHandleStateAnalyzer,
Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture, TestOf(typeof(AfterAutoHandleStateAnalyzer))]
public sealed class AfterAutoHandleStateAnalyzerTest
{
private const string SubscribeEventDef = """
using System;
namespace Robust.Shared.GameObjects;
public readonly struct EntityUid;
public abstract class EntitySystem
{
public void SubscribeLocalEvent<T, TEvent>() where TEvent : notnull { }
}
public interface IComponent;
public interface IComponentState;
""";
// A rare case for block-scoped namespace, I thought. Then I realized this
// only needed the one type definition.
private const string OtherTypeDefs = """
using System;
namespace JetBrains.Annotations
{
public sealed class BaseTypeRequiredAttribute(Type baseType) : Attribute;
}
""";
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<AfterAutoHandleStateAnalyzer, DefaultVerifier>
{
TestState = { Sources = { code } }
};
TestHelper.AddEmbeddedSources(test.TestState,
"Robust.Shared.Analyzers.ComponentNetworkGeneratorAuxiliary.cs",
"Robust.Shared.GameObjects.EventBusAttributes.cs");
test.TestState.Sources.Add(("EntitySystem.Subscriptions.cs", SubscribeEventDef));
test.TestState.Sources.Add(("Types.cs", OtherTypeDefs));
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task Test()
{
const string code = """
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
[AutoGenerateComponentState(true)]
public sealed class AutoGenTrue;
[AutoGenerateComponentState(true, true)]
public sealed class AutoGenTrueTrue;
public sealed class NotAutoGen;
[AutoGenerateComponentState]
public sealed class AutoGenNoArgs;
[AutoGenerateComponentState(false)]
public sealed class AutoGenFalse;
public sealed class Foo : EntitySystem
{
public void Good()
{
// Subscribing to other events works
SubscribeLocalEvent<AutoGenNoArgs, object>();
// First arg true allows subscribing
SubscribeLocalEvent<AutoGenTrue, AfterAutoHandleStateEvent>();
SubscribeLocalEvent<AutoGenTrueTrue, AfterAutoHandleStateEvent>();
}
public void Bad()
{
// Can't subscribe if AutoGenerateComponentState isn't even present
SubscribeLocalEvent<NotAutoGen, AfterAutoHandleStateEvent>();
// Can't subscribe if first arg is not specified/false
SubscribeLocalEvent<AutoGenNoArgs, AfterAutoHandleStateEvent>();
SubscribeLocalEvent<AutoGenFalse, AfterAutoHandleStateEvent>();
}
}
""";
await Verifier(code,
// /0/Test0.cs(29,9): error RA0040: Tried to subscribe to AfterAutoHandleStateEvent for 'NotAutoGen' which doesn't have an AutoGenerateComponentState attribute
VerifyCS.Diagnostic(AfterAutoHandleStateAnalyzer.MissingAttribute).WithSpan(29, 9, 29, 69).WithArguments("NotAutoGen"),
// /0/Test0.cs(32,9): error RA0041: Tried to subscribe to AfterAutoHandleStateEvent for 'AutoGenNoArgs' which doesn't have raiseAfterAutoHandleState set
VerifyCS.Diagnostic(AfterAutoHandleStateAnalyzer.MissingAttributeParam).WithSpan(32, 9, 32, 72).WithArguments("AutoGenNoArgs"),
// /0/Test0.cs(33,9): error RA0041: Tried to subscribe to AfterAutoHandleStateEvent for 'AutoGenFalse' which doesn't have raiseAfterAutoHandleState set
VerifyCS.Diagnostic(AfterAutoHandleStateAnalyzer.MissingAttributeParam).WithSpan(33, 9, 33, 71).WithArguments("AutoGenFalse")
);
}
}

View File

@@ -1,4 +1,4 @@
extern alias SerializationGenerator;
extern alias SerializationGenerator;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
@@ -126,6 +126,48 @@ public sealed class ComponentPauseGeneratorTest
""");
}
[Test]
public void TestDictionary()
{
var result = RunGenerator("""
[AutoGenerateComponentPause]
public sealed partial class FooComponent : IComponent
{
[AutoPausedField]
public Dictionary<string, TimeSpan> Foo;
}
""");
ExpectNoDiagnostics(result);
ExpectSource(
result,
"""
// <auto-generated />
using Robust.Shared.GameObjects;
public partial class FooComponent
{
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
}
private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
{
foreach (var key in component.Foo.Keys)
component.Foo[key] += args.PausedTime;
}
}
}
""");
}
[Test]
public void TestAutoState()
{

View File

@@ -203,6 +203,8 @@ public sealed class DataDefinitionAnalyzerTest
[NotYamlSerializable]
public sealed class NotSerializableClass { }
[NotYamlSerializable]
public readonly struct NotSerializableStruct { }
[DataDefinition]
public sealed partial class Foo
@@ -213,6 +215,21 @@ public sealed class DataDefinitionAnalyzerTest
[DataField]
public NotSerializableClass BadProperty { get; set; }
[DataField]
public NotSerializableClass? BadNullableField;
[DataField]
public NotSerializableStruct BadStructField;
[DataField]
public NotSerializableStruct BadStructProperty { get; set; }
[DataField]
public NotSerializableStruct? BadNullableStructField;
[DataField]
public NotSerializableStruct? BadNullableStructProperty { get; set; }
public NotSerializableClass GoodField; // Not a DataField, not a problem
public NotSerializableClass GoodProperty { get; set; } // Not a DataField, not a problem
@@ -220,10 +237,20 @@ public sealed class DataDefinitionAnalyzerTest
""";
await Verifier(code,
// /0/Test0.cs(10,12): error RA0033: Data field BadField in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(10, 12, 10, 32).WithArguments("BadField", "Foo", "NotSerializableClass"),
// /0/Test0.cs(13,12): error RA0033: Data field BadProperty in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(13, 12, 13, 32).WithArguments("BadProperty", "Foo", "NotSerializableClass")
// /0/Test0.cs(12,12): error RA0033: Data field BadField in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(12, 12, 12, 32).WithArguments("BadField", "Foo", "NotSerializableClass"),
// /0/Test0.cs(15,12): error RA0033: Data field BadProperty in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(15, 12, 15, 32).WithArguments("BadProperty", "Foo", "NotSerializableClass"),
// /0/Test0.cs(18,12): error RA0036: Data field BadNullableField in data definition Foo is type NotSerializableClass, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(18, 12, 18, 33).WithArguments("BadNullableField", "Foo", "NotSerializableClass"),
// /0/Test0.cs(21,12): error RA0036: Data field BadStructField in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(21, 12, 21, 33).WithArguments("BadStructField", "Foo", "NotSerializableStruct"),
// /0/Test0.cs(24,12): error RA0036: Data field BadStructProperty in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(24, 12, 24, 33).WithArguments("BadStructProperty", "Foo", "NotSerializableStruct"),
// /0/Test0.cs(27,12): error RA0036: Data field BadNullableStructField in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(27, 12, 27, 34).WithArguments("BadNullableStructField", "Foo", "NotSerializableStruct"),
// /0/Test0.cs(30,12): error RA0036: Data field BadNullableStructProperty in data definition Foo is type NotSerializableStruct, which is not YAML serializable
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(30, 12, 30, 34).WithArguments("BadNullableStructProperty", "Foo", "NotSerializableStruct")
);
}
}

View File

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

View File

@@ -0,0 +1,85 @@
#nullable enable
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 AfterAutoHandleStateAnalyzer : DiagnosticAnalyzer
{
private const string AfterAutoHandleStateEventName = "AfterAutoHandleStateEvent";
private const string AutoGenStateAttribute = "Robust.Shared.Analyzers.AutoGenerateComponentStateAttribute";
private const string SubscribeLocalEventName = "SubscribeLocalEvent";
public static readonly DiagnosticDescriptor MissingAttribute = new(
Diagnostics.IdAutoGenStateAttributeMissing,
"Unreachable AfterAutoHandleState subscription",
"Tried to subscribe to AfterAutoHandleStateEvent for '{0}' which doesn't have an "
+ "AutoGenerateComponentState attribute",
"Usage",
DiagnosticSeverity.Error,
true,
// Does this even show up anywhere in Rider? >:(
"You must mark your component with '[AutoGenerateComponentState(true)]' to subscribe to this event."
);
public static readonly DiagnosticDescriptor MissingAttributeParam = new(
Diagnostics.IdAutoGenStateParamMissing,
"Unreachable AfterAutoHandleState subscription",
"Tried to subscribe to AfterAutoHandleStateEvent for '{0}' which doesn't have "
+ "raiseAfterAutoHandleState set",
"Usage",
DiagnosticSeverity.Error,
true,
"The AutoGenerateComponentState attribute must be passed 'true' in order to subscribe to this event."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
[MissingAttribute, MissingAttributeParam];
public override void Initialize(AnalysisContext context)
{
// This is more to stop user error rather than code generation error
// (Plus this shouldn't affect code gen anyway)
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(compilationContext =>
{
var autoGenStateAttribute = compilationContext.Compilation.GetTypeByMetadataName(AutoGenStateAttribute);
// No attribute, no analyzer.
if (autoGenStateAttribute is null)
return;
compilationContext.RegisterOperationAction(
analysisContext => CheckEventSubscription(analysisContext, autoGenStateAttribute),
OperationKind.Invocation);
});
}
private static void CheckEventSubscription(OperationAnalysisContext context, ITypeSymbol autoGenStateAttribute)
{
if (context.Operation is not IInvocationOperation operation)
return;
// Check the method has the right name and has the right type args
if (operation.TargetMethod is not
{ Name: SubscribeLocalEventName, TypeArguments: [var component, { Name: AfterAutoHandleStateEventName }] })
return;
// Search the component's attributes for something matching autoGenStateAttribute
AttributeHelper.HasAttribute(component, autoGenStateAttribute, out var autoGenAttribute);
// First argument is raiseAfterAutoHandleState—note it shouldn't ever
// be null, since it has a default, but eh.
if (autoGenAttribute?.ConstructorArguments[0].Value is true)
return;
context.ReportDiagnostic(Diagnostic.Create(autoGenAttribute is null ? MissingAttribute : MissingAttributeParam,
operation.Syntax.GetLocation(),
component.Name));
}
}

View File

@@ -186,6 +186,8 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (context.SemanticModel.GetSymbolInfo(field.Declaration.Type).Symbol is not ITypeSymbol fieldTypeSymbol)
continue;
fieldTypeSymbol = TypeSymbolHelper.GetNullableUnderlyingTypeOrSelf(fieldTypeSymbol);
if (IsNotYamlSerializable(fieldSymbol, fieldTypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
@@ -239,6 +241,8 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (context.SemanticModel.GetSymbolInfo(property.Type).Symbol is not ITypeSymbol propertyTypeSymbol)
return;
propertyTypeSymbol = TypeSymbolHelper.GetNullableUnderlyingTypeOrSelf(propertyTypeSymbol);
if (IsNotYamlSerializable(propertySymbol, propertyTypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,

View File

@@ -213,4 +213,6 @@ public interface IMidiRenderer : IDisposable
/// Actually disposes of this renderer. Do NOT use outside the MIDI thread.
/// </summary>
internal void InternalDispose();
byte MinVolume { get; set; }
}

View File

@@ -42,7 +42,7 @@ internal sealed partial class MidiManager : IMidiManager
[Dependency] private readonly IRuntimeLog _runtime = default!;
private AudioSystem _audioSys = default!;
private SharedPhysicsSystem _broadPhaseSystem = default!;
private SharedPhysicsSystem _physics = default!;
private SharedTransformSystem _xformSystem = default!;
public IReadOnlyList<IMidiRenderer> Renderers
@@ -81,7 +81,7 @@ internal sealed partial class MidiManager : IMidiManager
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _gain = 0f;
private bool _volumeDirty = true;
private bool _gainDirty = true;
// Not reliable until Fluidsynth is initialized!
[ViewVariables(VVAccess.ReadWrite)]
@@ -96,7 +96,7 @@ internal sealed partial class MidiManager : IMidiManager
return;
_cfgMan.SetCVar(CVars.MidiVolume, clamped);
_volumeDirty = true;
_gainDirty = true;
}
}
@@ -114,7 +114,8 @@ internal sealed partial class MidiManager : IMidiManager
"/usr/share/sounds/sf2/TimGM6mb.sf2",
};
private static readonly string WindowsSoundfont = $@"{Environment.GetEnvironmentVariable("SystemRoot")}\system32\drivers\gm.dls";
private static readonly string WindowsSoundfont =
$@"{Environment.GetEnvironmentVariable("SystemRoot")}\system32\drivers\gm.dls";
private const string OsxSoundfont =
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
@@ -145,11 +146,13 @@ internal sealed partial class MidiManager : IMidiManager
{
if (FluidsynthInitialized || _failedInitialize) return;
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
{
_gain = value;
_volumeDirty = true;
}, true);
_cfgMan.OnValueChanged(CVars.MidiVolume,
value =>
{
_gain = value;
_gainDirty = true;
},
true);
_midiSawmill = _logger.GetSawmill("midi");
#if DEBUG
@@ -167,13 +170,15 @@ internal sealed partial class MidiManager : IMidiManager
// not a directory, preserve the old file and create an actual directory
else if (!_resourceManager.UserData.IsDir(CustomSoundfontDirectory))
{
_resourceManager.UserData.Rename(CustomSoundfontDirectory, CustomSoundfontDirectory.WithName(CustomSoundfontDirectory.Filename + ".old"));
_resourceManager.UserData.Rename(CustomSoundfontDirectory,
CustomSoundfontDirectory.WithName(CustomSoundfontDirectory.Filename + ".old"));
_resourceManager.UserData.CreateDir(CustomSoundfontDirectory);
}
try
{
NFluidsynth.Logger.SetLoggerMethod(_loggerDelegate); // Will cause a safe DllNotFoundException if not available.
NFluidsynth.Logger
.SetLoggerMethod(_loggerDelegate); // Will cause a safe DllNotFoundException if not available.
_settings = new Settings();
_settings["synth.sample-rate"].DoubleValue = 44100;
@@ -193,7 +198,7 @@ internal sealed partial class MidiManager : IMidiManager
//_settings["synth.verbose"].IntValue = 1; // Useful for debugging.
var midiParallel = _cfgMan.GetCVar(CVars.MidiParallelism);
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int)(Math.Log2(midiParallel) * 2048), 1, 65535);
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int) (Math.Log2(midiParallel) * 2048), 1, 65535);
_settings["synth.cpu-cores"].IntValue = Math.Clamp(midiParallel, 1, 256);
_midiSawmill.Debug($"Synth Cores: {_settings["synth.cpu-cores"].IntValue}");
@@ -219,7 +224,7 @@ internal sealed partial class MidiManager : IMidiManager
};
_audioSys = _entityManager.EntitySysManager.GetEntitySystem<AudioSystem>();
_broadPhaseSystem = _entityManager.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>();
_physics = _entityManager.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>();
_xformSystem = _entityManager.System<SharedTransformSystem>();
_entityManager.GetEntityQuery<PhysicsComponent>();
_entityManager.GetEntityQuery<TransformComponent>();
@@ -263,7 +268,8 @@ internal sealed partial class MidiManager : IMidiManager
{
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
var renderer =
new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
LoadSoundFontSetup(renderer);
@@ -273,6 +279,7 @@ internal sealed partial class MidiManager : IMidiManager
{
_renderers.Add(renderer);
}
return renderer;
}
finally
@@ -309,99 +316,23 @@ internal sealed partial class MidiManager : IMidiManager
_updateSemaphore.Release();
_volumeDirty = false;
_gainDirty = false;
}
private void UpdateRenderer(IMidiRenderer renderer, MapCoordinates listener)
{
// TODO: This should be sharing more code with AudioSystem.
try
{
if (renderer.Disposed)
return;
if (_volumeDirty)
{
renderer.Source.Gain = Gain;
}
if (!renderer.Mono)
{
renderer.Source.Global = true;
return;
}
MapCoordinates mapPos;
if (renderer.TrackingEntity is {} trackedEntity && !_entityManager.Deleted(trackedEntity))
{
renderer.TrackingCoordinates = _xformSystem.GetMapCoordinates(renderer.TrackingEntity.Value);
// Pause it if the attached entity is paused.
if (_entityManager.IsPaused(renderer.TrackingEntity))
{
renderer.Source.Pause();
return;
}
}
else if (renderer.TrackingCoordinates == null)
{
renderer.Source.Pause();
return;
}
mapPos = renderer.TrackingCoordinates.Value;
// If it's on a different map then just mute it, not pause.
if (mapPos.MapId == MapId.Nullspace || mapPos.MapId != listener.MapId)
{
renderer.Source.Gain = 0f;
return;
}
// Was previously muted maybe so try unmuting it?
if (renderer.Source.Gain == 0f)
{
renderer.Source.Gain = Gain;
}
var worldPos = mapPos.Position;
var delta = worldPos - listener.Position;
var distance = delta.Length();
// Update position
// Out of range so just clip it for us.
if (distance > renderer.Source.MaxDistance)
{
// Still keeps the source playing, just with no volume.
renderer.Source.Gain = 0f;
return;
}
// Same imprecision suppression as audiosystem.
if (distance > 0f && distance < 0.01f)
{
worldPos = listener.Position;
delta = Vector2.Zero;
distance = 0f;
}
renderer.Source.Position = worldPos;
// Update velocity (doppler).
if (!_entityManager.Deleted(renderer.TrackingEntity))
{
var velocity = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity.Value);
renderer.Source.Velocity = velocity;
}
if (!renderer.Source.Global)
UpdateLocalRenderer(renderer, listener);
else
{
renderer.Source.Velocity = Vector2.Zero;
}
// Update occlusion
var occlusion = _audioSys.GetOcclusion(listener, delta, distance, renderer.TrackingEntity);
renderer.Source.Occlusion = occlusion;
UpdateGlobalRenderer(renderer);
}
catch (Exception ex)
{
@@ -409,6 +340,58 @@ internal sealed partial class MidiManager : IMidiManager
}
}
private void UpdateLocalRenderer(IMidiRenderer renderer, MapCoordinates listener)
{
if (_entityManager.Deleted(renderer.TrackingEntity) || _entityManager.IsPaused(renderer.TrackingEntity))
{
renderer.Source.Gain = 0f;
return;
}
MapCoordinates mapCoords = _xformSystem.GetMapCoordinates(renderer.TrackingEntity.Value);
renderer.TrackingCoordinates = mapCoords;
if (mapCoords.MapId == MapId.Nullspace || mapCoords.MapId != listener.MapId)
{
renderer.Source.Gain = 0f;
return;
}
Vector2 mapPosition = mapCoords.Position;
Vector2 listenerDelta = mapPosition - listener.Position;
var listenerDeltaLength = listenerDelta.Length();
if (listenerDeltaLength > renderer.Source.MaxDistance)
{
renderer.Source.Gain = 0f;
return;
}
if (listenerDeltaLength is > 0f and < 0.01f)
{
mapPosition = listener.Position;
listenerDelta = Vector2.Zero;
listenerDeltaLength = 0f;
}
if (_gainDirty || renderer.Source.Gain == 0f)
renderer.Source.Gain = Gain;
renderer.Source.Position = mapPosition;
renderer.Source.Velocity = _physics.GetMapLinearVelocity(renderer.TrackingEntity.Value);
renderer.Source.Occlusion =
_audioSys.GetOcclusion(listener, listenerDelta, listenerDeltaLength, renderer.TrackingEntity);
}
private void UpdateGlobalRenderer(IMidiRenderer renderer)
{
if (_gainDirty)
renderer.Source.Gain = Gain;
}
/// <summary>
/// Main method for the thread rendering the midi audio.
/// </summary>
@@ -428,7 +411,7 @@ internal sealed partial class MidiManager : IMidiManager
{
if (!renderer.Disposed)
{
if (renderer.Master is { Disposed: true })
if (renderer.Master is {Disposed: true})
renderer.Master = null;
renderer.Render();

View File

@@ -214,6 +214,11 @@ internal sealed partial class MidiRenderer : IMidiRenderer
[ViewVariables]
public BitArray FilteredChannels { get; } = new(RobustMidiEvent.MaxChannels);
[ViewVariables]
public byte MinVolume { get => _minVolume; set => _minVolume = value; }
private byte _minVolume;
[ViewVariables(VVAccess.ReadWrite)]
public byte? VelocityOverride { get; set; } = null;
@@ -539,14 +544,7 @@ internal sealed partial class MidiRenderer : IMidiRenderer
if (velocity <= 0)
continue;
try
{
_synth.NoteOn(channel, key, velocity);
}
catch (FluidSynthInteropException e)
{
_midiSawmill.Error($"CH:{channel} KEY:{key} VEL:{velocity} {e.ToStringBetter()}");
}
_synth.TryNoteOn(channel, key, velocity);
}
}
@@ -574,7 +572,7 @@ internal sealed partial class MidiRenderer : IMidiRenderer
{
case RobustMidiCommand.NoteOff:
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = 0;
_synth.NoteOff(midiEvent.Channel, midiEvent.Key);
_synth.TryNoteOff(midiEvent.Channel, midiEvent.Key);
break;
case RobustMidiCommand.NoteOn:
@@ -583,7 +581,7 @@ internal sealed partial class MidiRenderer : IMidiRenderer
if (velocity == 0)
{
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = 0;
_synth.NoteOn(midiEvent.Channel, midiEvent.Key, velocity);
_synth.TryNoteOn(midiEvent.Channel, midiEvent.Key, velocity);
break;
}
@@ -591,10 +589,13 @@ internal sealed partial class MidiRenderer : IMidiRenderer
if (FilteredChannels[midiEvent.Channel])
break;
velocity = VelocityOverride ?? midiEvent.Velocity;
if (MinVolume > 0)
velocity = (byte)Math.Floor(MathHelper.Lerp(MinVolume, 127, (float)velocity / 127));
velocity = VelocityOverride ?? velocity;
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = velocity;
_synth.NoteOn(midiEvent.Channel, midiEvent.Key, velocity);
_synth.TryNoteOn(midiEvent.Channel, midiEvent.Key, velocity);
break;
case RobustMidiCommand.AfterTouch:

View File

@@ -154,6 +154,7 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
_sprite = new TabSpriteView();
_tabContainer.AddChild(_sprite);
_tabContainer.AddChild(TabCursorShapes());
}
public void OnClosed()
@@ -210,6 +211,53 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
return label;
}
private Control TabCursorShapes()
{
var box = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
};
var styleBox = new StyleBoxFlat
{
BackgroundColor = Color.Black
};
foreach (var cursorName in Enum.GetNames<CursorShape>())
{
// Go over names due to duplicate definitions in the enum.
var cursor = Enum.Parse<CursorShape>(cursorName);
// Wow was I bad at API design.
if (cursor == CursorShape.Custom)
continue;
var panel = new PanelContainer
{
PanelOverride = styleBox,
DefaultCursorShape = cursor,
MouseFilter = MouseFilterMode.Stop,
MinHeight = 30,
Children =
{
new Label
{
Text = cursorName,
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(4)
}
}
};
box.AddChild(panel);
}
return new ScrollContainer
{
Children = { box },
VScrollEnabled = true,
HScrollEnabled = false,
Name = nameof(Tab.TabCursorShapes),
};
}
public void SelectTab(Tab tab)
{
_tabContainer.CurrentTab = (int)tab;
@@ -226,6 +274,7 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
TextEdit = 6,
RichText = 7,
SpriteView = 8,
TabCursorShapes = 9,
}
}

View File

@@ -1,25 +0,0 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects;
public sealed class ScaleVisualsSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ScaleVisualsComponent, AppearanceChangeEvent>(OnChangeData);
}
private void OnChangeData(EntityUid uid, ScaleVisualsComponent component, ref AppearanceChangeEvent ev)
{
if (!ev.AppearanceData.TryGetValue(ScaleVisuals.Scale, out var scale) ||
ev.Sprite == null) return;
var vecScale = (Vector2)scale;
// Set it directly because prediction may call this multiple times.
ev.Sprite.Scale = vecScale;
}
}

View File

@@ -128,6 +128,19 @@ namespace Robust.Client.Graphics.Clyde
AddStandardCursor(StandardCursorShape.Hand, CursorShape.Hand);
AddStandardCursor(StandardCursorShape.HResize, CursorShape.HResize);
AddStandardCursor(StandardCursorShape.VResize, CursorShape.VResize);
AddStandardCursor(StandardCursorShape.Progress, CursorShape.Arrow);
AddStandardCursor(StandardCursorShape.NWSEResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.NESWResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.Move, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.NotAllowed, CursorShape.Arrow);
AddStandardCursor(StandardCursorShape.NWResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.NResize, CursorShape.VResize);
AddStandardCursor(StandardCursorShape.NEResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.EResize, CursorShape.HResize);
AddStandardCursor(StandardCursorShape.SEResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.SResize, CursorShape.VResize);
AddStandardCursor(StandardCursorShape.SWResize, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.WResize, CursorShape.HResize);
}
private sealed class CursorImpl : ICursor

View File

@@ -94,6 +94,19 @@ internal partial class Clyde
Add(StandardCursorShape.Hand, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_POINTER);
Add(StandardCursorShape.HResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_EW_RESIZE);
Add(StandardCursorShape.VResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NS_RESIZE);
Add(StandardCursorShape.Progress, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_PROGRESS);
Add(StandardCursorShape.NWSEResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NWSE_RESIZE);
Add(StandardCursorShape.NESWResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NESW_RESIZE);
Add(StandardCursorShape.Move, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_MOVE);
Add(StandardCursorShape.NotAllowed, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NOT_ALLOWED);
Add(StandardCursorShape.NWResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NW_RESIZE);
Add(StandardCursorShape.NResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_N_RESIZE);
Add(StandardCursorShape.NEResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NE_RESIZE);
Add(StandardCursorShape.EResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_E_RESIZE);
Add(StandardCursorShape.SEResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SE_RESIZE);
Add(StandardCursorShape.SResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_S_RESIZE);
Add(StandardCursorShape.SWResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SW_RESIZE);
Add(StandardCursorShape.WResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_W_RESIZE);
void Add(StandardCursorShape shape, SDL.SDL_SystemCursor sysCursor)
{

View File

@@ -15,6 +15,11 @@ namespace Robust.Client.Graphics
/// </summary>
IBeam,
/// <summary>
/// Alias for <see cref="IBeam"/>.
/// </summary>
Text = IBeam,
/// <summary>
/// The crosshair shape. Used when dragging and dropping.
/// </summary>
@@ -25,16 +30,135 @@ namespace Robust.Client.Graphics
/// </summary>
Hand,
/// <summary>
/// Alias for <see cref="Hand"/>
/// </summary>
Pointer = Hand,
/// <summary>
/// The horizontal resize shape. Used when mousing over something that can be horizontally resized.
/// </summary>
HResize,
/// <summary>
/// Alias for <see cref="EWResize"/>
/// </summary>
EWResize = HResize,
/// <summary>
/// The vertical resize shape. Used when mousing over something that can be vertically resized.
/// </summary>
VResize,
/// <summary>
/// Alias for <see cref="VResize"/>.
/// </summary>
NSResize = VResize,
/// <summary>
/// Program is busy doing something.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
Progress,
/// <summary>
/// Diagonal resize shape for northwest-southeast resizing.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
NWSEResize,
/// <summary>
/// Diagonal resize shape for northeast-southwest resizing.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
NESWResize,
/// <summary>
/// 4-way arrow move icon.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
Move,
/// <summary>
/// An action is not allowed.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
NotAllowed,
/// <summary>
/// One-directional resize to the northwest.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
NWResize,
/// <summary>
/// One-directional resize to the north.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
NResize,
/// <summary>
/// One-directional resize to the northeast.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
NEResize,
/// <summary>
/// One-directional resize to the east.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
EResize,
/// <summary>
/// One-directional resize to the southeast.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
SEResize,
/// <summary>
/// One-directional resize to the south.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
SResize,
/// <summary>
/// One-directional resize to the southwest.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
SWResize,
/// <summary>
/// One-directional resize to the west.
/// </summary>
/// <remarks>
/// This cursor is not always available and may be substituted.
/// </remarks>
WResize,
/// <summary>
/// Not a real value
/// </summary>

View File

@@ -11,14 +11,124 @@ namespace Robust.Client.UserInterface
/// <summary>
/// Default common cursor shapes available in the UI.
/// </summary>
/// <seealso cref="StandardCursorShape"/>
public enum CursorShape: byte
{
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.Arrow"/>
/// </summary>
Arrow,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.IBeam"/>
/// </summary>
IBeam,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.Text"/>
/// </summary>
Text = IBeam,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.Crosshair"/>
/// </summary>
Crosshair,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.Hand"/>
/// </summary>
Hand,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.Pointer"/>
/// </summary>
Pointer = Hand,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.HResize"/>
/// </summary>
HResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.EWResize"/>
/// </summary>
EWResize = HResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.VResize"/>
/// </summary>
VResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.NSResize"/>
/// </summary>
NSResize = VResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.Progress"/>
/// </summary>
Progress,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.NWSEResize"/>
/// </summary>
NWSEResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.NESWResize"/>
/// </summary>
NESWResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.Move"/>
/// </summary>
Move,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.NotAllowed"/>
/// </summary>
NotAllowed,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.NWResize"/>
/// </summary>
NWResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.NResize"/>
/// </summary>
NResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.NEResize"/>
/// </summary>
NEResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.EResize"/>
/// </summary>
EResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.SEResize"/>
/// </summary>
SEResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.SResize"/>
/// </summary>
SResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.SWResize"/>
/// </summary>
SWResize,
/// <summary>
/// Corresponds to <see cref="StandardCursorShape.WResize"/>
/// </summary>
WResize,
/// <summary>
/// Special cursor shape indicating that <see cref="CustomCursorShape"/> is set and being used.
/// </summary>

View File

@@ -69,7 +69,7 @@ namespace Robust.Client.UserInterface.Controls
var itemHeight = 0f;
if (item.Icon != null)
{
itemHeight = item.IconSize.Y;
itemHeight = item.IconSize.Y * item.IconScale;
}
itemHeight = Math.Max(itemHeight, ActualFont.GetHeight(UIScale));
@@ -111,21 +111,21 @@ namespace Robust.Client.UserInterface.Controls
Recalculate();
}
public void AddItems(IEnumerable<string> texts, Texture? icon = null, bool selectable = true, object? metadata = null)
public void AddItems(IEnumerable<string> texts, Texture? icon = null, bool selectable = true, object? metadata = null, float iconScale = 1)
{
var items = new ValueList<Item>();
foreach (var text in texts)
{
items.Add(new Item(this) {Text = text, Icon = icon, Selectable = selectable, Metadata = metadata});
items.Add(new Item(this) {Text = text, Icon = icon, IconScale = iconScale, Selectable = selectable, Metadata = metadata});
}
Add(items);
}
public Item AddItem(string text, Texture? icon = null, bool selectable = true, object? metadata = null)
public Item AddItem(string text, Texture? icon = null, bool selectable = true, object? metadata = null, float iconScale = 1)
{
var item = new Item(this) {Text = text, Icon = icon, Selectable = selectable, Metadata = metadata};
var item = new Item(this) {Text = text, Icon = icon, IconScale = iconScale, Selectable = selectable, Metadata = metadata};
Add(item);
return item;
}
@@ -477,7 +477,7 @@ namespace Robust.Client.UserInterface.Controls
var itemHeight = 0f;
if (item.Icon != null)
{
itemHeight = item.IconSize.Y;
itemHeight = item.IconSize.Y * item.IconScale;
}
itemHeight = Math.Max(itemHeight, font.GetHeight(UIScale));
@@ -496,19 +496,19 @@ namespace Robust.Client.UserInterface.Controls
{
if (item.IconRegion.Size == Vector2.Zero)
{
handle.DrawTextureRect(item.Icon, UIBox2.FromDimensions(drawOffset, item.Icon.Size),
handle.DrawTextureRect(item.Icon, UIBox2.FromDimensions(drawOffset, item.Icon.Size * item.IconScale),
item.IconModulate);
}
else
{
handle.DrawTextureRectRegion(item.Icon, UIBox2.FromDimensions(drawOffset, item.Icon.Size),
handle.DrawTextureRectRegion(item.Icon, UIBox2.FromDimensions(drawOffset, item.Icon.Size * item.IconScale),
item.IconRegion, item.IconModulate);
}
}
if (item.Text != null)
{
var textBox = new UIBox2(contentBox.Left + item.IconSize.X, contentBox.Top, contentBox.Right,
var textBox = new UIBox2(contentBox.Left + item.IconSize.X * item.IconScale, contentBox.Top, contentBox.Right,
contentBox.Bottom);
DrawTextInternal(handle, item.Text, textBox);
}
@@ -722,6 +722,7 @@ namespace Robust.Client.UserInterface.Controls
public Texture? Icon { get; set; }
public UIBox2 IconRegion { get; set; }
public Color IconModulate { get; set; } = Color.White;
public float IconScale { get; set; } = 1;
public bool Selectable { get; set; } = true;
public bool TooltipEnabled { get; set; } = true;
public UIBox2? Region { get; set; }

View File

@@ -326,7 +326,7 @@ namespace Robust.Client.UserInterface.Controls
var parentSize = control.Parent?.Size ?? Vector2.Zero;
var anchorLeft = control.GetValue<float>(AnchorLeftProperty);
var anchorTop = control.GetValue<float>(AnchorBottomProperty);
var anchorTop = control.GetValue<float>(AnchorTopProperty);
var anchorRight = control.GetValue<float>(AnchorRightProperty);
var anchorBottom = control.GetValue<float>(AnchorBottomProperty);

View File

@@ -119,9 +119,12 @@ namespace Robust.Client.UserInterface.CustomControls
case DragMode.Bottom | DragMode.Left:
case DragMode.Top | DragMode.Right:
cursor = CursorShape.NESWResize;
break;
case DragMode.Bottom | DragMode.Right:
case DragMode.Top | DragMode.Left:
cursor = CursorShape.Crosshair;
cursor = CursorShape.NWSEResize;
break;
}

View File

@@ -5,8 +5,8 @@
MinSize="350 200">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<LineEdit Name="SearchBar" Access="Public" HorizontalExpand="True" PlaceHolder="{Loc entity-spawn-window-search-bar-placeholder}"/>
<Button Name="ClearButton" Access="Public" Disabled="True" Text="{Loc entity-spawn-window-clear-button}" />
<LineEdit Name="SearchBar" Access="Public" HorizontalExpand="True" PlaceHolder="{Loc window-search-bar-placeholder}"/>
<Button Name="ClearButton" Access="Public" Disabled="True" Text="{Loc window-clear-button}" />
</BoxContainer>
<ScrollContainer Name="PrototypeScrollContainer" Access="Public" MinSize="200 0" VerticalExpand="True">
<PrototypeListContainer Name="PrototypeList" Access="Public"/>

View File

@@ -5,8 +5,8 @@
MinSize="300 200">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<LineEdit Name="SearchBar" Access="Public" HorizontalExpand="True" PlaceHolder="Search"/>
<Button Name="ClearButton" Access="Public" Text="Clear"/>
<LineEdit Name="SearchBar" Access="Public" HorizontalExpand="True" PlaceHolder="{Loc window-search-bar-placeholder}"/>
<Button Name="ClearButton" Access="Public" Text="{Loc window-clear-button}"/>
</BoxContainer>
<ItemList Name="TileList" Access="Public" VerticalExpand="True"/>
<BoxContainer Orientation="Horizontal">

View File

@@ -202,7 +202,14 @@ internal partial class UserInterfaceManager
return;
}
var shape = cursorTarget.DefaultCursorShape switch
var shape = MapCursorShape(cursorTarget.DefaultCursorShape);
_clyde.SetCursor(_clyde.GetStandardCursor(shape));
}
private static StandardCursorShape MapCursorShape(Control.CursorShape shape)
{
return shape switch
{
Control.CursorShape.Arrow => StandardCursorShape.Arrow,
Control.CursorShape.IBeam => StandardCursorShape.IBeam,
@@ -210,10 +217,21 @@ internal partial class UserInterfaceManager
Control.CursorShape.Crosshair => StandardCursorShape.Crosshair,
Control.CursorShape.VResize => StandardCursorShape.VResize,
Control.CursorShape.HResize => StandardCursorShape.HResize,
Control.CursorShape.Progress => StandardCursorShape.Progress,
Control.CursorShape.NWSEResize => StandardCursorShape.NWSEResize,
Control.CursorShape.NESWResize => StandardCursorShape.NESWResize,
Control.CursorShape.Move => StandardCursorShape.Move,
Control.CursorShape.NotAllowed => StandardCursorShape.NotAllowed,
Control.CursorShape.NWResize => StandardCursorShape.NWResize,
Control.CursorShape.NResize => StandardCursorShape.NResize,
Control.CursorShape.NEResize => StandardCursorShape.NEResize,
Control.CursorShape.EResize => StandardCursorShape.EResize,
Control.CursorShape.SEResize => StandardCursorShape.SEResize,
Control.CursorShape.SResize => StandardCursorShape.SResize,
Control.CursorShape.SWResize => StandardCursorShape.SWResize,
Control.CursorShape.WResize => StandardCursorShape.WResize,
_ => StandardCursorShape.Arrow
};
_clyde.SetCursor(_clyde.GetStandardCursor(shape));
}
public void MouseWheel(MouseWheelEventArgs args)

View File

@@ -1,38 +1,85 @@
using System.Linq;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
namespace Robust.Client.ViewVariables.Editors;
internal sealed class VVPropEditorProtoId<T> : VVPropEditor where T : class, IPrototype
{
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private ViewVariablesAddWindow? _addWindow;
private LineEdit? _lineEdit;
protected override Control MakeUI(object? value)
{
var lineEdit = new LineEdit
// ID LineEdit
_lineEdit = new LineEdit
{
Text = (ProtoId<T>) (value ?? ""),
Text = (ProtoId<T>)(value ?? ""),
PlaceHolder = _loc.GetString("vv-protoid-id-placeholder"),
Editable = !ReadOnly,
HorizontalExpand = true,
};
if (!ReadOnly)
{
lineEdit.OnTextEntered += e =>
_lineEdit.OnTextEntered += e =>
{
var id = (ProtoId<T>)e.Text;
if (!_protoManager.HasIndex(id))
{
return;
}
ValueChanged(id);
SetValue(e.Text);
};
}
return lineEdit;
// Select button
var selectButton = new Button
{
Text = _loc.GetString("vv-protoid-select-button-label"),
Disabled = ReadOnly,
};
selectButton.OnPressed += OnListButtonPressed;
// Container
var hBox = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children =
{
_lineEdit,
selectButton,
}
};
return hBox;
}
private void OnListButtonPressed(BaseButton.ButtonEventArgs args)
{
_addWindow?.Close();
var list = _protoManager.EnumeratePrototypes<T>().Select(p => p.ID);
_addWindow = new ViewVariablesAddWindow(list, _loc.GetString("vv-protoid-addwindow-title"));
_addWindow.AddButtonPressed += OnAddButtonPressed;
_addWindow.OpenCentered();
}
private void OnAddButtonPressed(ViewVariablesAddWindow.AddButtonPressedEventArgs args)
{
_lineEdit?.SetText(args.Entry);
_addWindow?.Close();
SetValue(args.Entry);
}
private void SetValue(string value)
{
var proto = (ProtoId<T>)value;
if (_protoManager.HasIndex(proto))
ValueChanged(proto, false);
}
}

View File

@@ -45,8 +45,8 @@ public static class AttributeHelper
}
public static bool HasAttribute(
INamedTypeSymbol symbol,
INamedTypeSymbol attribute,
ITypeSymbol symbol,
ITypeSymbol attribute,
[NotNullWhen(true)] out AttributeData? matchedAttribute)
{
matchedAttribute = null;

View File

@@ -43,6 +43,8 @@ public static class Diagnostics
public const string IdPrototypeNetSerializable = "RA0037";
public const string IdPrototypeSerializable = "RA0038";
public const string IdPrototypeInstantiation = "RA0039";
public const string IdAutoGenStateAttributeMissing = "RA0040";
public const string IdAutoGenStateParamMissing = "RA0041";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

View File

@@ -54,4 +54,19 @@ public static class TypeSymbolHelper
current = current.BaseType;
}
}
/// <summary>
/// If <paramref name="type"/> is a Nullable{T}, returns the <see cref="ITypeSymbol"/> of the underlying type.
/// Otherwise, returns <paramref name="type"/>.
/// </summary>
// Modified from https://www.meziantou.net/working-with-types-in-a-roslyn-analyzer.htm
public static ITypeSymbol GetNullableUnderlyingTypeOrSelf(ITypeSymbol type)
{
if (type is INamedTypeSymbol namedType && namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
{
return namedType.TypeArguments[0];
}
return type;
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Immutable;
using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -85,12 +85,17 @@ public sealed class ComponentPauseGenerator : IIncrementalGenerator
var invalid = false;
var nullable = false;
var dictionary = false;
if (namedType.Name != "TimeSpan")
{
if (namedType is { Name: "Nullable", TypeArguments: [{Name: "TimeSpan"}] })
{
nullable = true;
}
else if (namedType is { Name: "Dictionary", TypeArguments: [{}, {Name: "TimeSpan"}]})
{
dictionary = true;
}
else
{
invalid = true;
@@ -101,7 +106,7 @@ public sealed class ComponentPauseGenerator : IIncrementalGenerator
if (AttributeHelper.HasAttribute(member, AutoNetworkFieldAttributeName, out var _))
dirty = true;
fieldBuilder.Add(new FieldInfo(member.Name, nullable, invalid, member.Locations[0]));
fieldBuilder.Add(new FieldInfo(member.Name, nullable, invalid, dictionary, member.Locations[0]));
}
return new ComponentInfo(
@@ -181,6 +186,13 @@ public sealed class ComponentPauseGenerator : IIncrementalGenerator
component.{field.Name} = component.{field.Name}.Value + args.PausedTime;
""");
}
else if (field.Dictionary)
{
builder.AppendLine($"""
foreach (var key in component.{field.Name}.Keys)
component.{field.Name}[key] += args.PausedTime;
""");
}
else
{
builder.AppendLine($" component.{field.Name} += args.PausedTime;");
@@ -247,7 +259,7 @@ public sealed class ComponentPauseGenerator : IIncrementalGenerator
bool NotComponent,
Location Location);
public sealed record FieldInfo(string Name, bool Nullable, bool Invalid, Location Location);
public sealed record FieldInfo(string Name, bool Nullable, bool Invalid, bool Dictionary, Location Location);
public sealed record AllFieldInfo(string Name, string ParentDisplayName, Location Location);
}

View File

@@ -1,112 +0,0 @@
using System;
using System.Numerics;
using Robust.Server.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Systems;
namespace Robust.Server.Console.Commands;
public sealed class ScaleCommand : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entityManager = default!;
public override string Command => "scale";
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
switch (args.Length)
{
case 1:
return CompletionResult.FromOptions(CompletionHelper.NetEntities(args[0], entManager: _entityManager));
case 2:
return CompletionResult.FromHint(Loc.GetString("cmd-hint-float"));
default:
return CompletionResult.Empty;
}
}
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError($"Insufficient number of args supplied: expected 2 and received {args.Length}");
return;
}
if (!NetEntity.TryParse(args[0], out var netEntity))
{
shell.WriteError($"Unable to find entity {args[0]}");
return;
}
if (!float.TryParse(args[1], out var scale))
{
shell.WriteError($"Invalid scale supplied of {args[0]}");
return;
}
if (scale < 0f)
{
shell.WriteError($"Invalid scale supplied that is negative!");
return;
}
// Event for content to use
// We'll just set engine stuff here
var physics = _entityManager.System<SharedPhysicsSystem>();
var appearance = _entityManager.System<AppearanceSystem>();
var uid = _entityManager.GetEntity(netEntity);
_entityManager.EnsureComponent<ScaleVisualsComponent>(uid);
var @event = new ScaleEntityEvent();
_entityManager.EventBus.RaiseLocalEvent(uid, ref @event);
var appearanceComponent = _entityManager.EnsureComponent<AppearanceComponent>(uid);
if (!appearance.TryGetData<Vector2>(uid, ScaleVisuals.Scale, out var oldScale, appearanceComponent))
oldScale = Vector2.One;
appearance.SetData(uid, ScaleVisuals.Scale, oldScale * scale, appearanceComponent);
if (_entityManager.TryGetComponent(uid, out FixturesComponent? manager))
{
foreach (var (id, fixture) in manager.Fixtures)
{
switch (fixture.Shape)
{
case EdgeShape edge:
physics.SetVertices(uid, id, fixture,
edge,
edge.Vertex0 * scale,
edge.Vertex1 * scale,
edge.Vertex2 * scale,
edge.Vertex3 * scale, manager);
break;
case PhysShapeCircle circle:
physics.SetPositionRadius(uid, id, fixture, circle, circle.Position * scale, circle.Radius * scale, manager);
break;
case PolygonShape poly:
var verts = poly.Vertices;
for (var i = 0; i < poly.VertexCount; i++)
{
verts[i] *= scale;
}
physics.SetVertices(uid, id, fixture, poly, verts, manager);
break;
default:
throw new NotImplementedException();
}
}
}
}
[ByRefEvent]
public readonly record struct ScaleEntityEvent(EntityUid Uid) {}
}

View File

@@ -103,6 +103,7 @@ namespace Robust.Server.Player
if (!TryGetSessionById(user, out var session))
return;
RemoveSession(session.UserId);
SetStatus(session, SessionStatus.Disconnected);
SetAttachedEntity(session, null, out _, true);
@@ -112,7 +113,6 @@ namespace Robust.Server.Player
viewSys.RemoveViewSubscriber(eye, session);
}
RemoveSession(session.UserId);
PlayerCountMetric.Set(PlayerCount);
Dirty();
}

View File

@@ -17,12 +17,12 @@ public sealed class AutoGenerateComponentStateAttribute : Attribute
/// If this is true, the autogenerated code will raise a <see cref="AfterAutoHandleStateEvent"/> component event
/// so that user-defined systems can have effects after handling state without redefining all replication.
/// </summary>
public bool RaiseAfterAutoHandleState;
public readonly bool RaiseAfterAutoHandleState;
/// <summary>
/// Should delta states be generated for every field.
/// </summary>
public bool FieldDeltas;
public readonly bool FieldDeltas;
public AutoGenerateComponentStateAttribute(bool raiseAfterAutoHandleState = false, bool fieldDeltas = false)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
namespace Robust.Shared.Collections;
@@ -77,7 +77,7 @@ internal struct InvokeList<T>
for (var i = 0; i < _entries.Length; i++)
{
var entry = _entries[i];
if (equality.Equals(entry))
if (equality.Equals(entry.Equality))
{
entryIdx = i;
break;
@@ -94,14 +94,12 @@ internal struct InvokeList<T>
// Create new backing array and copy stuff into it.
var newEntries = new Entry[_entries.Length - 1];
for (var i = 0; i < entryIdx; i++)
for (int srcIdx = 0, dstIdx = 0; dstIdx < newEntries.Length; srcIdx++, dstIdx++)
{
newEntries[i] = _entries[i];
}
if (srcIdx == entryIdx)
srcIdx++;
for (var i = entryIdx + 1; i < _entries.Length; i++)
{
newEntries[entryIdx - 1] = _entries[entryIdx];
newEntries[dstIdx] = _entries[srcIdx];
}
return new InvokeList<T>

View File

@@ -2,6 +2,7 @@ using System;
using System.Numerics;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Shared.ColorNaming;
@@ -21,7 +22,8 @@ public static class ColorNaming
(float.DegreesToRadians(285f), "color-purple"),
(float.DegreesToRadians(330f), "color-pink"),
};
private static readonly (float Hue, string Loc) HueFallback = (float.DegreesToRadians(360f), "color-pink");
// one past 360 because we're now inclusive on the upper for testing if we're out of bounds
private static readonly (float Hue, string Loc) HueFallback = (float.DegreesToRadians(361f), "color-pink");
private const float BrownLightnessThreshold = 0.675f;
private static readonly LocId OrangeString = "color-orange";
@@ -63,7 +65,7 @@ public static class ColorNaming
var prevData = HueNames[i];
var nextData = i+1 < HueNames.Length ? HueNames[i+1] : HueFallback;
if (prevData.Hue >= hue || hue > nextData.Hue)
if (prevData.Hue > hue || hue >= nextData.Hue)
continue;
var loc = prevData.Loc;
@@ -85,7 +87,8 @@ public static class ColorNaming
return (localization.GetString(loc), adjustedLightness);
}
throw new ArgumentOutOfRangeException("oklch", $"colour ({oklch}) hue {hue} is outside of expected bounds");
DebugTools.Assert($"colour ({oklch}) hue {hue} is outside of expected bounds");
return (localization.GetString("color-unknown"), lightness);
}
private static string? DescribeChroma(Vector4 oklch, ILocalizationManager localization)

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -25,7 +26,7 @@ namespace Robust.Shared.Configuration
private const char TABLE_DELIMITER = '.';
protected readonly Dictionary<string, ConfigVar> _configVars = new();
private string? _configFile;
private ConfigFileStorage? _configFile;
protected bool _isServer;
protected readonly ReaderWriterLockSlim Lock = new();
@@ -182,7 +183,7 @@ namespace Robust.Shared.Configuration
{
using var file = File.OpenRead(configFile);
var result = LoadFromTomlStream(file);
_configFile = configFile;
SetSaveFile(configFile);
_sawmill.Info($"Configuration loaded from file");
return result;
}
@@ -195,7 +196,12 @@ namespace Robust.Shared.Configuration
public void SetSaveFile(string configFile)
{
_configFile = configFile;
_configFile = new ConfigFileStorageDisk { Path = configFile };
}
public void SetVirtualConfig()
{
_configFile = new ConfigFileStorageVirtual();
}
public void CheckUnusedCVars()
@@ -312,8 +318,27 @@ namespace Robust.Shared.Configuration
var memoryStream = new MemoryStream();
SaveToTomlStream(memoryStream, cvars);
memoryStream.Position = 0;
using var file = File.Create(_configFile);
memoryStream.CopyTo(file);
switch (_configFile)
{
case ConfigFileStorageDisk disk:
{
using var file = File.Create(disk.Path);
memoryStream.CopyTo(file);
break;
}
case ConfigFileStorageVirtual @virtual:
{
@virtual.Stream.SetLength(0);
memoryStream.CopyTo(@virtual.Stream);
break;
}
default:
{
throw new UnreachableException();
}
}
_sawmill.Info($"config saved to '{_configFile}'.");
}
catch (Exception e)
@@ -954,6 +979,30 @@ namespace Robust.Shared.Configuration
}
protected delegate void ValueChangedDelegate(object value, in CVarChangeInfo info);
private abstract class ConfigFileStorage;
private sealed class ConfigFileStorageDisk : ConfigFileStorage
{
public required string Path;
public override string ToString()
{
return Path;
}
}
private sealed class ConfigFileStorageVirtual : ConfigFileStorage
{
// I did not realize when adding this class that there is currently no way to *load* this data again.
// Oh well, might be useful for a future unit test.
public readonly MemoryStream Stream = new();
public override string ToString()
{
return "<VIRTUAL>";
}
}
}
[Serializable]

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
namespace Robust.Shared.Configuration;
public static class ConfigurationManagerExtensions
{
/// <summary>
/// Subscribe to multiple cvar in succession and dispose object to unsubscribe from all of them when needed.
/// </summary>
public static ConfigurationMultiSubscriptionBuilder SubscribeMultiple(this IConfigurationManager manager)
{
return new ConfigurationMultiSubscriptionBuilder(manager);
}
}
/// <summary>
/// Container for batch-unsubscription of config changed events.
/// Call Dispose() when subscriptions are not needed anymore.
/// </summary>
public sealed class ConfigurationMultiSubscriptionBuilder(IConfigurationManager manager) : IDisposable
{
private readonly List<Action> _unsubscribeActions = [];
/// <inheritdoc cref="IConfigurationManager.OnValueChanged{T}(CVarDef{T},Action{T},bool)"/>>
public ConfigurationMultiSubscriptionBuilder OnValueChanged<T>(
CVarDef<T> cVar,
CVarChanged<T> onValueChanged,
bool invokeImmediately = false
)
where T : notnull
{
manager.OnValueChanged(cVar, onValueChanged, invokeImmediately);
_unsubscribeActions.Add(() => manager.UnsubValueChanged(cVar, onValueChanged));
return this;
}
/// <inheritdoc cref="IConfigurationManager.OnValueChanged{T}(string,Action{T},bool)"/>>
public ConfigurationMultiSubscriptionBuilder OnValueChanged<T>(
string name,
CVarChanged<T> onValueChanged,
bool invokeImmediately = false
)
where T : notnull
{
manager.OnValueChanged(name, onValueChanged, invokeImmediately);
_unsubscribeActions.Add(() => manager.UnsubValueChanged(name, onValueChanged));
return this;
}
/// <inheritdoc cref="IConfigurationManager.OnValueChanged{T}(CVarDef{T},CVarChanged{T},bool)"/>>
public ConfigurationMultiSubscriptionBuilder OnValueChanged<T>(
CVarDef<T> cVar,
Action<T> onValueChanged,
bool invokeImmediately = false
)
where T : notnull
{
manager.OnValueChanged(cVar, onValueChanged, invokeImmediately);
_unsubscribeActions.Add(() => manager.UnsubValueChanged(cVar, onValueChanged));
return this;
}
/// <inheritdoc cref="IConfigurationManager.OnValueChanged{T}(string,CVarChanged{T},bool)"/>>
public ConfigurationMultiSubscriptionBuilder OnValueChanged<T>(
string name,
Action<T> onValueChanged,
bool invokeImmediately = false
)
where T : notnull
{
manager.OnValueChanged(name, onValueChanged, invokeImmediately);
_unsubscribeActions.Add(() => manager.UnsubValueChanged(name, onValueChanged));
return this;
}
/// <inheritdoc />
public void Dispose()
{
foreach (var action in _unsubscribeActions)
{
action();
}
_unsubscribeActions.Clear();
}
}

View File

@@ -10,6 +10,15 @@ namespace Robust.Shared.Configuration
void LoadCVarsFromAssembly(Assembly assembly);
void LoadCVarsFromType(Type containingType);
/// <summary>
/// Indicate that config should be stored in-memory.
/// </summary>
/// <remarks>
/// This suppresses warnings from <see cref="IConfigurationManager.SaveToFile"/>
/// if no config is otherwise loaded.
/// </remarks>
void SetVirtualConfig();
void Initialize(bool isServer);
void Shutdown();

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

View File

@@ -1,6 +0,0 @@
using Robust.Shared.GameStates;
namespace Robust.Shared.GameObjects;
[RegisterComponent, NetworkedComponent]
public sealed partial class ScaleVisualsComponent : Component {}

View File

@@ -1,30 +1,39 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown.Mapping;
using YamlDotNet.RepresentationModel;
namespace Robust.Shared.GameObjects
{
/// <summary>
/// Interface used to allow the map loader to override prototype data with map data.
/// Interface used to allow the map loader to override prototype data with map data.
/// </summary>
internal interface IEntityLoadContext
{
/// <summary>
/// Tries getting the data of the provided component
/// </summary>
/// <summary>Tries getting the data of the given component.</summary>
/// <param name="componentName">Name of component to find.</param>
/// <param name="component">Found component or null.</param>
/// <returns>True if the component was found, false otherwise.</returns>
/// <seealso cref="TryGetComponent{T}"/>
bool TryGetComponent(string componentName, [NotNullWhen(true)] out IComponent? component);
/// <summary>Tries getting the data of the given component.</summary>
/// <typeparam name="TComponent">Type of component to be found.</typeparam>
/// <param name="componentFactory">Component factory required for the lookup.</param>
/// <param name="component">Found component or null.</param>
/// <returns>True if the component was found, false otherwise.</returns>
/// <seealso cref="TryGetComponent"/>
bool TryGetComponent<TComponent>(
IComponentFactory componentFactory,
[NotNullWhen(true)] out TComponent? component
) where TComponent : class, IComponent, new();
/// <summary>
/// Gets all components registered for the entityloadcontext, overrides as well as extra components
/// Gets all components registered for the entityloadcontext, overrides as well as extra components
/// </summary>
IEnumerable<string> GetExtraComponentTypes();
/// <summary>
/// Checks whether a given component should be added to an entity. Used to prevent certain prototype components from being added while spawning an entity.
/// Checks whether a given component should be added to an entity.
/// Used to prevent certain prototype components from being added while spawning an entity.
/// </summary>
bool ShouldSkipComponent(string compName);
}

View File

@@ -1,11 +0,0 @@
using System;
using Robust.Shared.Serialization;
namespace Robust.Shared.GameObjects;
[Serializable, NetSerializable]
public enum ScaleVisuals : byte
{
// Blep
Scale,
}

View File

@@ -262,12 +262,24 @@ public abstract partial class SharedMapSystem
return (uid, AddComp<MapComponent>(uid), meta);
}
/// <summary>
/// Deletes a map with the specified map id.
/// </summary>
public void DeleteMap(MapId mapId)
{
if (TryGetMap(mapId, out var uid))
Del(uid);
}
/// <summary>
/// Deletes a map with the specified map id in the next tick.
/// </summary>
public void QueueDeleteMap(MapId mapId)
{
if (TryGetMap(mapId, out var uid))
QueueDel(uid);
}
public IEnumerable<MapId> GetAllMapIds()
{
return Maps.Keys;

View File

@@ -55,12 +55,25 @@ public sealed partial class NetManager
NetChannel channel,
NetMessage message)
{
if (!channel.IsConnected)
{
_logger.Error(
$"Tried to send message \"{message}\" to disconnected channel {channel}\n{Environment.StackTrace}");
return;
}
var packet = BuildMessage(message, channel.Connection.Peer);
var method = message.DeliveryMethod;
LogSend(message, method, packet);
var item = new EncryptChannelItem { Message = packet, Method = method };
var item = new EncryptChannelItem
{
Message = packet,
Method = method,
Owner = this,
RobustMessage = message,
};
// If the message is ordered, we have to send it to the encryption channel.
if (method is NetDeliveryMethod.ReliableOrdered
@@ -70,7 +83,7 @@ public sealed partial class NetManager
if (channel.EncryptionChannel is { } encryptionChannel)
{
var task = encryptionChannel.WriteAsync(item);
if (!task.IsCompleted)
if (!task.IsCompletedSuccessfully)
task.AsTask().Wait();
}
else
@@ -101,12 +114,20 @@ public sealed partial class NetManager
{
channel.Encryption?.Encrypt(item.Message);
channel.Connection.Peer.SendMessage(item.Message, channel.Connection, item.Method);
var result = channel.Connection.Peer.SendMessage(item.Message, channel.Connection, item.Method);
if (result is not (NetSendResult.Sent or NetSendResult.Queued))
{
// Logging stack trace here won't be useful as it'll likely be thread pooled on production scenarios.
item.Owner._logger.Warning(
$"Failed to send message {item.RobustMessage} to {channel} via Lidgren: {result}");
}
}
private struct EncryptChannelItem
{
public required NetOutgoingMessage Message { get; init; }
public required NetDeliveryMethod Method { get; init; }
public required NetOutgoingMessage Message;
public required NetDeliveryMethod Method;
public required NetMessage RobustMessage;
public required NetManager Owner;
}
}

View File

@@ -827,6 +827,10 @@ namespace Robust.Shared.Network
_assignedUsernames.Remove(channel.UserName);
_assignedUserIds.Remove(channel.UserId);
_channels.Remove(connection);
peer.RemoveChannel(channel);
channel.EncryptionChannel?.Complete();
#if EXCEPTION_TOLERANCE
try
{
@@ -842,9 +846,6 @@ namespace Robust.Shared.Network
_logger.Error("Caught exception in OnDisconnected handler:\n{0}", e);
}
#endif
_channels.Remove(connection);
peer.RemoveChannel(channel);
channel.EncryptionChannel?.Complete();
if (IsClient)
{

View File

@@ -1,5 +1,7 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
@@ -71,6 +73,45 @@ public abstract partial class SharedPhysicsSystem
_fixtures.FixtureUpdate(uid, manager: manager);
}
/// <summary>
/// Increases or decreases all fixtures of an entity in size by a certain factor.
/// </summary>
public void ScaleFixtures(Entity<FixturesComponent?> ent, float factor)
{
if (!Resolve(ent, ref ent.Comp))
return;
foreach (var (id, fixture) in ent.Comp.Fixtures)
{
switch (fixture.Shape)
{
case EdgeShape edge:
SetVertices(ent, id, fixture,
edge,
edge.Vertex0 * factor,
edge.Vertex1 * factor,
edge.Vertex2 * factor,
edge.Vertex3 * factor, ent.Comp);
break;
case PhysShapeCircle circle:
SetPositionRadius(ent, id, fixture, circle, circle.Position * factor, circle.Radius * factor, ent.Comp);
break;
case PolygonShape poly:
var verts = poly.Vertices;
for (var i = 0; i < poly.VertexCount; i++)
{
verts[i] *= factor;
}
SetVertices(ent, id, fixture, poly, verts, ent.Comp);
break;
default:
throw new NotImplementedException();
}
}
}
#region Collision Masks & Layers
/// <summary>

View File

@@ -413,6 +413,7 @@ namespace Robust.Shared.Prototypes
{
}
/// <inheritdoc />
public bool TryGetComponent(string componentName, [NotNullWhen(true)] out IComponent? component)
{
var success = TryGetValue(componentName, out var comp);
@@ -421,11 +422,30 @@ namespace Robust.Shared.Prototypes
return success;
}
/// <inheritdoc />
public bool TryGetComponent<TComponent>(
IComponentFactory componentFactory,
[NotNullWhen(true)] out TComponent? component
) where TComponent : class, IComponent, new()
{
component = null;
var componentName = componentFactory.GetComponentName<TComponent>();
if (TryGetComponent(componentName, out var foundComponent))
{
component = (TComponent)foundComponent;
return true;
}
return false;
}
/// <inheritdoc />
public IEnumerable<string> GetExtraComponentTypes()
{
return Keys;
}
/// <inheritdoc />
public bool ShouldSkipComponent(string compName)
{
return false; //Registries cannot represent the "remove this component" state.

View File

@@ -264,6 +264,10 @@ namespace Robust.UnitTesting
return;
var channel = (IntegrationNetChannel) recipient;
if (!channel.IsConnected)
throw new InvalidOperationException("Channel is not connected!");
channel.OtherChannel.TryWrite(SerializeNetMessage(message, channel.RemoteUid));
}

View File

@@ -765,6 +765,8 @@ namespace Robust.UnitTesting
(CVars.ResCheckBadFileExtensions.Name, "false")
});
cfg.SetVirtualConfig();
server.ContentStart = Options?.ContentStart ?? false;
var logHandler = Options?.OverrideLogHandler ?? (() => new TestLogHandler(cfg, "SERVER", _testOut));
if (server.Start(serverOptions, logHandler))
@@ -1033,6 +1035,8 @@ namespace Robust.UnitTesting
(CVars.ResCheckBadFileExtensions.Name, "false")
});
cfg.SetVirtualConfig();
GameLoop = new IntegrationGameLoop(DependencyCollection.Resolve<IGameTiming>(),
_fromInstanceWriter, _toInstanceReader);

View File

@@ -0,0 +1,42 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.Configuration;
using Robust.Shared.Log;
namespace Robust.UnitTesting.Shared.Configuration;
[Parallelizable(ParallelScope.All)]
[TestFixture]
[TestOf(typeof(ConfigurationManagerTest))]
internal sealed class ConfigurationIntegrationTest : RobustIntegrationTest
{
[Test]
public async Task TestSaveNoWarningServer()
{
using var server = StartServer(new ServerIntegrationOptions
{
FailureLogLevel = LogLevel.Warning
});
await server.WaitPost(() =>
{
// ReSharper disable once AccessToDisposedClosure
var cfg = server.Resolve<IConfigurationManager>();
cfg.SaveToFile();
});
}
[Test]
public async Task TestSaveNoWarningClient()
{
using var server = StartClient(new ClientIntegrationOptions
{
FailureLogLevel = LogLevel.Warning
});
await server.WaitPost(() =>
{
// ReSharper disable once AccessToDisposedClosure
var cfg = server.Resolve<IConfigurationManager>();
cfg.SaveToFile();
});
}
}

View File

@@ -43,10 +43,76 @@ namespace Robust.UnitTesting.Shared.Configuration
Assert.That(timesRan, Is.EqualTo(1), "UnsubValueChanged did not unsubscribe!");
}
[Test]
public void TestSubscribe_SubscribeMultipleThenUnsubscribe()
{
var mgr = MakeCfg();
mgr.RegisterCVar("foo.bar", 5);
var lastValueBar1 = 0;
var lastValueBar2 = 0;
var lastValueBar3 = 0;
var lastValueBar4 = 0;
var subscription = mgr.SubscribeMultiple()
.OnValueChanged<int>("foo.bar", value => lastValueBar1 = value)
.OnValueChanged<int>("foo.bar", value => lastValueBar2 = value)
.OnValueChanged<int>("foo.bar", value => lastValueBar3 = value)
.OnValueChanged<int>("foo.bar", value => lastValueBar4 = value);
mgr.SetCVar("foo.bar", 1);
Assert.That(lastValueBar1, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar2, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar3, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar4, Is.EqualTo(1), "OnValueChanged value was wrong!");
subscription.Dispose();
mgr.SetCVar("foo.bar", 10);
Assert.That(lastValueBar1, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar2, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar3, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar4, Is.EqualTo(1), "OnValueChanged value was wrong!");
}
[Test]
public void TestSubscribe_Unsubscribe()
{
var mgr = MakeCfg();
mgr.RegisterCVar("foo.bar", 5);
mgr.RegisterCVar("foo.foo", 2);
var lastValueBar = 0;
var lastValueFoo = 0;
var subscription = mgr.SubscribeMultiple()
.OnValueChanged<int>("foo.bar", value => lastValueBar = value)
.OnValueChanged<int>("foo.foo", value => lastValueFoo = value);
mgr.SetCVar("foo.bar", 1);
mgr.SetCVar("foo.foo", 3);
Assert.That(lastValueBar, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueFoo, Is.EqualTo(3), "OnValueChanged value was wrong!");
subscription.Dispose();
mgr.SetCVar("foo.bar", 10);
mgr.SetCVar("foo.foo", 30);
Assert.That(lastValueBar, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueFoo, Is.EqualTo(3), "OnValueChanged value was wrong!");
}
[Test]
public void TestOverrideDefaultValue()
{
var mgr = MakeCfg();
mgr.RegisterCVar("foo.bar", 5);
var value = 0;
@@ -71,7 +137,7 @@ namespace Robust.UnitTesting.Shared.Configuration
Assert.That(mgr.GetCVar<int>("foo.bar"), Is.EqualTo(7));
}
private ConfigurationManager MakeCfg()
private IConfigurationManager MakeCfg()
{
var collection = new DependencyCollection();
collection.RegisterInstance<IReplayRecordingManager>(new Mock<IReplayRecordingManager>().Object);

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Numerics;
using NetSerializer;
@@ -212,5 +213,37 @@ namespace Robust.UnitTesting.Shared.Serialization
stream.Position = 0;
Assert.That(() => Primitives.ReadPrimitive(stream, out string _), Throws.TypeOf<InvalidDataException>());
}
[Test]
public void TestImmutableDictionary()
{
var x = new Dictionary<string, int>
{
{ "A", 1 },
{ "B", 2 }
}.ToImmutableDictionary();
var serializer = new Serializer([typeof(ImmutableDictionary<string, int>)], new Settings());
var stream = new MemoryStream();
serializer.SerializeDirect(stream, x);
stream.Position = 0;
serializer.DeserializeDirect(stream, out ImmutableDictionary<string, int> read);
Assert.That(read, NUnit.Framework.Is.EquivalentTo(x));
}
[Test]
public void TestImmutableHashSet()
{
var x = new HashSet<string> {"A", "B"}.ToImmutableHashSet();
var serializer = new Serializer([typeof(ImmutableHashSet<string>)], new Settings());
var stream = new MemoryStream();
serializer.SerializeDirect(stream, x);
stream.Position = 0;
serializer.DeserializeDirect(stream, out ImmutableHashSet<string> read);
Assert.That(read, NUnit.Framework.Is.EquivalentTo(x));
}
}
}