mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99c5b0ad08 | ||
|
|
9e2ab2a917 | ||
|
|
36eb857b55 | ||
|
|
92d7f2723a | ||
|
|
91d3f67a94 | ||
|
|
b28b5ed09b | ||
|
|
30eed7957f | ||
|
|
73c1449811 | ||
|
|
958b5dd06d | ||
|
|
4002cbddb9 | ||
|
|
c38a14e78f | ||
|
|
5164d99996 | ||
|
|
c79217ab66 | ||
|
|
9ef7f7cb37 | ||
|
|
02ac314b1a | ||
|
|
8607ba1f16 | ||
|
|
2a9de462d5 | ||
|
|
c59ef5ab2d | ||
|
|
eba1d866fb | ||
|
|
ab2bff8f40 | ||
|
|
536fca4115 | ||
|
|
7a0d02463c | ||
|
|
6df53d60ed | ||
|
|
ff38e9f12a | ||
|
|
00c58c76a8 | ||
|
|
0bf99e173c | ||
|
|
eee771c5f1 | ||
|
|
2946cd866c | ||
|
|
9a2a3d658d | ||
|
|
d933f03a54 | ||
|
|
25bbb21dc8 | ||
|
|
4460454563 | ||
|
|
a2d8fa7a9b | ||
|
|
71f0491f10 | ||
|
|
b4c1618338 | ||
|
|
df0945f3cd | ||
|
|
8d477716b0 | ||
|
|
5c1a5e9826 | ||
|
|
6daa3ad2fc | ||
|
|
033a617102 | ||
|
|
b9b565d53e | ||
|
|
b7ea4d0cca | ||
|
|
919ec01477 | ||
|
|
e484eac29c | ||
|
|
eedadb250f | ||
|
|
3097784cd7 | ||
|
|
0fb41e06c8 | ||
|
|
0a79382a62 | ||
|
|
1f2b38a6d1 | ||
|
|
f760929527 | ||
|
|
f5ade69f6d | ||
|
|
9bfe889c86 | ||
|
|
e3954494e7 | ||
|
|
6697e36e84 | ||
|
|
dae4041e61 | ||
|
|
390f399750 | ||
|
|
28cf7442ce | ||
|
|
d8b03be651 | ||
|
|
16c7c71ca6 | ||
|
|
0245c371ae | ||
|
|
c8cb13f832 | ||
|
|
86ecfaa56b | ||
|
|
43a32e7015 | ||
|
|
83371885fa | ||
|
|
e686e1b4cc | ||
|
|
b1f9d011ce | ||
|
|
a2d0504368 | ||
|
|
7aa951ca48 | ||
|
|
75a80b7a8a | ||
|
|
69706b0257 | ||
|
|
10b191dff8 | ||
|
|
92ab3fb64b | ||
|
|
92a0c14383 | ||
|
|
5aaf6d0994 | ||
|
|
15f4da5e4b | ||
|
|
a528e87f3d | ||
|
|
4af67b1394 | ||
|
|
e8de9b98d3 | ||
|
|
a0ffeff4e5 | ||
|
|
07654564f3 | ||
|
|
7fbf8d05eb | ||
|
|
c12971cb9b | ||
|
|
2b6381c332 | ||
|
|
8149a3aaad | ||
|
|
4b39bf1f2d | ||
|
|
53394fff44 | ||
|
|
4bed20e070 | ||
|
|
e4b6af09f1 | ||
|
|
1ef29ae781 | ||
|
|
5686950421 | ||
|
|
2b54aa8984 | ||
|
|
859f150404 | ||
|
|
558f4b5b16 | ||
|
|
108366152b | ||
|
|
c55327e1d1 | ||
|
|
370e0fa0d0 | ||
|
|
4f9f82c20c | ||
|
|
43670a8ddd | ||
|
|
250313e1ed | ||
|
|
18d511d4b6 | ||
|
|
da9e5fb370 | ||
|
|
e3bac382ce | ||
|
|
179c6790b6 | ||
|
|
a7db5634df | ||
|
|
2daa86ff59 | ||
|
|
d6803f5294 | ||
|
|
bdcc0f7b9d | ||
|
|
ce49aa47cf | ||
|
|
c7d48b2526 | ||
|
|
6bb7f5b4ef | ||
|
|
2974310450 |
@@ -45,7 +45,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.2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.3" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
|
||||
|
||||
Submodule Lidgren.Network/Lidgren.Network updated: 45f89ca263...61a56c60bd
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -24,12 +24,16 @@
|
||||
<RobustInjectorsConfiguration>$(Configuration)</RobustInjectorsConfiguration>
|
||||
<RobustInjectorsConfiguration Condition="'$(Configuration)' == 'DebugOpt'">Debug</RobustInjectorsConfiguration>
|
||||
<RobustInjectorsConfiguration Condition="'$(Configuration)' == 'Tools'">Release</RobustInjectorsConfiguration>
|
||||
<RobustInjectorsConfiguration Condition="'$(UseArtifactsOutput)' == 'true' And '$(RuntimeIdentifier)' != ''">$(RobustInjectorsConfiguration)_$(RuntimeIdentifier)</RobustInjectorsConfiguration>
|
||||
<RobustInjectorsConfiguration Condition="'$(UseArtifactsOutput)' == 'true'">$(RobustInjectorsConfiguration.ToLower())</RobustInjectorsConfiguration>
|
||||
<CompileRobustXamlTaskAssemblyFile Condition="'$(UseArtifactsOutput)' != 'true'">$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\bin\$(RobustInjectorsConfiguration)\netstandard2.0\Robust.Client.Injectors.dll</CompileRobustXamlTaskAssemblyFile>
|
||||
<CompileRobustXamlTaskAssemblyFile Condition="'$(UseArtifactsOutput)' == 'true'">$(MSBuildThisFileDirectory)\..\..\artifacts\bin\Robust.Client.Injectors\$(RobustInjectorsConfiguration)\Robust.Client.Injectors.dll</CompileRobustXamlTaskAssemblyFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<UsingTask
|
||||
Condition="'$(_RobustUseExternalMSBuild)' != 'true' And $(DesignTimeBuild) != true"
|
||||
TaskName="CompileRobustXamlTask"
|
||||
AssemblyFile="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\bin\$(RobustInjectorsConfiguration)\netstandard2.0\Robust.Client.Injectors.dll"/>
|
||||
AssemblyFile="$(CompileRobustXamlTaskAssemblyFile)"/>
|
||||
<Target
|
||||
Name="CompileRobustXaml"
|
||||
Condition="Exists('@(IntermediateAssembly)')"
|
||||
|
||||
274
RELEASE-NOTES.md
274
RELEASE-NOTES.md
@@ -1,4 +1,4 @@
|
||||
# Release notes for RobustToolbox.
|
||||
# Release notes for RobustToolbox.
|
||||
|
||||
<!--
|
||||
NOTE: automatically updated sometimes by version.py.
|
||||
@@ -54,6 +54,278 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 217.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added `AddComponents` and `RemoveComponents` methods to EntityManager that handle EntityPrototype / ComponentRegistry bulk component changes.
|
||||
* Add double-clicking to LineEdit.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Properly ignore non-hard fixtures for IntersectRayWithPredicate.
|
||||
* Fix nullable TimeSpan addition on some platforms.
|
||||
|
||||
|
||||
## 217.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added `IRobustRandom.GetItems` extension methods for randomly picking multiple items from a collections.
|
||||
* Added `SharedPhysicsSystem.EffectiveCurTime`. This is effectively a variation of `IGameTiming.CurTime` that takes into account the current physics sub-step.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `MapComponent.LightingEnabled` not leaving FOV rendering in a broken state.
|
||||
|
||||
### Internal
|
||||
|
||||
* `Shuffle<T>(Span<T>, System.Random)` has been removed, just use the builtin method.
|
||||
|
||||
|
||||
## 217.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* TransformSystem.SetWorldPosition and SetWorldPositionRotation will now also perform parent updates as necessary. Previously it would just set the entity's LocalPosition which may break if they were inside of a container. Now they will be removed from their container and TryFindGridAt will run to correctly parent them to the new position. If the old functionality is desired then you can use GetInvWorldMatrix to update the LocalPosition (bearing in mind containers may prevent this).
|
||||
|
||||
### New features
|
||||
|
||||
* Implement VV for AudioParams on SoundSpecifiers.
|
||||
* Add AddUi to the shared UI system.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix the first measure of ScrollContainer bars.
|
||||
|
||||
|
||||
## 216.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The `net.low_lod_distance` cvar has been replaced with a new `net.pvs_priority_range`. Instead of limiting the range at which all entities are sent to a player, it now extends the range at which high priorities can be sent. The default value of this new cvar is 32.5, which is larger than the default `net.pvs_range` value of 25.
|
||||
|
||||
### New features
|
||||
|
||||
* You can now specify a component to not be saved to map files with `[UnsavedComponent]`.
|
||||
* Added `ITileDefinitionManager.TryGetDefinition`.
|
||||
* The map loader now tries to preserve the `tilemap` contents of map files, which should reduce diffs when re-saving a map after the game's internal tile IDs have changed.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix buffered audio sources not being disposed.
|
||||
|
||||
|
||||
## 215.3.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Revert zstd update.
|
||||
|
||||
|
||||
## 215.3.0
|
||||
|
||||
### New features
|
||||
|
||||
* `EntityQuery<T>` now has `HasComp` and `TryComp` methods that are shorter than its existing ones.
|
||||
* Added `PlacementInformation.UseEditorContext`.
|
||||
* Added `Vector2Helpers` functions for comparing ranges between vectors.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* `Texture.GetPixel()`: fixed off-by-one with Y coordinate.
|
||||
* `Texture.GetPixel()`: fix stack overflow when reading large images.
|
||||
* `Texture.GetPixel()`: use more widely compatible OpenGL calls.
|
||||
|
||||
### Other
|
||||
|
||||
* Disabled `net.mtu_expand` again by default, as it was causing issues.
|
||||
* Updated `SharpZstd` dependency.
|
||||
|
||||
|
||||
## 215.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Implement basic VV for SoundSpecifiers.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix QueueDel during EndCollideEvents from throwing while removing contacts.
|
||||
|
||||
|
||||
## 215.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add a CompletionHelper for audio filepaths that handles server packaging.
|
||||
* Add Random.NextAngle(min, max) method and Pick for `ValueList<T>`.
|
||||
* Added an `ICommonSession` parser for toolshed commands.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
|
||||
## 215.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Update Lidgren to 0.3.0
|
||||
|
||||
### New features
|
||||
|
||||
* Made a new `IMetricsManager` interface with an `UpdateMetrics` event that can be used to update Prometheus metrics whenever they are scraped.
|
||||
* Also added a `metrics.update_interval` CVar to go along with this, when metrics are scraped without usage of Prometheus directly.
|
||||
* IoC now contains an `IMeterFactory` implementation that you can use to instantiate metric meters.
|
||||
* `net.mtu_ipv6` CVar allows specifying a different MTU value for IPv6.
|
||||
* Allows `player:entity` to take a parameter representing the player name.
|
||||
* Add collection parsing to the dev window for UI.
|
||||
* Add a debug assert to Dirty(uid, comp) to catch mismatches being passed in.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Support transform states with unknown parents.
|
||||
* Fix serialization error logging.
|
||||
* Fix naming of ResizableMemoryRegion metrics.
|
||||
* Fix uncaught overflow exception when parsing NetEntities.
|
||||
|
||||
### Other
|
||||
|
||||
* The replay system now allows loading a replay with a mismatching serializer type hash. This means replays should be more robust against future version updates (engine security patches or .NET updates).
|
||||
* `CheckBox`'s interior texture is now vertically centered.
|
||||
* Lidgren.Network has been updated to [`v0.3.0`](https://github.com/space-wizards/SpaceWizards.Lidgren.Network/blob/v0.3.0/RELEASE-NOTES.md).
|
||||
* Lowered default IPv4 MTU to 900 (from 1000).
|
||||
* Automatic MTU expansion (`net.mtu_expand`) is now enabled by default.
|
||||
|
||||
### Internal
|
||||
|
||||
* Cleanup some Dirty component calls internally.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
* Add IsHardCollidable to SharedPhysicsSystem to determine if 2 entities would collide.
|
||||
|
||||
### Other
|
||||
|
||||
* Double the default maximum replay size.
|
||||
|
||||
|
||||
## 212.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add nullable methods for TryIndex / HasIndex on IPrototypeManager.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix TextureRect alignment where the strech mode is KeepCentered.
|
||||
|
||||
|
||||
## 212.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix passing array by `this` instead of by `ref`.
|
||||
|
||||
|
||||
## 212.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Change Collapsible controls default orientations to Vertical.
|
||||
|
||||
### New features
|
||||
|
||||
* Expose the Label control for Collapsible controls.
|
||||
* Add GetGridPosition that considers physics center-of-mass.
|
||||
* Add TileToVector methods to get the LocalPosition of tile-coords (taking into account tile size).
|
||||
* Add some more helper methods to PVS filters around EntityUids.
|
||||
* Add support for Dictionary AutoNetworkedFields.
|
||||
* Add EnsureLength method for arrays.
|
||||
* Add PushMarkup to FormattedMessage.
|
||||
* Add DrawPrimitives overload for `List<Vector2>`
|
||||
* Add more ValueList ctors that are faster.
|
||||
* Add ToMapCoordinates method for NetCoordinates.
|
||||
|
||||
### Other
|
||||
|
||||
* Remove ISerializationHooks obsoletion as they are useful in some rare cases.
|
||||
|
||||
### Internal
|
||||
|
||||
* Bump max pool size for robust jobs.
|
||||
|
||||
|
||||
## 211.0.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
@@ -11,6 +11,7 @@ cmd-parse-failure-uid = {$arg} is not a valid entity UID.
|
||||
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
|
||||
cmd-parse-failure-grid = {$arg} is not a valid grid.
|
||||
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
|
||||
cmd-parse-failure-session = There is no session with username: {$username}
|
||||
|
||||
cmd-error-file-not-found = Could not find file: {$file}.
|
||||
cmd-error-dir-not-found = Could not find directory: {$dir}.
|
||||
@@ -490,7 +491,7 @@ cmd-net_entityreport-help = Usage: net_entityreport
|
||||
cmd-net_refresh-desc = Requests a full server state.
|
||||
cmd-net_refresh-help = Usage: net_refresh
|
||||
|
||||
cmd-net_graph-desc = Toggles the net statistics pannel.
|
||||
cmd-net_graph-desc = Toggles the net statistics panel.
|
||||
cmd-net_graph-help = Usage: net_graph
|
||||
|
||||
cmd-net_watchent-desc = Dumps all network updates for an EntityId to the console.
|
||||
@@ -566,3 +567,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}.
|
||||
|
||||
@@ -70,9 +70,9 @@ command-description-ls-in =
|
||||
command-description-methods-get =
|
||||
Returns all methods associated with the input type.
|
||||
command-description-methods-overrides =
|
||||
Returns all methods overriden on the input type.
|
||||
Returns all methods overridden on the input type.
|
||||
command-description-methods-overridesfrom =
|
||||
Returns all methods overriden from the given type on the input type.
|
||||
Returns all methods overridden from the given type on the input type.
|
||||
command-description-cmd-moo =
|
||||
Asks the important questions.
|
||||
command-description-cmd-descloc =
|
||||
@@ -418,6 +418,6 @@ command-description-tee =
|
||||
This essentially lets you have a branch in your code to do multiple operations on one value.
|
||||
command-description-cmd-info =
|
||||
Returns a CommandSpec for the given command.
|
||||
On it's own, this means it'll print the comamnd's help message.
|
||||
On its own, this means it'll print the command's help message.
|
||||
command-description-comp-rm =
|
||||
Removes the given component from the entity.
|
||||
|
||||
@@ -10,3 +10,18 @@ view-variable-instance-entity-client-components-search-bar-placeholder = Search
|
||||
view-variable-instance-entity-server-components-search-bar-placeholder = Search
|
||||
view-variable-instance-entity-add-window-server-components = Add Component [S]
|
||||
view-variable-instance-entity-add-window-client-components = Add Component [C]
|
||||
|
||||
|
||||
## SoundSpecifier
|
||||
vv-sound-none = None
|
||||
vv-sound-path = Path
|
||||
vv-sound-collection = Collection
|
||||
|
||||
vv-sound-volume = volume
|
||||
vv-sound-pitch = Pitch
|
||||
vv-sound-max-distance = Max Distance
|
||||
vv-sound-rolloff-factor = Rolloff Factor
|
||||
vv-sound-reference-distance = Reference Distance
|
||||
vv-sound-loop = Loop
|
||||
vv-sound-play-offset = Play Offset (s)
|
||||
vv-sound-variation = Pitch variation
|
||||
|
||||
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);
|
||||
var component = entity.Comp;
|
||||
|
||||
if (source == null)
|
||||
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)
|
||||
@@ -411,13 +427,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
|
||||
}
|
||||
@@ -444,8 +460,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return null;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
|
||||
@@ -477,8 +496,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return null;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
|
||||
@@ -518,8 +540,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return null;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
|
||||
@@ -553,65 +578,65 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, entity, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, uid, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, uid, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
}
|
||||
@@ -621,7 +646,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;
|
||||
|
||||
|
||||
@@ -294,7 +294,6 @@ namespace Robust.Client.Console.Commands
|
||||
internal sealed class SnapGridGetCell : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
|
||||
public override string Command => "sggcell";
|
||||
|
||||
@@ -320,7 +319,7 @@ namespace Robust.Client.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
if (_map.TryGetGrid(_entManager.GetEntity(gridNet), out var grid))
|
||||
if (_entManager.TryGetComponent<MapGridComponent>(_entManager.GetEntity(gridNet), out var grid))
|
||||
{
|
||||
foreach (var entity in grid.GetAnchoredEntities(new Vector2i(
|
||||
int.Parse(indices.Split(',')[0], CultureInfo.InvariantCulture),
|
||||
@@ -429,7 +428,6 @@ namespace Robust.Client.Console.Commands
|
||||
internal sealed class GridTileCount : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
|
||||
public override string Command => "gridtc";
|
||||
|
||||
@@ -448,7 +446,7 @@ namespace Robust.Client.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
if (_map.TryGetGrid(gridUid, out var grid))
|
||||
if (_entManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
|
||||
{
|
||||
shell.WriteLine(grid.GetAllTiles().Count().ToString());
|
||||
}
|
||||
@@ -555,7 +553,7 @@ namespace Robust.Client.Console.Commands
|
||||
if (type != typeof(Control))
|
||||
cname = $"Control > {cname}";
|
||||
|
||||
returnVal.GetOrNew(cname).Add((member.Name, member.GetValue(control)?.ToString() ?? "null"));
|
||||
returnVal.GetOrNew(cname).Add((member.Name, GetMemberValue(member, control, ", ")));
|
||||
}
|
||||
|
||||
foreach (var (attachedProperty, value) in control.AllAttachedProperties)
|
||||
@@ -570,6 +568,28 @@ namespace Robust.Client.Console.Commands
|
||||
}
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
internal static string PropertyValuesString(Control control, string key)
|
||||
{
|
||||
var member = GetAllMembers(control).Find(m => m.Name == key);
|
||||
return GetMemberValue(member, control, "\n", "\"{0}\"");
|
||||
}
|
||||
|
||||
private static string GetMemberValue(MemberInfo? member, Control control, string separator, string
|
||||
wrap = "{0}")
|
||||
{
|
||||
var value = member?.GetValue(control);
|
||||
var o = value switch
|
||||
{
|
||||
ICollection<Control> controls => string.Join(separator,
|
||||
controls.Select(ctrl => $"{ctrl.Name}({ctrl.GetType()})")),
|
||||
ICollection<string> list => string.Join(separator, list),
|
||||
null => null,
|
||||
_ => value.ToString()
|
||||
};
|
||||
// Convert to quote surrounded string or null with no quotes
|
||||
return o is not null ? string.Format(wrap, o) : "null";
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SetClipboardCommand : LocalizedCommands
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -612,6 +612,8 @@ namespace Robust.Client
|
||||
{
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
|
||||
}
|
||||
|
||||
_audio.FlushALDisposeQueues();
|
||||
}
|
||||
|
||||
internal static void SetupLogging(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly IConsoleHost _conHost = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
private ISawmill _sawmillInputContext = default!;
|
||||
|
||||
@@ -151,7 +152,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
var pxform = Transform(pent);
|
||||
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
|
||||
var coords = EntityCoordinates.FromMap(EntityManager, pent, new MapCoordinates(wPos, pxform.MapID));
|
||||
var coords = EntityCoordinates.FromMap(pent, new MapCoordinates(wPos, pxform.MapID), _transform, EntityManager);
|
||||
|
||||
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
|
||||
|
||||
|
||||
@@ -700,7 +700,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
using var _ = _timing.StartStateApplicationArea();
|
||||
|
||||
// TODO repays optimize this.
|
||||
// TODO replays optimize this.
|
||||
// This currently just saves game states as they are applied.
|
||||
// However this is inefficient and may have redundant data.
|
||||
// E.g., we may record states: [10 to 15] [11 to 16] *error* [0 to 18] [18 to 19] [18 to 20] ...
|
||||
@@ -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)
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Shared;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -514,7 +515,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawLight && eye.DrawFov)
|
||||
{
|
||||
ApplyFovToBuffer(viewport, eye);
|
||||
var mapUid = _mapManager.GetMapEntityId(eye.Position.MapId);
|
||||
if (_entityManager.GetComponent<MapComponent>(mapUid).LightingEnabled)
|
||||
ApplyFovToBuffer(viewport, eye);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1201,7 +1201,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void LightResolutionScaleChanged(float newValue)
|
||||
{
|
||||
_lightResolutionScale = newValue;
|
||||
_lightResolutionScale = newValue > 0.05f ? newValue : 0.05f;
|
||||
RegenAllLightRts();
|
||||
}
|
||||
|
||||
|
||||
@@ -649,24 +649,30 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return $"ClydeTexture: ({TextureId})";
|
||||
}
|
||||
|
||||
public override Color GetPixel(int x, int y)
|
||||
public override unsafe Color GetPixel(int x, int y)
|
||||
{
|
||||
if (!_clyde._loadedTextures.TryGetValue(TextureId, out var loaded))
|
||||
{
|
||||
throw new DataException("Texture not found");
|
||||
}
|
||||
|
||||
Span<byte> rgba = stackalloc byte[4*this.Size.X*this.Size.Y];
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* p = rgba)
|
||||
{
|
||||
var curTexture2D = GL.GetInteger(GetPName.TextureBinding2D);
|
||||
var bufSize = 4 * loaded.Size.X * loaded.Size.Y;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(bufSize);
|
||||
|
||||
GL.GetTextureImage(loaded.OpenGLObject.Handle, 0, PF.Rgba, PT.UnsignedByte, 4*this.Size.X*this.Size.Y, (IntPtr) p);
|
||||
}
|
||||
GL.BindTexture(TextureTarget.Texture2D, loaded.OpenGLObject.Handle);
|
||||
|
||||
fixed (byte* p = buffer)
|
||||
{
|
||||
GL.GetnTexImage(TextureTarget.Texture2D, 0, PF.Rgba, PT.UnsignedByte, bufSize, (IntPtr) p);
|
||||
}
|
||||
int pixelPos = (this.Size.X*(this.Size.Y-y) + x)*4;
|
||||
return new Color(rgba[pixelPos+0], rgba[pixelPos+1], rgba[pixelPos+2], rgba[pixelPos+3]);
|
||||
|
||||
GL.BindTexture(TextureTarget.Texture2D, curTexture2D);
|
||||
|
||||
var pixelPos = (loaded.Size.X * (loaded.Size.Y - y - 1) + x) * 4;
|
||||
var color = new Color(buffer[pixelPos+0], buffer[pixelPos+1], buffer[pixelPos+2], buffer[pixelPos+3]);
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
@@ -63,6 +64,19 @@ namespace Robust.Client.Graphics
|
||||
|
||||
// ---- DrawPrimitives: Vector2 API ----
|
||||
|
||||
/// <summary>
|
||||
/// Draws arbitrary geometry primitives with a flat color.
|
||||
/// </summary>
|
||||
/// <param name="primitiveTopology">The topology of the primitives to draw.</param>
|
||||
/// <param name="vertices">The list of vertices to render.</param>
|
||||
/// <param name="color">The color to draw with.</param>
|
||||
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, List<Vector2> vertices,
|
||||
Color color)
|
||||
{
|
||||
var span = CollectionsMarshal.AsSpan(vertices);
|
||||
DrawPrimitives(primitiveTopology, span, color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws arbitrary geometry primitives with a flat color.
|
||||
/// </summary>
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Robust.Client.Placement.Modes
|
||||
|
||||
var snapToEntities = EntitySystem.Get<EntityLookupSystem>().GetEntitiesInRange(MouseCoords, SnapToRange)
|
||||
.Where(entity => pManager.EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype == pManager.CurrentPrototype && pManager.EntityManager.GetComponent<TransformComponent>(entity).MapID == mapId)
|
||||
.OrderBy(entity => (pManager.EntityManager.GetComponent<TransformComponent>(entity).WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager)).LengthSquared())
|
||||
.OrderBy(entity => (pManager.EntityManager.GetComponent<TransformComponent>(entity).WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager, pManager.EntityManager.System<SharedTransformSystem>())).LengthSquared())
|
||||
.ToList();
|
||||
|
||||
if (snapToEntities.Count == 0)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Placement.Modes
|
||||
@@ -24,7 +25,7 @@ namespace Robust.Client.Placement.Modes
|
||||
SnapSize = 1f;
|
||||
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
|
||||
{
|
||||
Grid = pManager.MapManager.GetGrid(gridId);
|
||||
Grid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
SnapSize = Grid.TileSize; //Find snap size for the grid.
|
||||
}
|
||||
else
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace Robust.Client.Placement.Modes
|
||||
SnapSize = 1f;
|
||||
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
|
||||
{
|
||||
Grid = pManager.MapManager.GetGrid(gridId);
|
||||
Grid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
SnapSize = Grid.TileSize; //Find snap size for the grid.
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Robust.Client.Placement.Modes
|
||||
{
|
||||
@@ -19,7 +20,7 @@ namespace Robust.Client.Placement.Modes
|
||||
|
||||
var gridId = MouseCoords.GetGridUid(pManager.EntityManager);
|
||||
|
||||
if (!pManager.MapManager.TryGetGrid(gridId, out var mapGrid))
|
||||
if (!pManager.EntityManager.TryGetComponent<MapGridComponent>(gridId, out var mapGrid))
|
||||
return;
|
||||
|
||||
CurrentTile = mapGrid.GetTileRef(MouseCoords);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Robust.Client.Placement.Modes
|
||||
{
|
||||
@@ -20,7 +21,7 @@ namespace Robust.Client.Placement.Modes
|
||||
|
||||
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
|
||||
{
|
||||
var mapGrid = pManager.MapManager.GetGrid(gridId);
|
||||
var mapGrid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
tileSize = mapGrid.TileSize; //convert from ushort to float
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Placement.Modes
|
||||
@@ -22,7 +23,7 @@ namespace Robust.Client.Placement.Modes
|
||||
|
||||
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
|
||||
{
|
||||
var mapGrid = pManager.MapManager.GetGrid(gridId);
|
||||
var mapGrid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
CurrentTile = mapGrid.GetTileRef(MouseCoords);
|
||||
tileSize = mapGrid.TileSize; //convert from ushort to float
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Robust.Client.Placement.Modes
|
||||
{
|
||||
@@ -20,7 +21,7 @@ namespace Robust.Client.Placement.Modes
|
||||
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
|
||||
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
|
||||
{
|
||||
var mapGrid = pManager.MapManager.GetGrid(gridId);
|
||||
var mapGrid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
tileSize = mapGrid.TileSize; //convert from ushort to float
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Log;
|
||||
using Direction = Robust.Shared.Maths.Direction;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Robust.Client.Placement
|
||||
{
|
||||
@@ -79,6 +80,10 @@ namespace Robust.Client.Placement
|
||||
private set
|
||||
{
|
||||
_isActive = value;
|
||||
|
||||
if (CurrentPermission?.UseEditorContext is false)
|
||||
return;
|
||||
|
||||
SwitchEditorContext(value);
|
||||
}
|
||||
}
|
||||
@@ -332,7 +337,7 @@ namespace Robust.Client.Placement
|
||||
|
||||
private void HandleTileChanged(ref TileChangedEvent args)
|
||||
{
|
||||
var coords = MapManager.GetGrid(args.NewTile.GridUid).GridTileToLocal(args.NewTile.GridIndices);
|
||||
var coords = EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid).GridTileToLocal(args.NewTile.GridIndices);
|
||||
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
|
||||
}
|
||||
|
||||
@@ -753,7 +758,7 @@ namespace Robust.Client.Placement
|
||||
// If we have actually placed something on a valid grid...
|
||||
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
|
||||
{
|
||||
var grid = MapManager.GetGrid(gridId);
|
||||
var grid = EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
|
||||
// no point changing the tile to the same thing.
|
||||
if (grid.GetTileRef(coordinates).Tile.TypeId == CurrentPermission.TileType)
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -115,11 +116,12 @@ namespace Robust.Client.Placement
|
||||
|
||||
var dirAng = pManager.Direction.ToAngle();
|
||||
var spriteSys = pManager.EntityManager.System<SpriteSystem>();
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
foreach (var coordinate in locationcollection)
|
||||
{
|
||||
if (!coordinate.IsValid(pManager.EntityManager))
|
||||
return; // Just some paranoia just in case
|
||||
var worldPos = coordinate.ToMapPos(pManager.EntityManager);
|
||||
var worldPos = coordinate.ToMapPos(pManager.EntityManager, transformSys);
|
||||
var worldRot = pManager.EntityManager.GetComponent<TransformComponent>(coordinate.EntityId).WorldRotation + dirAng;
|
||||
|
||||
sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
|
||||
@@ -136,11 +138,12 @@ namespace Robust.Client.Placement
|
||||
{
|
||||
var mouseScreen = pManager.InputManager.MouseScreenPosition;
|
||||
var mousePos = pManager.EyeManager.PixelToMap(mouseScreen);
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
|
||||
if (mousePos.MapId == MapId.Nullspace)
|
||||
yield break;
|
||||
|
||||
var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, pManager.EntityManager) - pManager.StartPoint;
|
||||
var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
|
||||
float iterations;
|
||||
Vector2 distance;
|
||||
if (Math.Abs(x) > Math.Abs(y))
|
||||
@@ -167,11 +170,12 @@ namespace Robust.Client.Placement
|
||||
{
|
||||
var mouseScreen = pManager.InputManager.MouseScreenPosition;
|
||||
var mousePos = pManager.EyeManager.PixelToMap(mouseScreen);
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
|
||||
if (mousePos.MapId == MapId.Nullspace)
|
||||
yield break;
|
||||
|
||||
var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, pManager.EntityManager) - pManager.StartPoint;
|
||||
var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
|
||||
|
||||
var xSign = Math.Sign(placementdiff.X);
|
||||
var ySign = Math.Sign(placementdiff.Y);
|
||||
@@ -193,9 +197,9 @@ namespace Robust.Client.Placement
|
||||
public TileRef GetTileRef(EntityCoordinates coordinates)
|
||||
{
|
||||
var gridUidOpt = coordinates.GetGridUid(pManager.EntityManager);
|
||||
return gridUidOpt is EntityUid gridUid && gridUid.IsValid() ? pManager.MapManager.GetGrid(gridUid).GetTileRef(MouseCoords)
|
||||
return gridUidOpt is EntityUid gridUid && gridUid.IsValid() ? pManager.EntityManager.GetComponent<MapGridComponent>(gridUid).GetTileRef(MouseCoords)
|
||||
: new TileRef(gridUidOpt ?? EntityUid.Invalid,
|
||||
MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager), Tile.Empty);
|
||||
MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager, pManager.EntityManager.System<SharedTransformSystem>()), Tile.Empty);
|
||||
}
|
||||
|
||||
public TextureResource GetSprite(string key)
|
||||
@@ -223,7 +227,8 @@ namespace Robust.Client.Placement
|
||||
}
|
||||
|
||||
var range = pManager.CurrentPermission!.Range;
|
||||
if (range > 0 && !pManager.EntityManager.GetComponent<TransformComponent>(controlled).Coordinates.InRange(pManager.EntityManager, coordinates, range))
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
if (range > 0 && !pManager.EntityManager.GetComponent<TransformComponent>(controlled).Coordinates.InRange(pManager.EntityManager, transformSys, coordinates, range))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
@@ -231,7 +236,8 @@ namespace Robust.Client.Placement
|
||||
public bool IsColliding(EntityCoordinates coordinates)
|
||||
{
|
||||
var bounds = pManager.ColliderAABB;
|
||||
var mapCoords = coordinates.ToMap(pManager.EntityManager);
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
var mapCoords = coordinates.ToMap(pManager.EntityManager, transformSys);
|
||||
var (x, y) = mapCoords.Position;
|
||||
|
||||
var collisionBox = Box2.FromDimensions(
|
||||
@@ -261,7 +267,8 @@ namespace Robust.Client.Placement
|
||||
return EntityCoordinates.FromMap(pManager.MapManager, mapCoords);
|
||||
}
|
||||
|
||||
return EntityCoordinates.FromMap(pManager.EntityManager, gridUid, mapCoords);
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
return EntityCoordinates.FromMap(gridUid, mapCoords, transformSys, pManager.EntityManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ public sealed partial class ReplayLoadManager
|
||||
return parsed.FirstOrDefault()?.Root as MappingDataNode;
|
||||
}
|
||||
|
||||
private (MappingDataNode YamlData, HashSet<string> CVars, TimeSpan Duration, TimeSpan StartTime, bool ClientSide)
|
||||
private (MappingDataNode YamlData, HashSet<string> CVars, TimeSpan? Duration, TimeSpan StartTime, bool ClientSide)
|
||||
LoadMetadata(IReplayFileReader fileReader)
|
||||
{
|
||||
_sawmill.Info($"Reading replay metadata");
|
||||
@@ -137,23 +137,16 @@ public sealed partial class ReplayLoadManager
|
||||
if (data == null)
|
||||
throw new Exception("Failed to load yaml metadata");
|
||||
|
||||
TimeSpan duration;
|
||||
var finalData = LoadYamlFinalMetadata(fileReader);
|
||||
TimeSpan? duration = finalData == null
|
||||
? null
|
||||
: TimeSpan.Parse(((ValueDataNode) finalData[MetaFinalKeyDuration]).Value);
|
||||
|
||||
if (finalData == null)
|
||||
{
|
||||
var msg = "Failed to load final yaml metadata";
|
||||
if (!_confMan.GetCVar(CVars.ReplayIgnoreErrors))
|
||||
throw new Exception(msg);
|
||||
_sawmill.Warning("Failed to load final yaml metadata. Partial/incomplete replay?");
|
||||
|
||||
_sawmill.Error(msg);
|
||||
duration = TimeSpan.FromDays(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
duration = TimeSpan.Parse(((ValueDataNode) finalData[MetaFinalKeyDuration]).Value);
|
||||
}
|
||||
|
||||
var typeHash = Convert.FromHexString(((ValueDataNode) data[MetaKeyTypeHash]).Value);
|
||||
var typeHashString = ((ValueDataNode) data[MetaKeyTypeHash]).Value;
|
||||
var typeHash = Convert.FromHexString(typeHashString);
|
||||
var stringHash = Convert.FromHexString(((ValueDataNode) data[MetaKeyStringHash]).Value);
|
||||
var startTick = ((ValueDataNode) data[MetaKeyStartTick]).Value;
|
||||
var timeBaseTick = ((ValueDataNode) data[MetaKeyBaseTick]).Value;
|
||||
@@ -161,7 +154,12 @@ public sealed partial class ReplayLoadManager
|
||||
var clientSide = bool.Parse(((ValueDataNode) data[MetaKeyIsClientRecording]).Value);
|
||||
|
||||
if (!typeHash.SequenceEqual(_serializer.GetSerializableTypesHash()))
|
||||
throw new Exception($"{nameof(IRobustSerializer)} hashes do not match. Loading replays using a bad replay-client version?");
|
||||
{
|
||||
if (!_confMan.GetCVar(CVars.ReplayIgnoreErrors))
|
||||
throw new Exception($"RobustSerializer hash mismatch. do not match. Client hash: {_serializer.GetSerializableTypesHashString()}, replay hash: {typeHashString}.");
|
||||
|
||||
_sawmill.Warning($"RobustSerializer hash mismatch. Replay may fail to load!");
|
||||
}
|
||||
|
||||
using var stringFile = fileReader.Open(FileStrings);
|
||||
var stringData = new byte[stringFile.Length];
|
||||
|
||||
@@ -90,6 +90,7 @@ public sealed partial class ReplayControlWidget : UIWidget // AKA Tardis - The f
|
||||
var maxIndex = Math.Max(1, replay.States.Count - 1);
|
||||
var state = replay.States[index];
|
||||
var replayTime = TimeSpan.FromSeconds(TickSlider.Value);
|
||||
var end = replay.Duration == null ? "N/A" : replay.Duration.Value.ToString(TimeFormat);
|
||||
|
||||
IndexLabel.Text = Loc.GetString("replay-time-box-index-label",
|
||||
("current", index), ("total", maxIndex), ("percentage", percentage));
|
||||
@@ -98,10 +99,10 @@ public sealed partial class ReplayControlWidget : UIWidget // AKA Tardis - The f
|
||||
("current", state.ToSequence), ("total", replay.States[^1].ToSequence), ("percentage", percentage));
|
||||
|
||||
TimeLabel.Text = Loc.GetString("replay-time-box-replay-time-label",
|
||||
("current", replayTime.ToString(TimeFormat)), ("end", replay.Duration.ToString(TimeFormat)), ("percentage", percentage));
|
||||
("current", replayTime.ToString(TimeFormat)), ("end", end), ("percentage", percentage));
|
||||
|
||||
var serverTime = (replayTime + replay.StartTime).ToString(TimeFormat);
|
||||
var duration = (replay.Duration + replay.StartTime).ToString(TimeFormat);
|
||||
string duration = replay.Duration == null ? "N/A" : (replay.Duration + replay.StartTime).Value.ToString(TimeFormat);
|
||||
ServerTimeLabel.Text = Loc.GetString("replay-time-box-server-time-label",
|
||||
("current", serverTime), ("end", duration), ("percentage", percentage));
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<PackageReference Include="Robust.Natives" />
|
||||
<PackageReference Include="System.Numerics.Vectors" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" PrivateAssets="compile" />
|
||||
<PackageReference Condition="'$(FullRelease)' != 'True'" Include="JetBrains.Profiler.Api" PrivateAssets="compile" />
|
||||
<PackageReference Condition="'$(RobustToolsBuild)' == 'True'" Include="JetBrains.Profiler.Api" PrivateAssets="compile" />
|
||||
<PackageReference Include="SpaceWizards.Sodium" PrivateAssets="compile" />
|
||||
<PackageReference Include="Microsoft.NET.ILLink.Tasks" />
|
||||
<PackageReference Include="TerraFX.Interop.Xlib" />
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
TextureRect = new TextureRect
|
||||
{
|
||||
StyleClasses = { StyleClassCheckBox },
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
};
|
||||
hBox.AddChild(TextureRect);
|
||||
|
||||
|
||||
@@ -29,9 +29,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
|
||||
public Collapsible()
|
||||
{}
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
}
|
||||
|
||||
public Collapsible(CollapsibleHeading header, CollapsibleBody body)
|
||||
public Collapsible(CollapsibleHeading header, CollapsibleBody body) : this()
|
||||
{
|
||||
AddChild(header);
|
||||
AddChild(body);
|
||||
@@ -39,12 +41,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public Collapsible(string title, CollapsibleBody body)
|
||||
public Collapsible(string title, CollapsibleBody body) : this(new CollapsibleHeading(title), body)
|
||||
{
|
||||
AddChild(new CollapsibleHeading(title));
|
||||
AddChild(body);
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
protected internal override void Draw(DrawingHandleScreen handle)
|
||||
@@ -105,11 +104,15 @@ namespace Robust.Client.UserInterface.Controls
|
||||
set => _chevron.Margin = value;
|
||||
}
|
||||
|
||||
private Label _title = new();
|
||||
/// <summary>
|
||||
/// Exposes the label for this heading.
|
||||
/// </summary>
|
||||
public Label Label { get; }
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => _title.Text;
|
||||
set => _title.Text = value;
|
||||
get => Label.Text;
|
||||
set => Label.Text = value;
|
||||
}
|
||||
|
||||
public CollapsibleHeading()
|
||||
@@ -118,8 +121,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var box = new BoxContainer();
|
||||
AddChild(box);
|
||||
box.AddChild(_chevron);
|
||||
_title = new Label();
|
||||
box.AddChild(_title);
|
||||
Label = new Label();
|
||||
box.AddChild(Label);
|
||||
}
|
||||
|
||||
public CollapsibleHeading(string title) : this()
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Numerics;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -20,6 +22,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public class LineEdit : Control
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private const float MouseScrollDelay = 0.001f;
|
||||
|
||||
@@ -46,6 +50,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
private bool _mouseSelectingText;
|
||||
private float _lastMousePosition;
|
||||
|
||||
private TimeSpan? _lastClickTime;
|
||||
private Vector2? _lastClickPosition;
|
||||
|
||||
private bool IsPlaceHolderVisible => string.IsNullOrEmpty(_text) && _placeHolder != null;
|
||||
|
||||
public event Action<LineEditEventArgs>? OnTextChanged;
|
||||
@@ -685,8 +692,26 @@ namespace Robust.Client.UserInterface.Controls
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
// Double-clicking. Clicks delay should be <= 250ms and the distance < 10 pixels.
|
||||
else if (args.Function == EngineKeyFunctions.UIClick && _lastClickPosition != null && _lastClickTime != null
|
||||
&& _timing.RealTime - _lastClickTime <= TimeSpan.FromMilliseconds(_cfgManager.GetCVar(CVars.DoubleClickDelay))
|
||||
&& (_lastClickPosition.Value - args.PointerLocation.Position).IsShorterThan(_cfgManager.GetCVar(CVars.DoubleClickRange)))
|
||||
{
|
||||
_lastClickTime = _timing.RealTime;
|
||||
_lastClickPosition = args.PointerLocation.Position;
|
||||
|
||||
_lastMousePosition = args.RelativePosition.X;
|
||||
|
||||
_selectionStart = TextEditShared.PrevWordPosition(_text, GetIndexAtPos(args.RelativePosition.X));
|
||||
_cursorPosition = TextEditShared.EndWordPosition(_text, GetIndexAtPos(args.RelativePosition.X));
|
||||
|
||||
args.Handle();
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastClickTime = _timing.RealTime;
|
||||
_lastClickPosition = args.PointerLocation.Position;
|
||||
|
||||
_mouseSelectingText = true;
|
||||
_lastMousePosition = args.RelativePosition.X;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
private bool _suppressScrollValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// If true then if we have a y-axis scroll it will convert it to an x-axis scroll.
|
||||
/// </summary>
|
||||
public bool FallbackDeltaScroll { get; set; } = true;
|
||||
|
||||
public int ScrollSpeedX { get; set; } = 50;
|
||||
public int ScrollSpeedY { get; set; } = 50;
|
||||
|
||||
@@ -118,10 +123,10 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (!ReturnMeasure)
|
||||
return Vector2.Zero;
|
||||
|
||||
if (_vScrollEnabled)
|
||||
if (_vScrollEnabled && size.Y >= availableSize.Y)
|
||||
size.X += _vScrollBar.DesiredSize.X;
|
||||
|
||||
if (_hScrollEnabled)
|
||||
if (_hScrollEnabled && size.X >= availableSize.X)
|
||||
size.Y += _hScrollBar.DesiredSize.Y;
|
||||
|
||||
return size;
|
||||
@@ -246,9 +251,19 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
if (_hScrollEnabled)
|
||||
{
|
||||
_hScrollBar.ValueTarget += args.Delta.X * ScrollSpeedX;
|
||||
var delta =
|
||||
args.Delta.X == 0f &&
|
||||
!_vScrollEnabled &&
|
||||
FallbackDeltaScroll ?
|
||||
-args.Delta.Y :
|
||||
args.Delta.X;
|
||||
|
||||
_hScrollBar.ValueTarget += delta * ScrollSpeedX;
|
||||
}
|
||||
|
||||
if (!_vScrollVisible && !_hScrollVisible)
|
||||
return;
|
||||
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -213,7 +213,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return UIBox2.FromDimensions(Vector2.Zero, TextureSizeTarget * UIScale);
|
||||
case StretchMode.KeepCentered:
|
||||
{
|
||||
var position = (PixelSize - TextureSizeTarget) / 2;
|
||||
var position = (Size - TextureSizeTarget) / 2;
|
||||
return UIBox2.FromDimensions(position, TextureSizeTarget * UIScale);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
|
||||
contents.TextMemory = FormatHelpers.FormatIntoMem(_textBuffer,
|
||||
$@"UP: {sentBytes / ONE_KIBIBYTE:N} KiB/s, {sentPackets} pckt/s, {LastSentBytes / ONE_KIBIBYTE:N} KiB, {LastSentPackets} pckt
|
||||
DOWN: {receivedBytes / ONE_KIBIBYTE:N} KiB/s, {receivedPackets} pckt/s, {LastReceivedBytes / ONE_KIBIBYTE:N} KiB, {LastReceivedPackets} pckt
|
||||
PING: {NetManager.ServerChannel?.Ping ?? -1} ms");
|
||||
PING: {NetManager.ServerChannel?.Ping ?? -1} ms, MTU: {NetManager.ServerChannel?.CurrentMtu} B");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,14 @@
|
||||
<BoxContainer Name="ControlTreeRoot" Orientation="Vertical" MouseFilter="Stop" />
|
||||
</PanelContainer>
|
||||
</ScrollContainer>
|
||||
<ScrollContainer HScrollEnabled="False">
|
||||
<ScrollContainer>
|
||||
<BoxContainer Name="ControlProperties" Orientation="Vertical" MouseFilter="Stop" />
|
||||
</ScrollContainer>
|
||||
</SplitContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
<!-- TODO Remove and replace with a popup container on WindowRoot -->
|
||||
<PopupContainer Name="PopupContainer" Access="Public">
|
||||
<DevWindowTabUIPopup Name="UIPopup" />
|
||||
</PopupContainer>
|
||||
</Control>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console.Commands;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -208,25 +209,40 @@ public sealed partial class DevWindowTabUI : Control
|
||||
});
|
||||
foreach (var (prop, value) in values)
|
||||
{
|
||||
ControlProperties.AddChild(new BoxContainer
|
||||
var button = new ContainerButton
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 3,
|
||||
Margin = new Thickness(3, 1),
|
||||
Children =
|
||||
{
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 3,
|
||||
Margin = new Thickness(3, 1),
|
||||
Children =
|
||||
{
|
||||
new Label { Text = $"{prop}", FontColorOverride = Color.GreenYellow },
|
||||
new Label { Text = ":" }, // this is for the non colored ":", intentional
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label { Text = $"{prop}", FontColorOverride = Color.GreenYellow },
|
||||
new Label { Text = ":" }, // this is for the non colored ":", intentional
|
||||
}
|
||||
},
|
||||
new Label { Text = $"{value}" },
|
||||
}
|
||||
},
|
||||
new Label { Text = $"{value}" },
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
button.OnPressed += _ =>
|
||||
{
|
||||
// TODO replace with parenting to popup container on WindowRoot
|
||||
UIPopup.Text = GuiDumpCommand.PropertyValuesString(SelectedControl, prop);
|
||||
var box = UIBox2.FromDimensions(UserInterfaceManager.MousePositionScaled.Position
|
||||
- GlobalPosition, Vector2.One);
|
||||
UIPopup.Open(box);
|
||||
};
|
||||
ControlProperties.AddChild(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<DevWindowTabUIPopup xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Robust.Client.UserInterface.DevWindowTabUIPopup"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics">
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#40404C" />
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
<Label Name="TextLabel" />
|
||||
</DevWindowTabUIPopup>
|
||||
@@ -0,0 +1,20 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
internal sealed partial class DevWindowTabUIPopup : Popup
|
||||
{
|
||||
public string? Text
|
||||
{
|
||||
get => TextLabel.Text;
|
||||
set => TextLabel.Text = value;
|
||||
}
|
||||
|
||||
public DevWindowTabUIPopup()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.ViewVariables.Editors;
|
||||
using Robust.Client.ViewVariables.Instances;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
@@ -29,6 +31,8 @@ namespace Robust.Client.ViewVariables
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IRobustSerializer _robustSerializer = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IResourceManager _resManager = default!;
|
||||
|
||||
private uint _nextReqId = 1;
|
||||
private readonly Vector2i _defaultWindowSize = (640, 420);
|
||||
@@ -126,6 +130,26 @@ namespace Robust.Client.ViewVariables
|
||||
return new VVPropEditorString();
|
||||
}
|
||||
|
||||
if (type == typeof(EntProtoId?))
|
||||
{
|
||||
return new VVPropEditorNullableEntProtoId();
|
||||
}
|
||||
|
||||
if (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))!;
|
||||
@@ -206,6 +230,12 @@ namespace Robust.Client.ViewVariables
|
||||
return new VVPropEditorTimeSpan();
|
||||
}
|
||||
|
||||
if (typeof(SoundSpecifier).IsAssignableFrom(type))
|
||||
{
|
||||
var control = new VVPropEditorSoundSpecifier(_protoManager, _resManager);
|
||||
return control;
|
||||
}
|
||||
|
||||
if (type == typeof(ViewVariablesBlobMembers.ServerKeyValuePairToken) ||
|
||||
type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Client.ViewVariables.Editors;
|
||||
|
||||
internal sealed class VVPropEditorNullableEntProtoId : VVPropEditor
|
||||
{
|
||||
protected override Control MakeUI(object? value)
|
||||
{
|
||||
var lineEdit = new LineEdit
|
||||
{
|
||||
Text = value is EntProtoId protoId ? protoId.Id : "",
|
||||
Editable = !ReadOnly,
|
||||
HorizontalExpand = true,
|
||||
};
|
||||
|
||||
if (!ReadOnly)
|
||||
{
|
||||
lineEdit.OnTextEntered += e =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(e.Text))
|
||||
{
|
||||
ValueChanged(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.ViewVariables.Editors;
|
||||
|
||||
public sealed class VVPropEditorSoundSpecifier : VVPropEditor
|
||||
{
|
||||
private readonly IPrototypeManager _protoManager;
|
||||
private readonly IResourceManager _resManager;
|
||||
|
||||
// Need to cache to some level just to make sure each edit doesn't reset the specifier to the default.
|
||||
|
||||
private SoundSpecifier? _specifier;
|
||||
|
||||
public VVPropEditorSoundSpecifier(IPrototypeManager protoManager, IResourceManager resManager)
|
||||
{
|
||||
_protoManager = protoManager;
|
||||
_resManager = resManager;
|
||||
}
|
||||
|
||||
protected override Control MakeUI(object? value)
|
||||
{
|
||||
var typeButton = new OptionButton()
|
||||
{
|
||||
Disabled = ReadOnly,
|
||||
};
|
||||
|
||||
typeButton.AddItem(Loc.GetString("vv-sound-none"));
|
||||
typeButton.AddItem(Loc.GetString("vv-sound-collection"), 1);
|
||||
typeButton.AddItem(Loc.GetString("vv-sound-path"), 2);
|
||||
|
||||
var editBox = new LineEdit()
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Editable = !ReadOnly,
|
||||
};
|
||||
|
||||
var pathControls = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
typeButton,
|
||||
editBox
|
||||
},
|
||||
SetSize = new Vector2(384f, 32f)
|
||||
};
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SoundCollectionSpecifier collection:
|
||||
typeButton.SelectId(1);
|
||||
editBox.Text = collection.Collection ?? string.Empty;
|
||||
_specifier = collection;
|
||||
break;
|
||||
case SoundPathSpecifier path:
|
||||
typeButton.SelectId(2);
|
||||
editBox.Text = path.Path.ToString();
|
||||
_specifier = path;
|
||||
break;
|
||||
default:
|
||||
_specifier = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
typeButton.OnItemSelected += args =>
|
||||
{
|
||||
typeButton.SelectId(args.Id);
|
||||
editBox.Text = string.Empty;
|
||||
|
||||
editBox.Editable = !ReadOnly && typeButton.SelectedId > 0;
|
||||
|
||||
if (typeButton.SelectedId == 0)
|
||||
{
|
||||
// Dummy value
|
||||
ValueChanged(new SoundPathSpecifier(""));
|
||||
}
|
||||
};
|
||||
|
||||
editBox.OnTextEntered += args =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(args.Text))
|
||||
return;
|
||||
|
||||
switch (typeButton.SelectedId)
|
||||
{
|
||||
case 1:
|
||||
if (!_protoManager.HasIndex<SoundCollectionPrototype>(args.Text))
|
||||
return;
|
||||
|
||||
_specifier = new SoundCollectionSpecifier(args.Text)
|
||||
{
|
||||
Params = _specifier?.Params ?? AudioParams.Default,
|
||||
};
|
||||
ValueChanged(_specifier);
|
||||
break;
|
||||
case 2:
|
||||
var path = new ResPath(args.Text);
|
||||
|
||||
if (!_resManager.ContentFileExists(path))
|
||||
return;
|
||||
|
||||
_specifier = new SoundPathSpecifier(args.Text)
|
||||
{
|
||||
Params = _specifier?.Params ?? AudioParams.Default,
|
||||
};
|
||||
|
||||
ValueChanged(_specifier);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Audio params
|
||||
|
||||
/* Volume */
|
||||
|
||||
var volumeEdit = new LineEdit()
|
||||
{
|
||||
Text = _specifier?.Params.Volume.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
|
||||
HorizontalExpand = true,
|
||||
Editable = !ReadOnly && _specifier != null,
|
||||
};
|
||||
|
||||
volumeEdit.OnTextEntered += args =>
|
||||
{
|
||||
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
|
||||
return;
|
||||
|
||||
_specifier.Params = _specifier.Params.WithVolume(floatValue);
|
||||
ValueChanged(_specifier);
|
||||
};
|
||||
|
||||
var volumeContainer = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label()
|
||||
{
|
||||
Text = Loc.GetString("vv-sound-volume"),
|
||||
},
|
||||
volumeEdit,
|
||||
}
|
||||
};
|
||||
|
||||
/* Pitch */
|
||||
|
||||
var pitchEdit = new LineEdit()
|
||||
{
|
||||
Text = _specifier?.Params.Pitch.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
|
||||
HorizontalExpand = true,
|
||||
Editable = !ReadOnly && _specifier != null,
|
||||
};
|
||||
|
||||
pitchEdit.OnTextEntered += args =>
|
||||
{
|
||||
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
|
||||
return;
|
||||
|
||||
_specifier.Params = _specifier.Params.WithPitchScale(floatValue);
|
||||
ValueChanged(_specifier);
|
||||
};
|
||||
|
||||
var pitchContainer = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label()
|
||||
{
|
||||
Text = Loc.GetString("vv-sound-pitch"),
|
||||
},
|
||||
pitchEdit,
|
||||
}
|
||||
};
|
||||
|
||||
/* MaxDistance */
|
||||
|
||||
var maxDistanceEdit = new LineEdit()
|
||||
{
|
||||
Text = _specifier?.Params.MaxDistance.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
|
||||
HorizontalExpand = true,
|
||||
Editable = !ReadOnly && _specifier != null,
|
||||
};
|
||||
|
||||
maxDistanceEdit.OnTextEntered += args =>
|
||||
{
|
||||
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
|
||||
return;
|
||||
|
||||
_specifier.Params = _specifier.Params.WithMaxDistance(floatValue);
|
||||
ValueChanged(_specifier);
|
||||
};
|
||||
|
||||
var maxDistanceContainer = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label()
|
||||
{
|
||||
Text = Loc.GetString("vv-sound-max-distance"),
|
||||
},
|
||||
maxDistanceEdit,
|
||||
}
|
||||
};
|
||||
|
||||
/* RolloffFactor */
|
||||
|
||||
var rolloffFactorEdit = new LineEdit()
|
||||
{
|
||||
Text = _specifier?.Params.RolloffFactor.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
|
||||
HorizontalExpand = true,
|
||||
Editable = !ReadOnly && _specifier != null,
|
||||
};
|
||||
|
||||
rolloffFactorEdit.OnTextEntered += args =>
|
||||
{
|
||||
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
|
||||
return;
|
||||
|
||||
_specifier.Params = _specifier.Params.WithRolloffFactor(floatValue);
|
||||
ValueChanged(_specifier);
|
||||
};
|
||||
|
||||
var rolloffFactorContainer = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label()
|
||||
{
|
||||
Text = Loc.GetString("vv-sound-rolloff-factor"),
|
||||
},
|
||||
rolloffFactorEdit,
|
||||
}
|
||||
};
|
||||
|
||||
/* ReferenceDistance */
|
||||
|
||||
var referenceDistanceEdit = new LineEdit()
|
||||
{
|
||||
Text = _specifier?.Params.ReferenceDistance.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
|
||||
HorizontalExpand = true,
|
||||
Editable = !ReadOnly && _specifier != null,
|
||||
};
|
||||
|
||||
referenceDistanceEdit.OnTextEntered += args =>
|
||||
{
|
||||
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
|
||||
return;
|
||||
|
||||
_specifier.Params = _specifier.Params.WithReferenceDistance(floatValue);
|
||||
ValueChanged(_specifier);
|
||||
};
|
||||
|
||||
var referenceDistanceContainer = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label()
|
||||
{
|
||||
Text = Loc.GetString("vv-sound-reference-distance"),
|
||||
},
|
||||
referenceDistanceEdit,
|
||||
}
|
||||
};
|
||||
|
||||
/* Loop */
|
||||
|
||||
var loopButton = new Button()
|
||||
{
|
||||
Text = Loc.GetString("vv-sound-loop"),
|
||||
Pressed = _specifier?.Params.Loop ?? false,
|
||||
ToggleMode = true,
|
||||
Disabled = ReadOnly || _specifier == null,
|
||||
};
|
||||
|
||||
loopButton.OnPressed += args =>
|
||||
{
|
||||
if (_specifier == null)
|
||||
return;
|
||||
|
||||
_specifier.Params = _specifier.Params.WithLoop(args.Button.Pressed);
|
||||
ValueChanged(_specifier);
|
||||
};
|
||||
|
||||
/* PlayOffsetSeconds */
|
||||
|
||||
var playOffsetEdit = new LineEdit()
|
||||
{
|
||||
Text = _specifier?.Params.PlayOffsetSeconds.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
|
||||
HorizontalExpand = true,
|
||||
Editable = !ReadOnly && _specifier != null,
|
||||
};
|
||||
|
||||
playOffsetEdit.OnTextEntered += args =>
|
||||
{
|
||||
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
|
||||
return;
|
||||
|
||||
_specifier.Params = _specifier.Params.WithPlayOffset(floatValue);
|
||||
ValueChanged(_specifier);
|
||||
};
|
||||
|
||||
var playOffsetContainer = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label()
|
||||
{
|
||||
Text = Loc.GetString("vv-sound-play-offset"),
|
||||
},
|
||||
playOffsetEdit,
|
||||
}
|
||||
};
|
||||
|
||||
/* Variation */
|
||||
|
||||
var variationEdit = new LineEdit()
|
||||
{
|
||||
Text = _specifier?.Params.Variation.ToString() ?? string.Empty,
|
||||
HorizontalExpand = true,
|
||||
Editable = !ReadOnly && _specifier != null,
|
||||
};
|
||||
|
||||
variationEdit.OnTextEntered += args =>
|
||||
{
|
||||
if (!float.TryParse(args.Text, out var floatValue) || _specifier == null)
|
||||
return;
|
||||
|
||||
_specifier.Params = _specifier.Params.WithVariation(floatValue);
|
||||
ValueChanged(_specifier);
|
||||
};
|
||||
|
||||
var variationContainer = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Label()
|
||||
{
|
||||
Text = Loc.GetString("vv-sound-variation"),
|
||||
},
|
||||
variationEdit,
|
||||
}
|
||||
};
|
||||
|
||||
var audioParamsControls = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
volumeContainer,
|
||||
pitchContainer,
|
||||
maxDistanceContainer,
|
||||
rolloffFactorContainer,
|
||||
referenceDistanceContainer,
|
||||
loopButton,
|
||||
playOffsetContainer,
|
||||
variationContainer,
|
||||
}
|
||||
};
|
||||
|
||||
var controls = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
pathControls,
|
||||
audioParamsControls,
|
||||
}
|
||||
};
|
||||
|
||||
return controls;
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
var entity = Spawn("Audio", MapCoordinates.Nullspace);
|
||||
var audio = SetupAudio(entity, filename, audioParams);
|
||||
@@ -78,8 +78,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return null;
|
||||
|
||||
if (TerminatingOrDeleted(uid))
|
||||
{
|
||||
Log.Error($"Tried to play audio on a terminating / deleted entity {ToPrettyString(uid)}. Trace: {Environment.StackTrace}");
|
||||
@@ -94,8 +97,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return null;
|
||||
|
||||
if (TerminatingOrDeleted(uid))
|
||||
{
|
||||
Log.Error($"Tried to play audio on a terminating / deleted entity {ToPrettyString(uid)}. Trace: {Environment.StackTrace}");
|
||||
@@ -109,8 +115,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return null;
|
||||
|
||||
if (TerminatingOrDeleted(coordinates.EntityId))
|
||||
{
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}. Trace: {Environment.StackTrace}");
|
||||
@@ -128,9 +137,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return null;
|
||||
|
||||
if (TerminatingOrDeleted(coordinates.EntityId))
|
||||
{
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}. Trace: {Environment.StackTrace}");
|
||||
@@ -176,12 +188,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return audio;
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, Filter.SinglePlayer(recipient), false, audioParams);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayGlobal(filename, actor.PlayerSession, audioParams);
|
||||
@@ -189,12 +201,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return null;
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, Filter.SinglePlayer(recipient), uid, false, audioParams);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayEntity(filename, actor.PlayerSession, uid, audioParams);
|
||||
@@ -202,12 +214,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return null;
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, Filter.SinglePlayer(recipient), coordinates, false, audioParams);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayStatic(filename, actor.PlayerSession, coordinates, audioParams);
|
||||
@@ -238,7 +250,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...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace Robust.Server
|
||||
[Dependency] private readonly IWatchdogApi _watchdogApi = default!;
|
||||
[Dependency] private readonly HubManager _hubManager = default!;
|
||||
[Dependency] private readonly IScriptHost _scriptHost = default!;
|
||||
[Dependency] private readonly IMetricsManager _metricsManager = default!;
|
||||
[Dependency] private readonly IMetricsManagerInternal _metricsManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IRobustMappedStringSerializer _stringSerializer = default!;
|
||||
[Dependency] private readonly ILocalizationManagerInternal _loc = default!;
|
||||
@@ -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();
|
||||
@@ -749,6 +749,8 @@ namespace Robust.Server
|
||||
_hubManager.Heartbeat();
|
||||
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
|
||||
|
||||
_metricsManager.FrameUpdate();
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
|
||||
@@ -66,7 +66,7 @@ public sealed class SpinCommand : LocalizedCommands
|
||||
}
|
||||
|
||||
var physicsSystem = _entities.System<SharedPhysicsSystem>();
|
||||
physicsSystem.SetAngularDamping(physics, drag);
|
||||
physicsSystem.SetAngularDamping(target.Value, physics, drag);
|
||||
physicsSystem.SetAngularVelocity(target.Value, speed, body: physics);
|
||||
}
|
||||
}
|
||||
|
||||
89
Robust.Server/DataMetrics/MetricsManager.Factory.cs
Normal file
89
Robust.Server/DataMetrics/MetricsManager.Factory.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.DataMetrics;
|
||||
|
||||
internal sealed partial class MetricsManager : IMeterFactory
|
||||
{
|
||||
private readonly Dictionary<string, List<CachedMeter>> _meterCache = new();
|
||||
private readonly object _meterCacheLock = new();
|
||||
|
||||
Meter IMeterFactory.Create(MeterOptions options)
|
||||
{
|
||||
if (options.Scope != null && options.Scope != this)
|
||||
throw new InvalidOperationException("Cannot specify a custom scope when creating a meter");
|
||||
|
||||
lock (_meterCacheLock)
|
||||
{
|
||||
if (LockedFindCachedMeter(options) is { } cached)
|
||||
return cached.Meter;
|
||||
|
||||
var meter = new Meter(options.Name, options.Version, options.Tags, this);
|
||||
var meterList = _meterCache.GetOrNew(options.Name);
|
||||
meterList.Add(new CachedMeter(options.Version, TagsToDict(options.Tags), meter));
|
||||
return meter;
|
||||
}
|
||||
}
|
||||
|
||||
private CachedMeter? LockedFindCachedMeter(MeterOptions options)
|
||||
{
|
||||
if (!_meterCache.TryGetValue(options.Name, out var metersList))
|
||||
return null;
|
||||
|
||||
var tagsDict = TagsToDict(options.Tags);
|
||||
|
||||
foreach (var cachedMeter in metersList)
|
||||
{
|
||||
if (cachedMeter.Version == options.Version && TagsMatch(tagsDict, cachedMeter.Tags))
|
||||
return cachedMeter;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TagsMatch(Dictionary<string, object?> a, Dictionary<string, object?> b)
|
||||
{
|
||||
if (a.Count != b.Count)
|
||||
return false;
|
||||
|
||||
foreach (var (key, valueA) in a)
|
||||
{
|
||||
if (!b.TryGetValue(key, out var valueB))
|
||||
return false;
|
||||
|
||||
if (!Equals(valueA, valueB))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> TagsToDict(IEnumerable<KeyValuePair<string, object?>>? tags)
|
||||
{
|
||||
return tags?.ToDictionary() ?? [];
|
||||
}
|
||||
|
||||
private void DisposeMeters()
|
||||
{
|
||||
lock (_meterCacheLock)
|
||||
{
|
||||
foreach (var meters in _meterCache.Values)
|
||||
{
|
||||
foreach (var meter in meters)
|
||||
{
|
||||
meter.Meter.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CachedMeter(string? version, Dictionary<string, object?> tags, Meter meter)
|
||||
{
|
||||
public readonly string? Version = version;
|
||||
public readonly Dictionary<string, object?> Tags = tags;
|
||||
public readonly Meter Meter = meter;
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,20 @@ internal sealed partial class MetricsManager
|
||||
private sealed class ManagedHttpListenerMetricsServer : MetricHandler
|
||||
{
|
||||
private readonly ISawmill _sawmill;
|
||||
private readonly Func<CancellationToken, Task>? _beforeCollect;
|
||||
private readonly HttpListener _listener;
|
||||
private readonly CollectorRegistry _registry;
|
||||
|
||||
public ManagedHttpListenerMetricsServer(ISawmill sawmill, string host, int port, string url = "metrics/",
|
||||
CollectorRegistry? registry = null)
|
||||
public ManagedHttpListenerMetricsServer(
|
||||
ISawmill sawmill,
|
||||
string host,
|
||||
int port,
|
||||
string url = "metrics/",
|
||||
CollectorRegistry? registry = null,
|
||||
Func<CancellationToken, Task>? beforeCollect = null)
|
||||
{
|
||||
_sawmill = sawmill;
|
||||
_beforeCollect = beforeCollect;
|
||||
_listener = new HttpListener();
|
||||
_listener.Prefixes.Add($"http://{host}:{port}/{url}");
|
||||
_registry = registry ?? Metrics.DefaultRegistry;
|
||||
@@ -57,6 +64,12 @@ internal sealed partial class MetricsManager
|
||||
{
|
||||
MetricsEvents.Log.ScrapeStart();
|
||||
|
||||
// prometheus-net does have a "before collect" callback of its own.
|
||||
// But it doesn't get ran before stuff like their System.Diagnostics.Metrics integration,
|
||||
// So I'm just gonna make my own here.
|
||||
if (_beforeCollect != null)
|
||||
await _beforeCollect(cancel);
|
||||
|
||||
var stream = resp.OutputStream;
|
||||
// prometheus-net is a terrible library and have to do all this insanity,
|
||||
// just to handle the ScrapeFailedException correctly.
|
||||
|
||||
62
Robust.Server/DataMetrics/MetricsManager.UpdateMetrics.cs
Normal file
62
Robust.Server/DataMetrics/MetricsManager.UpdateMetrics.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Server.DataMetrics;
|
||||
|
||||
internal sealed partial class MetricsManager
|
||||
{
|
||||
//
|
||||
// Handles the implementation of the "UpdateMetrics" callback.
|
||||
//
|
||||
|
||||
public event Action? UpdateMetrics;
|
||||
|
||||
private TimeSpan _fixedUpdateInterval;
|
||||
private TimeSpan _nextFixedUpdate;
|
||||
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
private void InitializeUpdateMetrics()
|
||||
{
|
||||
_cfg.OnValueChanged(
|
||||
CVars.MetricsUpdateInterval,
|
||||
seconds =>
|
||||
{
|
||||
_fixedUpdateInterval = TimeSpan.FromSeconds(seconds);
|
||||
_nextFixedUpdate = _gameTiming.RealTime + _fixedUpdateInterval;
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
public void FrameUpdate()
|
||||
{
|
||||
if (_fixedUpdateInterval == TimeSpan.Zero)
|
||||
return;
|
||||
|
||||
var time = _gameTiming.RealTime;
|
||||
|
||||
if (_nextFixedUpdate > time)
|
||||
return;
|
||||
|
||||
_nextFixedUpdate = time + _fixedUpdateInterval;
|
||||
|
||||
_sawmill.Verbose("Running fixed metrics update");
|
||||
UpdateMetrics?.Invoke();
|
||||
}
|
||||
|
||||
private async Task BeforeCollectCallback(CancellationToken cancel)
|
||||
{
|
||||
if (UpdateMetrics == null)
|
||||
return;
|
||||
|
||||
await _taskManager.TaskOnMainThread(() =>
|
||||
{
|
||||
UpdateMetrics?.Invoke();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,54 @@
|
||||
using System;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Prometheus;
|
||||
using Prometheus.DotNetRuntime;
|
||||
using Prometheus.DotNetRuntime.Metrics.Producers;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using EventSource = System.Diagnostics.Tracing.EventSource;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Robust.Server.DataMetrics;
|
||||
|
||||
internal sealed partial class MetricsManager : IMetricsManager, IDisposable
|
||||
/// <summary>
|
||||
/// Manages OpenTelemetry metrics exposure.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If enabled via <see cref="CVars.MetricsEnabled"/>, metrics about the game server are exposed via a HTTP server
|
||||
/// in an OpenTelemetry-compatible format (Prometheus).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Metrics can be added through the types in <c>System.Diagnostics.Metrics</c> or <c>Prometheus</c>.
|
||||
/// IoC contains an implementation of <see cref="IMeterFactory"/> that can be used to instantiate meters.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IMetricsManager
|
||||
{
|
||||
/// <summary>
|
||||
/// An event that gets raised on the main thread when complex metrics should be updated.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This event is raised on the main thread before a Prometheus collection happens,
|
||||
/// and also with a fixed interval if <see cref="CVars.MetricsUpdateInterval"/> is set.
|
||||
/// You can use it to update complex metrics that can't "just" be stuffed into a counter.
|
||||
/// </remarks>
|
||||
event Action UpdateMetrics;
|
||||
}
|
||||
|
||||
internal sealed partial class MetricsManager : IMetricsManagerInternal, IDisposable
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = default!;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
@@ -55,6 +83,8 @@ internal sealed partial class MetricsManager : IMetricsManager, IDisposable
|
||||
{
|
||||
_cfg.OnValueChanged(cVar, _ => Reload());
|
||||
}
|
||||
|
||||
InitializeUpdateMetrics();
|
||||
}
|
||||
|
||||
private async Task Stop()
|
||||
@@ -73,6 +103,8 @@ internal sealed partial class MetricsManager : IMetricsManager, IDisposable
|
||||
|
||||
async void IDisposable.Dispose()
|
||||
{
|
||||
DisposeMeters();
|
||||
|
||||
await Stop();
|
||||
|
||||
_initialized = false;
|
||||
@@ -100,7 +132,12 @@ internal sealed partial class MetricsManager : IMetricsManager, IDisposable
|
||||
|
||||
_sawmill.Info("Prometheus metrics enabled, host: {1} port: {0}", port, host);
|
||||
var sawmill = Logger.GetSawmill("metrics.server");
|
||||
_metricServer = new ManagedHttpListenerMetricsServer(sawmill, host, port);
|
||||
_metricServer = new ManagedHttpListenerMetricsServer(
|
||||
sawmill,
|
||||
host,
|
||||
port,
|
||||
registry: Metrics.DefaultRegistry,
|
||||
beforeCollect: BeforeCollectCallback);
|
||||
_metricServer.Start();
|
||||
|
||||
if (_cfg.GetCVar(CVars.MetricsRuntime))
|
||||
@@ -190,7 +227,8 @@ internal sealed partial class MetricsManager : IMetricsManager, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IMetricsManager
|
||||
internal interface IMetricsManagerInternal : IMetricsManager
|
||||
{
|
||||
void Initialize();
|
||||
void FrameUpdate();
|
||||
}
|
||||
|
||||
@@ -280,6 +280,9 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
// Load the prototype data onto entities, e.g. transform parents, etc.
|
||||
LoadEntities(data);
|
||||
|
||||
// Assign MapSaveTileMapComponent to all read grids.
|
||||
SaveGridTileMap(data);
|
||||
|
||||
// Build the scene graph / transform hierarchy to know the order to startup entities.
|
||||
// This also allows us to swap out the root node up front if necessary.
|
||||
BuildEntityHierarchy(data);
|
||||
@@ -576,6 +579,19 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
meta.LastComponentRemoved = _timing.CurTick;
|
||||
}
|
||||
|
||||
private void SaveGridTileMap(MapData mapData)
|
||||
{
|
||||
DebugTools.Assert(_context.TileMap != null);
|
||||
|
||||
foreach (var entity in mapData.EntitiesToDeserialize.Keys)
|
||||
{
|
||||
if (HasComp<MapGridComponent>(entity))
|
||||
{
|
||||
EnsureComp<MapSaveTileMapComponent>(entity).TileMap = _context.TileMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildEntityHierarchy(MapData mapData)
|
||||
{
|
||||
_stopwatch.Restart();
|
||||
@@ -981,28 +997,74 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
var gridQuery = GetEntityQuery<MapGridComponent>();
|
||||
var tileDefs = new HashSet<int>();
|
||||
|
||||
Dictionary<int, string>? origTileMap = null;
|
||||
foreach (var ent in entities)
|
||||
{
|
||||
if (!gridQuery.TryGetComponent(ent, out var grid))
|
||||
continue;
|
||||
|
||||
var tileEnumerator = grid.GetAllTilesEnumerator(false);
|
||||
|
||||
var tileEnumerator = _mapSystem.GetAllTilesEnumerator(ent, grid, ignoreEmpty: false);
|
||||
while (tileEnumerator.MoveNext(out var tileRef))
|
||||
{
|
||||
tileDefs.Add(tileRef.Value.Tile.TypeId);
|
||||
}
|
||||
|
||||
if (TryComp(ent, out MapSaveTileMapComponent? saveTileMap))
|
||||
origTileMap ??= saveTileMap.TileMap;
|
||||
}
|
||||
|
||||
Dictionary<int, int> tileIdMap;
|
||||
if (origTileMap != null)
|
||||
{
|
||||
tileIdMap = new Dictionary<int, int>();
|
||||
|
||||
// We are re-saving a map, so we have an original tile map we can preserve.
|
||||
foreach (var (origId, prototypeId) in origTileMap)
|
||||
{
|
||||
// Skip removed tile definitions.
|
||||
if (!_tileDefManager.TryGetDefinition(prototypeId, out var definition))
|
||||
continue;
|
||||
|
||||
tileIdMap.Add(definition.TileId, origId);
|
||||
}
|
||||
|
||||
// Assign new IDs for all new tile types.
|
||||
var nextId = 0;
|
||||
foreach (var tileId in tileDefs)
|
||||
{
|
||||
if (tileIdMap.ContainsKey(tileId))
|
||||
continue;
|
||||
|
||||
// New tile, assign new ID that isn't taken by original tile map.
|
||||
while (origTileMap.ContainsKey(nextId))
|
||||
{
|
||||
nextId += 1;
|
||||
}
|
||||
|
||||
tileIdMap.Add(tileId, nextId);
|
||||
nextId += 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make no-op tile ID map.
|
||||
tileIdMap = tileDefs.ToDictionary(x => x, x => x);
|
||||
}
|
||||
|
||||
DebugTools.Assert(
|
||||
tileIdMap.Count == tileIdMap.Values.Distinct().Count(),
|
||||
"Tile ID map has double mapped values??");
|
||||
|
||||
_context.TileWriteMap = tileIdMap;
|
||||
|
||||
var tileMap = new MappingDataNode();
|
||||
rootNode.Add("tilemap", tileMap);
|
||||
var ordered = new List<int>(tileDefs);
|
||||
ordered.Sort();
|
||||
|
||||
foreach (var tyleId in ordered)
|
||||
foreach (var (nativeId, mapId) in tileIdMap.OrderBy(x => x.Key))
|
||||
{
|
||||
var tileDef = _tileDefManager[tyleId];
|
||||
tileMap.Add(tyleId.ToString(CultureInfo.InvariantCulture), tileDef.ID);
|
||||
tileMap.Add(
|
||||
mapId.ToString(CultureInfo.InvariantCulture),
|
||||
_tileDefManager[nativeId].ID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1176,11 +1238,12 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
|
||||
foreach (var component in EntityManager.GetComponents(entityUid))
|
||||
{
|
||||
if (component is MapSaveIdComponent)
|
||||
var compType = component.GetType();
|
||||
var registration = _factory.GetRegistration(compType);
|
||||
if (registration.Unsaved)
|
||||
continue;
|
||||
|
||||
var compType = component.GetType();
|
||||
var compName = _factory.GetComponentName(compType);
|
||||
var compName = registration.Name;
|
||||
_context.CurrentComponent = compName;
|
||||
MappingDataNode? compMapping;
|
||||
MappingDataNode? protMapping = null;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Robust.Server.GameObjects
|
||||
/// This component stores the previous map UID of entities from map load.
|
||||
/// This can then be used to re-serialize the entity with the same UID for the merge driver to recognize.
|
||||
/// </remarks>
|
||||
[RegisterComponent]
|
||||
[RegisterComponent, UnsavedComponent]
|
||||
public sealed partial class MapSaveIdComponent : Component
|
||||
{
|
||||
public int Uid { get; set; }
|
||||
|
||||
24
Robust.Server/GameObjects/MapSaveTileMapComponent.cs
Normal file
24
Robust.Server/GameObjects/MapSaveTileMapComponent.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Used by <see cref="MapLoaderSystem"/> to track the original tile map from when a map was loaded.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This component is used to reduce differences on map saving, by making it so that a tile map can be re-used between map saves even if internal engine IDs change.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This component is created on every grid entity read during map load.
|
||||
/// This means loading a multi-grid map will create multiple of these components.
|
||||
/// When re-saving the map, the map loader will arbitrarily choose which available <see cref="MapSaveTileMapComponent"/>
|
||||
/// to use.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RegisterComponent, UnsavedComponent]
|
||||
internal sealed partial class MapSaveTileMapComponent : Component
|
||||
{
|
||||
public Dictionary<int, string> TileMap = [];
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,84 +48,89 @@ internal sealed partial class PvsSystem
|
||||
// We add chunk-size here so that its consistent with the normal PVS range setting.
|
||||
// I.e., distance here is the Chebyshev distance to the centre of each chunk, but the normal pvs range only
|
||||
// required that the chunk be touching the box, not the centre.
|
||||
var limit = distance < (_lowLodDistance + ChunkSize) / 2
|
||||
var count = distance <= (_viewSize + ChunkSize) / 2
|
||||
? chunk.Contents.Count
|
||||
: chunk.LodCounts[0];
|
||||
|
||||
// If the PVS budget is exceeded, it should still be safe to send all of the chunk's direct children, though
|
||||
// after that we have no guarantee that an entity's parent got sent.
|
||||
var directChildren = Math.Min(limit, chunk.LodCounts[2]);
|
||||
|
||||
// Send entities on the chunk.
|
||||
var span = CollectionsMarshal.AsSpan(chunk.Contents);
|
||||
for (var i = 0; i < limit; i++)
|
||||
var span = CollectionsMarshal.AsSpan(chunk.Contents)[..count];
|
||||
foreach (ref var ent in span)
|
||||
{
|
||||
var ent = span[i];
|
||||
if ((mask & ent.Comp.VisibilityMask) != ent.Comp.VisibilityMask)
|
||||
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))
|
||||
limit = directChildren;
|
||||
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
|
||||
if ((mask & meta.VisMask) == meta.VisMask)
|
||||
AddEntity(session, ref ent, ref meta, fromTick);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 void 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;
|
||||
|
||||
if (meta.LifeStage >= EntityLifeStage.Terminating)
|
||||
{
|
||||
Log.Error($"Attempted to send deleted entity: {ToPrettyString(ent.Uid)}");
|
||||
EntityManager.QueueDeleteEntity(ent.Uid);
|
||||
return;
|
||||
}
|
||||
|
||||
var (entered,budgetExceeded) = IsEnteringPvsRange(ref data, fromTick, ref session.Budget);
|
||||
|
||||
if (budgetExceeded)
|
||||
return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (entered)
|
||||
{
|
||||
var state = GetEntityState(session.Session, ent.Uid, data.EntityLastAcked, ent.Meta);
|
||||
session.States.Add(state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (meta.LastModifiedTick <= fromTick)
|
||||
return;
|
||||
|
||||
var entState = GetEntityState(session.Session, ent.Uid, fromTick , ent.Meta);
|
||||
|
||||
if (!entState.Empty)
|
||||
session.States.Add(entState);
|
||||
}
|
||||
|
||||
/// <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 +152,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 +171,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 +185,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;
|
||||
@@ -59,12 +62,14 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
public bool CullingEnabled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the side of the view bounds square.
|
||||
/// Size of the side of the view bounds square. Related to <see cref="CVars.NetMaxUpdateRange"/>
|
||||
/// </summary>
|
||||
private float _viewSize;
|
||||
|
||||
// see CVars.NetLowLodDistance
|
||||
private float _lowLodDistance;
|
||||
/// <summary>
|
||||
/// Size of the side of the priority view bounds square. Related to <see cref="CVars.NetPvsPriorityRange"/>
|
||||
/// </summary>
|
||||
private float _priorityViewSize;
|
||||
|
||||
/// <summary>
|
||||
/// Per-tick ack data to avoid re-allocating.
|
||||
@@ -75,6 +80,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 +116,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,20 +135,23 @@ 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);
|
||||
Subs.CVar(_configManager, CVars.NetLowLodRange, OnLodChanged, true);
|
||||
Subs.CVar(_configManager, CVars.NetPvsPriorityRange, OnPriorityRangeChanged, true);
|
||||
Subs.CVar(_configManager, CVars.NetForceAckThreshold, OnForceAckChanged, true);
|
||||
Subs.CVar(_configManager, CVars.NetPvsAsync, OnAsyncChanged, true);
|
||||
Subs.CVar(_configManager, CVars.NetPvsCompressLevel, ResetParallelism, true);
|
||||
|
||||
_serverGameStateManager.ClientAck += OnClientAck;
|
||||
_serverGameStateManager.ClientRequestFull += OnClientRequestFull;
|
||||
_parallelMgr.ParallelCountChanged += ResetParallelism;
|
||||
|
||||
InitializeDirty();
|
||||
|
||||
_parallelMgr.ParallelCountChanged += ResetParallelism;
|
||||
InitializePvsArray();
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -147,15 +160,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,16 +273,18 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
session.LastReceivedAck = _gameTiming.CurTick;
|
||||
session.RequestedFull = true;
|
||||
ClearSendHistory(session);
|
||||
ClearPlayerPvsData(session);
|
||||
}
|
||||
|
||||
private void OnViewsizeChanged(float value)
|
||||
{
|
||||
_viewSize = value;
|
||||
_viewSize = Math.Max(ChunkSize, value);
|
||||
OnPriorityRangeChanged(_configManager.GetCVar(CVars.NetPvsPriorityRange));
|
||||
}
|
||||
|
||||
private void OnLodChanged(float value)
|
||||
private void OnPriorityRangeChanged(float value)
|
||||
{
|
||||
_lowLodDistance = Math.Clamp(value, ChunkSize, 100f);
|
||||
_priorityViewSize = Math.Max(_viewSize, value);
|
||||
}
|
||||
|
||||
private void OnForceAckChanged(int value)
|
||||
@@ -342,31 +364,25 @@ 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)
|
||||
continue;
|
||||
|
||||
DebugTools.Assert(toSendSet.Contains(GetNetEntity(pUid)),
|
||||
$"Attempted to send an entity without sending it's parents. Entity: {ToPrettyString(pUid)}.");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -374,7 +390,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
private (Vector2 worldPos, float range, EntityUid? map) CalcViewBounds(Entity<TransformComponent, EyeComponent?> eye)
|
||||
{
|
||||
var size = Math.Max(eye.Comp2?.PvsSize ?? _viewSize, 1);
|
||||
var size = Math.Max(eye.Comp2?.PvsSize ?? _priorityViewSize, 1);
|
||||
return (_transform.GetWorldPosition(eye.Comp1), size / 2f, eye.Comp1.MapUid);
|
||||
}
|
||||
|
||||
@@ -409,7 +425,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();
|
||||
|
||||
@@ -104,12 +104,16 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
|
||||
root.Add("version", new ValueDataNode("6"));
|
||||
|
||||
gridNode.Value = SerializeTiles(value);
|
||||
Dictionary<int, int>? tileWriteMap = null;
|
||||
if (context is MapSerializationContext mapContext)
|
||||
tileWriteMap = mapContext.TileWriteMap;
|
||||
|
||||
gridNode.Value = SerializeTiles(value, tileWriteMap);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static string SerializeTiles(MapChunk chunk)
|
||||
private static string SerializeTiles(MapChunk chunk, Dictionary<int, int>? tileWriteMap)
|
||||
{
|
||||
// number of bytes written per tile, because sizeof(Tile) is useless.
|
||||
const int structSize = 6;
|
||||
@@ -125,7 +129,11 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
for (ushort x = 0; x < chunk.ChunkSize; x++)
|
||||
{
|
||||
var tile = chunk.GetTile(x, y);
|
||||
writer.Write(tile.TypeId);
|
||||
var typeId = tile.TypeId;
|
||||
if (tileWriteMap != null)
|
||||
typeId = tileWriteMap[typeId];
|
||||
|
||||
writer.Write(typeId);
|
||||
writer.Write((byte)tile.Flags);
|
||||
writer.Write(tile.Variant);
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ namespace Robust.Server.Physics
|
||||
grids.Add(foundSplits);
|
||||
}
|
||||
|
||||
var oldGrid = _mapManager.GetGrid(uid);
|
||||
var oldGrid = Comp<MapGridComponent>(uid);
|
||||
var oldGridUid = uid;
|
||||
|
||||
// Split time
|
||||
@@ -605,7 +605,7 @@ namespace Robust.Server.Physics
|
||||
|
||||
DebugTools.Assert(chunk.FilledTiles > 0);
|
||||
|
||||
var grid = _mapManager.GetGrid(gridEuid);
|
||||
var grid = Comp<MapGridComponent>(gridEuid);
|
||||
var group = CreateNodes(gridEuid, grid, chunk);
|
||||
_nodes[gridEuid][chunk.Indices] = group;
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" PrivateAssets="compile" />
|
||||
<PackageReference Include="SpaceWizards.Sodium" PrivateAssets="compile" />
|
||||
<PackageReference Include="SharpZstd.Interop" PrivateAssets="compile" />
|
||||
<PackageReference Condition="'$(FullRelease)' != 'True'" Include="JetBrains.Profiler.Api" />
|
||||
<PackageReference Condition="'$(RobustToolsBuild)' == 'True'" Include="JetBrains.Profiler.Api" />
|
||||
<PackageReference Include="Microsoft.NET.ILLink.Tasks" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using Robust.Server.Configuration;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.DataMetrics;
|
||||
@@ -80,6 +81,8 @@ namespace Robust.Server
|
||||
deps.Register<IWatchdogApi, WatchdogApi>();
|
||||
deps.Register<IScriptHost, ScriptHost>();
|
||||
deps.Register<IMetricsManager, MetricsManager>();
|
||||
deps.Register<IMetricsManagerInternal, MetricsManager>();
|
||||
deps.Register<IMeterFactory, MetricsManager>();
|
||||
deps.Register<IAuthManager, AuthManager>();
|
||||
deps.Register<HubManager, HubManager>();
|
||||
deps.Register<IRobustSerializer, ServerRobustSerializer>();
|
||||
|
||||
@@ -69,6 +69,12 @@ public sealed class PlayerCommand : ToolshedCommand
|
||||
{
|
||||
return sessions.AttachedEntity ?? default;
|
||||
}
|
||||
|
||||
[CommandImplementation("entity")]
|
||||
public EntityUid GetPlayerEntity([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] string username)
|
||||
{
|
||||
return GetPlayerEntity(Immediate(ctx, username));
|
||||
}
|
||||
}
|
||||
|
||||
public record struct NoSuchPlayerError(string Username) : IConError
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -296,7 +297,6 @@ namespace Robust.Server.ViewVariables
|
||||
|
||||
output = prototype;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Server.ViewVariables.Traits;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network.Messages;
|
||||
@@ -96,6 +97,9 @@ namespace Robust.Server.ViewVariables
|
||||
if (value is EntityUid uid)
|
||||
return IoCManager.Resolve<IEntityManager>().GetComponentOrNull<MetaDataComponent>(uid)?.NetEntity ?? NetEntity.Invalid;
|
||||
|
||||
if (value is SoundSpecifier)
|
||||
return value;
|
||||
|
||||
var valType = value.GetType();
|
||||
if (!valType.IsValueType)
|
||||
{
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using static Microsoft.CodeAnalysis.SymbolDisplayFormat;
|
||||
using static Microsoft.CodeAnalysis.SymbolDisplayMiscellaneousOptions;
|
||||
|
||||
namespace Robust.Shared.CompNetworkGenerator
|
||||
{
|
||||
@@ -19,19 +17,25 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
private const string GlobalEntityUidName = "global::Robust.Shared.GameObjects.EntityUid";
|
||||
private const string GlobalNullableEntityUidName = "global::Robust.Shared.GameObjects.EntityUid?";
|
||||
|
||||
private const string GlobalNetEntityName = "global::Robust.Shared.GameObjects.NetEntity";
|
||||
private const string GlobalNetEntityNullableName = "global::Robust.Shared.GameObjects.NetEntity?";
|
||||
|
||||
private const string GlobalEntityCoordinatesName = "global::Robust.Shared.Map.EntityCoordinates";
|
||||
private const string GlobalNullableEntityCoordinatesName = "global::Robust.Shared.Map.EntityCoordinates?";
|
||||
|
||||
private const string GlobalEntityUidSetName = "global::System.Collections.Generic.HashSet<global::Robust.Shared.GameObjects.EntityUid>";
|
||||
private const string GlobalNetEntityUidSetName = "global::System.Collections.Generic.HashSet<global::Robust.Shared.GameObjects.NetEntity>";
|
||||
private const string GlobalNetEntityUidSetName = $"global::System.Collections.Generic.HashSet<{GlobalNetEntityName}>";
|
||||
|
||||
private const string GlobalEntityUidListName = "global::System.Collections.Generic.List<global::Robust.Shared.GameObjects.EntityUid>";
|
||||
private const string GlobalNetEntityUidListName = "global::System.Collections.Generic.List<global::Robust.Shared.GameObjects.NetEntity>";
|
||||
private const string GlobalNetEntityUidListName = $"global::System.Collections.Generic.List<{GlobalNetEntityName}>";
|
||||
|
||||
private const string GlobalDictionaryName = "global::System.Collections.Generic.Dictionary<TKey, TValue>";
|
||||
private const string GlobalHashSetName = "global::System.Collections.Generic.HashSet<T>";
|
||||
private const string GlobalListName = "global::System.Collections.Generic.List<T>";
|
||||
|
||||
private static readonly SymbolDisplayFormat FullNullableFormat =
|
||||
FullyQualifiedFormat.WithMiscellaneousOptions(IncludeNullableReferenceTypeModifier);
|
||||
|
||||
private static string? GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle)
|
||||
{
|
||||
var nameSpace = classSymbol.ContainingNamespace.ToDisplayString();
|
||||
@@ -132,7 +136,7 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
|
||||
foreach (var (type, name) in fields)
|
||||
{
|
||||
var typeDisplayStr = type.ToDisplayString(FullyQualifiedFormat);
|
||||
var typeDisplayStr = type.ToDisplayString(FullNullableFormat);
|
||||
var nullable = type.NullableAnnotation == NullableAnnotation.Annotated;
|
||||
var nullableAnnotation = nullable ? "?" : string.Empty;
|
||||
|
||||
@@ -181,6 +185,62 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
|
||||
break;
|
||||
default:
|
||||
if (type is INamedTypeSymbol { TypeArguments.Length: 2 } named &&
|
||||
named.ConstructedFrom.ToDisplayString(FullyQualifiedFormat) == GlobalDictionaryName)
|
||||
{
|
||||
var key = named.TypeArguments[0].ToDisplayString(FullNullableFormat);
|
||||
var keyNullable = key.EndsWith("?");
|
||||
|
||||
var value = named.TypeArguments[1].ToDisplayString(FullNullableFormat);
|
||||
var valueNullable = value.EndsWith("?");
|
||||
|
||||
if (key is GlobalEntityUidName or GlobalNullableEntityUidName)
|
||||
{
|
||||
key = keyNullable ? GlobalNetEntityNullableName : GlobalNetEntityName;
|
||||
|
||||
var ensureGeneric = $"{componentName}, {value}";
|
||||
if (value is GlobalEntityUidName or GlobalNullableEntityUidName)
|
||||
{
|
||||
value = valueNullable ? GlobalNetEntityNullableName : GlobalNetEntityName;
|
||||
ensureGeneric = componentName;
|
||||
}
|
||||
|
||||
stateFields.Append($@"
|
||||
public Dictionary<{key}, {value}> {name} = default!;");
|
||||
|
||||
getStateInit.Append($@"
|
||||
{name} = GetNetEntityDictionary(component.{name}),");
|
||||
|
||||
if (valueNullable && value is not GlobalNetEntityName and not GlobalNetEntityNullableName)
|
||||
{
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityDictionaryNullableValue<{componentName}, {value}>(state.{name}, uid, component.{name});");
|
||||
}
|
||||
else
|
||||
{
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityDictionary<{ensureGeneric}>(state.{name}, uid, component.{name});");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (value is GlobalEntityUidName or GlobalNullableEntityUidName)
|
||||
{
|
||||
value = valueNullable ? GlobalNetEntityNullableName : GlobalNetEntityName;
|
||||
|
||||
stateFields.Append($@"
|
||||
public Dictionary<{key}, {value}> {name} = default!;");
|
||||
|
||||
getStateInit.Append($@"
|
||||
{name} = GetNetEntityDictionary(component.{name}),");
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityDictionary<{componentName}, {key}>(state.{name}, uid, component.{name});");
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stateFields.Append($@"
|
||||
public {typeDisplayStr} {name} = default!;");
|
||||
|
||||
@@ -193,7 +253,7 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
if (state.{name} == null)
|
||||
component.{name} = null;
|
||||
component.{name} = null!;
|
||||
else
|
||||
component.{name} = new(state.{name});");
|
||||
}
|
||||
@@ -219,6 +279,7 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
}
|
||||
|
||||
return $@"// <auto-generated />
|
||||
#nullable enable
|
||||
using System;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,192 @@ public static class Vector2Helpers
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the lengths of two vectors.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avoids square root computations by using squared lengths.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// a positive value if <paramref name="a"/> is longer than <paramref name="b"/>,
|
||||
/// a negative value if <paramref name="b"/> is longer than <paramref name="a"/>,
|
||||
/// or 0 if <paramref name="a"/> and <paramref name="b"/> have equal lengths.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float CompareLength(Vector2 a, Vector2 b)
|
||||
{
|
||||
return a.LengthSquared() - b.LengthSquared();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the length of a vector with a scalar.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avoids a square root computation by using squared length.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// a positive value if <paramref name="vec"/> is longer than <paramref name="scalar"/>,
|
||||
/// a negative value if <paramref name="vec"/> is shorter than <paramref name="scalar"/>,
|
||||
/// or 0 if <paramref name="vec"/> has a length equal to <paramref name="scalar"/>.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float CompareLength(Vector2 vec, float scalar)
|
||||
{
|
||||
return vec.LengthSquared() - (scalar * scalar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the length of this vector with a scalar.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avoids a square root computation by using squared length.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// a positive value if this vector is longer than <paramref name="scalar"/>,
|
||||
/// a negative value if this vector is shorter than <paramref name="scalar"/>,
|
||||
/// or 0 if this vector has a length equal to <paramref name="scalar"/>.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float CompareLengthTo(this Vector2 vec, float scalar)
|
||||
{
|
||||
return CompareLength(vec, scalar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the length of this vector with another.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avoids square root computations by using squared lengths.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// a positive value if this vector is longer than <paramref name="otherVec"/>,
|
||||
/// a negative value if this vector is shorter than <paramref name="otherVec"/>,
|
||||
/// or 0 if this vector and <paramref name="otherVec"/> have equal lengths.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float CompareLengthTo(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector greater than <paramref name="scalar"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsLongerThan(this Vector2 vec, float scalar)
|
||||
{
|
||||
return CompareLength(vec, scalar) > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector greater than the length of <paramref name="otherVec"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsLongerThan(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec) > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector greater than or equal to <paramref name="scalar"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsLongerThanOrEqualTo(this Vector2 vec, float scalar)
|
||||
{
|
||||
return CompareLength(vec, scalar) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector greater than or equal to the length of <paramref name="otherVec"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsLongerThanOrEqualTo(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector less than <paramref name="scalar"/>?
|
||||
/// </summary>
|
||||
/// <param name="vec"></param>
|
||||
/// <param name="scalar"></param>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsShorterThan(this Vector2 vec, float scalar)
|
||||
{
|
||||
return CompareLength(vec, scalar) < 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector less than the length of <paramref name="otherVec"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsShorterThan(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec) < 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector less than or equal to <paramref name="scalar"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsShorterThanOrEqualTo(this Vector2 vec, float scalar)
|
||||
{
|
||||
return CompareLength(vec, scalar) <= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector less than or equal to the length of <paramref name="otherVec"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsShorterThanOrEqualTo(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec) <= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this vector's length is equal to <paramref name="scalar"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool LengthEquals(this Vector2 thisVec, float scalar)
|
||||
{
|
||||
return CompareLength(thisVec, scalar) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is this vector the same length as <paramref name="otherVec"/>?
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsEqualLengthTo(this Vector2 thisVec, Vector2 otherVec)
|
||||
{
|
||||
return CompareLength(thisVec, otherVec) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the length of a vector with a scalar.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avoids a square root computation by using squared length.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// a positive value if <paramref name="vec"/> is shorter than <paramref name="scalar"/>,
|
||||
/// a negative value if <paramref name="vec"/> is longer than <paramref name="scalar"/>,
|
||||
/// or 0 if <paramref name="vec"/> has a length equal to <paramref name="scalar"/>.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float CompareLength(float scalar, Vector2 vec)
|
||||
{
|
||||
return (scalar * scalar) - vec.LengthSquared();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the length of this vector zero?
|
||||
/// </summary>
|
||||
public static bool IsLengthZero(this Vector2 vec)
|
||||
{
|
||||
return vec.LengthSquared() == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the cross product on a scalar and a vector. In 2D this produces
|
||||
/// a vector.
|
||||
|
||||
@@ -67,12 +67,12 @@ namespace Robust.Shared.Scripting
|
||||
|
||||
public MapGridComponent getgrid(int i)
|
||||
{
|
||||
return map.GetGrid(new EntityUid(i));
|
||||
return ent.GetComponent<MapGridComponent>(new EntityUid(i));
|
||||
}
|
||||
|
||||
public MapGridComponent getgrid(EntityUid mapId)
|
||||
{
|
||||
return map.GetGrid(mapId);
|
||||
return ent.GetComponent<MapGridComponent>(mapId);
|
||||
}
|
||||
|
||||
public T res<T>()
|
||||
|
||||
@@ -74,4 +74,31 @@ namespace Robust.Shared.Asynchronous
|
||||
/// </remarks>
|
||||
void BlockWaitOnTask(Task task);
|
||||
}
|
||||
|
||||
internal static class TaskManagerExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Run a callback on the main thread, returning a task that represents its completion.
|
||||
/// </summary>
|
||||
/// <seealso cref="ITaskManager.RunOnMainThread"/>
|
||||
public static Task TaskOnMainThread(this ITaskManager taskManager, Action callback)
|
||||
{
|
||||
var tcs = new TaskCompletionSource();
|
||||
|
||||
taskManager.RunOnMainThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
callback();
|
||||
tcs.SetResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
tcs.TrySetException(e);
|
||||
}
|
||||
});
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,6 @@ namespace Robust.Shared.Audio
|
||||
[DataDefinition]
|
||||
public partial struct AudioParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The DistanceModel to use for this specific source.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Attenuation Attenuation { get; set; } = Attenuation.LinearDistanceClamped;
|
||||
|
||||
/// <summary>
|
||||
/// Base volume to play the audio at, in dB.
|
||||
/// </summary>
|
||||
@@ -45,13 +39,13 @@ namespace Robust.Shared.Audio
|
||||
/// Scale for the audio pitch.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Pitch { get; set; } = Default.Pitch;
|
||||
public float Pitch
|
||||
{
|
||||
get => _pitch;
|
||||
set => _pitch = MathF.Max(0f, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audio bus to play on.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string BusName { get; set; } = Default.BusName;
|
||||
private float _pitch = Default.Pitch;
|
||||
|
||||
/// <summary>
|
||||
/// Only applies to positional audio.
|
||||
@@ -89,7 +83,7 @@ namespace Robust.Shared.Audio
|
||||
/// <summary>
|
||||
/// The "default" audio configuration.
|
||||
/// </summary>
|
||||
public static readonly AudioParams Default = new(0, 1, "Master", SharedAudioSystem.DefaultSoundRange, 1, 1, false, 0f);
|
||||
public static readonly AudioParams Default = new(0, 1, SharedAudioSystem.DefaultSoundRange, 1, 1, false, 0f);
|
||||
|
||||
// explicit parameterless constructor required so that default values get set properly.
|
||||
public AudioParams() { }
|
||||
@@ -97,21 +91,19 @@ namespace Robust.Shared.Audio
|
||||
public AudioParams(
|
||||
float volume,
|
||||
float pitch,
|
||||
string busName,
|
||||
float maxDistance,
|
||||
float refDistance,
|
||||
bool loop,
|
||||
float playOffsetSeconds,
|
||||
float? variation = null)
|
||||
: this(volume, pitch, busName, maxDistance, 1, refDistance, loop, playOffsetSeconds, variation)
|
||||
: this(volume, pitch, maxDistance, 1, refDistance, loop, playOffsetSeconds, variation)
|
||||
{
|
||||
}
|
||||
|
||||
public AudioParams(float volume, float pitch, string busName, float maxDistance,float rolloffFactor, float refDistance, bool loop, float playOffsetSeconds, float? variation = null) : this()
|
||||
public AudioParams(float volume, float pitch, float maxDistance,float rolloffFactor, float refDistance, bool loop, float playOffsetSeconds, float? variation = null) : this()
|
||||
{
|
||||
Volume = volume;
|
||||
Pitch = pitch;
|
||||
BusName = busName;
|
||||
MaxDistance = maxDistance;
|
||||
RolloffFactor = rolloffFactor;
|
||||
ReferenceDistance = refDistance;
|
||||
@@ -167,18 +159,6 @@ namespace Robust.Shared.Audio
|
||||
return me;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of this instance with a new bus name set, for easy chaining.
|
||||
/// </summary>
|
||||
/// <param name="bus">The new bus name.</param>
|
||||
[Pure]
|
||||
public readonly AudioParams WithBusName(string bus)
|
||||
{
|
||||
var me = this;
|
||||
me.BusName = bus;
|
||||
return me;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of this instance with a new max distance set, for easy chaining.
|
||||
/// </summary>
|
||||
@@ -227,18 +207,6 @@ namespace Robust.Shared.Audio
|
||||
return me;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of this instance with attenuation set, for easy chaining.
|
||||
/// </summary>
|
||||
/// <param name="attenuation">The new attenuation.</param>
|
||||
[Pure]
|
||||
public readonly AudioParams WithAttenuation(Attenuation attenuation)
|
||||
{
|
||||
var me = this;
|
||||
me.Attenuation = attenuation;
|
||||
return me;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public readonly AudioParams WithPlayOffset(float offset)
|
||||
{
|
||||
|
||||
@@ -234,7 +234,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="playerFilter">The set of players that will hear the sound.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file globally, without position.
|
||||
@@ -253,7 +253,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="recipient">The player that will hear the sound.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file globally, without position.
|
||||
@@ -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.
|
||||
@@ -274,7 +274,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="recipient">The player that will hear the sound.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file globally, without position.
|
||||
@@ -294,7 +294,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="playerFilter">The set of players that will hear the sound.</param>
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file following an entity.
|
||||
@@ -303,7 +303,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="recipient">The player that will hear the sound.</param>
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file following an entity.
|
||||
@@ -312,7 +312,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="recipient">The player that will hear the sound.</param>
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file following an entity.
|
||||
@@ -378,7 +378,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
|
||||
/// <param name="coordinates">The EntityCoordinates to attach the audio source to.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string filename,
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string? filename,
|
||||
EntityCoordinates coordinates, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
@@ -387,7 +387,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string filename, EntityUid uid,
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string? filename, EntityUid uid,
|
||||
AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
@@ -419,7 +419,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="playerFilter">The set of players that will hear the sound.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file at a static position.
|
||||
@@ -428,7 +428,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="recipient">The player that will hear the sound.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file at a static position.
|
||||
@@ -437,7 +437,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="recipient">The player that will hear the sound.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
[return: NotNullIfNotNull("filename")]
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file at a static position.
|
||||
|
||||
@@ -65,16 +65,26 @@ namespace Robust.Shared
|
||||
CVarDef.Create("net.pool_size", 512, CVar.CLIENT | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum UDP payload size to send.
|
||||
/// Maximum UDP payload size to send by default, for IPv4.
|
||||
/// </summary>
|
||||
/// <seealso cref="NetMtuExpand"/>
|
||||
/// <seealso cref="NetMtuIpv6"/>
|
||||
public static readonly CVarDef<int> NetMtu =
|
||||
CVarDef.Create("net.mtu", 1000, CVar.ARCHIVE);
|
||||
CVarDef.Create("net.mtu", 900, CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum UDP payload size to send by default, for IPv6.
|
||||
/// </summary>
|
||||
/// <seealso cref="NetMtu"/>
|
||||
/// <seealso cref="NetMtuExpand"/>
|
||||
public static readonly CVarDef<int> NetMtuIpv6 =
|
||||
CVarDef.Create("net.mtu_ipv6", NetPeerConfiguration.kDefaultMTUV6, CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// If set, automatically try to detect MTU above <see cref="NetMtu"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="NetMtu"/>
|
||||
/// <seealso cref="NetMtuIpv6"/>
|
||||
/// <seealso cref="NetMtuExpandFrequency"/>
|
||||
/// <seealso cref="NetMtuExpandFailAttempts"/>
|
||||
public static readonly CVarDef<bool> NetMtuExpand =
|
||||
@@ -183,6 +193,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.
|
||||
@@ -191,18 +225,24 @@ namespace Robust.Shared
|
||||
CVarDef.Create("net.pvs_async", true, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// View size to take for PVS calculations,
|
||||
/// as the size of the sides of a square centered on the view points of clients.
|
||||
/// View size to take for PVS calculations, as the size of the sides of a square centered on the view points of
|
||||
/// clients. See also <see cref="NetPvsPriorityRange"/>.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> NetMaxUpdateRange =
|
||||
CVarDef.Create("net.pvs_range", 25f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Chunks whose centre is further than this distance away from a player's eye will contain fewer entities.
|
||||
/// This has no effect if it is smaller than <see cref="NetMaxUpdateRange"/>
|
||||
/// A variant of <see cref="NetMaxUpdateRange"/> that is used to limit the view-distance of entities with the
|
||||
/// <see cref="MetaDataFlags.PvsPriority"/> flag set. This can be used to extend the range at which certain
|
||||
/// entities become visible.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> NetLowLodRange =
|
||||
CVarDef.Create("net.low_lod_distance", 100f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
/// <remarks>
|
||||
/// This is useful for entities like lights and occluders to try and prevent noticeable pop-in as players
|
||||
/// move around. Note that this has no effect if it is less than <see cref="NetMaxUpdateRange"/>, and that this
|
||||
/// only works for entities that are directly parented to a grid or map.
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<float> NetPvsPriorityRange =
|
||||
CVarDef.Create("net.pvs_priority_range", 32.5f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed delay between the current tick and a client's last acknowledged tick before we send the
|
||||
@@ -392,6 +432,35 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<int> MetricsPort =
|
||||
CVarDef.Create("metrics.port", 44880, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Sets a fixed interval (seconds) for internal collection of certain metrics,
|
||||
/// when not using the Prometheus metrics server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Most metrics are internally implemented directly via the prometheus-net library.
|
||||
/// These metrics can only be scraped by the Prometheus metrics server (<see cref="MetricsEnabled"/>).
|
||||
/// However, newer metrics are implemented with the <c>System.Diagnostics.Metrics</c> library in the .NET runtime.
|
||||
/// These metrics can be scraped through more means, such as <c>dotnet counters</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// While many metrics are simple counters that can "just" be reported,
|
||||
/// some metrics require more advanced internal work and need some code to be ran internally
|
||||
/// before their values are made current. When collecting metrics via a
|
||||
/// method other than the Prometheus metrics server, these metrics pose a problem,
|
||||
/// as there is no way for the game to update them before collection properly.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This CVar acts as a fallback: if set to a value other than 0 (disabled),
|
||||
/// these metrics will be internally updated at the interval provided.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This does not need to be enabled if metrics are collected exclusively via the Prometheus metrics server.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<float> MetricsUpdateInterval =
|
||||
CVarDef.Create("metrics.update_interval", 0f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Enable detailed runtime metrics. Empty to disable.
|
||||
/// </summary>
|
||||
@@ -817,6 +886,22 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<string> RenderFOVColor =
|
||||
CVarDef.Create("render.fov_color", Color.Black.ToHex(), CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* CONTROLS
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds to wait to consider double-click delays.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> DoubleClickDelay =
|
||||
CVarDef.Create("controls.double_click_delay", 250, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Range in pixels for double-clicks
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> DoubleClickRange =
|
||||
CVarDef.Create("controls.double_click_range", 10, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* DISPLAY
|
||||
*/
|
||||
@@ -1031,6 +1116,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);
|
||||
|
||||
@@ -1498,7 +1589,7 @@ namespace Robust.Shared
|
||||
/// Maximum compressed size of a replay recording (in kilobytes) before recording automatically stops.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<long> ReplayMaxCompressedSize = CVarDef.Create("replay.max_compressed_size",
|
||||
1024L * 256, CVar.ARCHIVE);
|
||||
1024L * 512, CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum uncompressed size of a replay recording (in kilobytes) before recording automatically stops.
|
||||
@@ -1581,7 +1672,8 @@ namespace Robust.Shared
|
||||
/// original exception rather than sending people on a wild-goose chase to find a non-existent bug.
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<bool> ReplayIgnoreErrors =
|
||||
CVarDef.Create("replay.ignore_errors", false, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
CVarDef.Create("replay.ignore_errors", false, CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* CFG
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,9 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Robust.Shared.Collections;
|
||||
|
||||
@@ -50,6 +52,41 @@ public struct ValueList<T> : IEnumerable<T>
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a list by copying the contents of the source list.
|
||||
/// </summary>
|
||||
public ValueList(List<T> list) : this(list, 0, list.Count)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a list by copying the contents of the source list.
|
||||
/// </summary>
|
||||
public ValueList(List<T> list, int start, int count)
|
||||
{
|
||||
_items = new T[count];
|
||||
|
||||
var liSpan = CollectionsMarshal.AsSpan(list)[start..(start + count)];
|
||||
liSpan.CopyTo(_items);
|
||||
|
||||
Count = count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a list by copying the contents of the source list.
|
||||
/// </summary>
|
||||
public ValueList(IReadOnlyCollection<T> list)
|
||||
{
|
||||
var count = list.Count;
|
||||
_items = new T[count];
|
||||
|
||||
foreach (var entry in list)
|
||||
{
|
||||
var size = Count;
|
||||
AddNoResize(entry, size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a list by copying the contents from another enumerable.
|
||||
/// </summary>
|
||||
@@ -157,12 +194,10 @@ public struct ValueList<T> : IEnumerable<T>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(T item)
|
||||
{
|
||||
var array = _items;
|
||||
var size = Count;
|
||||
if ((uint)size < (uint)Capacity)
|
||||
{
|
||||
Count = size + 1;
|
||||
array![size] = item;
|
||||
AddNoResize(item, size);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -170,6 +205,14 @@ public struct ValueList<T> : IEnumerable<T>
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void AddNoResize(T item, int size)
|
||||
{
|
||||
var array = _items;
|
||||
Count = size + 1;
|
||||
array![size] = item;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ref T AddRef()
|
||||
{
|
||||
@@ -522,4 +565,54 @@ public struct ValueList<T> : IEnumerable<T>
|
||||
RemoveAt(Count - 1);
|
||||
return old;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a range of values from the source list.
|
||||
/// </summary>
|
||||
public void AddRange(ValueList<T> list)
|
||||
{
|
||||
var liSpan = list.Span;
|
||||
AddRange(liSpan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a range of values from the source list.
|
||||
/// </summary>
|
||||
public void AddRange(List<T> list)
|
||||
{
|
||||
var liSpan = CollectionsMarshal.AsSpan(list);
|
||||
AddRange(liSpan);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void AddRange(Span<T> span)
|
||||
{
|
||||
var spanCount = span.Length;
|
||||
EnsureCapacity(Count + spanCount);
|
||||
var target = new Span<T>(_items, Count, spanCount);
|
||||
span.CopyTo(target);
|
||||
Count += spanCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills this with default data up to the specified count.
|
||||
/// </summary>
|
||||
public void EnsureLength(int newCount)
|
||||
{
|
||||
if (Count > newCount)
|
||||
return;
|
||||
|
||||
EnsureCapacity(newCount);
|
||||
var region = new Span<T>(_items, Count, (newCount - Count));
|
||||
region.Clear();
|
||||
Count = newCount;
|
||||
}
|
||||
|
||||
public void AddRange(IEnumerable<T> select)
|
||||
{
|
||||
foreach (var result in select)
|
||||
{
|
||||
Add(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
@@ -12,11 +12,16 @@ internal sealed class DumpSerializerTypeMapCommand : LocalizedCommands
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
foreach (var (type, index) in _robustSerializer.GetTypeMap().OrderBy(x => x.Value))
|
||||
{
|
||||
shell.WriteLine($"{index}: {type}");
|
||||
}
|
||||
var stream = new MemoryStream();
|
||||
((RobustSerializer)_robustSerializer).GetHashManifest(stream, true);
|
||||
stream.Position = 0;
|
||||
|
||||
using var streamReader = new StreamReader(stream);
|
||||
shell.WriteLine($"Hash: {_robustSerializer.GetSerializableTypesHashString()}");
|
||||
shell.WriteLine("Manifest:");
|
||||
while (streamReader.ReadLine() is { } line)
|
||||
{
|
||||
shell.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ sealed class RemoveGridCommand : LocalizedCommands
|
||||
|
||||
var gridIdNet = NetEntity.Parse(args[0]);
|
||||
|
||||
if (!_entManager.TryGetEntity(gridIdNet, out var gridId) || !_map.GridExists(gridId))
|
||||
if (!_entManager.TryGetEntity(gridIdNet, out var gridId) || !_entManager.HasComponent<MapGridComponent>(gridId))
|
||||
{
|
||||
shell.WriteError($"Grid {gridId} does not exist.");
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -23,7 +26,34 @@ public static class CompletionHelper
|
||||
public static IEnumerable<CompletionOption> Booleans => new[]
|
||||
{ new CompletionOption(bool.FalseString), new CompletionOption(bool.TrueString) };
|
||||
|
||||
public static IEnumerable<CompletionOption> ContentFilePath(string arg, IResourceManager res)
|
||||
/// <summary>
|
||||
/// Special-cased file handler for audio that accounts for serverside completion.
|
||||
/// </summary>
|
||||
public static IEnumerable<CompletionOption> AudioFilePath(string arg, IPrototypeManager protoManager,
|
||||
IResourceManager res)
|
||||
{
|
||||
var resPath = GetUpdatedPath(arg);
|
||||
var paths = new HashSet<string>();
|
||||
|
||||
foreach (var path in res.ContentGetDirectoryEntries(resPath))
|
||||
{
|
||||
paths.Add(path);
|
||||
}
|
||||
|
||||
foreach (var audioProto in protoManager.EnumeratePrototypes<AudioMetadataPrototype>())
|
||||
{
|
||||
var hero = new ResPath(audioProto.ID);
|
||||
|
||||
if (!hero.TryRelativeTo(resPath, out _))
|
||||
continue;
|
||||
|
||||
paths.Add(hero.GetNextSegment(resPath).ToString());
|
||||
}
|
||||
|
||||
return GetPaths(resPath, paths, res);
|
||||
}
|
||||
|
||||
private static ResPath GetUpdatedPath(string arg)
|
||||
{
|
||||
var curPath = arg;
|
||||
if (!curPath.StartsWith("/"))
|
||||
@@ -31,12 +61,18 @@ public static class CompletionHelper
|
||||
|
||||
var resPath = new ResPath(curPath);
|
||||
|
||||
if (!curPath.EndsWith("/")){
|
||||
if (!curPath.EndsWith("/"))
|
||||
{
|
||||
resPath /= "..";
|
||||
resPath = resPath.Clean();
|
||||
}
|
||||
|
||||
var options = res.ContentGetDirectoryEntries(resPath)
|
||||
return resPath;
|
||||
}
|
||||
|
||||
private static IEnumerable<CompletionOption> GetPaths(ResPath resPath, IEnumerable<string> inputs, IResourceManager res)
|
||||
{
|
||||
var options = inputs
|
||||
.OrderBy(c => c)
|
||||
.Select(c =>
|
||||
{
|
||||
@@ -51,6 +87,12 @@ public static class CompletionHelper
|
||||
return options;
|
||||
}
|
||||
|
||||
public static IEnumerable<CompletionOption> ContentFilePath(string arg, IResourceManager res)
|
||||
{
|
||||
var resPath = GetUpdatedPath(arg);
|
||||
return GetPaths(resPath, res.ContentGetDirectoryEntries(resPath), res);
|
||||
}
|
||||
|
||||
public static IEnumerable<CompletionOption> ContentDirPath(string arg, IResourceManager res)
|
||||
{
|
||||
var curPath = arg;
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed record CompletionResult(CompletionOption[] Options, string? Hint)
|
||||
/// <summary>
|
||||
/// Possible option to tab-complete in a <see cref="CompletionResult"/>.
|
||||
/// </summary>
|
||||
public record struct CompletionOption(string Value, string? Hint = null, CompletionOptionFlags Flags = default)
|
||||
public record struct CompletionOption(string Value, string? Hint = null, CompletionOptionFlags Flags = default) : IComparable<CompletionOption>
|
||||
{
|
||||
/// <summary>
|
||||
/// The value that will be filled in if completed.
|
||||
@@ -54,6 +54,12 @@ public record struct CompletionOption(string Value, string? Hint = null, Complet
|
||||
/// Flags that control how this completion is used.
|
||||
/// </summary>
|
||||
public CompletionOptionFlags Flags { get; set; } = Flags;
|
||||
|
||||
public int CompareTo(CompletionOption other)
|
||||
{
|
||||
var valueComparison = string.Compare(Value, other.Value, StringComparison.CurrentCultureIgnoreCase);
|
||||
return valueComparison;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,46 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Shared.Enums
|
||||
namespace Robust.Shared.Enums;
|
||||
|
||||
public sealed class PlacementInformation
|
||||
{
|
||||
public sealed class PlacementInformation
|
||||
{
|
||||
public string? EntityType { get; set; }
|
||||
public bool IsTile { get; set; }
|
||||
public EntityUid MobUid { get; set; }
|
||||
public string? PlacementOption { get; set; }
|
||||
public int Range { get; set; }
|
||||
public int TileType { get; set; }
|
||||
public int Uses { get; set; } = 1;
|
||||
}
|
||||
/// <summary>
|
||||
/// Entity prototype to be placed
|
||||
/// </summary>
|
||||
public string? EntityType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indiciates if the entity prototype to be placed is in fact a tile
|
||||
/// </summary>
|
||||
public bool IsTile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID of the mob that has permission to place the prototype
|
||||
/// </summary>
|
||||
public EntityUid MobUid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the placement alignment
|
||||
/// </summary>
|
||||
public string? PlacementOption { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines the max range at which the entity prototype can be placed
|
||||
/// </summary>
|
||||
public int Range { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int TileType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of times the entity can be placed
|
||||
/// </summary>
|
||||
public int Uses { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the input context should switch to 'editor' mode
|
||||
/// </summary>
|
||||
public bool UseEditorContext { get; set; } = true;
|
||||
}
|
||||
|
||||
28
Robust.Shared/GameObjects/ComponentAttributes.cs
Normal file
28
Robust.Shared/GameObjects/ComponentAttributes.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a component as being automatically registered by <see cref="IComponentFactory.DoAutoRegistrations" />
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
[BaseTypeRequired(typeof(IComponent))]
|
||||
[MeansImplicitUse]
|
||||
public sealed class RegisterComponentAttribute : Attribute;
|
||||
|
||||
/// <summary>
|
||||
/// Defines Name that this component is represented with in prototypes.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ComponentProtoNameAttribute(string prototypeName) : Attribute
|
||||
{
|
||||
public string PrototypeName { get; } = prototypeName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a component as not being saved when saving maps/grids.
|
||||
/// </summary>
|
||||
/// <seealso cref="ComponentRegistration.Unsaved"/>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class UnsavedComponentAttribute : Attribute;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user