Compare commits

...

30 Commits

Author SHA1 Message Date
ElectroJr
63df90f86f Version: 222.2.0 2024-05-14 23:01:41 +12:00
Vasilis
51f0c60bd3 Do not log wrong and correct watchdog token into info logs (#5133)
* Lets not do this?

* Webedit 2
2024-05-14 02:12:24 +02:00
metalgearsloth
a9ed53f47b Run BUI range checks in parallel (#5118)
These still take almost half-ms in server tick time on live as it has to do a raycast for everyone that has storage or their PDA open or whatever. Haven't benchmarked with a lot of clients but easiest way to tell is just check grafanaTM and I'm not sure how to check this locally.
2024-05-14 09:03:26 +10:00
Amy
41c40f1a94 Fix checking wrong property in TryGetVariableType() (#5120)
Co-authored-by: amylizzle <amylizzle@users.noreply.github.com>
2024-05-14 08:44:19 +10:00
deltanedas
6e61c35d35 add missing Comp inline to EntityQuery (#5123)
Co-authored-by: deltanedas <@deltanedas:kde.org>
2024-05-14 08:43:03 +10:00
ShadowCommander
aae0a8bc51 Add doc comments to CreateEntityUninitialized (#5131) 2024-05-14 08:42:34 +10:00
DrSmugleaf
cb543240c6 Fix clients mispredicting a character's gender (#5119)
* Fix clients mispredicting a character's gender

* Allow nullable value in set
2024-05-12 22:20:21 -07:00
metalgearsloth
1654ab06f5 Revert "Add audio stream name to entity name" (#5127)
This reverts commit d2a2afe82e.
2024-05-13 10:53:30 +10:00
Brandon Li
211245215e remove XAMLIL after generating populate method (#5126) 2024-05-13 01:34:32 +02:00
ShadowCommander
10aaaa65c5 Add editorconfig wrapping settings (#5125) 2024-05-12 17:50:19 +02:00
ShadowCommander
d2a2afe82e Add audio stream name to entity name (#5121) 2024-05-10 17:58:38 +02:00
Pieter-Jan Briers
025d90d281 Change build upload server to Suns 2024-05-09 09:48:49 +02:00
metalgearsloth
c229f2e312 Fix valuelist ensurecapacity(0) throwing (#5113)
_items is still null so.
2024-05-08 20:46:45 +02:00
metalgearsloth
fe051a3577 Minor bui thing (#5114) 2024-05-08 19:59:13 +10:00
DrSmugleaf
51a0ef1e60 Fix error when JointRelayTargetComponent shuts down while applying state (#5110) 2024-05-08 19:21:55 +10:00
DrSmugleaf
702dfef5fc Version: 222.1.1 2024-05-07 22:28:07 -07:00
DrSmugleaf
a0c1ad246f Fix never setting BoundUserInterface.State (#5111)
* Fix never setting BoundUserInterface.State

* Make setter internal
2024-05-07 20:51:26 -07:00
metalgearsloth
1153888bd1 Add truncate for filesaving (#5098)
* Add truncate for filesaving

If I expose it to content I pretty much always want truncate to be honest.

* Update Robust.Client/UserInterface/FileDialogManager.cs

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2024-05-07 00:58:57 +02:00
Amy
ccbb6ddec7 Add method for getting type of var (#5070)
Co-authored-by: amylizzle <amylizzle@users.noreply.github.com>
2024-05-06 20:39:32 +02:00
metalgearsloth
970da5f717 Version: 222.1.0 2024-05-06 13:43:11 +10:00
Pieter-Jan Briers
4d528dd577 Analyzer to ban uncached regexes (#5107)
Using static Regex functions that take in a pattern is bad, because they constantly have to be re-parsed. Cache the Regex instance.
2024-05-06 10:30:31 +10:00
Pieter-Jan Briers
c83720b163 Update Lidgren to v0.3.1 2024-05-06 01:55:05 +02:00
Pieter-Jan Briers
bd87a805d4 Add CVars to turn Lidgren's error/warning logs off.
Combined with upcoming Lidgren changes, this should make DDoS-induced warning log spam not cause huge server perf issues anymore.
2024-05-06 01:47:27 +02:00
Leon Friedrich
fff42fb2b4 Partially fix UI assert (#5100)
* Partially fix UI assert

* Avoid breaking change in BoundKeyEventArgs

This is a public constructor, as much as it maybe shouldn't be. Adding this parameter is a breaking change.

* Replace .Disposed checks with ! .VisibleInTree

Control disposal should not be used anymore.

* Release notes

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-05-06 01:29:34 +02:00
metalgearsloth
4500669f65 Version: 222.0.0 2024-05-06 08:54:19 +10:00
Pieter-Jan Briers
7d19ea9338 Fix compiler error from merges
EntProtoId PR was incompatible with the PR to change EntityPrototype methods to require IComponentFactory passed in
2024-05-06 00:32:54 +02:00
Leon Friedrich
2dc610907d Make IComponentFactory argument in EntityPrototype mandatory (#5101) 2024-05-05 23:00:10 +02:00
DrSmugleaf
beb1c4b1fb Add EntProtoId<T> (#5097)
* Add EntProtoId<T>

* Fix error messages

* Shorten error messages

* Make services non-optional
2024-05-05 22:59:45 +02:00
metalgearsloth
7e331eaa75 Defer clientside BUI opens (#5073)
* Defer clientside BUI opens

Needs content fix first as storage UI breaks.

* tweaks

* Re-revert this because it seems needed
2024-05-03 12:58:19 +10:00
Leon Friedrich
caf9e45ad9 Fix PVS iterating over duplicate chunks when a a client has multiple viewers/eyes (#5094) 2024-05-03 05:52:56 +10:00
38 changed files with 668 additions and 115 deletions

View File

@@ -7,6 +7,18 @@ indent_size = 4
trim_trailing_whitespace = true
charset = utf-8
max_line_length = 120
# ReSharper properties
resharper_csharp_max_line_length = 120
resharper_csharp_wrap_after_declaration_lpar = true
resharper_csharp_wrap_arguments_style = chop_if_long
resharper_csharp_wrap_parameters_style = chop_if_long
resharper_keep_existing_attribute_arrangement = true
resharper_place_field_attribute_on_same_line = if_owner_is_single_line
resharper_wrap_chained_binary_patterns = chop_if_long
resharper_wrap_chained_method_calls = chop_if_long
[*.{csproj,xml,yml,dll.config,targets,props}]
indent_size = 2

View File

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

View File

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

View File

@@ -54,6 +54,69 @@ END TEMPLATE-->
*None yet*
## 222.2.0
### New features
* Added `EntityQuery.Comp()` (abbreviation of `GetComponent()`)
### Bugfixes
* Fix `SerializationManager.TryGetVariableType` checking the wrong property.
* Fixed GrammarSystem mispredicting a character's gender
### Other
* User interface system now performs range checks in parallel
## 222.1.1
### Bugfixes
* Fixed never setting BoundUserInterface.State.
### Other
* Add truncate for filesaving.
* Add method for getting the type of a data field by name from ISerializationManager.
## 222.1.0
### New features
* Added `BoundKeyEventArgs.IsRepeat`.
* Added `net.lidgren_log_warning` and `net.lidgren_log_error` CVars.
### Bugfixes
* Fix assert trip when holding repeatable keybinds.
### Other
* Updated Lidgren to v0.3.1. This should provide performance improvements if warning/error logs are disabled.
## 222.0.0
### Breaking changes
* Mark IComponentFactory argument in EntityPrototype as mandatory.
### New features
* Add `EntProtoId<T>` to check for components on the attached entity as well.
### Bugfixes
* Fix PVS iterating duplicate chunks for multiple viewsubscriptions.
### Other
* Defer clientside BUI opens if it's the first state that comes in.
## 221.2.0
### New features

View File

@@ -0,0 +1,57 @@
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.NoUncachedRegexAnalyzer>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class NoUncachedRegexAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<NoUncachedRegexAnalyzer, NUnitVerifier>()
{
TestState =
{
Sources = { code }
},
};
// 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 System.Text.RegularExpressions;
public static class Foo
{
public static void Bad()
{
Regex.Replace("foo", "bar", "baz");
}
public static void Good()
{
var r = new Regex("bar");
r.Replace("foo", "baz");
}
}
""";
await Verifier(code,
// /0/Test0.cs(7,9): warning RA0026: Usage of a static Regex function that takes in a pattern string. This can cause constant re-parsing of the pattern.
VerifyCS.Diagnostic().WithSpan(7, 9, 7, 43)
);
}
}

View File

@@ -0,0 +1,66 @@
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 NoUncachedRegexAnalyzer : DiagnosticAnalyzer
{
private const string RegexTypeName = "Regex";
private const string RegexType = $"System.Text.RegularExpressions.{RegexTypeName}";
private static readonly DiagnosticDescriptor Rule = new (
Diagnostics.IdUncachedRegex,
"Use of uncached static Regex function",
"Usage of a static Regex function that takes in a pattern string. This can cause constant re-parsing of the pattern.",
"Usage",
DiagnosticSeverity.Warning,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public static readonly HashSet<string> BadFunctions =
[
"Count",
"EnumerateMatches",
"IsMatch",
"Match",
"Matches",
"Replace",
"Split"
];
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterOperationAction(CheckInvocation, OperationKind.Invocation);
}
private static void CheckInvocation(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation invocation)
return;
// All Regex functions we care about are static.
var targetMethod = invocation.TargetMethod;
if (!targetMethod.IsStatic)
return;
// Bail early.
if (targetMethod.ContainingType.Name != "Regex")
return;
var regexType = context.Compilation.GetTypeByMetadataName(RegexType);
if (!SymbolEqualityComparer.Default.Equals(regexType, targetMethod.ContainingType))
return;
if (!BadFunctions.Contains(targetMethod.Name))
return;
context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.Syntax.GetLocation()));
}
}

View File

@@ -239,6 +239,7 @@ namespace Robust.Build.Tasks
engine.LogErrorEvent(new BuildErrorEventArgs("XAMLIL", "", res.FilePath, 0, 0, 0, 0,
$"{res.FilePath}: {e.Message}", "", "CompileRobustXaml"));
}
res.Remove();
}
return true;
}

View File

@@ -346,7 +346,7 @@ namespace Robust.Client.Input
{
if (binding.CanRepeat)
{
return SetBindState(binding, BoundKeyState.Down, uiOnly);
return SetBindState(binding, BoundKeyState.Down, uiOnly, isRepeat);
}
return true;
@@ -375,7 +375,7 @@ namespace Robust.Client.Input
SetBindState(binding, BoundKeyState.Up);
}
private bool SetBindState(KeyBinding binding, BoundKeyState state, bool uiOnly = false)
private bool SetBindState(KeyBinding binding, BoundKeyState state, bool uiOnly = false, bool isRepeat = false)
{
if (binding.BindingType == KeyBindingType.Command && state == BoundKeyState.Down)
{
@@ -387,6 +387,7 @@ namespace Robust.Client.Input
// I honestly have no idea what the best solution here is.
// note from the future: context switches won't cause re-entrancy anymore because InputContextContainer defers context switches
DebugTools.Assert(!_currentlyFindingViewport, "Re-entrant key events??");
DebugTools.Assert(!isRepeat || binding.CanRepeat);
try
{
@@ -399,7 +400,7 @@ namespace Robust.Client.Input
binding.State = state;
var eventArgs = new BoundKeyEventArgs(binding.Function, binding.State,
MouseScreenPosition, binding.CanFocus);
MouseScreenPosition, binding.CanFocus, isRepeat);
// UI returns true here into blockPass if it wants to prevent us from giving input events
// to the viewport, but doesn't want it hard-handled so we keep processing possible key actions.

View File

@@ -13,7 +13,7 @@ namespace Robust.Client.UserInterface
return Task.FromResult<Stream?>(null);
}
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null)
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
{
return Task.FromResult<(Stream fileStream, bool alreadyExisted)?>(null);
}

View File

@@ -51,7 +51,7 @@ namespace Robust.Client.UserInterface
return await OpenFileNfd(filters);
}
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters)
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
{
var name = await GetSaveFileName(filters);
if (name == null)
@@ -61,7 +61,7 @@ namespace Robust.Client.UserInterface
try
{
return (File.Open(name, FileMode.Open), true);
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open), true);
}
catch (FileNotFoundException)
{

View File

@@ -28,6 +28,7 @@ namespace Robust.Client.UserInterface
/// The file stream the user chose to save to, and whether the file already existed.
/// Null if the user cancelled the action.
/// </returns>
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null);
/// <param name="truncate">Should we truncate an existing file to 0-size then write or append.</param>
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true);
}
}

View File

@@ -114,8 +114,10 @@ internal partial class UserInterfaceManager
args.Handle();
}
// Attempt to ensure that keybind-up events only get raised after a single keybind-down.
DebugTools.Assert(!_focusedControls.ContainsKey(args.Function));
// Attempt to ensure that keybind-up events get raised after a keybind-down.
DebugTools.Assert(!_focusedControls.TryGetValue(args.Function, out var existing)
|| !existing.VisibleInTree
|| args.IsRepeat && existing == control);
_focusedControls[args.Function] = control;
OnKeyBindDown?.Invoke(control);
@@ -124,7 +126,7 @@ internal partial class UserInterfaceManager
public void KeyBindUp(BoundKeyEventArgs args)
{
// Only raise keybind-up for the control on which we previously raised keybind-down
if (!_focusedControls.Remove(args.Function, out var control) || control.Disposed)
if (!_focusedControls.Remove(args.Function, out var control) || !control.VisibleInTree)
return;
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,

View File

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

View File

@@ -51,10 +51,15 @@ internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<P
public (GameTick ToTick, List<PvsIndex> PreviouslySent)? LastSent;
/// <summary>
/// Visible chunks, sorted by proximity to the clients's viewers;
/// Visible chunks, sorted by proximity to the client's viewers.
/// </summary>
public readonly List<(PvsChunk Chunk, float ChebyshevDistance)> Chunks = new();
/// <summary>
/// Unsorted set of visible chunks. Used to construct the <see cref="Chunks"/> list.
/// </summary>
public readonly HashSet<PvsChunk> ChunkSet = new();
/// <summary>
/// Squared distance ta all of the visible chunks.
/// </summary>
@@ -117,6 +122,7 @@ internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<P
{
PlayerStates.Clear();
Chunks.Clear();
ChunkSet.Clear();
States.Clear();
State = null;
}

View File

@@ -90,15 +90,13 @@ internal sealed partial class PvsSystem
foreach (var session in _sessions)
{
session.Chunks.Clear();
session.ChunkSet.Clear();
GetSessionViewers(session);
foreach (var eye in session.Viewers)
{
GetVisibleChunks(eye, session.Chunks);
GetVisibleChunks(eye, session.ChunkSet);
}
// The list of visible chunks should be unique.
DebugTools.Assert(session.Chunks.Select(x => x.Chunk).ToHashSet().Count == session.Chunks.Count);
}
DebugTools.Assert(_dirtyChunks.ToHashSet().Count == _dirtyChunks.Count);
DebugTools.Assert(_cleanChunks.ToHashSet().Count == _cleanChunks.Count);
@@ -108,7 +106,7 @@ internal sealed partial class PvsSystem
/// Get the chunks visible to a single entity and add them to a player's set of visible chunks.
/// </summary>
private void GetVisibleChunks(Entity<TransformComponent, EyeComponent?> eye,
List<(PvsChunk Chunk, float ChebyshevDistance)> playerChunks)
HashSet<PvsChunk> chunks)
{
var (viewPos, range, mapUid) = CalcViewBounds(eye);
if (mapUid is not {} map)
@@ -121,7 +119,7 @@ internal sealed partial class PvsSystem
if (!_chunks.TryGetValue(loc, out var chunk))
continue;
playerChunks.Add((chunk, default));
chunks.Add(chunk);
if (chunk.UpdateQueued)
continue;
@@ -147,7 +145,7 @@ internal sealed partial class PvsSystem
if (!_chunks.TryGetValue(loc, out var chunk))
continue;
playerChunks.Add((chunk, default));
chunks.Add(chunk);
if (chunk.UpdateQueued)
continue;

View File

@@ -137,15 +137,19 @@ internal sealed partial class PvsSystem
if (!CullingEnabled || session.DisableCulling)
return;
var chunkSet = session.ChunkSet;
var chunks = session.Chunks;
var distances = session.ChunkDistanceSq;
DebugTools.AssertEqual(chunks.Count, 0);
distances.Clear();
distances.EnsureCapacity(chunks.Count);
distances.EnsureCapacity(chunkSet.Count);
chunks.EnsureCapacity(chunkSet.Count);
// Assemble list of chunks and their distances to the nearest eye.
foreach (ref var tuple in CollectionsMarshal.AsSpan(chunks))
foreach(var chunk in chunkSet)
{
var chunk = tuple.Chunk;
var dist = float.MaxValue;
var chebDist = float.MaxValue;
@@ -165,7 +169,7 @@ internal sealed partial class PvsSystem
}
distances.Add(dist);
tuple.ChebyshevDistance = chebDist;
chunks.Add((chunk, chebDist));
}
// Sort chunks based on distances

View File

@@ -68,7 +68,7 @@ namespace Robust.Server.ServerStatus
if (auth != _watchdogToken)
{
// Holy shit nobody read these logs please.
_sawmill.Info(@"Failed auth: ""{0}"" vs ""{1}""", auth, _watchdogToken);
_sawmill.Verbose(@"Failed auth: ""{0}"" vs ""{1}""", auth, _watchdogToken);
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
return true;
}

View File

@@ -368,6 +368,21 @@ namespace Robust.Shared
public static readonly CVarDef<float> NetHappyEyeballsDelay =
CVarDef.Create("net.happy_eyeballs_delay", 0.025f, CVar.CLIENTONLY);
/// <summary>
/// Controls whether the networking library will log warning messages.
/// </summary>
/// <remarks>
/// Disabling this should make the networking layer more resilient against some DDoS attacks.
/// </remarks>
public static readonly CVarDef<bool> NetLidgrenLogWarning =
CVarDef.Create("net.lidgren_log_warning", true);
/// <summary>
/// Controls whether the networking library will log error messages.
/// </summary>
public static readonly CVarDef<bool> NetLidgrenLogError =
CVarDef.Create("net.lidgren_log_error", true);
/**
* SUS
*/

View File

@@ -294,6 +294,9 @@ public struct ValueList<T> : IEnumerable<T>
if (Capacity < capacity)
Grow(capacity);
if (capacity == 0)
return capacity;
return _items!.Length;
}

View File

@@ -289,6 +289,12 @@ namespace Robust.Shared.GameObjects
return GetRegistration(componentType).Name;
}
[Pure]
public string GetComponentName<T>() where T : IComponent, new()
{
return GetRegistration<T>().Name;
}
[Pure]
public string GetComponentName(ushort netID)
{
@@ -324,7 +330,7 @@ namespace Robust.Shared.GameObjects
public ComponentRegistration GetRegistration<T>() where T : IComponent, new()
{
return GetRegistration(typeof(T));
return GetRegistration(CompIdx.Index<T>());
}
public ComponentRegistration GetRegistration(IComponent component)

View File

@@ -2,45 +2,35 @@ using System;
using System.Collections.Generic;
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects.Components.Localization
namespace Robust.Shared.GameObjects.Components.Localization;
/// <summary>
/// Overrides grammar attributes specified in prototypes or localization files.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
// [Access(typeof(GrammarSystem))] TODO access
public sealed partial class GrammarComponent : Component
{
/// <summary>
/// Overrides grammar attributes specified in prototypes or localization files.
/// </summary>
[RegisterComponent]
[NetworkedComponent()]
public sealed partial class GrammarComponent : Component
[DataField, AutoNetworkedField]
public Dictionary<string, string> Attributes = new();
[ViewVariables]
public Gender? Gender
{
[DataField("attributes")]
public Dictionary<string, string> Attributes { get; private set; } = new();
get => Attributes.TryGetValue("gender", out var g) ? Enum.Parse<Gender>(g, true) : null;
[Obsolete("Use GrammarSystem.SetGender instead")]
set => IoCManager.Resolve<IEntityManager>().System<GrammarSystem>().SetGender((Owner, this), value);
}
[ViewVariables]
public Gender? Gender
{
get => Attributes.TryGetValue("gender", out var g) ? Enum.Parse<Gender>(g, true) : null;
set
{
if (value.HasValue)
Attributes["gender"] = value.Value.ToString();
else
Attributes.Remove("gender");
}
}
[ViewVariables]
public bool? ProperNoun
{
get => Attributes.TryGetValue("proper", out var g) ? bool.Parse(g) : null;
set
{
if (value.HasValue)
Attributes["proper"] = value.Value.ToString();
else
Attributes.Remove("proper");
}
}
[ViewVariables]
public bool? ProperNoun
{
get => Attributes.TryGetValue("proper", out var g) ? bool.Parse(g) : null;
[Obsolete("Use GrammarSystem.SetProperNoun instead")]
set => IoCManager.Resolve<IEntityManager>().System<GrammarSystem>().SetProperNoun((Owner, this), value);
}
}

View File

@@ -0,0 +1,39 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects.Components.Localization;
namespace Robust.Shared.GameObjects;
public sealed class GrammarSystem : EntitySystem
{
public void Clear(Entity<GrammarComponent> grammar)
{
grammar.Comp.Attributes.Clear();
Dirty(grammar);
}
public bool TryGet(Entity<GrammarComponent> grammar, string key, [NotNullWhen(true)] out string? value)
{
return grammar.Comp.Attributes.TryGetValue(key, out value);
}
public void Set(Entity<GrammarComponent> grammar, string key, string? value)
{
if (value == null)
grammar.Comp.Attributes.Remove(key);
else
grammar.Comp.Attributes[key] = value;
Dirty(grammar);
}
public void SetGender(Entity<GrammarComponent> grammar, Gender? gender)
{
Set(grammar, "gender", gender?.ToString());
}
public void SetProperNoun(Entity<GrammarComponent> grammar, bool? proper)
{
Set(grammar, "proper", proper?.ToString());
}
}

View File

@@ -19,7 +19,7 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// The last received state object sent from the server.
/// </summary>
protected BoundUserInterfaceState? State { get; private set; }
protected internal BoundUserInterfaceState? State { get; internal set; }
protected BoundUserInterface(EntityUid owner, Enum uiKey)
{

View File

@@ -1586,6 +1586,13 @@ namespace Robust.Shared.GameObjects
return default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public TComp1 Comp(EntityUid uid)
{
return GetComponent(uid);
}
#region Internal
/// <summary>

View File

@@ -281,6 +281,7 @@ namespace Robust.Shared.GameObjects
#region Entity Management
/// <inheritdoc />
public EntityUid CreateEntityUninitialized(string? prototypeName, EntityUid euid, ComponentRegistry? overrides = null)
{
return CreateEntity(prototypeName, out _, overrides);

View File

@@ -160,6 +160,9 @@ namespace Robust.Shared.GameObjects
[Pure]
string GetComponentName(Type componentType);
[Pure]
string GetComponentName<T>() where T : IComponent, new();
/// <summary>
/// Gets the name of a component, throwing an exception if it does not exist.
/// </summary>

View File

@@ -71,12 +71,43 @@ namespace Robust.Shared.GameObjects
/// </summary>
public event Action? AfterEntityFlush;
/// <summary>
/// Creates an uninitialized entity.
/// </summary>
/// <param name="prototypeName"><inheritdoc cref="CreateEntityUninitialized(string?, MapCoordinates , ComponentRegistry?, Angle)"/></param>
/// <param name="euid">Does nothing. Used to be the forced EntityUid of the new entity.</param>
/// <param name="overrides"><inheritdoc cref="CreateEntityUninitialized(string?, MapCoordinates , ComponentRegistry?, Angle)"/></param>
/// <returns><inheritdoc cref="CreateEntityUninitialized(string?, MapCoordinates , ComponentRegistry?, Angle)"/></returns>
[Obsolete($"Use one of the other {nameof(CreateEntityUninitialized)} overloads. euid no longer does anything.")]
EntityUid CreateEntityUninitialized(string? prototypeName, EntityUid euid, ComponentRegistry? overrides = null);
/// <summary>
/// Creates an uninitialized entity.
/// </summary>
/// <param name="prototypeName"><inheritdoc cref="CreateEntityUninitialized(string?, MapCoordinates , ComponentRegistry?, Angle)"/></param>
/// <param name="overrides"><inheritdoc cref="CreateEntityUninitialized(string?, MapCoordinates , ComponentRegistry?, Angle)"/></param>
/// <returns><inheritdoc cref="CreateEntityUninitialized(string?, MapCoordinates , ComponentRegistry?, Angle)"/></returns>
EntityUid CreateEntityUninitialized(string? prototypeName, ComponentRegistry? overrides = null);
/// <summary>
/// Creates an uninitialized entity and sets its position to the EntityCoordinates provided.
/// </summary>
/// <param name="prototypeName"><inheritdoc cref="CreateEntityUninitialized(string?, MapCoordinates , ComponentRegistry?, Angle)"/></param>
/// <param name="coordinates">Coordinates to set position and parent of the newly spawned entity to.</param>
/// <param name="overrides"><inheritdoc cref="CreateEntityUninitialized(string?, MapCoordinates , ComponentRegistry?, Angle)"/></param>
/// <returns><inheritdoc cref="CreateEntityUninitialized(string?, MapCoordinates , ComponentRegistry?, Angle)"/></returns>
EntityUid CreateEntityUninitialized(string? prototypeName, EntityCoordinates coordinates, ComponentRegistry? overrides = null);
/// <summary>
/// Creates an uninitialized entity and puts it on the grid or map at the MapCoordinates provided.
/// </summary>
/// <param name="prototypeName">Name of the <see cref="EntityPrototype"/> to spawn.</param>
/// <param name="coordinates">Coordinates to place the newly spawned entity.</param>
/// <param name="overrides">Overrides to add or remove components that differ from the prototype.</param>
/// <param name="rotation">Map rotation to set the newly spawned entity to.</param>
/// <returns>A new uninitialized entity.</returns>
/// <remarks>If there is a grid at the <paramref name="coordinates"/>, the entity will be parented to the grid.
/// Otherwise, it will be parented to the map.</remarks>
EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!);
void InitializeAndStartEntity(EntityUid entity, MapId? mapId = null);

View File

@@ -10,6 +10,7 @@ using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -20,6 +21,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
[Dependency] private readonly IDynamicTypeFactory _factory = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IParallelManager _parallel = default!;
[Dependency] private readonly IReflectionManager _reflection = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly SharedTransformSystem _transforms = default!;
@@ -29,6 +31,8 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
private EntityQuery<UserInterfaceComponent> _uiQuery;
private EntityQuery<UserInterfaceUserComponent> _userQuery;
private ActorRangeCheckJob _rangeJob;
public override void Initialize()
{
base.Initialize();
@@ -38,6 +42,12 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
_uiQuery = GetEntityQuery<UserInterfaceComponent>();
_userQuery = GetEntityQuery<UserInterfaceUserComponent>();
_rangeJob = new()
{
System = this,
XformQuery = _xformQuery,
};
SubscribeAllEvent<BoundUIWrapMessage>((msg, args) =>
{
if (args.SenderSession.AttachedEntity is not { } player)
@@ -48,6 +58,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
SubscribeLocalEvent<UserInterfaceComponent, OpenBoundInterfaceMessage>(OnUserInterfaceOpen);
SubscribeLocalEvent<UserInterfaceComponent, CloseBoundInterfaceMessage>(OnUserInterfaceClosed);
SubscribeLocalEvent<UserInterfaceComponent, ComponentStartup>(OnUserInterfaceStartup);
SubscribeLocalEvent<UserInterfaceComponent, ComponentShutdown>(OnUserInterfaceShutdown);
SubscribeLocalEvent<UserInterfaceComponent, ComponentGetState>(OnUserInterfaceGetState);
SubscribeLocalEvent<UserInterfaceComponent, ComponentHandleState>(OnUserInterfaceHandleState);
@@ -55,28 +66,10 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentShutdown>(OnActorShutdown);
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentGetStateAttemptEvent>(OnGetStateAttempt);
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentGetState>(OnActorGetState);
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentHandleState>(OnActorHandleState);
_player.PlayerStatusChanged += OnStatusChange;
}
private void OnStatusChange(object? sender, SessionStatusEventArgs e)
{
var attachedEnt = e.Session.AttachedEntity;
if (attachedEnt == null)
return;
// Content can't handle it yet sadly :(
CloseUserUis(attachedEnt.Value);
}
public override void Shutdown()
{
base.Shutdown();
_player.PlayerStatusChanged -= OnStatusChange;
}
/// <summary>
@@ -135,6 +128,11 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
#region User
private void OnActorShutdown(Entity<UserInterfaceUserComponent> ent, ref ComponentShutdown args)
{
CloseUserUis((ent.Owner, ent.Comp));
}
private void OnGetStateAttempt(Entity<UserInterfaceUserComponent> ent, ref ComponentGetStateAttemptEvent args)
{
if (args.Cancelled || args.Player?.AttachedEntity != ent.Owner)
@@ -207,11 +205,10 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
foreach (var key in keys)
{
if (!uiComp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
if (!uiComp.ClientOpenInterfaces.Remove(key, out var cBui))
continue;
cBui.Dispose();
uiComp.ClientOpenInterfaces.Remove(key);
}
}
}
@@ -233,10 +230,8 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
Dirty(ent);
// If the actor is also deleting then don't worry about updating what they have open.
if (!TerminatingOrDeleted(actor))
if (!TerminatingOrDeleted(actor) && _userQuery.TryComp(actor, out var actorComp))
{
var actorComp = EnsureComp<UserInterfaceUserComponent>(actor);
if (actorComp.OpenInterfaces.TryGetValue(ent.Owner, out var keys))
{
keys.Remove(args.UiKey);
@@ -282,6 +277,20 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
EnsureClientBui(ent, args.UiKey, ent.Comp.Interfaces[args.UiKey]);
}
private void OnUserInterfaceStartup(Entity<UserInterfaceComponent> ent, ref ComponentStartup args)
{
// PlayerAttachedEvent will catch some of these.
foreach (var (key, bui) in ent.Comp.ClientOpenInterfaces)
{
bui.Open();
if (ent.Comp.States.TryGetValue(key, out var state))
{
bui.UpdateState(state);
}
}
}
private void OnUserInterfaceShutdown(EntityUid uid, UserInterfaceComponent component, ComponentShutdown args)
{
foreach (var bui in component.ClientOpenInterfaces.Values)
@@ -387,17 +396,21 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (!ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
continue;
cBui.State = buiState;
cBui.UpdateState(buiState);
}
// If UI not open then open it
var attachedEnt = _player.LocalEntity;
// If we get the first state for an ent coming in then don't open BUIs yet, just defer it until later.
var open = ent.Comp.LifeStage > ComponentLifeStage.Added;
if (attachedEnt != null)
{
foreach (var (key, value) in ent.Comp.Interfaces)
{
EnsureClientBui(ent, key, value);
EnsureClientBui(ent, key, value, open);
}
}
}
@@ -405,7 +418,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
/// <summary>
/// Opens a client's BUI if not already open and applies the state to it.
/// </summary>
private void EnsureClientBui(Entity<UserInterfaceComponent> entity, Enum key, InterfaceData data)
private void EnsureClientBui(Entity<UserInterfaceComponent> entity, Enum key, InterfaceData data, bool open = true)
{
// If it's out BUI open it up and apply the state, otherwise do nothing.
var player = _player.LocalEntity;
@@ -428,10 +441,16 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
var boundUserInterface = (BoundUserInterface) _factory.CreateInstance(type, [entity.Owner, key]);
entity.Comp.ClientOpenInterfaces[key] = boundUserInterface;
// This is just so we don't open while applying UI states.
if (!open)
return;
boundUserInterface.Open();
if (entity.Comp.States.TryGetValue(key, out var buiState))
{
boundUserInterface.State = buiState;
boundUserInterface.UpdateState(buiState);
}
}
@@ -877,6 +896,8 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
public override void Update(float frameTime)
{
var query = AllEntityQuery<ActiveUserInterfaceComponent, UserInterfaceComponent>();
// Run these in parallel because it's expensive.
_rangeJob.ActorRanges.Clear();
// Handles closing the BUI if actors move out of range of them.
while (query.MoveNext(out var uid, out _, out var uiComp))
@@ -890,24 +911,26 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (data.InteractionRange <= 0f || actors.Count == 0)
continue;
// Okay so somehow UISystem is high up on the server profile
// If that's actually still a problem turn this into an IParallelRobustJob and slam all the UIs in parallel.
var xform = _xformQuery.GetComponent(uid);
var coordinates = xform.Coordinates;
var mapId = xform.MapID;
for (var i = actors.Count - 1; i >= 0; i--)
foreach (var actor in actors)
{
var actor = actors[i];
if (CheckRange(uid, key, data, actor, coordinates, mapId))
continue;
// Using the non-predicted one here seems fine?
CloseUi((uid, uiComp), key, actor);
_rangeJob.ActorRanges.Add((uid, key, data, actor, false));
}
}
}
_parallel.ProcessNow(_rangeJob, _rangeJob.ActorRanges.Count);
foreach (var data in _rangeJob.ActorRanges)
{
var uid = data.Ui;
var actor = data.Actor;
var key = data.Key;
if (data.Result || Deleted(uid) || Deleted(actor) || !_uiQuery.TryComp(uid, out var uiComp))
continue;
CloseUi((uid, uiComp), key, actor);
}
}
/// <summary>
@@ -944,6 +967,32 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
return uiCoordinates.InRange(EntityManager, _transforms, actorXform.Coordinates, data.InteractionRange);
}
/// <summary>
/// Used for running UI raycast checks in parallel.
/// </summary>
private record struct ActorRangeCheckJob() : IParallelRobustJob
{
public EntityQuery<TransformComponent> XformQuery;
public SharedUserInterfaceSystem System;
public readonly List<(EntityUid Ui, Enum Key, InterfaceData Data, EntityUid Actor, bool Result)> ActorRanges = new();
public void Execute(int index)
{
var data = ActorRanges[index];
if (!XformQuery.TryComp(data.Ui, out var uiXform))
{
data.Result = false;
}
else
{
data.Result = System.CheckRange(data.Ui, data.Key, data.Data, data.Actor, uiXform.Coordinates, uiXform.MapID);
}
ActorRanges[index] = data;
}
}
}
/// <summary>

View File

@@ -33,6 +33,11 @@ namespace Robust.Shared.Input
public bool Handled { get; private set; }
/// <summary>
/// Is this a repeated keypress (i.e., are they holding down the key)?
/// </summary>
public readonly bool IsRepeat;
/// <summary>
/// Constructs a new instance of <see cref="BoundKeyEventArgs"/>.
/// </summary>
@@ -47,6 +52,23 @@ namespace Robust.Shared.Input
CanFocus = canFocus;
}
/// <summary>
/// Constructs a new instance of <see cref="BoundKeyEventArgs"/>.
/// </summary>
/// <param name="function">Bound key that that is changing.</param>
/// <param name="state">New state of the function.</param>
/// <param name="pointerLocation">Current Pointer location in screen coordinates.</param>
/// <param name="isRepeat"></param>
public BoundKeyEventArgs(
BoundKeyFunction function,
BoundKeyState state,
ScreenCoordinates pointerLocation,
bool canFocus,
bool isRepeat = false) : this(function, state, pointerLocation, canFocus)
{
IsRepeat = isRepeat;
}
/// <summary>
/// Mark this event as handled.
/// </summary>

View File

@@ -17,6 +17,8 @@ namespace Robust.Shared.Localization
{
internal sealed partial class LocalizationManager
{
private static readonly Regex RegexWordMatch = new Regex(@"\w+");
private void AddBuiltInFunctions(FluentBundle bundle)
{
// Grammatical gender / pronouns
@@ -108,7 +110,7 @@ namespace Robust.Shared.Localization
var a = new LocValueString("a");
var an = new LocValueString("an");
var m = Regex.Match(input, @"\w+");
var m = RegexWordMatch.Match(input);
if (m.Success)
{
word = m.Groups[0].Value;

View File

@@ -249,6 +249,9 @@ namespace Robust.Shared.Network
IsServer = isServer;
_config.OnValueChanged(CVars.NetLidgrenLogWarning, LidgrenLogWarningChanged);
_config.OnValueChanged(CVars.NetLidgrenLogError, LidgrenLogErrorChanged);
_config.OnValueChanged(CVars.NetVerbose, NetVerboseChanged);
if (isServer)
{
@@ -272,6 +275,22 @@ namespace Robust.Shared.Network
}
}
private void LidgrenLogWarningChanged(bool newValue)
{
foreach (var netPeer in _netPeers)
{
netPeer.Peer.Configuration.SetMessageTypeEnabled(NetIncomingMessageType.WarningMessage, newValue);
}
}
private void LidgrenLogErrorChanged(bool newValue)
{
foreach (var netPeer in _netPeers)
{
netPeer.Peer.Configuration.SetMessageTypeEnabled(NetIncomingMessageType.ErrorMessage, newValue);
}
}
private void OnAuthModeChanged(int mode)
{
Auth = (AuthMode)mode;
@@ -422,6 +441,8 @@ namespace Robust.Shared.Network
_config.UnsubValueChanged(CVars.NetFakeLagMin, _fakeLagMinChanged);
_config.UnsubValueChanged(CVars.NetFakeLagRand, _fakeLagRandomChanged);
_config.UnsubValueChanged(CVars.NetFakeDuplicates, FakeDuplicatesChanged);
_config.UnsubValueChanged(CVars.NetLidgrenLogWarning, LidgrenLogWarningChanged);
_config.UnsubValueChanged(CVars.NetLidgrenLogError, LidgrenLogErrorChanged);
_serializer.ClientHandshakeComplete -= OnSerializerOnClientHandshakeComplete;
@@ -576,6 +597,14 @@ namespace Robust.Shared.Network
// ping the client once per second.
netConfig.PingInterval = 1f;
netConfig.SetMessageTypeEnabled(
NetIncomingMessageType.WarningMessage,
_config.GetCVar(CVars.NetLidgrenLogWarning));
netConfig.SetMessageTypeEnabled(
NetIncomingMessageType.ErrorMessage,
_config.GetCVar(CVars.NetLidgrenLogError));
var poolSize = _config.GetCVar(CVars.NetPoolSize);
if (poolSize <= 0)

View File

@@ -48,6 +48,9 @@ public abstract partial class SharedJointSystem
private void OnRelayShutdown(EntityUid uid, JointRelayTargetComponent component, ComponentShutdown args)
{
if (_gameTiming.ApplyingState)
return;
foreach (var relay in component.Relayed)
{
if (TerminatingOrDeleted(relay) || !_jointsQuery.TryGetComponent(relay, out var joint))

View File

@@ -1,4 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
@@ -42,3 +45,59 @@ public readonly record struct EntProtoId(string Id) : IEquatable<string>, ICompa
public override string ToString() => Id ?? string.Empty;
}
/// <inheritdoc cref="EntProtoId"/>
[Serializable]
public readonly record struct EntProtoId<T>(string Id) : IEquatable<string>, IComparable<EntProtoId> where T : IComponent, new()
{
public static implicit operator string(EntProtoId<T> protoId)
{
return protoId.Id;
}
public static implicit operator EntProtoId(EntProtoId<T> protoId)
{
return new EntProtoId(protoId.Id);
}
public static implicit operator EntProtoId<T>(string id)
{
return new EntProtoId<T>(id);
}
public static implicit operator EntProtoId<T>?(string? id)
{
return id == null ? default(EntProtoId<T>?) : new EntProtoId<T>(id);
}
public bool Equals(string? other)
{
return Id == other;
}
public int CompareTo(EntProtoId other)
{
return string.Compare(Id, other.Id, StringComparison.Ordinal);
}
public override string ToString() => Id ?? string.Empty;
public T Get(IPrototypeManager? prototypes, IComponentFactory compFactory)
{
prototypes ??= IoCManager.Resolve<IPrototypeManager>();
var proto = prototypes.Index(this);
if (!proto.TryGetComponent(out T? comp, compFactory))
{
throw new ArgumentException($"{nameof(EntityPrototype)} {proto.ID} has no {nameof(T)}");
}
return comp;
}
public bool TryGet([NotNullWhen(true)] out T? comp, IPrototypeManager? prototypes, IComponentFactory compFactory)
{
prototypes ??= IoCManager.Resolve<IPrototypeManager>();
var proto = prototypes.Index(this);
return proto.TryGetComponent(out comp, compFactory);
}
}

View File

@@ -11,6 +11,7 @@ using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Prototypes
@@ -168,28 +169,37 @@ namespace Robust.Shared.Prototypes
_loc = IoCManager.Resolve<ILocalizationManager>();
}
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component, IComponentFactory? factory = null) where T : IComponent
[Obsolete("Pass in IComponentFactory")]
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component)
where T : IComponent
{
if (factory == null)
{
factory = IoCManager.Resolve<IComponentFactory>();
}
var compName = IoCManager.Resolve<IComponentFactory>().GetComponentName(typeof(T));
return TryGetComponent(compName, out component);
}
var compName = factory.GetComponentName(typeof(T));
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component, IComponentFactory factory) where T : IComponent, new()
{
var compName = factory.GetComponentName<T>();
return TryGetComponent(compName, out component);
}
public bool TryGetComponent<T>(string name, [NotNullWhen(true)] out T? component) where T : IComponent
{
DebugTools.AssertEqual(IoCManager.Resolve<IComponentFactory>().GetComponentName(typeof(T)), name);
if (!Components.TryGetValue(name, out var componentUnCast))
{
component = default;
return false;
}
// There are no duplicate component names
// TODO Sanity check with names being in an attribute of the type instead
component = (T) componentUnCast.Component;
if (componentUnCast.Component is not T cast)
{
component = default;
return false;
}
component = cast;
return true;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Markdown;
@@ -440,5 +441,7 @@ namespace Robust.Shared.Serialization.Manager
}
#endregion
public bool TryGetVariableType(Type type, string variableName, [NotNullWhen(true)] out Type? variableType);
}
}

View File

@@ -264,6 +264,26 @@ namespace Robust.Shared.Serialization.Manager
return dataDefinition != null;
}
public bool TryGetVariableType(Type type, string variableName, [NotNullWhen(true)] out Type? variableType)
{
if (!TryGetDefinition(type, out var definition))
{
variableType = null;
return false;
}
var foundFieldDef = definition.BaseFieldDefinitions.FirstOrDefault(fieldDef => fieldDef?.Attribute is DataFieldAttribute attr && attr.Tag==variableName, null);
if(foundFieldDef != null)
{
variableType = foundFieldDef.BackingField.FieldType;
return true;
}
else
{
variableType = null;
return false;
}
}
private Type ResolveConcreteType(Type baseType, string typeName)
{
var type = ReflectionManager.YamlTypeTagLookup(baseType, typeName);

View File

@@ -1,8 +1,11 @@
using Robust.Shared.IoC;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
@@ -40,3 +43,49 @@ public sealed class EntProtoIdSerializer : ITypeSerializer<EntProtoId, ValueData
return source;
}
}
/// <summary>
/// Serializer used automatically for <see cref="EntProtoId"/> types.
/// </summary>
[TypeSerializer]
public sealed class EntProtoIdSerializer<T> : ITypeSerializer<EntProtoId<T>, ValueDataNode>, ITypeCopyCreator<EntProtoId<T>> where T : IComponent, new()
{
public ValidationNode Validate(ISerializationManager serialization, ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context = null)
{
var prototypes = dependencies.Resolve<IPrototypeManager>();
if (!prototypes.TryGetMapping(typeof(EntityPrototype), node.Value, out var mapping))
return new ErrorNode(node, $"No {nameof(EntityPrototype)} found with id {node.Value} that has a {typeof(T).Name}");
if (!mapping.TryGet("components", out SequenceDataNode? components))
return new ErrorNode(node, $"{nameof(EntityPrototype)} {node.Value} doesn't have a {typeof(T).Name}.");
var compFactory = dependencies.Resolve<IComponentFactory>();
var registration = compFactory.GetRegistration<T>();
foreach (var componentNode in components)
{
if (componentNode is MappingDataNode component &&
component.TryGet("type", out ValueDataNode? compName) &&
compName.Value == registration.Name)
{
return new ValidatedValueNode(node);
}
}
return new ErrorNode(node, $"{nameof(EntityPrototype)} {node.Value} doesn't have a {typeof(T).Name}.");
}
public EntProtoId<T> Read(ISerializationManager serialization, ValueDataNode node, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null, InstantiationDelegate<EntProtoId<T>>? instanceProvider = null)
{
return new EntProtoId<T>(node.Value);
}
public DataNode Write(ISerializationManager serialization, EntProtoId<T> value, IDependencyCollection dependencies, bool alwaysWrite = false, ISerializationContext? context = null)
{
return new ValueDataNode(value.Id);
}
public EntProtoId<T> CreateCopy(ISerializationManager serializationManager, EntProtoId<T> source, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null)
{
return source;
}
}