mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2a1521c95 | ||
|
|
a26b48414b | ||
|
|
614a03036b | ||
|
|
8c8a3c0e17 | ||
|
|
b1f9d011ce | ||
|
|
a2d0504368 | ||
|
|
7aa951ca48 | ||
|
|
75a80b7a8a | ||
|
|
69706b0257 | ||
|
|
10b191dff8 | ||
|
|
92ab3fb64b | ||
|
|
92a0c14383 | ||
|
|
5aaf6d0994 | ||
|
|
15f4da5e4b | ||
|
|
a528e87f3d | ||
|
|
4af67b1394 | ||
|
|
e8de9b98d3 | ||
|
|
a0ffeff4e5 | ||
|
|
07654564f3 | ||
|
|
7fbf8d05eb | ||
|
|
c12971cb9b | ||
|
|
2b6381c332 | ||
|
|
8149a3aaad | ||
|
|
4b39bf1f2d | ||
|
|
53394fff44 | ||
|
|
4bed20e070 |
6
.github/workflows/publish-client.yml
vendored
6
.github/workflows/publish-client.yml
vendored
@@ -33,10 +33,10 @@ jobs:
|
||||
mkdir "release/${{ steps.parse_version.outputs.version }}"
|
||||
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
|
||||
|
||||
- name: Upload files to centcomm
|
||||
- name: Upload files to Suns
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: centcomm.spacestation14.io
|
||||
host: suns.spacestation14.com
|
||||
username: robust-build-push
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
|
||||
source: "release/${{ steps.parse_version.outputs.version }}"
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Update manifest JSON
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: centcomm.spacestation14.io
|
||||
host: suns.spacestation14.com
|
||||
username: robust-build-push
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
|
||||
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
We actually set ManagePackageVersionsCentrally manually in another import file.
|
||||
Since .NET SDK 8.0.300, ManagePackageVersionsCentrally is automatically set if Directory.Packages.props exists.
|
||||
https://github.com/NuGet/NuGet.Client/pull/5572
|
||||
We actively negate this here, as we have some packages in tree we don't want such automatic behavior for.
|
||||
We use Directory.Build.props to get copy the state *after* our MSBuild config but before Nuget's config.
|
||||
-->
|
||||
<ManagePackageVersionsCentrally />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
@@ -45,7 +55,7 @@
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
|
||||
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.3" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -54,6 +54,80 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 214.2.2
|
||||
|
||||
|
||||
## 214.2.1
|
||||
|
||||
|
||||
## 214.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added a `Undetachable` entity metadata flag, which stops the client from moving an entity to nullspace when it moves out of PVS range.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix tooltips not clamping to the left side of the viewport.
|
||||
* Fix global audio property not being properly set.
|
||||
|
||||
### Internal
|
||||
|
||||
* The server game state / PVS code has been rewritten. It should be somewhat faster now, albeit at the cost of using more memory. The current engine version may be unstable.
|
||||
|
||||
|
||||
## 214.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed connection denial always causing redial.
|
||||
|
||||
|
||||
## 214.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added the `pvs_override_info` command for debugging PVS overrides.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix VV for prototype structs.
|
||||
* Fix audio limits for clientside audio.
|
||||
|
||||
|
||||
## 214.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `NetStructuredDisconnectMessages` has received a complete overhaul and has been moved to `NetDisconnectMessage`. The API is no longer designed such that consumers must pass around JSON nodes, as they are not in sandbox (and clunky).
|
||||
|
||||
### New features
|
||||
|
||||
* Add a basic default concurrent audio limit of 16 for a single filepath to avoid overflowing audio sources.
|
||||
* `NetConnectingArgs.Deny()` can now pass along structured data that will be received by the client.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed cursor position bugs when an empty `TextEdit` has a multi-line place holder.
|
||||
* Fixed empty `TextEdit` throwing exception if cursor is moved left.
|
||||
|
||||
|
||||
## 213.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Remove obsoleted BaseContainer methods.
|
||||
|
||||
### New features
|
||||
|
||||
* Add EntityManager.RaiseSharedEvent where the event won't go to the attached client but will be predicted locally on their end.
|
||||
* Add GetEntitiesInRange override that takes in EntityCoordinates and an EntityUid hashset.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Check if a sprite entity is deleted before drawing in SpriteView.
|
||||
|
||||
|
||||
## 212.2.0
|
||||
|
||||
### New features
|
||||
|
||||
@@ -566,3 +566,9 @@ cmd-reloadtiletextures-help = Usage: reloadtiletextures
|
||||
cmd-audio_length-desc = Shows the length of an audio file
|
||||
cmd-audio_length-help = Usage: audio_length { cmd-audio_length-arg-file-name }
|
||||
cmd-audio_length-arg-file-name = <file name>
|
||||
|
||||
## PVS
|
||||
cmd-pvs-override-info-desc = Prints information about any PVS overrides associated with an entity.
|
||||
cmd-pvs-override-info-empty = Entity {$nuid} has no PVS overrides.
|
||||
cmd-pvs-override-info-global = Entity {$nuid} has a global override.
|
||||
cmd-pvs-override-info-clients = Entity {$nuid} has a session override for {$clients}.
|
||||
|
||||
57
Robust.Client/Audio/AudioSystem.Limits.cs
Normal file
57
Robust.Client/Audio/AudioSystem.Limits.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
|
||||
public sealed partial class AudioSystem
|
||||
{
|
||||
/*
|
||||
* Handles limiting concurrent sounds for audio to avoid blowing the source budget on one sound getting spammed.
|
||||
*/
|
||||
|
||||
private readonly Dictionary<string, int> _playingCount = new();
|
||||
|
||||
private int _maxConcurrent;
|
||||
|
||||
private void InitializeLimit()
|
||||
{
|
||||
Subs.CVar(CfgManager, CVars.AudioDefaultConcurrent, SetConcurrentLimit, true);
|
||||
}
|
||||
|
||||
private void SetConcurrentLimit(int obj)
|
||||
{
|
||||
_maxConcurrent = obj;
|
||||
}
|
||||
|
||||
private bool TryAudioLimit(string sound)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sound))
|
||||
return true;
|
||||
|
||||
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_playingCount, sound, out _);
|
||||
|
||||
if (count >= _maxConcurrent)
|
||||
return false;
|
||||
|
||||
count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RemoveAudioLimit(string sound)
|
||||
{
|
||||
if (!_playingCount.TryGetValue(sound, out var count))
|
||||
return;
|
||||
|
||||
count--;
|
||||
|
||||
if (count <= 0)
|
||||
{
|
||||
_playingCount.Remove(sound);
|
||||
return;
|
||||
}
|
||||
|
||||
_playingCount[sound] = count;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
[Dependency] private readonly IParallelManager _parMan = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
[Dependency] private readonly IAudioInternal _audio = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
|
||||
@@ -51,6 +52,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
private EntityUid? _listenerGrid;
|
||||
private UpdateAudioJob _updateAudioJob;
|
||||
|
||||
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
|
||||
private float _maxRayLength;
|
||||
@@ -108,6 +110,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
|
||||
Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
|
||||
InitializeLimit();
|
||||
}
|
||||
|
||||
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
|
||||
@@ -163,26 +166,37 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return;
|
||||
}
|
||||
|
||||
SetupSource(component, audioResource);
|
||||
SetupSource((uid, component), audioResource);
|
||||
component.Loaded = true;
|
||||
}
|
||||
|
||||
private void SetupSource(AudioComponent component, AudioResource audioResource, TimeSpan? length = null)
|
||||
private void SetupSource(Entity<AudioComponent> entity, AudioResource audioResource, TimeSpan? length = null)
|
||||
{
|
||||
var source = _audio.CreateAudioSource(audioResource);
|
||||
|
||||
if (source == null)
|
||||
var component = entity.Comp;
|
||||
|
||||
if (TryAudioLimit(component.FileName))
|
||||
{
|
||||
Log.Error($"Error creating audio source for {audioResource}");
|
||||
DebugTools.Assert(false);
|
||||
source = component.Source;
|
||||
var newSource = _audio.CreateAudioSource(audioResource);
|
||||
|
||||
if (newSource == null)
|
||||
{
|
||||
Log.Error($"Error creating audio source for {audioResource}");
|
||||
DebugTools.Assert(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
component.Source = newSource;
|
||||
}
|
||||
}
|
||||
|
||||
component.Source = source;
|
||||
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
|
||||
{
|
||||
_metadata.SetFlag(entity.Owner, MetaDataFlags.Undetachable, true);
|
||||
}
|
||||
|
||||
// Need to set all initial data for first frame.
|
||||
ApplyAudioParams(component.Params, component);
|
||||
source.Global = component.Global;
|
||||
component.Source.Global = component.Global;
|
||||
|
||||
// Don't play until first frame so occlusion etc. are correct.
|
||||
component.Gain = 0f;
|
||||
@@ -202,6 +216,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
{
|
||||
// Breaks with prediction?
|
||||
component.Source.Dispose();
|
||||
|
||||
RemoveAudioLimit(component.FileName);
|
||||
}
|
||||
|
||||
private void OnAudioAttenuation(int obj)
|
||||
@@ -576,13 +592,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return PlayGlobal(filename, audioParams);
|
||||
}
|
||||
|
||||
public override void LoadStream<T>(AudioComponent component, T stream)
|
||||
public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
|
||||
{
|
||||
if (stream is AudioStream audioStream)
|
||||
{
|
||||
TryGetAudio(audioStream, out var audio);
|
||||
SetupSource(component, audio!, audioStream.Length);
|
||||
component.Loaded = true;
|
||||
SetupSource(entity, audio!, audioStream.Length);
|
||||
entity.Comp.Loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,7 +637,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
var audioP = audioParams ?? AudioParams.Default;
|
||||
var entity = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
|
||||
var comp = SetupAudio(entity, null, audioP, stream.Length);
|
||||
LoadStream(comp, stream);
|
||||
LoadStream((entity, comp), stream);
|
||||
EntityManager.InitializeAndStartEntity(entity);
|
||||
var source = comp.Source;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Robust.Client.Console.Commands
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var type = Type.GetType(args[0]);
|
||||
var type = GetType(args[0]);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
@@ -25,6 +25,17 @@ namespace Robust.Client.Console.Commands
|
||||
shell.WriteLine(sig);
|
||||
}
|
||||
}
|
||||
|
||||
private Type? GetType(string name)
|
||||
{
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (assembly.GetType(name) is { } type)
|
||||
return type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
|
||||
{
|
||||
var textEdit = new TextEdit
|
||||
{
|
||||
Placeholder = new Rope.Leaf("You deleted the lipsum OwO")
|
||||
Placeholder = new Rope.Leaf("You deleted the lipsum\nOwO")
|
||||
};
|
||||
TabContainer.SetTabTitle(textEdit, "TextEdit");
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -134,6 +135,24 @@ namespace Robust.Client.GameObjects
|
||||
EventBus.RaiseEvent(EventSource.Local, new EntitySessionMessage<T>(eventArgs, msg));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void RaiseSharedEvent<T>(T message, EntityUid? user = null)
|
||||
{
|
||||
if (user == null || user != _playerManager.LocalEntity || !_gameTiming.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
EventBus.RaiseEvent(EventSource.Local, ref message);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void RaiseSharedEvent<T>(T message, ICommonSession? user = null)
|
||||
{
|
||||
if (user == null || user != _playerManager.LocalSession || !_gameTiming.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
EventBus.RaiseEvent(EventSource.Local, ref message);
|
||||
}
|
||||
|
||||
#region IEntityNetworkManager impl
|
||||
|
||||
public override IEntityNetworkManager EntityNetManager => this;
|
||||
|
||||
@@ -1120,7 +1120,7 @@ namespace Robust.Client.GameStates
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((meta.Flags & MetaDataFlags.Detached) != 0)
|
||||
if ((meta.Flags & (MetaDataFlags.Detached | MetaDataFlags.Undetachable)) != 0)
|
||||
continue;
|
||||
|
||||
if (lastStateApplied.HasValue)
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
private float _value;
|
||||
private float _page;
|
||||
private bool _rounded;
|
||||
private int _roundingDecimals = 0;
|
||||
|
||||
public event Action<Range>? OnValueChanged;
|
||||
|
||||
@@ -86,6 +87,17 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
public int RoundingDecimals
|
||||
{
|
||||
get => _roundingDecimals;
|
||||
set
|
||||
{
|
||||
_roundingDecimals = value;
|
||||
_ensureValueClamped();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void SetValueWithoutEvent(float newValue)
|
||||
{
|
||||
newValue = ClampValue(newValue);
|
||||
@@ -107,7 +119,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
if (_rounded)
|
||||
{
|
||||
value = MathF.Round(value);
|
||||
value = MathF.Round(value, _roundingDecimals);
|
||||
}
|
||||
return MathHelper.Clamp(value, _minValue, _maxValue-_page);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
private SpriteSystem? _sprite;
|
||||
private SharedTransformSystem? _transform;
|
||||
IEntityManager _entMan;
|
||||
private readonly IEntityManager _entMan;
|
||||
|
||||
[ViewVariables]
|
||||
public SpriteComponent? Sprite => Entity?.Comp1;
|
||||
@@ -143,6 +143,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (netEnt == NetEnt)
|
||||
return;
|
||||
|
||||
// The Entity is getting set later in the ResolveEntity method
|
||||
// because the client may not have received it yet.
|
||||
Entity = null;
|
||||
NetEnt = netEnt;
|
||||
}
|
||||
@@ -256,28 +258,19 @@ namespace Robust.Client.UserInterface.Controls
|
||||
[NotNullWhen(true)] out SpriteComponent? sprite,
|
||||
[NotNullWhen(true)] out TransformComponent? xform)
|
||||
{
|
||||
if (NetEnt != null && Entity == null && _entMan.TryGetEntity(NetEnt, out var ent))
|
||||
SetEntity(ent);
|
||||
|
||||
if (Entity != null)
|
||||
{
|
||||
(uid, sprite, xform) = Entity.Value;
|
||||
return true;
|
||||
return !_entMan.Deleted(uid);
|
||||
}
|
||||
|
||||
sprite = null;
|
||||
xform = null;
|
||||
uid = default;
|
||||
|
||||
if (NetEnt == null)
|
||||
return false;
|
||||
|
||||
if (!_entMan.TryGetEntity(NetEnt, out var ent))
|
||||
return false;
|
||||
|
||||
SetEntity(ent);
|
||||
if (Entity == null)
|
||||
return false;
|
||||
|
||||
(uid, sprite, xform) = Entity.Value;
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +576,7 @@ public sealed class TextEdit : Control
|
||||
|
||||
var newPos = CursorShiftedLeft();
|
||||
// Explicit newlines work kinda funny with bias, so keep it at top there.
|
||||
var bias = Rope.Index(TextRope, newPos) == '\n'
|
||||
var bias = _cursorPosition.Index == TextLength || Rope.Index(TextRope, newPos) == '\n'
|
||||
? LineBreakBias.Top
|
||||
: LineBreakBias.Bottom;
|
||||
|
||||
@@ -940,6 +940,13 @@ public sealed class TextEdit : Control
|
||||
|
||||
private CursorPos GetIndexAtHorizontalPos(int line, float horizontalPos)
|
||||
{
|
||||
// If the placeholder is visible, this function does not return correct results because it looks at TextRope,
|
||||
// but _lineBreaks is configured for the display rope.
|
||||
// Bail out early in this case, the function is not currently used in any situation in any location
|
||||
// where something else is desired if the placeholder is visible.
|
||||
if (IsPlaceholderVisible)
|
||||
return default;
|
||||
|
||||
var contentBox = PixelSizeBox;
|
||||
var font = GetFont();
|
||||
var uiScale = UIScale;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
@@ -41,20 +42,16 @@ namespace Robust.Client.UserInterface
|
||||
tooltip.Measure(Vector2Helpers.Infinity);
|
||||
var combinedMinSize = tooltip.DesiredSize;
|
||||
|
||||
LayoutContainer.SetPosition(tooltip, new Vector2(screenPosition.X, screenPosition.Y - combinedMinSize.Y));
|
||||
// If it overflows right bounds then just place left on the edge.
|
||||
var right = MathF.Min(screenPosition.X + combinedMinSize.X, screenBounds.X);
|
||||
|
||||
var right = tooltip.Position.X + combinedMinSize.X;
|
||||
var top = tooltip.Position.Y;
|
||||
// However, better to clamp the end of the tooltip instead of the start.
|
||||
var left = MathF.Max(0f, right - combinedMinSize.X);
|
||||
|
||||
if (right > screenBounds.X)
|
||||
{
|
||||
LayoutContainer.SetPosition(tooltip, new(screenPosition.X - combinedMinSize.X, tooltip.Position.Y));
|
||||
}
|
||||
var bottom = MathF.Min(screenPosition.Y, screenBounds.Y);
|
||||
var top = MathF.Max(0f, bottom - combinedMinSize.Y);
|
||||
|
||||
if (top < 0f)
|
||||
{
|
||||
LayoutContainer.SetPosition(tooltip, new(tooltip.Position.X, 0f));
|
||||
}
|
||||
LayoutContainer.SetPosition(tooltip, new Vector2(left, top));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,22 @@ namespace Robust.Client.ViewVariables
|
||||
return new VVPropEditorString();
|
||||
}
|
||||
|
||||
if (type == typeof(EntProtoId) ||
|
||||
type == typeof(EntProtoId?))
|
||||
{
|
||||
return new VVPropEditorEntProtoId();
|
||||
}
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ProtoId<>))
|
||||
{
|
||||
var editor =
|
||||
(VVPropEditor)Activator.CreateInstance(
|
||||
typeof(VVPropEditorProtoId<>).MakeGenericType(type.GenericTypeArguments[0]))!;
|
||||
|
||||
IoCManager.InjectDependencies(editor);
|
||||
return editor;
|
||||
}
|
||||
|
||||
if (typeof(IPrototype).IsAssignableFrom(type) || typeof(ViewVariablesBlobMembers.PrototypeReferenceToken).IsAssignableFrom(type))
|
||||
{
|
||||
return (VVPropEditor)Activator.CreateInstance(typeof(VVPropEditorIPrototype<>).MakeGenericType(type))!;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Client.ViewVariables.Editors;
|
||||
|
||||
internal sealed class VVPropEditorEntProtoId : VVPropEditor
|
||||
{
|
||||
protected override Control MakeUI(object? value)
|
||||
{
|
||||
var lineEdit = new LineEdit
|
||||
{
|
||||
Text = (EntProtoId) (value ?? ""),
|
||||
Editable = !ReadOnly,
|
||||
HorizontalExpand = true,
|
||||
};
|
||||
|
||||
if (!ReadOnly)
|
||||
{
|
||||
lineEdit.OnTextEntered += e =>
|
||||
{
|
||||
ValueChanged((EntProtoId) e.Text);
|
||||
};
|
||||
}
|
||||
|
||||
return lineEdit;
|
||||
}
|
||||
}
|
||||
38
Robust.Client/ViewVariables/Editors/VVPropEditorProtoId.cs
Normal file
38
Robust.Client/ViewVariables/Editors/VVPropEditorProtoId.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Client.ViewVariables.Editors;
|
||||
|
||||
internal sealed class VVPropEditorProtoId<T> : VVPropEditor where T : class, IPrototype
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
|
||||
protected override Control MakeUI(object? value)
|
||||
{
|
||||
var lineEdit = new LineEdit
|
||||
{
|
||||
Text = (ProtoId<T>) (value ?? ""),
|
||||
Editable = !ReadOnly,
|
||||
HorizontalExpand = true,
|
||||
};
|
||||
|
||||
if (!ReadOnly)
|
||||
{
|
||||
lineEdit.OnTextEntered += e =>
|
||||
{
|
||||
var id = (ProtoId<T>)e.Text;
|
||||
|
||||
if (!_protoManager.HasIndex(id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ValueChanged(id);
|
||||
};
|
||||
}
|
||||
|
||||
return lineEdit;
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
}
|
||||
|
||||
public override void LoadStream<T>(AudioComponent component, T stream)
|
||||
public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
|
||||
{
|
||||
// TODO: Yeah remove this...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,8 +653,8 @@ namespace Robust.Server
|
||||
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||
|
||||
// shut down networking, kicking all players.
|
||||
var shutdownReasonWithRedial = NetStructuredDisconnectMessages.Encode($"Server shutting down: {_shutdownReason}", true);
|
||||
_network.Shutdown(shutdownReasonWithRedial);
|
||||
var shutdownReasonWithRedial = new NetDisconnectMessage($"Server shutting down: {_shutdownReason}", true);
|
||||
_network.Shutdown(shutdownReasonWithRedial.Encode());
|
||||
|
||||
// shutdown entities
|
||||
_entityManager.Cleanup();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -7,6 +8,7 @@ namespace Robust.Server.GameObjects
|
||||
{
|
||||
public sealed class VisibilitySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly PvsSystem _pvs = default!;
|
||||
[Dependency] private readonly IViewVariablesManager _vvManager = default!;
|
||||
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
@@ -133,6 +135,7 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
var xform = _xformQuery.GetComponent(uid);
|
||||
meta.VisibilityMask = mask;
|
||||
_pvs.SyncMetadata(meta);
|
||||
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Prometheus;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -42,7 +43,7 @@ namespace Robust.Server.GameObjects
|
||||
#endif
|
||||
|
||||
private ISawmill _netEntSawmill = default!;
|
||||
private EntityQuery<ActorComponent> _actorQuery;
|
||||
private PvsSystem _pvs = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -57,7 +58,7 @@ namespace Robust.Server.GameObjects
|
||||
public override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
_actorQuery = GetEntityQuery<ActorComponent>();
|
||||
_pvs = System<PvsSystem>();
|
||||
}
|
||||
|
||||
EntityUid IServerEntityManagerInternal.AllocEntity(EntityPrototype? prototype)
|
||||
@@ -103,6 +104,40 @@ namespace Robust.Server.GameObjects
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void RaiseSharedEvent<T>(T message, EntityUid? user = null)
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
var filter = Filter.Broadcast().RemoveWhereAttachedEntity(e => e == user.Value);
|
||||
foreach (var session in filter.Recipients)
|
||||
{
|
||||
EntityNetManager.SendSystemNetworkMessage(message, session.Channel);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EntityNetManager.SendSystemNetworkMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void RaiseSharedEvent<T>(T message, ICommonSession? user = null)
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
var filter = Filter.Broadcast().RemovePlayer(user);
|
||||
foreach (var session in filter.Recipients)
|
||||
{
|
||||
EntityNetManager.SendSystemNetworkMessage(message, session.Channel);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EntityNetManager.SendSystemNetworkMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearTicks(EntityUid entity, EntityPrototype prototype)
|
||||
{
|
||||
foreach (var (netId, component) in GetNetComponents(entity))
|
||||
@@ -116,6 +151,12 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
internal override void SetLifeStage(MetaDataComponent meta, EntityLifeStage stage)
|
||||
{
|
||||
base.SetLifeStage(meta, stage);
|
||||
_pvs.SyncMetadata(meta);
|
||||
}
|
||||
|
||||
#region IEntityNetworkManager impl
|
||||
|
||||
public override IEntityNetworkManager EntityNetManager => this;
|
||||
|
||||
@@ -42,7 +42,7 @@ internal sealed class PvsChunk
|
||||
/// <remarks>
|
||||
/// This already includes <see cref="Map"/>, <see cref="Root"/>, and <see cref="Children"/>
|
||||
/// </remarks>
|
||||
public readonly List<Entity<MetaDataComponent>> Contents = new();
|
||||
public readonly List<ChunkEntity> Contents = new();
|
||||
|
||||
/// <summary>
|
||||
/// The unique location identifier for this chunk.
|
||||
@@ -68,8 +68,8 @@ internal sealed class PvsChunk
|
||||
// the same chunk can be repopulated more than once.
|
||||
private List<HashSet<EntityUid>> _childSets = new();
|
||||
private List<HashSet<EntityUid>> _nextChildSets = new();
|
||||
private List<Entity<MetaDataComponent>> _lowPriorityChildren = new();
|
||||
private List<Entity<MetaDataComponent>> _anchoredChildren = new();
|
||||
private List<ChunkEntity> _lowPriorityChildren = new();
|
||||
private List<ChunkEntity> _anchoredChildren = new();
|
||||
|
||||
/// <summary>
|
||||
/// Effective "counts" of <see cref="Contents"/> that should be used to limit the number of entities in a chunk that
|
||||
@@ -151,11 +151,11 @@ internal sealed class PvsChunk
|
||||
childMeta.LastPvsLocation = Location;
|
||||
|
||||
if ((childMeta.Flags & MetaDataFlags.PvsPriority) == MetaDataFlags.PvsPriority)
|
||||
Contents.Add((child, childMeta));
|
||||
Contents.Add(new ChunkEntity(child, childMeta));
|
||||
else if (childXform.Anchored)
|
||||
_anchoredChildren.Add((child, childMeta));
|
||||
_anchoredChildren.Add(new(child, childMeta));
|
||||
else
|
||||
_lowPriorityChildren.Add((child, childMeta));
|
||||
_lowPriorityChildren.Add(new(child, childMeta));
|
||||
|
||||
var subCount = childXform._children.Count;
|
||||
if (subCount == 0)
|
||||
@@ -196,7 +196,7 @@ internal sealed class PvsChunk
|
||||
}
|
||||
|
||||
childMeta.LastPvsLocation = Location;
|
||||
Contents.Add((child, childMeta));
|
||||
Contents.Add(new(child, childMeta));
|
||||
|
||||
var subCount = childXform._children.Count;
|
||||
if (subCount == 0)
|
||||
@@ -241,10 +241,10 @@ internal sealed class PvsChunk
|
||||
set.Add(Map.Owner);
|
||||
foreach (var child in Contents)
|
||||
{
|
||||
var parent = query.GetComponent(child).ParentUid;
|
||||
var parent = query.GetComponent(child.Uid).ParentUid;
|
||||
DebugTools.Assert(set.Contains(parent),
|
||||
"A child's parent is not in the chunk, or is not listed first.");
|
||||
DebugTools.Assert(set.Add(child), "Child appears more than once in the chunk.");
|
||||
DebugTools.Assert(set.Add(child.Uid), "Child appears more than once in the chunk.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,4 +274,11 @@ internal sealed class PvsChunk
|
||||
? $"map-{Root.Owner}-{Location.Indices}"
|
||||
: $"grid-{Root.Owner}-{Location.Indices}";
|
||||
}
|
||||
|
||||
public readonly struct ChunkEntity(EntityUid uid, MetaDataComponent meta)
|
||||
{
|
||||
public readonly EntityUid Uid = uid;
|
||||
public readonly PvsIndex Ptr = meta.PvsData;
|
||||
public readonly MetaDataComponent Meta = meta;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -14,26 +15,24 @@ namespace Robust.Server.GameStates;
|
||||
/// <summary>
|
||||
/// Class for storing session specific PVS data.
|
||||
/// </summary>
|
||||
internal sealed class PvsSession(ICommonSession session)
|
||||
internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<PvsData> memoryRegion)
|
||||
{
|
||||
public readonly ICommonSession Session = session;
|
||||
|
||||
public readonly ResizableMemoryRegion<PvsData> DataMemory = memoryRegion;
|
||||
|
||||
public INetChannel Channel => Session.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// All <see cref="EntityUid"/>s that this session saw during the last <see cref="PvsSystem.DirtyBufferSize"/> ticks.
|
||||
/// All entities that this session saw during the last <see cref="PvsSystem.DirtyBufferSize"/> ticks.
|
||||
/// </summary>
|
||||
public readonly OverflowDictionary<GameTick, List<PvsData>> PreviouslySent = new(PvsSystem.DirtyBufferSize);
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary containing data about all entities that this client has ever seen.
|
||||
/// </summary>
|
||||
public readonly Dictionary<NetEntity, PvsData> Entities = new();
|
||||
public readonly OverflowDictionary<GameTick, List<PvsIndex>> PreviouslySent = new(PvsSystem.DirtyBufferSize);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="PreviouslySent"/> overflow in case a player's last ack is more than
|
||||
/// <see cref="PvsSystem.DirtyBufferSize"/> ticks behind the current tick.
|
||||
/// </summary>
|
||||
public (GameTick Tick, List<PvsData> SentEnts)? Overflow;
|
||||
public (GameTick Tick, List<PvsIndex> SentEnts)? Overflow;
|
||||
|
||||
/// <summary>
|
||||
/// The client's current visibility mask.
|
||||
@@ -43,13 +42,13 @@ internal sealed class PvsSession(ICommonSession session)
|
||||
/// <summary>
|
||||
/// The list that is currently being prepared for sending.
|
||||
/// </summary>
|
||||
public List<PvsData>? ToSend;
|
||||
public List<PvsIndex>? ToSend;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ToSend"/> list from the previous tick. Also caches the current tick that the PVS leave message
|
||||
/// should belong to, in case the processing is ever run asynchronously with normal system/game ticking.
|
||||
/// </summary>
|
||||
public (GameTick ToTick, List<PvsData> PreviouslySent)? LastSent;
|
||||
public (GameTick ToTick, List<PvsIndex> PreviouslySent)? LastSent;
|
||||
|
||||
/// <summary>
|
||||
/// Visible chunks, sorted by proximity to the clients's viewers;
|
||||
@@ -126,10 +125,12 @@ internal sealed class PvsSession(ICommonSession session)
|
||||
/// <summary>
|
||||
/// Class for storing session-specific information about when an entity was last sent to a player.
|
||||
/// </summary>
|
||||
internal sealed class PvsData(NetEntity entity) : IEquatable<PvsData>
|
||||
/// <remarks>
|
||||
/// Size is padded to 16 bytes so
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 16)]
|
||||
internal struct PvsData
|
||||
{
|
||||
public readonly NetEntity NetEntity = entity;
|
||||
|
||||
/// <summary>
|
||||
/// Tick at which this entity was last sent to a player.
|
||||
/// </summary>
|
||||
@@ -146,22 +147,55 @@ internal sealed class PvsData(NetEntity entity) : IEquatable<PvsData>
|
||||
/// present in that state.
|
||||
/// </summary>
|
||||
public GameTick EntityLastAcked;
|
||||
}
|
||||
|
||||
public bool Equals(PvsData? other)
|
||||
{
|
||||
DebugTools.Assert((NetEntity != other?.NetEntity) || ReferenceEquals(this, other));
|
||||
return NetEntity == other?.NetEntity;
|
||||
}
|
||||
/// <summary>
|
||||
/// Specialized struct with the same size as <see cref="PvsData"/> that is used to store metadata in the pinned PVsData array
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 16)]
|
||||
internal struct PvsMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Tick at which this entity was last sent to a player.
|
||||
/// </summary>
|
||||
public NetEntity NetEntity;
|
||||
|
||||
public override int GetHashCode()
|
||||
public GameTick LastModifiedTick;
|
||||
public ushort VisMask;
|
||||
public EntityLifeStage LifeStage;
|
||||
#if DEBUG
|
||||
// This struct is padded to a size of 16 so it's aligned to cache boundaries nicely.
|
||||
// We have this extra space that isn't being used,
|
||||
// so I'm opting to use them to make debugging the free list easier.
|
||||
// "Marker" overlaps with the field used by the free list (which occupies the unused memory of PvsMetadata).
|
||||
// So we set it to a bogus value and BAM! Errors are obvious!
|
||||
private byte Pad0;
|
||||
public uint Marker;
|
||||
#endif
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 16)]
|
||||
internal struct PvsMetadataFreeLink
|
||||
{
|
||||
#if DEBUG
|
||||
static unsafe PvsMetadataFreeLink()
|
||||
{
|
||||
return NetEntity.GetHashCode();
|
||||
DebugTools.Assert(sizeof(PvsMetadataFreeLink) == sizeof(PvsMetadata));
|
||||
}
|
||||
#endif
|
||||
|
||||
public int Pad0;
|
||||
public int Pad1;
|
||||
public int Pad2;
|
||||
// We offset the NextFree to be at the end of the struct.
|
||||
// This is so that it overlaps with the debug Marker field of PvsMetadata instead of real data.
|
||||
public PvsIndex NextFree;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct for storing information about the current number of entities that are being sent to the player this tick.
|
||||
/// Used to enforce pvs budgets.
|
||||
/// </summary>
|
||||
internal struct PvsBudget
|
||||
{
|
||||
public int NewLimit;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -12,6 +14,7 @@ namespace Robust.Server.GameStates;
|
||||
public sealed class PvsOverrideSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IConsoleHost _console = default!;
|
||||
|
||||
private readonly HashSet<EntityUid> _hasOverride = new();
|
||||
|
||||
@@ -28,8 +31,64 @@ public sealed class PvsOverrideSystem : EntitySystem
|
||||
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
|
||||
SubscribeLocalEvent<GridInitializeEvent>(OnGridCreated);
|
||||
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
|
||||
|
||||
// TODO console commands for adding/removing overrides?
|
||||
_console.RegisterCommand(
|
||||
"pvs_override_info",
|
||||
Loc.GetString("cmd-pvs-override-info-desc"),
|
||||
"pvs_override_info",
|
||||
GetPvsInfo,
|
||||
GetCompletion);
|
||||
}
|
||||
|
||||
#region Console Commands
|
||||
|
||||
/// <summary>
|
||||
/// Debug command for displaying PVS override information.
|
||||
/// </summary>
|
||||
private void GetPvsInfo(IConsoleShell shell, string argstr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetEntity.TryParse(args[0], out var nuid) || !TryGetEntity(nuid, out var uid))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-uid"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_hasOverride.Contains(uid.Value))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-pvs-override-info-empty", ("nuid", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
if (GlobalOverride.Contains(uid.Value) || ForceSend.Contains(uid.Value))
|
||||
shell.WriteLine(Loc.GetString("cmd-pvs-override-info-global", ("nuid", args[0])));
|
||||
|
||||
HashSet<ICommonSession> sessions = new();
|
||||
sessions.UnionWith(SessionOverrides.Where(x => x.Value.Contains(uid.Value)).Select(x => x.Key));
|
||||
sessions.UnionWith(SessionForceSend.Where(x => x.Value.Contains(uid.Value)).Select(x => x.Key));
|
||||
if (sessions.Count == 0)
|
||||
return;
|
||||
|
||||
var clients = string.Join(", ", sessions.Select(x => x.ToString()));
|
||||
shell.WriteLine(Loc.GetString("cmd-pvs-override-info-clients", ("nuid", args[0]), ("clients", clients)));
|
||||
}
|
||||
|
||||
private CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
return CompletionResult.Empty;
|
||||
|
||||
return CompletionResult.FromHintOptions(CompletionHelper.NetEntities(args[0], EntityManager), "NetEntity");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs ev)
|
||||
{
|
||||
if (ev.NewStatus != SessionStatus.Disconnected)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Prometheus;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Threading;
|
||||
@@ -105,7 +106,7 @@ internal sealed partial class PvsSystem
|
||||
private void ProcessQueuedAck(PvsSession session)
|
||||
{
|
||||
var ackedTick = session.LastReceivedAck;
|
||||
List<PvsData>? ackedEnts;
|
||||
List<PvsIndex>? ackedEnts;
|
||||
|
||||
if (session.Overflow != null && session.Overflow.Value.Tick <= ackedTick)
|
||||
{
|
||||
@@ -125,12 +126,12 @@ internal sealed partial class PvsSystem
|
||||
else if (!session.PreviouslySent.TryGetValue(ackedTick, out ackedEnts))
|
||||
return;
|
||||
|
||||
foreach (var data in CollectionsMarshal.AsSpan(ackedEnts))
|
||||
foreach (ref var intPtr in CollectionsMarshal.AsSpan(ackedEnts))
|
||||
{
|
||||
data.EntityLastAcked = ackedTick;
|
||||
ref var data = ref session.DataMemory.GetRef(intPtr.Index);
|
||||
DebugTools.AssertNotEqual(data.LastSeen, GameTick.Zero);
|
||||
DebugTools.Assert(data.LastSeen >= ackedTick); // LastSent may equal ackedTick if the packet was sent reliably.
|
||||
DebugTools.Assert(!session.Entities.TryGetValue(data.NetEntity, out var old)
|
||||
|| ReferenceEquals(data, old));
|
||||
data.EntityLastAcked = ackedTick;
|
||||
}
|
||||
|
||||
// The client acked a tick. If they requested a full state, this ack happened some time after that, so we can safely set this to false
|
||||
|
||||
384
Robust.Server/GameStates/PvsSystem.DataStorage.cs
Normal file
384
Robust.Server/GameStates/PvsSystem.DataStorage.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameStates;
|
||||
|
||||
// This partial class handles the PvsData memory. This array stores information about when each entity was last sent to
|
||||
// each player. This is somewhat faster than using a per-player Dictionary<EntityUid, PvsData>, though it can be less
|
||||
// memory efficient.
|
||||
internal sealed partial class PvsSystem
|
||||
{
|
||||
// This is used for asserts.
|
||||
private HashSet<PvsIndex> _assignedEnts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Recently returned indexes from deleted entities. These get moved to <see cref="_pendingReturns"/> before
|
||||
/// moving back into the free list.
|
||||
/// </summary>
|
||||
private List<PvsIndex> _incomingReturns = new();
|
||||
|
||||
/// <summary>
|
||||
/// Recently returned pointers from deleted entities. These will get returned to the free list
|
||||
/// after a minimum amount of time has passed, to ensure that processing late game-state ack messages doesn't
|
||||
/// write data to deleted entities.
|
||||
/// </summary>
|
||||
private List<PvsIndex> _pendingReturns = new();
|
||||
|
||||
/// <summary>
|
||||
/// Tick at which the <see cref="_pendingReturns"/> were last processed.
|
||||
/// </summary>
|
||||
private GameTick _lastReturn = GameTick.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Memory region to store <see cref="PvsMetadata"/> instances and the free list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Unused elements form a linked list out of <see cref="PvsMetadataFreeLink"/> elements.
|
||||
/// </remarks>
|
||||
private ResizableMemoryRegion<PvsMetadata> _metadataMemory = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The head of the PVS data free list. This is the first element that will be used if a new one is needed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the value is <see cref="PvsIndex.Invalid"/>,
|
||||
/// there are no more free elements and the next allocation must expand the memory.
|
||||
/// </remarks>
|
||||
private PvsIndex _dataFreeListHead;
|
||||
|
||||
private WaitHandle? _deletionTask;
|
||||
|
||||
/// <summary>
|
||||
/// Expand the size of <see cref="_metadataMemory"/> (and all session data stores) one iteration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This ensures that we have at least one free list slot.
|
||||
/// </remarks>
|
||||
private void ExpandEntityCapacity()
|
||||
{
|
||||
var initial = _metadataMemory.CurrentSize;
|
||||
|
||||
var entityGrowth = _configManager.GetCVar(CVars.NetPvsEntityGrowth);
|
||||
var newSize = initial + (entityGrowth <= 0 ? initial : entityGrowth);
|
||||
newSize = Math.Min(newSize, _metadataMemory.MaxSize);
|
||||
if (newSize == initial)
|
||||
throw new InvalidOperationException("Out of PVS entity capacity! Increase net.pvs_entity_max!");
|
||||
|
||||
Log.Debug($"Growing PvsData memory from {initial} -> {newSize} entities");
|
||||
|
||||
_metadataMemory.Expand(newSize);
|
||||
foreach (var playerSession in PlayerData.Values)
|
||||
{
|
||||
playerSession.DataMemory.Expand(newSize);
|
||||
}
|
||||
|
||||
var newSlots = _metadataMemory.GetSpan<PvsMetadataFreeLink>()[initial..];
|
||||
InitializeFreeList(newSlots, initial, ref _dataFreeListHead);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize <see cref="_metadataMemory"/> and the free list.
|
||||
/// </summary>
|
||||
private void InitializePvsArray()
|
||||
{
|
||||
var initialCount = _configManager.GetCVar(CVars.NetPvsEntityInitial);
|
||||
var maxCount = _configManager.GetCVar(CVars.NetPvsEntityMax);
|
||||
|
||||
if (initialCount <= 0 || maxCount <= 0)
|
||||
throw new InvalidOperationException("net.pvs_entity_initial and net.pvs_entity_max must be positive");
|
||||
|
||||
_metadataMemory = new ResizableMemoryRegion<PvsMetadata>(maxCount, initialCount);
|
||||
|
||||
ResetDataMemory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a section of the free list.
|
||||
/// </summary>
|
||||
/// <param name="memory">The section of the free list to initialize.</param>
|
||||
/// <param name="baseOffset">What offset in the total PVS data this section starts at.</param>
|
||||
/// <param name="head">The current head storage of the free list to update.</param>
|
||||
private static void InitializeFreeList(Span<PvsMetadataFreeLink> memory, int baseOffset, ref PvsIndex head)
|
||||
{
|
||||
for (var i = 0; i < memory.Length; i++)
|
||||
{
|
||||
memory[i].NextFree = new PvsIndex(baseOffset + i + 1);
|
||||
}
|
||||
|
||||
memory[^1].NextFree = head;
|
||||
head = new PvsIndex(baseOffset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all PVS data. After this function is called,
|
||||
/// <see cref="ResetDataMemory"/> must be called if the system isn't being shut down.
|
||||
/// </summary>
|
||||
private void ClearPvsData()
|
||||
{
|
||||
_leaveTask?.WaitOne();
|
||||
_leaveTask = null;
|
||||
|
||||
_deletionTask?.WaitOne();
|
||||
_deletionTask = null;
|
||||
|
||||
_incomingReturns.Clear();
|
||||
_pendingReturns.Clear();
|
||||
_deletionJob.ToClear.Clear();
|
||||
_assignedEnts.Clear();
|
||||
|
||||
// Remove all pointers stored in any player's PVS send-histories. Required to avoid accidentally writing to
|
||||
// invalid bits of memory while processing late game-state acks. This also forces all players to receive a full
|
||||
// game state, in lieu of sending the required PVS leave messages.
|
||||
foreach (var session in PlayerData.Values)
|
||||
{
|
||||
session.DataMemory.Clear();
|
||||
ForceFullState(session);
|
||||
}
|
||||
|
||||
_metadataMemory.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-initialize the memory in <see cref="_metadataMemory"/> after it was fully cleared on reset.
|
||||
/// </summary>
|
||||
private void ResetDataMemory()
|
||||
{
|
||||
_dataFreeListHead = PvsIndex.Invalid;
|
||||
InitializeFreeList(_metadataMemory.GetSpan<PvsMetadataFreeLink>(), 0, ref _dataFreeListHead);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shrink <see cref="_metadataMemory"/> (and all sessions) back down to initial entity size after clear.
|
||||
/// </summary>
|
||||
private void ShrinkDataMemory()
|
||||
{
|
||||
DebugTools.Assert(EntityManager.EntityCount == 0);
|
||||
|
||||
var initialCount = _configManager.GetCVar(CVars.NetPvsEntityInitial);
|
||||
|
||||
if (initialCount != _metadataMemory.CurrentSize)
|
||||
{
|
||||
Log.Debug($"Shrinking PVS data from {_metadataMemory.CurrentSize} -> {initialCount} entities");
|
||||
|
||||
_metadataMemory.Shrink(initialCount);
|
||||
foreach (var player in PlayerData.Values)
|
||||
{
|
||||
player.DataMemory.Shrink(initialCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method shuffles the entity free list. This is used to avoid accidental / unrealistic cache locality
|
||||
/// in benchmarks.
|
||||
/// </summary>
|
||||
internal void ShufflePointers(int seed)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
/*List<IntPtr> ptrs = new(_pointerPool);
|
||||
_pointerPool.Clear();
|
||||
|
||||
var rng = new Random(seed);
|
||||
var n = ptrs.Count;
|
||||
while (n > 0)
|
||||
{
|
||||
var k = rng.Next(n);
|
||||
_pointerPool.Push(ptrs[k]);
|
||||
ptrs[k] = ptrs[^1];
|
||||
ptrs.RemoveAt(--n);
|
||||
}*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all of this sessions' PvsData for all entities. This effectively means that PVS will act as if the player
|
||||
/// had never been sent information about any entity. Used when returning the player's index offset to the pool.
|
||||
/// </summary>
|
||||
private void ClearPlayerPvsData(PvsSession session)
|
||||
{
|
||||
session.DataMemory.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all of this entity' PvsData entries. This effectively means that PVS will act as if no player
|
||||
/// had never been sent information about this entity. Used when returning the entity's index back to the free list.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ClearEntityPvsData(PvsIndex index)
|
||||
{
|
||||
foreach (var playerData in PlayerData.Values)
|
||||
{
|
||||
ref var entry = ref playerData.DataMemory.GetRef(index.Index);
|
||||
entry = default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the NetEntity associated with a given <see cref="PvsIndex"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private NetEntity IndexToNetEntity(PvsIndex index)
|
||||
{
|
||||
DebugTools.Assert(_assignedEnts.Contains(index));
|
||||
return _metadataMemory.GetRef(index.Index).NetEntity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ResizableMemoryRegion{T}"/> suitable for assigning to a new <see cref="PvsSession"/>.
|
||||
/// </summary>
|
||||
private ResizableMemoryRegion<PvsData> CreateSessionDataMemory()
|
||||
{
|
||||
return new ResizableMemoryRegion<PvsData>(_metadataMemory.MaxSize, _metadataMemory.CurrentSize);
|
||||
}
|
||||
|
||||
private static void FreeSessionDataMemory(PvsSession session)
|
||||
{
|
||||
session.DataMemory.Dispose();
|
||||
}
|
||||
|
||||
private void OnEntityAdded(Entity<MetaDataComponent> entity)
|
||||
{
|
||||
DebugTools.Assert(entity.Comp.PvsData.Index == default);
|
||||
|
||||
AssignEntityPointer(entity.Comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a free entity index and assign it to an entity.
|
||||
/// </summary>
|
||||
private void AssignEntityPointer(MetaDataComponent meta)
|
||||
{
|
||||
if (_dataFreeListHead == PvsIndex.Invalid)
|
||||
{
|
||||
ExpandEntityCapacity();
|
||||
DebugTools.Assert(_dataFreeListHead != PvsIndex.Invalid);
|
||||
}
|
||||
|
||||
var index = _dataFreeListHead;
|
||||
DebugTools.Assert(_assignedEnts.Add(index));
|
||||
ref var metadata = ref _metadataMemory.GetRef(index.Index);
|
||||
ref var freeLink = ref Unsafe.As<PvsMetadata, PvsMetadataFreeLink>(ref metadata);
|
||||
_dataFreeListHead = freeLink.NextFree;
|
||||
|
||||
// TODO: re-introduce this assert.
|
||||
// DebugTools.AssertEqual(((PvsMetadata*) ptr)->NetEntity, NetEntity.Invalid);
|
||||
DebugTools.AssertNotEqual(meta.NetEntity, NetEntity.Invalid);
|
||||
|
||||
meta.PvsData = index;
|
||||
metadata.NetEntity = meta.NetEntity;
|
||||
metadata.LastModifiedTick = meta.LastModifiedTick;
|
||||
metadata.VisMask = meta.VisibilityMask;
|
||||
metadata.LifeStage = meta.EntityLifeStage;
|
||||
#if DEBUG
|
||||
metadata.Marker = uint.MaxValue;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return an entity's index in the data array back to the free list of available indices.
|
||||
/// </summary>
|
||||
private void OnEntityDeleted(Entity<MetaDataComponent> entity)
|
||||
{
|
||||
var ptr = entity.Comp.PvsData;
|
||||
entity.Comp.PvsData = default;
|
||||
|
||||
if (ptr == default)
|
||||
return;
|
||||
|
||||
_incomingReturns.Add(ptr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately return all data indexes back to the pool after flushing all entities.
|
||||
/// </summary>
|
||||
private void AfterEntityFlush()
|
||||
{
|
||||
DebugTools.Assert(EntityManager.EntityCount == 0);
|
||||
|
||||
ClearPvsData();
|
||||
ShrinkDataMemory();
|
||||
ResetDataMemory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This update method periodically returns entity indices back to the pool, once we are sure no old
|
||||
/// game state acks will use indices to that entity.
|
||||
/// </summary>
|
||||
private void ProcessDeletions()
|
||||
{
|
||||
var curTick = _gameTiming.CurTick;
|
||||
|
||||
if (curTick < _lastReturn + (uint)ForceAckThreshold + 1)
|
||||
return;
|
||||
|
||||
if (curTick < _lastReturn)
|
||||
throw new InvalidOperationException($"Time travel is not supported");
|
||||
|
||||
_leaveTask?.WaitOne();
|
||||
_leaveTask = null;
|
||||
|
||||
_deletionTask?.WaitOne();
|
||||
_deletionTask = null;
|
||||
|
||||
_lastReturn = curTick;
|
||||
|
||||
foreach (var index in CollectionsMarshal.AsSpan(_deletionJob.ToClear))
|
||||
{
|
||||
ReturnEntity(index);
|
||||
}
|
||||
_deletionJob.ToClear.Clear();
|
||||
|
||||
// Cycle lists.
|
||||
(_deletionJob.ToClear, _pendingReturns, _incomingReturns) = (_pendingReturns, _incomingReturns, _deletionJob.ToClear);
|
||||
|
||||
if (_deletionJob.ToClear.Count == 0)
|
||||
return;
|
||||
|
||||
#if DEBUG
|
||||
foreach (var index in CollectionsMarshal.AsSpan(_deletionJob.ToClear))
|
||||
{
|
||||
DebugTools.Assert(_assignedEnts.Remove(index));
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_deletionJob.ToClear.Count > 16)
|
||||
{
|
||||
_deletionTask = _parallelManager.Process(_deletionJob, _deletionJob.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var index in CollectionsMarshal.AsSpan(_deletionJob.ToClear))
|
||||
{
|
||||
ClearEntityPvsData(index);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReturnEntity(PvsIndex index)
|
||||
{
|
||||
DebugTools.Assert(!_assignedEnts.Contains(index));
|
||||
ref var freeLink = ref _metadataMemory.GetRef<PvsMetadataFreeLink>(index.Index);
|
||||
freeLink.NextFree = _dataFreeListHead;
|
||||
_dataFreeListHead = index;
|
||||
}
|
||||
|
||||
private record struct PvsDeletionsJob(PvsSystem _pvs) : IParallelRobustJob
|
||||
{
|
||||
public int BatchSize => 8;
|
||||
private PvsSystem _pvs = _pvs;
|
||||
public List<PvsIndex> ToClear = new();
|
||||
|
||||
public int Count => ToClear.Count;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
_pvs.ClearEntityPvsData(ToClear[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,12 @@ namespace Robust.Server.GameStates
|
||||
|
||||
private void OnEntityDirty(Entity<MetaDataComponent> uid)
|
||||
{
|
||||
if (uid.Comp.PvsData != default)
|
||||
{
|
||||
ref var meta = ref _metadataMemory.GetRef(uid.Comp.PvsData.Index);
|
||||
meta.LastModifiedTick = uid.Comp.EntityLastModifiedTick;
|
||||
}
|
||||
|
||||
if (!_addEntities[_currentIndex].Contains(uid))
|
||||
_dirtyEntities[_currentIndex].Add(uid);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
@@ -25,11 +26,6 @@ internal sealed partial class PvsSystem
|
||||
{
|
||||
var meta = ev.Entity.Comp;
|
||||
|
||||
foreach (var sessionData in PlayerData.Values)
|
||||
{
|
||||
sessionData.Entities.Remove(meta.NetEntity);
|
||||
}
|
||||
|
||||
_deletedEntities.Add(meta.NetEntity);
|
||||
_deletedTick.Add(_gameTiming.CurTick);
|
||||
RemoveEntityFromChunk(ev.Entity.Owner, meta);
|
||||
@@ -111,4 +107,14 @@ internal sealed partial class PvsSystem
|
||||
DebugTools.AssertNull(xform.MapUid);
|
||||
AssertNullspace(xform.ParentUid);
|
||||
}
|
||||
|
||||
internal void SyncMetadata(MetaDataComponent meta)
|
||||
{
|
||||
if (meta.PvsData == default)
|
||||
return;
|
||||
|
||||
ref var ptr = ref _metadataMemory.GetRef(meta.PvsData.Index);
|
||||
ptr.VisMask = meta.VisibilityMask;
|
||||
ptr.LifeStage = meta.EntityLifeStage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Prometheus;
|
||||
@@ -47,12 +48,14 @@ internal sealed partial class PvsSystem
|
||||
return;
|
||||
|
||||
var (toTick, lastSent) = session.LastSent.Value;
|
||||
foreach (var data in CollectionsMarshal.AsSpan(lastSent))
|
||||
|
||||
foreach (var intPtr in CollectionsMarshal.AsSpan(lastSent))
|
||||
{
|
||||
ref var data = ref session.DataMemory.GetRef(intPtr.Index);
|
||||
if (data.LastSeen == toTick)
|
||||
continue;
|
||||
|
||||
session.LeftView.Add(data.NetEntity);
|
||||
session.LeftView.Add(IndexToNetEntity(intPtr));
|
||||
data.LastLeftView = toTick;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -8,21 +10,23 @@ namespace Robust.Server.GameStates;
|
||||
// range/chunk restrictions.
|
||||
internal sealed partial class PvsSystem
|
||||
{
|
||||
private readonly List<Entity<MetaDataComponent>> _cachedForceOverride = new();
|
||||
private readonly List<Entity<MetaDataComponent>> _cachedGlobalOverride = new();
|
||||
private readonly List<PvsChunk.ChunkEntity> _cachedForceOverride = new();
|
||||
private readonly List<PvsChunk.ChunkEntity> _cachedGlobalOverride = new();
|
||||
|
||||
private readonly HashSet<EntityUid> _forceOverrideSet = new();
|
||||
private readonly HashSet<EntityUid> _globalOverrideSet = new();
|
||||
|
||||
private void AddAllOverrides(PvsSession session)
|
||||
{
|
||||
var mask = session.VisMask;
|
||||
var fromTick = session.FromTick;
|
||||
RaiseExpandEvent(session, fromTick);
|
||||
|
||||
foreach (var entity in _cachedGlobalOverride)
|
||||
foreach (ref var ent in CollectionsMarshal.AsSpan(_cachedGlobalOverride))
|
||||
{
|
||||
if (!AddEntity(session, entity, fromTick))
|
||||
break;
|
||||
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
|
||||
if ((mask & meta.VisMask) == meta.VisMask)
|
||||
AddEntity(session, ref ent, ref meta, fromTick);
|
||||
}
|
||||
|
||||
if (!_pvsOverride.SessionOverrides.TryGetValue(session.Session, out var sessionOverrides))
|
||||
@@ -42,10 +46,13 @@ internal sealed partial class PvsSystem
|
||||
// Ignore PVS budgets
|
||||
session.Budget = new() {NewLimit = int.MaxValue, EnterLimit = int.MaxValue};
|
||||
|
||||
var mask = session.VisMask;
|
||||
var fromTick = session.FromTick;
|
||||
foreach (var entity in _cachedForceOverride)
|
||||
foreach (ref var ent in CollectionsMarshal.AsSpan(_cachedForceOverride))
|
||||
{
|
||||
AddEntity(session, entity, fromTick);
|
||||
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
|
||||
if ((mask & meta.VisMask) == meta.VisMask)
|
||||
AddEntity(session, ref ent, ref meta, fromTick);
|
||||
}
|
||||
|
||||
foreach (var uid in session.Viewers)
|
||||
@@ -156,7 +163,7 @@ internal sealed partial class PvsSystem
|
||||
|
||||
private bool CacheOverrideParents(
|
||||
EntityUid uid,
|
||||
List<Entity<MetaDataComponent>> list,
|
||||
List<PvsChunk.ChunkEntity> list,
|
||||
HashSet<EntityUid> set,
|
||||
out TransformComponent xform)
|
||||
{
|
||||
@@ -175,11 +182,11 @@ internal sealed partial class PvsSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
list.Add((uid, meta));
|
||||
list.Add(new(uid, meta));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CacheOverrideChildren(TransformComponent xform, List<Entity<MetaDataComponent>> list, HashSet<EntityUid> set)
|
||||
private void CacheOverrideChildren(TransformComponent xform, List<PvsChunk.ChunkEntity> list, HashSet<EntityUid> set)
|
||||
{
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
@@ -190,7 +197,7 @@ internal sealed partial class PvsSystem
|
||||
}
|
||||
|
||||
if (set.Add(child))
|
||||
list.Add((child, _metaQuery.GetComponent(child)));
|
||||
list.Add(new(child, _metaQuery.GetComponent(child)));
|
||||
|
||||
CacheOverrideChildren(childXform, list, set);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -15,8 +16,8 @@ internal sealed partial class PvsSystem
|
||||
/// </summary>
|
||||
private const int MaxVisPoolSize = 1024;
|
||||
|
||||
private readonly ObjectPool<List<PvsData>> _entDataListPool
|
||||
= new DefaultObjectPool<List<PvsData>>(new ListPolicy<PvsData>(), MaxVisPoolSize);
|
||||
private readonly ObjectPool<List<PvsIndex>> _entDataListPool
|
||||
= new DefaultObjectPool<List<PvsIndex>>(new ListPolicy<PvsIndex>(), MaxVisPoolSize);
|
||||
|
||||
private readonly ObjectPool<HashSet<EntityUid>> _uidSetPool
|
||||
= new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), MaxVisPoolSize);
|
||||
|
||||
@@ -72,7 +72,10 @@ internal sealed partial class PvsSystem
|
||||
private PvsSession GetOrNewPvsSession(ICommonSession session)
|
||||
{
|
||||
if (!PlayerData.TryGetValue(session, out var pvsSession))
|
||||
PlayerData[session] = pvsSession = new(session);
|
||||
{
|
||||
var memoryRegion = CreateSessionDataMemory();
|
||||
PlayerData[session] = pvsSession = new(session, memoryRegion);
|
||||
}
|
||||
|
||||
return pvsSession;
|
||||
}
|
||||
@@ -193,6 +196,5 @@ internal sealed partial class PvsSystem
|
||||
|
||||
session.PreviouslySent.Clear();
|
||||
session.LastSent = null;
|
||||
session.Entities.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,71 +61,91 @@ internal sealed partial class PvsSystem
|
||||
for (var i = 0; i < limit; i++)
|
||||
{
|
||||
var ent = span[i];
|
||||
if ((mask & ent.Comp.VisibilityMask) != ent.Comp.VisibilityMask)
|
||||
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
|
||||
|
||||
if ((mask & meta.VisMask) != meta.VisMask)
|
||||
continue;
|
||||
|
||||
// TODO PVS improve this somehow
|
||||
// Having entities "leave" pvs view just because the pvs entry budget was exceeded sucks.
|
||||
// This probably requires changing client game state manager to support receiving entities with unknown parents.
|
||||
// Probably needs to do something similar to pending net entity states, but for entity spawning.
|
||||
if (!AddEntity(session, ent, fromTick))
|
||||
if (!AddEntity(session, ref ent, ref meta, fromTick))
|
||||
limit = directChildren;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to add an entity to the to-send lists, while respecting pvs budgets.
|
||||
/// </summary>
|
||||
/// <returns>Returns false if the entity would exceed the client's PVS budget.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool AddEntity(PvsSession session, ref PvsChunk.ChunkEntity ent, ref PvsMetadata meta,
|
||||
GameTick fromTick)
|
||||
{
|
||||
DebugTools.Assert(fromTick < _gameTiming.CurTick);
|
||||
ref var data = ref session.DataMemory.GetRef(ent.Ptr.Index);
|
||||
|
||||
if (data.LastSeen == _gameTiming.CurTick)
|
||||
return true;
|
||||
|
||||
if (meta.LifeStage >= EntityLifeStage.Terminating)
|
||||
{
|
||||
Log.Error($"Attempted to send deleted entity: {ToPrettyString(ent.Uid)}");
|
||||
EntityManager.QueueDeleteEntity(ent.Uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
var (entered,budgetExceeded) = IsEnteringPvsRange(ref data, fromTick, ref session.Budget);
|
||||
|
||||
if (budgetExceeded)
|
||||
return false;
|
||||
|
||||
data.LastSeen = _gameTiming.CurTick;
|
||||
session.ToSend!.Add(ent.Ptr);
|
||||
|
||||
if (session.RequestedFull)
|
||||
{
|
||||
var state = GetFullEntityState(session.Session, ent.Uid, ent.Meta);
|
||||
session.States.Add(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entered)
|
||||
{
|
||||
var state = GetEntityState(session.Session, ent.Uid, data.EntityLastAcked, ent.Meta);
|
||||
session.States.Add(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (meta.LastModifiedTick <= fromTick)
|
||||
return true;
|
||||
|
||||
var entState = GetEntityState(session.Session, ent.Uid, fromTick , ent.Meta);
|
||||
|
||||
if (!entState.Empty)
|
||||
session.States.Add(entState);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to add an entity to the to-send lists, while respecting pvs budgets.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool AddEntity(PvsSession session, Entity<MetaDataComponent> entity, GameTick fromTick)
|
||||
{
|
||||
var nuid = entity.Comp.NetEntity;
|
||||
ref var data = ref CollectionsMarshal.GetValueRefOrAddDefault(session.Entities, nuid, out var exists);
|
||||
if (!exists)
|
||||
data = new(nuid);
|
||||
DebugTools.Assert(fromTick < _gameTiming.CurTick);
|
||||
ref var data = ref session.DataMemory.GetRef(entity.Comp.PvsData.Index);
|
||||
|
||||
if (entity.Comp.Deleted)
|
||||
{
|
||||
Log.Error($"Attempted to send deleted entity: {ToPrettyString(entity, entity)}");
|
||||
session.Entities.Remove(entity.Comp.NetEntity);
|
||||
return false;
|
||||
}
|
||||
|
||||
DebugTools.AssertEqual(data!.NetEntity, entity.Comp.NetEntity);
|
||||
if (data.LastSeen == _gameTiming.CurTick)
|
||||
return true;
|
||||
|
||||
var (entered,budgetExceeded) = IsEnteringPvsRange(data, fromTick, ref session.Budget);
|
||||
var (entered,budgetExceeded) = IsEnteringPvsRange(ref data, fromTick, ref session.Budget);
|
||||
|
||||
if (budgetExceeded)
|
||||
return false;
|
||||
|
||||
if (!AddToSendList(session, data, entity, fromTick, entered))
|
||||
return false;
|
||||
|
||||
DebugTools.AssertNotEqual(data.LastSeen, GameTick.Zero);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method adds an entity to the list of visible entities, updates the last-seen tick, and computes any
|
||||
/// required game states.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool AddToSendList(PvsSession session, PvsData data, Entity<MetaDataComponent> entity, GameTick fromTick,
|
||||
bool entered)
|
||||
{
|
||||
DebugTools.Assert(fromTick < _gameTiming.CurTick);
|
||||
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
if (data == null)
|
||||
{
|
||||
Log.Error($"Encountered null EntityData.");
|
||||
return false;
|
||||
}
|
||||
|
||||
DebugTools.AssertNotEqual(data.LastSeen, _gameTiming.CurTick);
|
||||
DebugTools.Assert(data.EntityLastAcked <= fromTick || fromTick == GameTick.Zero);
|
||||
var (uid, meta) = entity;
|
||||
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
@@ -147,19 +167,18 @@ internal sealed partial class PvsSystem
|
||||
}
|
||||
|
||||
data.LastSeen = _gameTiming.CurTick;
|
||||
session.ToSend!.Add(data);
|
||||
EntityState state;
|
||||
session.ToSend!.Add(entity.Comp.PvsData);
|
||||
|
||||
if (session.RequestedFull)
|
||||
{
|
||||
state = GetFullEntityState(session.Session, uid, meta);
|
||||
var state = GetFullEntityState(session.Session, uid, meta);
|
||||
session.States.Add(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entered)
|
||||
{
|
||||
state = GetEntityState(session.Session, uid, data.EntityLastAcked, meta);
|
||||
var state = GetEntityState(session.Session, uid, data.EntityLastAcked, meta);
|
||||
session.States.Add(state);
|
||||
return true;
|
||||
}
|
||||
@@ -167,10 +186,10 @@ internal sealed partial class PvsSystem
|
||||
if (meta.EntityLastModifiedTick <= fromTick)
|
||||
return true;
|
||||
|
||||
state = GetEntityState(session.Session, uid, fromTick , meta);
|
||||
var entState = GetEntityState(session.Session, uid, fromTick , meta);
|
||||
|
||||
if (!state.Empty)
|
||||
session.States.Add(state);
|
||||
if (!entState.Empty)
|
||||
session.States.Add(entState);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -181,7 +200,7 @@ internal sealed partial class PvsSystem
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private (bool Entering, bool BudgetExceeded) IsEnteringPvsRange(
|
||||
PvsData data,
|
||||
ref PvsData data,
|
||||
GameTick fromTick,
|
||||
ref PvsBudget budget)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@@ -15,6 +16,8 @@ using Robust.Server.Replays;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
@@ -75,6 +78,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
private PvsAckJob _ackJob;
|
||||
private PvsChunkJob _chunkJob;
|
||||
private PvsLeaveJob _leaveJob;
|
||||
private PvsDeletionsJob _deletionJob;
|
||||
|
||||
private EntityQuery<EyeComponent> _eyeQuery;
|
||||
private EntityQuery<MetaDataComponent> _metaQuery;
|
||||
@@ -110,6 +114,10 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (Marshal.SizeOf<PvsMetadata>() != Marshal.SizeOf<PvsData>())
|
||||
throw new Exception($"Pvs struct sizes must match");
|
||||
|
||||
_deletionJob = new PvsDeletionsJob(this);
|
||||
_leaveJob = new PvsLeaveJob(this);
|
||||
_chunkJob = new PvsChunkJob(this);
|
||||
_ackJob = new PvsAckJob(this);
|
||||
@@ -125,6 +133,9 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
_transform.OnGlobalMoveEvent += OnEntityMove;
|
||||
EntityManager.EntityAdded += OnEntityAdded;
|
||||
EntityManager.EntityDeleted += OnEntityDeleted;
|
||||
EntityManager.AfterEntityFlush += AfterEntityFlush;
|
||||
|
||||
Subs.CVar(_configManager, CVars.NetPVS, SetPvs, true);
|
||||
Subs.CVar(_configManager, CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
|
||||
@@ -135,10 +146,10 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
_serverGameStateManager.ClientAck += OnClientAck;
|
||||
_serverGameStateManager.ClientRequestFull += OnClientRequestFull;
|
||||
_parallelMgr.ParallelCountChanged += ResetParallelism;
|
||||
|
||||
InitializeDirty();
|
||||
|
||||
_parallelMgr.ParallelCountChanged += ResetParallelism;
|
||||
InitializePvsArray();
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -147,15 +158,22 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||
_transform.OnGlobalMoveEvent -= OnEntityMove;
|
||||
EntityManager.EntityAdded -= OnEntityAdded;
|
||||
EntityManager.EntityDeleted -= OnEntityDeleted;
|
||||
EntityManager.AfterEntityFlush -= AfterEntityFlush;
|
||||
|
||||
_parallelMgr.ParallelCountChanged -= ResetParallelism;
|
||||
|
||||
_serverGameStateManager.ClientAck -= OnClientAck;
|
||||
_serverGameStateManager.ClientRequestFull -= OnClientRequestFull;
|
||||
|
||||
ClearPvsData();
|
||||
ShutdownDirty();
|
||||
_leaveTask?.WaitOne();
|
||||
_leaveTask = null;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
ProcessDeletions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -253,6 +271,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
session.LastReceivedAck = _gameTiming.CurTick;
|
||||
session.RequestedFull = true;
|
||||
ClearSendHistory(session);
|
||||
ClearPlayerPvsData(session);
|
||||
}
|
||||
|
||||
private void OnViewsizeChanged(float value)
|
||||
@@ -342,19 +361,20 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
{
|
||||
var toSend = pvsSession.ToSend;
|
||||
var toSendSet = new HashSet<NetEntity>(toSend!.Count);
|
||||
foreach (var data in toSend)
|
||||
|
||||
foreach (var intPtr in toSend)
|
||||
{
|
||||
toSendSet.Add(data.NetEntity);
|
||||
toSendSet.Add(IndexToNetEntity(intPtr));
|
||||
}
|
||||
DebugTools.AssertEqual(toSend.Count, toSendSet.Count);
|
||||
|
||||
foreach (var data in CollectionsMarshal.AsSpan(toSend))
|
||||
foreach (var intPtr in CollectionsMarshal.AsSpan(toSend))
|
||||
{
|
||||
ref var data = ref pvsSession.DataMemory.GetRef(intPtr.Index);
|
||||
DebugTools.AssertEqual(data.LastSeen, _gameTiming.CurTick);
|
||||
DebugTools.Assert(ReferenceEquals(data, pvsSession.Entities[data.NetEntity]));
|
||||
|
||||
// if an entity is visible, its parents should always be visible.
|
||||
if (_xformQuery.GetComponent(GetEntity(data.NetEntity)).ParentUid is not {Valid: true} pUid)
|
||||
if (_xformQuery.GetComponent(GetEntity(IndexToNetEntity(intPtr))).ParentUid is not {Valid: true} pUid)
|
||||
continue;
|
||||
|
||||
DebugTools.Assert(toSendSet.Contains(GetNetEntity(pUid)),
|
||||
@@ -362,11 +382,11 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
}
|
||||
|
||||
pvsSession.PreviouslySent.TryGetValue(_gameTiming.CurTick - 1, out var lastSent);
|
||||
foreach (var data in CollectionsMarshal.AsSpan(lastSent))
|
||||
foreach (var intPtr in CollectionsMarshal.AsSpan(lastSent))
|
||||
{
|
||||
DebugTools.Assert(!pvsSession.Entities.TryGetValue(data.NetEntity, out var old) || ReferenceEquals(data, old));
|
||||
ref var data = ref pvsSession.DataMemory.GetRef(intPtr.Index);
|
||||
DebugTools.Assert(data.LastSeen != GameTick.Zero);
|
||||
DebugTools.AssertEqual(toSendSet.Contains(data.NetEntity), data.LastSeen == _gameTiming.CurTick);
|
||||
DebugTools.AssertEqual(toSendSet.Contains(IndexToNetEntity(intPtr)), data.LastSeen == _gameTiming.CurTick);
|
||||
DebugTools.Assert(data.LastSeen == _gameTiming.CurTick
|
||||
|| data.LastSeen == _gameTiming.CurTick - 1);
|
||||
}
|
||||
@@ -409,7 +429,10 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
foreach (var session in _disconnected)
|
||||
{
|
||||
if (PlayerData.Remove(session, out var pvsSession))
|
||||
{
|
||||
ClearSendHistory(pvsSession);
|
||||
FreeSessionDataMemory(pvsSession);
|
||||
}
|
||||
}
|
||||
|
||||
var ackJob = ProcessQueuedAcks();
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.Replays;
|
||||
|
||||
@@ -13,7 +14,7 @@ internal sealed class ReplayRecordingManager : SharedReplayRecordingManager, ISe
|
||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
||||
|
||||
private PvsSystem _pvs = default!;
|
||||
private PvsSession _pvsSession = new(default!) { DisableCulling = true };
|
||||
private PvsSession _pvsSession = new(default!, new ResizableMemoryRegion<PvsData>(1)) { DisableCulling = true };
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -697,6 +697,32 @@ namespace Robust.Shared.Maths
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Round up (ceiling) a value to a multiple of a known power of two.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to round up.</param>
|
||||
/// <param name="powerOfTwo">
|
||||
/// The power of two to round up to a multiple of. The result is undefined if this is not a power of two.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// The result is undefined if either value is negative.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of integer to operate on.</typeparam>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// MathHelper.CeilMultiplyPowerOfTwo(5, 4) // 8
|
||||
/// MathHelper.CeilMultiplyPowerOfTwo(4, 4) // 4
|
||||
/// MathHelper.CeilMultiplyPowerOfTwo(8, 4) // 8
|
||||
/// </code>
|
||||
/// </example>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T CeilMultipleOfPowerOfTwo<T>(T value, T powerOfTwo) where T : IBinaryInteger<T>
|
||||
{
|
||||
var mask = powerOfTwo - T.One;
|
||||
var remainder = value & mask;
|
||||
return remainder == T.Zero ? value : (value | mask) + T.One;
|
||||
}
|
||||
|
||||
#endregion Public Members
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
return sound == null ? null : PlayGlobal(GetSound(sound), recipient, sound.Params);
|
||||
}
|
||||
|
||||
public abstract void LoadStream<T>(AudioComponent component, T stream);
|
||||
public abstract void LoadStream<T>(Entity<AudioComponent> entity, T stream);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file globally, without position.
|
||||
|
||||
@@ -183,6 +183,30 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<bool> NetPVS =
|
||||
CVarDef.Create("net.pvs", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Size increments for the automatic growth of Pvs' entity data storage. 0 will increase it by factors of 2
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> NetPvsEntityGrowth =
|
||||
CVarDef.Create("net.pvs_entity_growth", 1 << 16, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Initial size of PVS' entity data storage.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> NetPvsEntityInitial =
|
||||
CVarDef.Create("net.pvs_entity_initial", 1 << 16, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum ever size of PVS' entity data storage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Arbitrarily set to a default of 16 million entities.
|
||||
/// Increasing this parameter does not increase real memory usage, only virtual.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<int> NetPvsEntityMax =
|
||||
CVarDef.Create("net.pvs_entity_max", 1 << 24, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// If false, this will run more parts of PVS synchronously. This will generally slow it down, can be useful
|
||||
/// for collecting tick timing metrics.
|
||||
@@ -1031,6 +1055,12 @@ namespace Robust.Shared
|
||||
* AUDIO
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Default limit for concurrently playing an audio file.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> AudioDefaultConcurrent =
|
||||
CVarDef.Create("audio.default_concurrent", 16, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<int> AudioAttenuation =
|
||||
CVarDef.Create("audio.attenuation", (int) Attenuation.LinearDistanceClamped, CVar.REPLICATED | CVar.ARCHIVE);
|
||||
|
||||
|
||||
@@ -71,46 +71,6 @@ namespace Robust.Shared.Containers
|
||||
[DataField("showEnts")]
|
||||
public bool ShowContents { get; set; }
|
||||
|
||||
[Obsolete("Use container system method")]
|
||||
public bool Insert(
|
||||
EntityUid toinsert,
|
||||
IEntityManager? entMan = null,
|
||||
TransformComponent? transform = null,
|
||||
TransformComponent? ownerTransform = null,
|
||||
MetaDataComponent? meta = null,
|
||||
PhysicsComponent? physics = null,
|
||||
bool force = false)
|
||||
{
|
||||
IoCManager.Resolve(ref entMan);
|
||||
return entMan.System<SharedContainerSystem>().Insert((toinsert, transform, meta, physics), this, ownerTransform, force);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the given entity can be inserted into this container.
|
||||
/// </summary>
|
||||
/// <param name="assumeEmpty">Whether to assume that the container is currently empty.</param>
|
||||
protected internal virtual bool CanInsert(EntityUid toInsert, bool assumeEmpty, IEntityManager entMan) => true;
|
||||
|
||||
[Obsolete("Use container system method")]
|
||||
public bool Remove(
|
||||
EntityUid toRemove,
|
||||
IEntityManager? entMan = null,
|
||||
TransformComponent? xform = null,
|
||||
MetaDataComponent? meta = null,
|
||||
bool reparent = true,
|
||||
bool force = false,
|
||||
EntityCoordinates? destination = null,
|
||||
Angle? localRotation = null
|
||||
)
|
||||
{
|
||||
IoCManager.Resolve(ref entMan);
|
||||
return entMan.System<SharedContainerSystem>().Remove((toRemove, xform, meta), this, reparent, force, destination, localRotation);
|
||||
}
|
||||
|
||||
[Obsolete("Use container system method")]
|
||||
public void ForceRemove(EntityUid toRemove, IEntityManager? entMan = null, MetaDataComponent? meta = null)
|
||||
=> Remove(toRemove, entMan, meta: meta, reparent: false, force: true);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the entity is contained in this container.
|
||||
/// This is not recursive, so containers of children are not checked.
|
||||
@@ -120,18 +80,10 @@ namespace Robust.Shared.Containers
|
||||
public abstract bool Contains(EntityUid contained);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the container and marks it as deleted.
|
||||
/// Whether the given entity can be inserted into this container.
|
||||
/// </summary>
|
||||
[Obsolete("use system method")]
|
||||
public void Shutdown(IEntityManager? entMan = null, INetManager? _ = null)
|
||||
{
|
||||
IoCManager.Resolve(ref entMan);
|
||||
entMan.System<SharedContainerSystem>().ShutdownContainer(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Access(typeof(SharedContainerSystem))]
|
||||
protected internal abstract void InternalShutdown(IEntityManager entMan, SharedContainerSystem system, bool isClient);
|
||||
/// <param name="assumeEmpty">Whether to assume that the container is currently empty.</param>
|
||||
protected internal virtual bool CanInsert(EntityUid toInsert, bool assumeEmpty, IEntityManager entMan) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Implement to store the reference in whatever form you want
|
||||
@@ -148,5 +100,14 @@ namespace Robust.Shared.Containers
|
||||
/// <param name="entMan"></param>
|
||||
[Access(typeof(SharedContainerSystem))]
|
||||
protected internal abstract void InternalRemove(EntityUid toRemove, IEntityManager entMan);
|
||||
|
||||
/// <summary>
|
||||
/// Implement to clear the container and mark it as deleted.
|
||||
/// </summary>
|
||||
/// <param name="entMan"></param>
|
||||
/// <param name="system"></param>
|
||||
/// <param name=isClient"></param>
|
||||
[Access(typeof(SharedContainerSystem))]
|
||||
protected internal abstract void InternalShutdown(IEntityManager entMan, SharedContainerSystem system, bool isClient);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Robust.Shared.ContentPack
|
||||
String("short").ThenReturn(PrimitiveTypeCode.Int16);
|
||||
|
||||
private static readonly Parser<char, PrimitiveTypeCode> UInt16TypeParser =
|
||||
String("ushort").ThenReturn(PrimitiveTypeCode.UInt32);
|
||||
String("ushort").ThenReturn(PrimitiveTypeCode.UInt16);
|
||||
|
||||
private static readonly Parser<char, PrimitiveTypeCode> Int32TypeParser =
|
||||
String("int").ThenReturn(PrimitiveTypeCode.Int32);
|
||||
|
||||
@@ -84,12 +84,146 @@ Types:
|
||||
- "bool get_HasContents()"
|
||||
Lidgren.Network:
|
||||
NetBuffer:
|
||||
All: True
|
||||
Methods:
|
||||
- "byte[] get_Data()"
|
||||
- "void set_Data(byte[])"
|
||||
- "int get_LengthBytes()"
|
||||
- "void set_LengthBytes(int)"
|
||||
- "int get_LengthBits()"
|
||||
- "void set_LengthBits(int)"
|
||||
- "long get_Position()"
|
||||
- "void set_Position(long)"
|
||||
- "int get_PositionInBytes()"
|
||||
- "byte[] PeekDataBuffer()"
|
||||
- "bool PeekBoolean()"
|
||||
- "byte PeekByte()"
|
||||
- "sbyte PeekSByte()"
|
||||
- "byte PeekByte(int)"
|
||||
- "System.Span`1<byte> PeekBytes(System.Span`1<byte>)"
|
||||
- "byte[] PeekBytes(int)"
|
||||
- "void PeekBytes(byte[], int, int)"
|
||||
- "short PeekInt16()"
|
||||
- "ushort PeekUInt16()"
|
||||
- "int PeekInt32()"
|
||||
- "int PeekInt32(int)"
|
||||
- "uint PeekUInt32()"
|
||||
- "uint PeekUInt32(int)"
|
||||
- "ulong PeekUInt64()"
|
||||
- "long PeekInt64()"
|
||||
- "ulong PeekUInt64(int)"
|
||||
- "long PeekInt64(int)"
|
||||
- "float PeekFloat()"
|
||||
- "System.Half PeekHalf()"
|
||||
- "float PeekSingle()"
|
||||
- "double PeekDouble()"
|
||||
- "string PeekString()"
|
||||
- "int PeekStringSize()"
|
||||
- "bool ReadBoolean()"
|
||||
- "byte ReadByte()"
|
||||
- "bool ReadByte(ref byte)"
|
||||
- "sbyte ReadSByte()"
|
||||
- "byte ReadByte(int)"
|
||||
- "System.Span`1<byte> ReadBytes(System.Span`1<byte>)"
|
||||
- "byte[] ReadBytes(int)"
|
||||
- "bool ReadBytes(int, ref byte[])"
|
||||
- "bool TryReadBytes(System.Span`1<byte>)"
|
||||
- "void ReadBytes(byte[], int, int)"
|
||||
- "void ReadBits(System.Span`1<byte>, int)"
|
||||
- "void ReadBits(byte[], int, int)"
|
||||
- "short ReadInt16()"
|
||||
- "ushort ReadUInt16()"
|
||||
- "int ReadInt32()"
|
||||
- "bool ReadInt32(ref int)"
|
||||
- "int ReadInt32(int)"
|
||||
- "uint ReadUInt32()"
|
||||
- "bool ReadUInt32(ref uint)"
|
||||
- "uint ReadUInt32(int)"
|
||||
- "ulong ReadUInt64()"
|
||||
- "long ReadInt64()"
|
||||
- "ulong ReadUInt64(int)"
|
||||
- "long ReadInt64(int)"
|
||||
- "float ReadFloat()"
|
||||
- "System.Half ReadHalf()"
|
||||
- "float ReadSingle()"
|
||||
- "bool ReadSingle(ref float)"
|
||||
- "double ReadDouble()"
|
||||
- "uint ReadVariableUInt32()"
|
||||
- "bool ReadVariableUInt32(ref uint)"
|
||||
- "int ReadVariableInt32()"
|
||||
- "long ReadVariableInt64()"
|
||||
- "ulong ReadVariableUInt64()"
|
||||
- "float ReadSignedSingle(int)"
|
||||
- "float ReadUnitSingle(int)"
|
||||
- "float ReadRangedSingle(float, float, int)"
|
||||
- "int ReadRangedInteger(int, int)"
|
||||
- "long ReadRangedInteger(long, long)"
|
||||
- "string ReadString()"
|
||||
- "bool ReadString(ref string)"
|
||||
- "double ReadTime(Lidgren.Network.NetConnection, bool)"
|
||||
- "System.Net.IPEndPoint ReadIPEndPoint()"
|
||||
- "void SkipPadBits()"
|
||||
- "void ReadPadBits()"
|
||||
- "void SkipPadBits(int)"
|
||||
- "void EnsureBufferSize(int)"
|
||||
- "void Write(bool)"
|
||||
- "void Write(byte)"
|
||||
- "void WriteAt(int, byte)"
|
||||
- "void Write(sbyte)"
|
||||
- "void Write(byte, int)"
|
||||
- "void Write(byte[])"
|
||||
- "void Write(System.ReadOnlySpan`1<byte>)"
|
||||
- "void Write(byte[], int, int)"
|
||||
- "void Write(ushort)"
|
||||
- "void WriteAt(int, ushort)"
|
||||
- "void Write(ushort, int)"
|
||||
- "void Write(short)"
|
||||
- "void WriteAt(int, short)"
|
||||
- "void Write(int)"
|
||||
- "void WriteAt(int, int)"
|
||||
- "void Write(uint)"
|
||||
- "void WriteAt(int, uint)"
|
||||
- "void Write(uint, int)"
|
||||
- "void Write(int, int)"
|
||||
- "void Write(ulong)"
|
||||
- "void WriteAt(int, ulong)"
|
||||
- "void Write(ulong, int)"
|
||||
- "void Write(long)"
|
||||
- "void Write(long, int)"
|
||||
- "void Write(System.Half)"
|
||||
- "void Write(float)"
|
||||
- "void Write(double)"
|
||||
- "int WriteVariableUInt32(uint)"
|
||||
- "int WriteVariableInt32(int)"
|
||||
- "int WriteVariableInt64(long)"
|
||||
- "int WriteVariableUInt64(ulong)"
|
||||
- "void WriteSignedSingle(float, int)"
|
||||
- "void WriteUnitSingle(float, int)"
|
||||
- "void WriteRangedSingle(float, float, float, int)"
|
||||
- "int WriteRangedInteger(int, int, int)"
|
||||
- "int WriteRangedInteger(long, long, long)"
|
||||
- "void Write(string)"
|
||||
- "void Write(System.Net.IPEndPoint)"
|
||||
- "void WriteTime(bool)"
|
||||
- "void WriteTime(double, bool)"
|
||||
- "void WritePadBits()"
|
||||
- "void WritePadBits(int)"
|
||||
- "void Write(Lidgren.Network.NetBuffer)"
|
||||
- "void Zero(int)"
|
||||
- "void .ctor()"
|
||||
NetDeliveryMethod: { }
|
||||
NetIncomingMessage:
|
||||
All: True
|
||||
Methods:
|
||||
- "Lidgren.Network.NetIncomingMessageType get_MessageType()"
|
||||
- "Lidgren.Network.NetDeliveryMethod get_DeliveryMethod()"
|
||||
- "int get_SequenceChannel()"
|
||||
- "System.Net.IPEndPoint get_SenderEndPoint()"
|
||||
- "Lidgren.Network.NetConnection get_SenderConnection()"
|
||||
- "double get_ReceiveTime()"
|
||||
- "double ReadTime(bool)"
|
||||
- "string ToString()"
|
||||
NetOutgoingMessage:
|
||||
All: True
|
||||
Methods:
|
||||
- "string ToString()"
|
||||
Nett:
|
||||
CommentLocation: { } # Enum
|
||||
Toml:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.ContentPack
|
||||
@@ -135,11 +136,37 @@ namespace Robust.Shared.ContentPack
|
||||
path = path.Directory;
|
||||
|
||||
var fullPath = GetFullPath(path);
|
||||
Process.Start(new ProcessStartInfo
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
UseShellExecute = true,
|
||||
FileName = fullPath,
|
||||
});
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "explorer.exe",
|
||||
Arguments = ".",
|
||||
WorkingDirectory = fullPath,
|
||||
});
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "open",
|
||||
Arguments = ".",
|
||||
WorkingDirectory = fullPath,
|
||||
});
|
||||
}
|
||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD())
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "xdg-open",
|
||||
Arguments = ".",
|
||||
WorkingDirectory = fullPath,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Opening OS windows not supported on this OS");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -199,6 +200,11 @@ namespace Robust.Shared.GameObjects
|
||||
// (Creation can still be cleared though)
|
||||
ClearCreationTick();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offset into internal PVS data.
|
||||
/// </summary>
|
||||
internal PvsIndex PvsData;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
@@ -221,16 +227,32 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
Detached = 1 << 2,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates this entity can never be handled by the client as PVS detached.
|
||||
/// </summary>
|
||||
Undetachable = 1 << 3,
|
||||
|
||||
/// <summary>
|
||||
/// If true, then this entity is considered a "high priority" entity and will be sent to players from further
|
||||
/// away. Useful for things like light sources and occluders. Only works if the entity is directly parented to
|
||||
/// a grid or map.
|
||||
/// </summary>
|
||||
PvsPriority = 1 << 3,
|
||||
PvsPriority = 1 << 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key struct for uniquely identifying a PVS chunk.
|
||||
/// </summary>
|
||||
internal readonly record struct PvsChunkLocation(EntityUid Uid, Vector2i Indices);
|
||||
|
||||
/// <summary>
|
||||
/// An opaque index into the PVS data arrays on the server.
|
||||
/// </summary>
|
||||
internal readonly record struct PvsIndex(int Index)
|
||||
{
|
||||
/// <summary>
|
||||
/// An invalid index. This is also used as a marker value in the free list.
|
||||
/// </summary>
|
||||
public static readonly PvsIndex Invalid = new PvsIndex(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public partial class EntityManager
|
||||
/// </summary>
|
||||
internal void SetNetEntity(EntityUid uid, NetEntity netEntity, MetaDataComponent component)
|
||||
{
|
||||
DebugTools.Assert(component.NetEntity == NetEntity.Invalid || _netMan.IsClient);
|
||||
DebugTools.Assert(!NetEntityLookup.ContainsKey(netEntity));
|
||||
NetEntityLookup[netEntity] = (uid, component);
|
||||
component.NetEntity = netEntity;
|
||||
|
||||
@@ -906,6 +906,16 @@ namespace Robust.Shared.GameObjects
|
||||
DebugTools.Assert("Why are you raising predictive events on the server?");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises an event locally on client or networked on server.
|
||||
/// </summary>
|
||||
public abstract void RaiseSharedEvent<T>(T message, EntityUid? user = null) where T : EntityEventArgs;
|
||||
|
||||
/// <summary>
|
||||
/// Raises an event locally on client or networked on server.
|
||||
/// </summary>
|
||||
public abstract void RaiseSharedEvent<T>(T message, ICommonSession? user = null) where T : EntityEventArgs;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for generating a new EntityUid for an entity currently being created.
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
@@ -744,9 +745,20 @@ public sealed partial class EntityLookupSystem
|
||||
}
|
||||
|
||||
public HashSet<EntityUid> GetEntitiesInRange(EntityCoordinates coordinates, float range, LookupFlags flags = DefaultFlags)
|
||||
{
|
||||
var ents = new HashSet<EntityUid>();
|
||||
GetEntitiesInRange(coordinates, range, ents, flags);
|
||||
return ents;
|
||||
}
|
||||
|
||||
public void GetEntitiesInRange(EntityCoordinates coordinates, float range, HashSet<EntityUid> entities, LookupFlags flags = DefaultFlags)
|
||||
{
|
||||
var mapPos = coordinates.ToMap(EntityManager, _transform);
|
||||
return GetEntitiesInRange(mapPos, range, flags);
|
||||
|
||||
if (mapPos.MapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
GetEntitiesInRange(mapPos.MapId, mapPos.Position, range, entities, flags);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -616,7 +616,7 @@ public abstract partial class SharedTransformSystem
|
||||
TransformComponent? newParent = null,
|
||||
TransformComponent? oldParent = null)
|
||||
{
|
||||
SetCoordinates((uid, xform, MetaData(uid)), value, rotation, unanchor, newParent, oldParent);
|
||||
SetCoordinates((uid, xform, _metaQuery.GetComponent(uid)), value, rotation, unanchor, newParent, oldParent);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Robust.Shared.Network
|
||||
{
|
||||
@@ -31,9 +30,10 @@ namespace Robust.Shared.Network
|
||||
/// </summary>
|
||||
public sealed class NetConnectingArgs : EventArgs
|
||||
{
|
||||
public bool IsDenied => DenyReason != null;
|
||||
public bool IsDenied => DenyReasonData != null;
|
||||
|
||||
public string? DenyReason { get; private set; }
|
||||
public string? DenyReason => DenyReasonData?.Text;
|
||||
public NetDenyReason? DenyReasonData { get; private set; }
|
||||
|
||||
public NetUserData UserData { get; }
|
||||
|
||||
@@ -48,7 +48,12 @@ namespace Robust.Shared.Network
|
||||
|
||||
public void Deny(string reason)
|
||||
{
|
||||
DenyReason = reason;
|
||||
Deny(new NetDenyReason(reason));
|
||||
}
|
||||
|
||||
public void Deny(NetDenyReason reason)
|
||||
{
|
||||
DenyReasonData = reason;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,12 +70,29 @@ namespace Robust.Shared.Network
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains a reason for denying a client connection to the game server.
|
||||
/// </summary>
|
||||
/// <param name="Text">The textual reason, presented to the user.</param>
|
||||
/// <param name="AdditionalProperties">
|
||||
/// Additional JSON properties that will be included in the <see cref="NetDisconnectMessage"/>.
|
||||
/// Valid value types are: string, int, float, bool.
|
||||
/// </param>
|
||||
/// <seealso cref="NetDisconnectMessage"/>
|
||||
/// <seealso cref="NetConnectingArgs"/>
|
||||
public record NetDenyReason(string Text, Dictionary<string, object> AdditionalProperties)
|
||||
{
|
||||
public NetDenyReason(string Text) : this(Text, new Dictionary<string, object>())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structured reason common interface.
|
||||
/// </summary>
|
||||
public interface INetStructuredReason
|
||||
{
|
||||
JsonObject StructuredReason { get; }
|
||||
NetDisconnectMessage Message { get; }
|
||||
string Reason { get; }
|
||||
bool RedialFlag { get; }
|
||||
}
|
||||
@@ -80,33 +102,33 @@ namespace Robust.Shared.Network
|
||||
/// </summary>
|
||||
public sealed class NetConnectFailArgs : EventArgs, INetStructuredReason
|
||||
{
|
||||
public NetConnectFailArgs(string reason) : this(NetStructuredDisconnectMessages.Decode(reason))
|
||||
public NetConnectFailArgs(string reason) : this(NetDisconnectMessage.Decode(reason))
|
||||
{
|
||||
}
|
||||
|
||||
public NetConnectFailArgs(JsonObject reason)
|
||||
internal NetConnectFailArgs(NetDisconnectMessage reason)
|
||||
{
|
||||
StructuredReason = reason;
|
||||
Message = reason;
|
||||
}
|
||||
|
||||
public JsonObject StructuredReason { get; }
|
||||
public string Reason => NetStructuredDisconnectMessages.ReasonOf(StructuredReason);
|
||||
public bool RedialFlag => NetStructuredDisconnectMessages.RedialFlagOf(StructuredReason);
|
||||
public NetDisconnectMessage Message { get; }
|
||||
public string Reason => Message.Reason;
|
||||
public bool RedialFlag => Message.RedialFlag;
|
||||
}
|
||||
|
||||
public sealed class NetDisconnectedArgs : NetChannelArgs, INetStructuredReason
|
||||
{
|
||||
public NetDisconnectedArgs(INetChannel channel, string reason) : this(channel, NetStructuredDisconnectMessages.Decode(reason))
|
||||
public NetDisconnectedArgs(INetChannel channel, string reason) : this(channel, NetDisconnectMessage.Decode(reason))
|
||||
{
|
||||
}
|
||||
|
||||
public NetDisconnectedArgs(INetChannel channel, JsonObject reason) : base(channel)
|
||||
internal NetDisconnectedArgs(INetChannel channel, NetDisconnectMessage reason) : base(channel)
|
||||
{
|
||||
StructuredReason = reason;
|
||||
Message = reason;
|
||||
}
|
||||
|
||||
public JsonObject StructuredReason { get; }
|
||||
public string Reason => NetStructuredDisconnectMessages.ReasonOf(StructuredReason);
|
||||
public bool RedialFlag => NetStructuredDisconnectMessages.RedialFlagOf(StructuredReason);
|
||||
public NetDisconnectMessage Message { get; }
|
||||
public string Reason => Message.Reason;
|
||||
public bool RedialFlag => Message.RedialFlag;
|
||||
}
|
||||
}
|
||||
|
||||
259
Robust.Shared/Network/NetDisconnectMessage.cs
Normal file
259
Robust.Shared/Network/NetDisconnectMessage.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Network;
|
||||
|
||||
// Why did this dinky class grow to this LOC...
|
||||
|
||||
/// <summary>
|
||||
/// Stores structured information about why a connection was denied or disconnected.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The core networking layer (Lidgren) allows passing plain strings for disconnect reasons.
|
||||
/// We can beam a structured format (like JSON) over this,
|
||||
/// but Lidgren also generates messages internally (such as on timeout).
|
||||
/// This class is responsible for bridging the two to produce consistent results.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Disconnect messages are just a simple key/value format.
|
||||
/// Valid value types are <see cref="int"/>, <see cref="float"/>, <see cref="bool"/>, and <see cref="string"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class NetDisconnectMessage
|
||||
{
|
||||
private const string LidgrenDisconnectedPrefix = "Disconnected: ";
|
||||
|
||||
/// <summary>
|
||||
/// The reason given if none was included in the structured message.
|
||||
/// </summary>
|
||||
internal const string DefaultReason = "unknown reason";
|
||||
|
||||
/// <summary>
|
||||
/// The default redial flag given if none was included in the structured message.
|
||||
/// </summary>
|
||||
internal const bool DefaultRedialFlag = false;
|
||||
|
||||
/// <summary>
|
||||
/// The key of the <see cref="Reason"/> value.
|
||||
/// </summary>
|
||||
public const string ReasonKey = "reason";
|
||||
|
||||
/// <summary>
|
||||
/// The key of the <see cref="RedialFlag"/> value.
|
||||
/// </summary>
|
||||
public const string RedialKey = "redial";
|
||||
|
||||
internal readonly Dictionary<string, object> Values;
|
||||
|
||||
internal NetDisconnectMessage(Dictionary<string, object> values)
|
||||
{
|
||||
Values = values;
|
||||
}
|
||||
|
||||
internal NetDisconnectMessage(
|
||||
string reason = DefaultReason,
|
||||
bool redialFlag = DefaultRedialFlag)
|
||||
{
|
||||
Values = new Dictionary<string, object>
|
||||
{
|
||||
{ ReasonKey, reason },
|
||||
{ RedialKey, redialFlag }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The human-readable reason for why the disconnection happened.
|
||||
/// </summary>
|
||||
/// <seealso cref="ReasonKey"/>
|
||||
public string Reason => StringOf(ReasonKey, DefaultReason);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the client should "redial" to reconnect to the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Redial means the client gets restarted by the launcher, to enable an update to occur.
|
||||
/// This is generally set if the disconnection reason is some sort of version mismatch.
|
||||
/// </remarks>
|
||||
/// <seealso cref="RedialKey"/>
|
||||
public bool RedialFlag => BoolOf(RedialKey, DefaultRedialFlag);
|
||||
|
||||
/// <summary>
|
||||
/// Decode from a disconnect message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If structured JSON can be extracted, it is used.
|
||||
/// Otherwise, or if the format is invalid, the entire input is returned as disconnect reason.
|
||||
/// </para>
|
||||
/// <para>Invalid JSON values (e.g. arrays) are discarded.</para>
|
||||
/// </remarks>
|
||||
/// <param name="text">The disconnect reason from Lidgren's disconnect message.</param>
|
||||
internal static NetDisconnectMessage Decode(string text)
|
||||
{
|
||||
var start = text.AsMemory().TrimStart();
|
||||
// Lidgren generates this prefix internally.
|
||||
if (start.Span.StartsWith(LidgrenDisconnectedPrefix))
|
||||
start = start[LidgrenDisconnectedPrefix.Length..];
|
||||
// If it starts with { it's probably a JSON object.
|
||||
if (start.Span.StartsWith("{"))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var node = JsonDocument.Parse(start);
|
||||
DebugTools.Assert(node.RootElement.ValueKind == JsonValueKind.Object);
|
||||
return JsonToReason(node.RootElement);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Discard the exception
|
||||
}
|
||||
}
|
||||
|
||||
// Something went wrong. That probably means it's not a structured reason.
|
||||
// Or worst case scenario, some poor end-user has to look at half-broken JSON.
|
||||
return new NetDisconnectMessage(new Dictionary<string, object>
|
||||
{
|
||||
{ ReasonKey, text }
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode to a textual string, that can be embedded into a disconnect message.
|
||||
/// </summary>
|
||||
internal string Encode()
|
||||
{
|
||||
return JsonSerializer.Serialize(Values);
|
||||
}
|
||||
|
||||
private static NetDisconnectMessage JsonToReason(JsonElement obj)
|
||||
{
|
||||
DebugTools.Assert(obj.ValueKind == JsonValueKind.Object);
|
||||
|
||||
var dict = new Dictionary<string, object>();
|
||||
foreach (var property in obj.EnumerateObject())
|
||||
{
|
||||
object value;
|
||||
switch (property.Value.ValueKind)
|
||||
{
|
||||
case JsonValueKind.String:
|
||||
value = property.Value.GetString()!;
|
||||
break;
|
||||
case JsonValueKind.Number:
|
||||
if (property.Value.TryGetInt32(out var valueInt))
|
||||
value = valueInt;
|
||||
else
|
||||
value = property.Value.GetSingle();
|
||||
break;
|
||||
case JsonValueKind.True:
|
||||
case JsonValueKind.False:
|
||||
value = property.Value.GetBoolean();
|
||||
break;
|
||||
default:
|
||||
// Discard invalid values intentionally.
|
||||
continue;
|
||||
}
|
||||
|
||||
dict[property.Name] = value;
|
||||
}
|
||||
|
||||
return new NetDisconnectMessage(dict);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a value by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to look up.</param>
|
||||
/// <returns>
|
||||
/// Null if no such value exists, otherwise an object of one of the valid types (int, float, string, bool).
|
||||
/// </returns>
|
||||
public object? ValueOf(string key)
|
||||
{
|
||||
return Values.GetValueOrDefault(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="string"/> value by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to look up.</param>
|
||||
/// <param name="defaultValue">Default value to return if the value does not exist or is the wrong type.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="string"/> value with the given key,
|
||||
/// or <paramref name="defaultValue"/> if no such value exists or it's a different type.
|
||||
/// </returns>
|
||||
[return: NotNullIfNotNull(nameof(defaultValue))]
|
||||
public string? StringOf(string key, string? defaultValue = null)
|
||||
{
|
||||
if (ValueOf(key) is not string valueString)
|
||||
return defaultValue;
|
||||
|
||||
return valueString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="bool"/> value by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to look up.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="bool"/> value with the given key, or <see langword="null" /> if no such value exists or it's a different type.
|
||||
/// </returns>
|
||||
public bool? BoolOf(string key) => ValueOf(key) as bool?;
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="bool"/> value by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to look up.</param>
|
||||
/// <param name="defaultValue">Default value to return if the value does not exist or is the wrong type.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="bool"/> value with the given key,
|
||||
/// or <paramref name="defaultValue"/> if no such value exists or it's a different type.
|
||||
/// </returns>
|
||||
public bool BoolOf(string key, bool defaultValue) => BoolOf(key) ?? defaultValue;
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="int"/> value by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to look up.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="int"/> value with the given key, or <see langword="null" /> if no such value exists or it's a different type.
|
||||
/// </returns>
|
||||
public int? Int32Of(string key) => ValueOf(key) as int?;
|
||||
|
||||
/// <summary>
|
||||
/// Get an <see cref="Int32"/> value by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to look up.</param>
|
||||
/// <param name="defaultValue">Default value to return if the value does not exist or is the wrong type.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="Int32"/> value with the given key,
|
||||
/// or <paramref name="defaultValue"/> if no such value exists or it's a different type.
|
||||
/// </returns>
|
||||
public int Int32Of(string key, int defaultValue) => Int32Of(key) ?? defaultValue;
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="float"/> value by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to look up.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="float"/> value with the given key, or <see langword="null" /> if no such value exists or it's a different type.
|
||||
/// </returns>
|
||||
public float? SingleOf(string key)
|
||||
{
|
||||
var value = ValueOf(key);
|
||||
return value as float? ?? value as int?;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="Single"/> value by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to look up.</param>
|
||||
/// <param name="defaultValue">Default value to return if the value does not exist or is the wrong type.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="Single"/> value with the given key,
|
||||
/// or <paramref name="defaultValue"/> if no such value exists or it's a different type.
|
||||
/// </returns>
|
||||
public float SingleOf(string key, float defaultValue) => SingleOf(key) ?? defaultValue;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace Robust.Shared.Network
|
||||
{
|
||||
partial class NetManager
|
||||
{
|
||||
private readonly static string DisconnectReasonWrongKey = NetStructuredDisconnectMessages.Encode("Token decryption failed.\nPlease reconnect to this server from the launcher.", true);
|
||||
private static readonly string DisconnectReasonWrongKey = new NetDisconnectMessage("Token decryption failed.\nPlease reconnect to this server from the launcher.", true).Encode();
|
||||
|
||||
private readonly byte[] _cryptoPrivateKey = new byte[CryptoBox.SecretKeyBytes];
|
||||
|
||||
@@ -211,9 +211,15 @@ namespace Robust.Shared.Network
|
||||
|
||||
var endPoint = connection.RemoteEndPoint;
|
||||
var connect = await OnConnecting(endPoint, userData, type);
|
||||
if (connect.IsDenied)
|
||||
if (connect.DenyReasonData is { } deny)
|
||||
{
|
||||
connection.Disconnect($"Connection denied: {connect.DenyReason}");
|
||||
var denyMsg = $"Connect denied: {deny.Text}";
|
||||
var structured = new NetDisconnectMessage(denyMsg);
|
||||
foreach (var (k, v) in deny.AdditionalProperties)
|
||||
{
|
||||
structured.Values[k] = v;
|
||||
}
|
||||
connection.Disconnect(structured.Encode());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Robust.Shared.Network;
|
||||
|
||||
/// <summary>
|
||||
/// Structured disconnection utilities.
|
||||
/// These use JsonNode so that Content may add it's own data.
|
||||
/// Note that to prevent encoding a NetStructuredDisco value within a NetStructuredDisco value,
|
||||
/// these should be encoded at the "highest level".
|
||||
/// Whatever generates the final "reason" value is responsible for performing NetStructuredDisco.Encode.
|
||||
/// </summary>
|
||||
public static class NetStructuredDisconnectMessages
|
||||
{
|
||||
public const string ReasonKey = "reason";
|
||||
public const string RedialKey = "redial";
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a structured disconnect message into a JsonObject.
|
||||
/// That can then be extended with additional properties.
|
||||
/// </summary>
|
||||
public static JsonObject EncodeObject(string text, bool redialFlag = false)
|
||||
{
|
||||
JsonObject obj = new();
|
||||
obj[ReasonKey] = text;
|
||||
obj[RedialKey] = redialFlag;
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a structured disconnect message.
|
||||
/// Note that using this kind of gets in the way of adding content properties.
|
||||
/// </summary>
|
||||
public static string Encode(string text, bool redialFlag = false) => Encode(EncodeObject(text, redialFlag));
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a structured disconnect message from a JsonObject.
|
||||
/// </summary>
|
||||
public static string Encode(JsonObject obj) => obj.ToJsonString();
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a structured disconnect message.
|
||||
/// This is designed assuming the input isn't necessarily a structured disconnect message.
|
||||
/// As such this will always produce output that can be passed to ReasonOf.
|
||||
/// </summary>
|
||||
public static JsonObject Decode(string text)
|
||||
{
|
||||
var start = text.AsSpan().TrimStart();
|
||||
// Lidgren generates this prefix internally.
|
||||
var lidgrenDisconnectedPrefix = "Disconnected: ";
|
||||
if (start.StartsWith(lidgrenDisconnectedPrefix))
|
||||
start = start.Slice(lidgrenDisconnectedPrefix.Length);
|
||||
// If it starts with { it's probably a JSON object.
|
||||
if (start.StartsWith("{"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var node = JsonNode.Parse(new string(start));
|
||||
if (node != null)
|
||||
return (JsonObject)node;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Discard the exception
|
||||
}
|
||||
}
|
||||
|
||||
// Something went wrong, so...
|
||||
JsonObject fallback = new();
|
||||
fallback[ReasonKey] = text;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a property as a JsonValue.
|
||||
/// </summary>
|
||||
public static JsonValue? ValueOf(JsonObject obj, string key)
|
||||
{
|
||||
if (obj.TryGetPropertyValue(key, out var val))
|
||||
if (val is JsonValue)
|
||||
return ((JsonValue) val);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode a string property.
|
||||
/// </summary>
|
||||
public static string StringOf(JsonObject obj, string key, string def)
|
||||
{
|
||||
var val = ValueOf(obj, key);
|
||||
if (val != null && val.TryGetValue(out string? res))
|
||||
return res;
|
||||
return def;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grab the redial flag.
|
||||
/// </summary>
|
||||
public static bool BoolOf(JsonObject obj, string key, bool def)
|
||||
{
|
||||
var val = ValueOf(obj, key);
|
||||
if (val != null && val.TryGetValue(out bool res))
|
||||
return res;
|
||||
return def;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode a reason.
|
||||
/// </summary>
|
||||
public static string ReasonOf(JsonObject obj) => StringOf(obj, ReasonKey, "unknown reason");
|
||||
|
||||
/// <summary>
|
||||
/// Grab the redial flag.
|
||||
/// </summary>
|
||||
public static bool RedialFlagOf(JsonObject obj) => BoolOf(obj, RedialKey, false);
|
||||
}
|
||||
|
||||
338
Robust.Shared/Utility/ResizableMemoryRegion.cs
Normal file
338
Robust.Shared/Utility/ResizableMemoryRegion.cs
Normal file
@@ -0,0 +1,338 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Maths;
|
||||
using static TerraFX.Interop.Windows.Windows;
|
||||
using static TerraFX.Interop.Windows.MEM;
|
||||
using static TerraFX.Interop.Windows.PAGE;
|
||||
|
||||
namespace Robust.Shared.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation detail to store metrics for <see cref="ResizableMemoryRegion{T}"/>.
|
||||
/// </summary>
|
||||
internal static class ResizableMemoryRegionMetrics
|
||||
{
|
||||
public static readonly Meter Meter = new("Robust.Shared.Utility.ResizableMemoryRegion");
|
||||
|
||||
public const string GaugeName = "robust_resizable_memory_used_bytes";
|
||||
}
|
||||
|
||||
// TODO: Proper implementation on Linux that uses mmap()/madvise()/mprotect().
|
||||
|
||||
/// <summary>
|
||||
/// An unmanaged region of memory that can be dynamically resized without requiring copying.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The maximum size of the memory region must be specified in the constructor. This reserves virtual memory for later,
|
||||
/// but does not charge actual physical memory or commit charge and therefore costs nothing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The "real" allocated memory region can be expanded by calling <see cref="Expand"/>.
|
||||
/// This will increase resource consumption and make more memory available for use.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Allocated memory starts initialized to 0.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of elements stored in the memory region.</typeparam>
|
||||
internal sealed unsafe class ResizableMemoryRegion<T> : IDisposable where T : unmanaged
|
||||
{
|
||||
private static readonly UpDownCounter<long> MemoryUsageGauge =
|
||||
ResizableMemoryRegionMetrics.Meter.CreateUpDownCounter<long>(
|
||||
ResizableMemoryRegionMetrics.GaugeName,
|
||||
"bytes",
|
||||
"The amount of committed memory used by ResizableMemoryRegion<T> instances.",
|
||||
new[] { new KeyValuePair<string, object?>("type", typeof(T).FullName) });
|
||||
|
||||
/// <summary>
|
||||
/// The pointer to the start of the allocated memory region. Use with care!
|
||||
/// </summary>
|
||||
public T* BaseAddress { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum amount of elements that can be stored in this memory region.
|
||||
/// </summary>
|
||||
public int MaxSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current space (in elements) commit that is directly accessible.
|
||||
/// </summary>
|
||||
public int CurrentSize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ResizableMemoryRegion{T}"/> with a certain maximum and initial size.
|
||||
/// </summary>
|
||||
/// <param name="maxElementSize">The maximum amount of elements ever stored in this memory region.</param>
|
||||
/// <param name="initialElementSize">
|
||||
/// The initial amount of elements that will be immediately accessible without using <see cref="Expand"/>.
|
||||
/// </param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown if the memory region is already initialized.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown if <paramref name="maxElementSize"/> is zero
|
||||
/// or <paramref name="initialElementSize"/> is greater than <paramref name="maxElementSize"/>.
|
||||
/// </exception>
|
||||
public ResizableMemoryRegion(int maxElementSize, int initialElementSize = 0)
|
||||
{
|
||||
if (BaseAddress != null)
|
||||
throw new InvalidOperationException("Memory region is already initialized!");
|
||||
|
||||
if (initialElementSize > maxElementSize)
|
||||
throw new ArgumentException("initialSize must be smaller than maxSize");
|
||||
|
||||
if (maxElementSize == 0)
|
||||
throw new ArgumentException("Cannot allocate a 0-byte memory region!");
|
||||
|
||||
var maxByteSize = checked((nuint)sizeof(T) * (nuint)maxElementSize);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// On Windows, we MEM_RESERVE a large chunk of memory and then MEM_COMMIT it later when expanding.
|
||||
|
||||
BaseAddress = (T*)VirtualAlloc(null, maxByteSize, MEM_RESERVE, PAGE_NOACCESS);
|
||||
if (BaseAddress == null)
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-Windows systems use some form of overcommit,
|
||||
// and therefore we don't need to separately reserve and commit memory.
|
||||
// So we can just calloc() a fuck-huge chunk of memory and call it a day.
|
||||
//
|
||||
// It's important that we use calloc() and don't manually fill the memory region,
|
||||
// as that will avoid immediately assigning unused memory pages.
|
||||
//
|
||||
// Note that we still pretend to client code that this works the same as on Windows,
|
||||
// e.g. the memory is not writable until expanded into. We do this so that client code does not prematurely
|
||||
// populate the memory, e.g. PVS code filling it with a free list.
|
||||
BaseAddress = (T*)NativeMemory.AllocZeroed(maxByteSize);
|
||||
|
||||
// What about Linux with overcommit disabled? Not a real use case, ignored.
|
||||
}
|
||||
|
||||
MaxSize = maxElementSize;
|
||||
|
||||
Expand(initialElementSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expand the committed space for this <see cref="ResizableMemoryRegion{T}"/> to have space for at least
|
||||
/// <paramref name="newElementSize"/> elements.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This operation happens without copying and existing references to the memory region remain valid.
|
||||
/// </remarks>
|
||||
/// <param name="newElementSize">The minimum amount of elements that should fit in the memory region.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown if <paramref name="newElementSize"/> is greater than <see cref="MaxSize"/>.
|
||||
/// </exception>
|
||||
public void Expand(int newElementSize)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (newElementSize > MaxSize)
|
||||
throw new ArgumentException("Cannot expand memory region past max size.", nameof(newElementSize));
|
||||
|
||||
if (newElementSize <= CurrentSize)
|
||||
return;
|
||||
|
||||
var previousSize = CurrentSize;
|
||||
|
||||
var newByteSize = (nuint)sizeof(T) * (nuint)newElementSize;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var ret = VirtualAlloc(BaseAddress, newByteSize, MEM_COMMIT, PAGE_READWRITE);
|
||||
if (ret == null)
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nada. On overcommit systems we don't need to do anything.
|
||||
}
|
||||
|
||||
CurrentSize = newElementSize;
|
||||
|
||||
MemoryUsageGauge.Add((newElementSize - previousSize) * sizeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shrink this <see cref="ResizableMemoryRegion{T}"/> to reduce the amount of memory used.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Existing references inside the shrank-away region of memory become undefined to read or write from,
|
||||
/// and can cause access violations.
|
||||
/// </remarks>
|
||||
/// <param name="newElementSize">
|
||||
/// The new size of the committed region of memory.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown if trying to shrink to a size larger than the current size, or when trying to shrink to a negative size.
|
||||
/// </exception>
|
||||
public void Shrink(int newElementSize)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (newElementSize > CurrentSize)
|
||||
throw new ArgumentException("Cannot shrink to a larger size!", nameof(newElementSize));
|
||||
|
||||
if (newElementSize < 0)
|
||||
throw new ArgumentException("Cannot shrink to a negative size!", nameof(newElementSize));
|
||||
|
||||
var currentByteSize = (nuint)sizeof(T) * (nuint)CurrentSize;
|
||||
var newByteSize = (nuint)sizeof(T) * (nuint)newElementSize;
|
||||
|
||||
// If the new max size cuts a page in the middle we can't free it so round up to the next page.
|
||||
var newPageSize = MathHelper.CeilMultipleOfPowerOfTwo(newByteSize, (nuint) Environment.SystemPageSize);
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var freeBaseAddress = (byte*)BaseAddress + newPageSize;
|
||||
var freeLength = currentByteSize - newPageSize;
|
||||
var result = VirtualFree(freeBaseAddress, freeLength, MEM_DECOMMIT);
|
||||
if (!result)
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing to do on operating systems without advanced memory management.
|
||||
}
|
||||
|
||||
CurrentSize = newElementSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="Span{T}"/> over the committed region of memory.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Span{T}"/> over the committed region of memory.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<T> GetSpan()
|
||||
{
|
||||
// If the memory region is disposed, CurrentSize is 0 so it's impossible to use the (nullptr) BaseAddress.
|
||||
// This means you can't dereference the span anyways, so that works out fine!
|
||||
return new Span<T>(BaseAddress, CurrentSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="Span{T}"/> over the committed region of memory, cast to a different type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is equivalent to using <see cref="MemoryMarshal.Cast{TFrom, TTo}(Span{TFrom})"/> on the result of <see cref="GetSpan"/>.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TCast">The type to cast the memory region to.</typeparam>
|
||||
/// <returns>A <see cref="Span{T}"/> over the committed region of memory.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<TCast> GetSpan<TCast>() where TCast : unmanaged
|
||||
{
|
||||
return MemoryMarshal.Cast<T, TCast>(GetSpan());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a mutable reference to a single element of the memory region.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the element desired.</param>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// Thrown if <paramref name="index"/> is greater or equal to <see cref="CurrentSize"/>.
|
||||
/// </exception>
|
||||
/// <returns>A mutable reference to the element.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ref T GetRef(int index)
|
||||
{
|
||||
// If the memory region is disposed, CurrentSize is 0 and this check always fails.
|
||||
if (index >= CurrentSize)
|
||||
ThrowIndexOutOfRangeException();
|
||||
|
||||
return ref *(BaseAddress + index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a mutable reference to a single element of the memory region, cast to a different type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is equivalent to using <see cref="M:System.Runtime.CompilerServices.Unsafe.As``2(``0@)"/> on the result of <see cref="GetSpan"/>.
|
||||
/// </remarks>
|
||||
/// <param name="index">The index of the element desired.</param>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// Thrown if <paramref name="index"/> is greater or equal to <see cref="CurrentSize"/>.
|
||||
/// </exception>
|
||||
/// <returns>A mutable reference to the element.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ref TCast GetRef<TCast>(int index) where TCast : unmanaged
|
||||
{
|
||||
return ref Unsafe.As<T, TCast>(ref GetRef(index));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the contents of the memory stored in this <see cref="ResizableMemoryRegion{T}"/> back to zero.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does not change the <see cref="CurrentSize"/>.
|
||||
/// </remarks>
|
||||
public void Clear()
|
||||
{
|
||||
GetSpan().Clear();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (BaseAddress == null)
|
||||
ThrowNotInitialized();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowNotInitialized()
|
||||
{
|
||||
throw new InvalidOperationException("Memory region is not initialized!");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowIndexOutOfRangeException()
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
private void ReleaseUnmanagedResources()
|
||||
{
|
||||
if (BaseAddress == null)
|
||||
return;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var result = VirtualFree(BaseAddress, 0, MEM_RELEASE);
|
||||
if (!result)
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
|
||||
}
|
||||
else
|
||||
{
|
||||
NativeMemory.Free(BaseAddress);
|
||||
}
|
||||
|
||||
MemoryUsageGauge.Add(-CurrentSize * sizeof(T));
|
||||
|
||||
BaseAddress = null;
|
||||
CurrentSize = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Release the backing memory for this <see cref="ResizableMemoryRegion{T}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Existing references to memory of the memory region become invalid and should no longer be used.
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
ReleaseUnmanagedResources();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~ResizableMemoryRegion()
|
||||
{
|
||||
ReleaseUnmanagedResources();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Numerics;
|
||||
using NUnit.Framework;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.UnitTesting.Client.UserInterface.Controls;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(TextEdit))]
|
||||
public sealed class TextEditTest : RobustUnitTest
|
||||
{
|
||||
public override UnitTestProject Project => UnitTestProject.Client;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void Setup()
|
||||
{
|
||||
IoCManager.Resolve<IUserInterfaceManagerInternal>().InitializeTesting();
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/space-wizards/RobustToolbox/issues/4953
|
||||
// It was possible to move the cursor up/down if there was multi-line placeholder text.
|
||||
[Test]
|
||||
public void TestInvalidMoveInPlaceholder()
|
||||
{
|
||||
var textEdit = new TextEdit { Placeholder = new Rope.Leaf("Foo\nBar") };
|
||||
textEdit.Arrange(new UIBox2(0, 0, 200, 200));
|
||||
|
||||
var click = new GUIBoundKeyEventArgs(EngineKeyFunctions.TextCursorDown, BoundKeyState.Down, new ScreenCoordinates(), true, Vector2.Zero, Vector2.Zero);
|
||||
textEdit.KeyBindDown(click);
|
||||
textEdit.KeyBindUp(click);
|
||||
|
||||
Assert.That(textEdit.CursorPosition.Index, Is.Zero);
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/space-wizards/RobustToolbox/issues/4957
|
||||
// Moving left (with the arrow keys) in an empty TextEdit would cause an exception.
|
||||
[Test]
|
||||
public void TestEmptyMoveLeft()
|
||||
{
|
||||
var textEdit = new TextEdit();
|
||||
textEdit.Arrange(new UIBox2(0, 0, 200, 200));
|
||||
|
||||
var click = new GUIBoundKeyEventArgs(EngineKeyFunctions.TextCursorLeft, BoundKeyState.Down, new ScreenCoordinates(), true, Vector2.Zero, Vector2.Zero);
|
||||
textEdit.KeyBindDown(click);
|
||||
textEdit.KeyBindUp(click);
|
||||
}
|
||||
}
|
||||
@@ -139,4 +139,3 @@ public sealed class GridDeleteSingleTileRemoveTestTest : RobustIntegrationTest
|
||||
Assert.That(cQuery.GetComponent(cEntity.Value).ParentUid, Is.EqualTo(cMap));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
113
Robust.UnitTesting/Shared/Networking/NetDisconnectMessageTest.cs
Normal file
113
Robust.UnitTesting/Shared/Networking/NetDisconnectMessageTest.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Robust.UnitTesting.Shared.Networking;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
[TestOf(typeof(NetDisconnectMessage))]
|
||||
internal sealed class NetDisconnectMessageTest
|
||||
{
|
||||
[Test]
|
||||
[TestCase("Disconnected: bye", "Disconnected: bye", NetDisconnectMessage.DefaultRedialFlag)]
|
||||
[TestCase("Disconnected: {\"reason\": \"bye\"}", "bye", NetDisconnectMessage.DefaultRedialFlag)]
|
||||
[TestCase("Disconnected: {}", NetDisconnectMessage.DefaultReason, NetDisconnectMessage.DefaultRedialFlag)]
|
||||
[TestCase("Disconnected: {\"redial\": true}", NetDisconnectMessage.DefaultReason, true)]
|
||||
[TestCase("Disconnected: {\"redial\": true, \"foobar\": 5}", NetDisconnectMessage.DefaultReason, true)]
|
||||
[TestCase("Disconnected: {\"redial\": true, \"foobar\": 5, \"reason\": \"asdf\"}", "asdf", true)]
|
||||
[TestCase("Disconnected: {", "Disconnected: {", NetDisconnectMessage.DefaultRedialFlag)]
|
||||
[TestCase("Disconnected: {\"a\":[]}", NetDisconnectMessage.DefaultReason, NetDisconnectMessage.DefaultRedialFlag)]
|
||||
[TestCase("{\"redial\": true, \"foobar\": 5, \"reason\": \"asdf\"}", "asdf", true)]
|
||||
public void TestBasicDecode(string encoded, string reasonExpected, bool redialExpected)
|
||||
{
|
||||
var parsed = NetDisconnectMessage.Decode(encoded);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed.Reason, Is.EqualTo(reasonExpected));
|
||||
Assert.That(parsed.RedialFlag, Is.EqualTo(redialExpected));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEncode()
|
||||
{
|
||||
var value = new NetDisconnectMessage("foobar", true)
|
||||
{
|
||||
Values =
|
||||
{
|
||||
["asdf"] = 20.5f
|
||||
}
|
||||
};
|
||||
|
||||
var encoded = value.Encode();
|
||||
TestContext.Write($"Encoded: {encoded}\n");
|
||||
var decodedAgain = NetDisconnectMessage.Decode(encoded);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedAgain.Reason, Is.EqualTo("foobar"));
|
||||
Assert.That(decodedAgain.RedialFlag, Is.EqualTo(true));
|
||||
Assert.That(decodedAgain.SingleOf("asdf"), Is.EqualTo(20.5f));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefaultConstructor()
|
||||
{
|
||||
var value = new NetDisconnectMessage("foobar");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(value.Reason, Is.EqualTo("foobar"));
|
||||
Assert.That(value.RedialFlag, Is.EqualTo(false));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestValueOfInt()
|
||||
{
|
||||
var parsed = NetDisconnectMessage.Decode("{\"foobar\": 5}");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed.Int32Of("foobar"), Is.EqualTo(5));
|
||||
Assert.That(parsed.Int32Of("asdf"), Is.Null);
|
||||
Assert.That(parsed.Int32Of("asdf", 7), Is.EqualTo(7));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestValueOfFloat()
|
||||
{
|
||||
var parsed = NetDisconnectMessage.Decode("{\"foobar\": 5.5}");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed.SingleOf("foobar"), Is.EqualTo(5.5f));
|
||||
Assert.That(parsed.SingleOf("asdf"), Is.Null);
|
||||
Assert.That(parsed.SingleOf("asdf", 7), Is.EqualTo(7f));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestValueOfFloatInt()
|
||||
{
|
||||
var parsed = NetDisconnectMessage.Decode("{\"foobar\": 5}");
|
||||
|
||||
Assert.That(parsed.SingleOf("foobar"), Is.EqualTo(5f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestValueOfString()
|
||||
{
|
||||
var parsed = NetDisconnectMessage.Decode("{\"foobar\": \"real\"}");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(parsed.StringOf("foobar"), Is.EqualTo("real"));
|
||||
Assert.That(parsed.StringOf("asdf"), Is.Null);
|
||||
Assert.That(parsed.StringOf("asdf", "honk"), Is.EqualTo("honk"));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
@@ -304,5 +305,23 @@ namespace Robust.UnitTesting.Shared.Utility
|
||||
|
||||
Assert.That(MathHelper.Mod(val, mod), Is.EqualTo(result).Within(0.00000000001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(4, 4, ExpectedResult = 4)]
|
||||
[TestCase(5, 4, ExpectedResult = 8)]
|
||||
[TestCase(7, 4, ExpectedResult = 8)]
|
||||
[TestCase(8, 4, ExpectedResult = 8)]
|
||||
[TestCase(4U, 4U, ExpectedResult = 4U)]
|
||||
[TestCase(5U, 4U, ExpectedResult = 8U)]
|
||||
[TestCase(7U, 4U, ExpectedResult = 8U)]
|
||||
[TestCase(8U, 4U, ExpectedResult = 8U)]
|
||||
[TestCase(4L, 4L, ExpectedResult = 4L)]
|
||||
[TestCase(5L, 4L, ExpectedResult = 8L)]
|
||||
[TestCase(7L, 4L, ExpectedResult = 8L)]
|
||||
[TestCase(8L, 4L, ExpectedResult = 8L)]
|
||||
public T TestCeilMultipleOfPowerOfTwo<T>(T value, T powerOfTwo) where T : IBinaryInteger<T>
|
||||
{
|
||||
return MathHelper.CeilMultipleOfPowerOfTwo(value, powerOfTwo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user