mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
139b6f796c | ||
|
|
2ee7c35fd3 | ||
|
|
56eae5ad08 | ||
|
|
0662cae224 | ||
|
|
0e2b00edd0 | ||
|
|
d48f7ecb5b | ||
|
|
e064b7a4f9 | ||
|
|
654480862e | ||
|
|
353c044b52 | ||
|
|
3dda8d9e93 | ||
|
|
41ea10083d | ||
|
|
6290bb7af1 | ||
|
|
348ab70a8d | ||
|
|
47e11e988c | ||
|
|
c459b55052 | ||
|
|
f10e96a6d1 | ||
|
|
e8bac558c6 | ||
|
|
ffa3bb7202 | ||
|
|
9bcdc95651 | ||
|
|
543088ea1f | ||
|
|
56daa63783 | ||
|
|
9fe9730d4a | ||
|
|
a1a7ea92d9 | ||
|
|
0dec6a425f | ||
|
|
56ced913b7 | ||
|
|
76b46479b6 | ||
|
|
de9a8d286a | ||
|
|
a1df0fb4af | ||
|
|
e6bc5a1057 | ||
|
|
11b24579a2 | ||
|
|
685d002bb7 | ||
|
|
2e0d18aeaf | ||
|
|
06dbff0429 | ||
|
|
15958a9447 | ||
|
|
fd5a4d9b8a | ||
|
|
6d958847cb | ||
|
|
8a04a4f3a5 | ||
|
|
7104a4f459 | ||
|
|
f29949a32c | ||
|
|
3bbbabf238 | ||
|
|
d95aca3d9e | ||
|
|
e14537074e | ||
|
|
af2d01981f | ||
|
|
7df23e047c | ||
|
|
5c7ab43049 | ||
|
|
8f75560ec4 | ||
|
|
b323c8bd1e | ||
|
|
faef44daaa | ||
|
|
fbc706f37b |
@@ -55,7 +55,7 @@
|
||||
<PackageVersion Include="Serilog" Version="4.2.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
|
||||
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
144
RELEASE-NOTES.md
144
RELEASE-NOTES.md
@@ -54,6 +54,150 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 248.0.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Don't throw in overlay rendering if MapUid not found.
|
||||
|
||||
### Internal
|
||||
|
||||
* Reduce EntityManager.IsDefault allocations.
|
||||
|
||||
|
||||
## 248.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Bump ImageSharp version.
|
||||
* Fix instances of NaN gain for audio where a negative-infinity value is being used for volume.
|
||||
|
||||
|
||||
## 248.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Use `Entity<MapGridComponent>` for TileChangedEvent instead of EntityUid.
|
||||
* Audio files are no longer tempo perfect when being played if the offset is small. At some point in the future an AudioParams bool is likely to be added to enforce this.
|
||||
* MoveProxy method args got changed in the B2DynamicTree update.
|
||||
* ResPath will now assert in debug if you pass in an invalid path containing the non-standardized directory separator.
|
||||
|
||||
### New features
|
||||
|
||||
* Added a new `MapLoaderSystem.TryLoadGrid()` override that loads a grid onto a newly created map.
|
||||
* Added a CVar for the endbuffer for audio. If an audio file will play below this length (for PVS reasons) it will be ignored.
|
||||
* Added Regex.Count + StringBuilder.Chars setter to the sandbox.
|
||||
* Added a public API for PhysicsHull.
|
||||
* Made MapLoader log more helpful.
|
||||
* Add TryLoadGrid override that also creates a map at the same time.
|
||||
* Updated B2Dynamictree to the latest Box2D V3 version.
|
||||
* Added SetItems to ItemList control to set items without removing the existing ones.
|
||||
* Shaders, textures, and audio will now hot reload automatically to varying degrees. Also added IReloadManager to handle watching for file-system changes and relaying events.
|
||||
* Wrap BUI disposes in a try-catch in case of exceptions.
|
||||
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix some instances of invalid PlaybackPositions being set.
|
||||
* Play audio from the start of a file if it's only just come into PVS range / had its state handled.
|
||||
* Fix TryCopyComponents.
|
||||
* Use shell.WriteError if TryLoad fails for mapping commands.
|
||||
* Fix UI control position saving causing exceptions where the entity is cleaned-up alongside a state change.
|
||||
* Fix Map NetId completions.
|
||||
* Fix some ResPath calls using the wrong paths.
|
||||
|
||||
### Internal
|
||||
|
||||
* Remove some unused local variables and the associated warnings.
|
||||
|
||||
|
||||
## 247.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added functions for copying components to `IEntityManager` and `EntitySystem`.
|
||||
* Sound played from sound collections is now sent as "collection ID + index" over the network instead of the final filename.
|
||||
* This enables integration of future accessibility systems.
|
||||
* Added a new `ResolvedSoundSpecifier` to represent played sounds. Methods that previously took a filename now take a `ResolvedSoundSpecifier`, with an implicit cast from string being interpreted as a raw filename.
|
||||
* `VisibilitySystem` has been made accessible to shared as `SharedVisibilitySystem`.
|
||||
* `ScrollContainer` now has properties exposing `Value` and `ValueTarget` on its internal scroll bars.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix prototype hot reload crashing when adding a new component already exists on an entity.
|
||||
* Fix maps failing to save in some cases related to tilemap IDs.
|
||||
* Fix `Regex.Escape(string)` not being available in sandbox.
|
||||
* Prototypes that parent themselves directly won't cause the game to hang on an infinite loop anymore.
|
||||
* Fixed disconnecting during a connection attempt leaving the client stuck in a phantom state.
|
||||
|
||||
### Internal
|
||||
|
||||
* More warning cleanup.
|
||||
|
||||
## 247.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added support for `Color[]` shader uniforms
|
||||
* Added optional minimumDistance parameter to `SharedJointSystem.CreateDistanceJoint()`
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed `EntitySystem.DirtyFields()` not actually marking fields as dirty.
|
||||
|
||||
### Other
|
||||
|
||||
* Updated the Yamale map file format validator to support v7 map/grid files.
|
||||
|
||||
|
||||
## 247.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `ITileDefinitionManager.AssignAlias` and general tile alias functionality has been removed. `TileAliasPrototype` still exist, but are only used during entity deserialization.
|
||||
* `IMapManager.AddUninitializedMap` has been removed. Use the map-init options on `CreateMap()` instead.
|
||||
* Re-using a MapId will now log a warning. This may cause some integration tests to fail if they are configured to fail
|
||||
when warnings are logged.
|
||||
* The minimum supported map format / version has been increased from 2 to 3.
|
||||
* The server-side `MapLoaderSystem` and associated classes & structs has been moved to `Robust.Shared`, and has been significantly modified.
|
||||
* The `TryLoad` and `Save` methods have been replaced with grid, map, generic entity variants. I.e, `SaveGrid`, `SaveMap`, and `SaveEntities`.
|
||||
* Most of the serialization logic and methods have been moved out of `MapLoaderSystem` and into new `EntitySerializer`
|
||||
and `EntityDeserializer` classes, which also replace the old `MapSerializationContext`.
|
||||
* The `MapLoadOptions` class has been split into `MapLoadOptions`, `SerializationOptions`, and `DeserializationOptions`
|
||||
structs.
|
||||
* The interaction between PVS overrides and visibility masks / layers have changed:
|
||||
* Any forced entities (i.e., `PvsOverrideSystem.AddForceSend()`) now ignore visibility masks.
|
||||
* Any global & session overrides (`PvsOverrideSystem.AddGlobalOverride()` & `PvsOverrideSystem.AddSessionOverride()`) now respect visibility masks.
|
||||
* Entities added via the `ExpandPvsEvent` respect visibility masks.
|
||||
* The mask used for any global/session overrides can be modified via `ExpandPvsEvent.Mask`.
|
||||
* Toolshed Changes:
|
||||
* The signature of Toolshed type parsers have changed. Instead of taking in an optional command argument name string, they now take in a `CommandArgument` struct.
|
||||
* Toolshed commands can no longer contain a '|', as this symbol is now used for explicitly piping the output of one command to another. command pipes. The existing `|` and '|~' commands have been renamed to `bitor` and `bitnotor`.
|
||||
* Semicolon terminated command blocks in toolshed commands no longer return anything. I.e., `i { i 2 ; }` is no longer a valid command, as the block has no return value.
|
||||
|
||||
### New features
|
||||
|
||||
* The current map format/version has increased from 6 to 7 and now contains more information to try support serialization of maps with null-space entities and full game saves.
|
||||
* `IEntitySystemManager` now provides access to the system `IDependencyCollection`.
|
||||
* Toolshed commands now support optional and `params T[]` arguments. optional / variable length commands can be terminated using ';' or '|'.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed entity deserialization for components with a data fields that have a AlwaysPushInheritance Attribute
|
||||
* Audio entities attached to invisible / masked entities should no longer be able to temporarily make those entities visible to all players.
|
||||
* The map-like Toolshed commands now work when a collection is piped in.
|
||||
* Fixed a bug in toolshed that could cause it to preferentially use the incorrect command implementation.
|
||||
* E.g., passing a concrete enumerable type would previously use the command implementation that takes in an unconstrained generic parameter `T` instead of a dedicated `IEnumeerable<T>` implementation.
|
||||
|
||||
### Other
|
||||
|
||||
* `MapChangedEvent` has been marked as obsolete, and should be replaced with `MapCreatedEvent` and `MapRemovedEvent.
|
||||
* The default auto-completion hint for Toolshed commands have been changed and somewhat standardized. Most parsers should now generate a hint of the form:
|
||||
* `<name (Type)>` for mandatory arguments
|
||||
* `[name (Type)]` for optional arguments
|
||||
* `[name (Type)]...` for variable length arguments (i.e., for `params T[]`)
|
||||
|
||||
|
||||
## 246.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
@@ -156,6 +156,7 @@ cmd-savemap-not-exist = Target map does not exist.
|
||||
cmd-savemap-init-warning = Attempted to save a post-init map without forcing the save.
|
||||
cmd-savemap-attempt = Attempting to save map {$mapId} to {$path}.
|
||||
cmd-savemap-success = Map successfully saved.
|
||||
cmd-savemap-error = Could not save map! See server log for details.
|
||||
cmd-hint-savemap-id = <MapID>
|
||||
cmd-hint-savemap-path = <Path>
|
||||
cmd-hint-savemap-force = [bool]
|
||||
@@ -293,7 +294,7 @@ cmd-lsgrid-desc = Lists grids.
|
||||
cmd-lsgrid-help = lsgrid
|
||||
|
||||
cmd-addmap-desc = Adds a new empty map to the round. If the mapID already exists, this command does nothing.
|
||||
cmd-addmap-help = addmap <mapID> [initialize]
|
||||
cmd-addmap-help = addmap <mapID> [pre-init]
|
||||
|
||||
cmd-rmmap-desc = Removes a map from the world. You cannot remove nullspace.
|
||||
cmd-rmmap-help = rmmap <mapId>
|
||||
|
||||
@@ -42,8 +42,7 @@ command-description-as =
|
||||
command-description-count =
|
||||
Counts the amount of entries in it's input, returning an integer.
|
||||
command-description-map =
|
||||
Maps the input over the given block, with the provided expected return type.
|
||||
This command may be modified to not need an explicit return type in the future.
|
||||
Maps the input over the given block.
|
||||
command-description-select =
|
||||
Selects N objects or N% of objects from the input.
|
||||
One can additionally invert this command with not to make it select everything except N objects instead.
|
||||
@@ -149,7 +148,7 @@ command-description-max =
|
||||
Returns the maximum of two values.
|
||||
command-description-BitAndCommand =
|
||||
Performs bitwise AND.
|
||||
command-description-BitOrCommand =
|
||||
command-description-bitor =
|
||||
Performs bitwise OR.
|
||||
command-description-BitXorCommand =
|
||||
Performs bitwise XOR.
|
||||
@@ -203,11 +202,11 @@ command-description-mappos =
|
||||
command-description-pos =
|
||||
Returns an entity's coordinates.
|
||||
command-description-tp-coords =
|
||||
Teleports the target to the given coordinates.
|
||||
Teleports the given entities to the target coordinates.
|
||||
command-description-tp-to =
|
||||
Teleports the target to the given other entity.
|
||||
Teleports the given entities to the target entity.
|
||||
command-description-tp-into =
|
||||
Teleports the target "into" the given other entity, attaching it at (0 0) relative to it.
|
||||
Teleports the given entities "into" the target entity, attaching it at (0 0) relative to it.
|
||||
command-description-comp-get =
|
||||
Gets the given component from the given entity.
|
||||
command-description-comp-add =
|
||||
@@ -277,7 +276,7 @@ command-description-ModVecCommand =
|
||||
Performs the modulus operation over the input with the given constant right-hand value.
|
||||
command-description-BitAndNotCommand =
|
||||
Performs bitwise AND-NOT over the input.
|
||||
command-description-BitOrNotCommand =
|
||||
command-description-bitornot =
|
||||
Performs bitwise OR-NOT over the input.
|
||||
command-description-BitXnorCommand =
|
||||
Performs bitwise XNOR over the input.
|
||||
|
||||
@@ -136,6 +136,7 @@ cmd-savemap-not-exist = O mapa de destino não existe.
|
||||
cmd-savemap-init-warning = Tentativa de salvar um mapa pós-inicialização sem forçar o salvamento.
|
||||
cmd-savemap-attempt = Tentando salvar o mapa {$mapId} em {$path}.
|
||||
cmd-savemap-success = Mapa salvo com sucesso.
|
||||
cmd-savemap-error = Não foi possível salvar o mapa! Consulte o log do servidor para obter detalhes.
|
||||
cmd-hint-savemap-id = <MapID>
|
||||
cmd-hint-savemap-path = <Path>
|
||||
cmd-hint-savemap-force = [bool]
|
||||
|
||||
@@ -26,10 +26,10 @@ public class PhysicsTumblerBenchmark
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
var physics = entManager.System<SharedPhysicsSystem>();
|
||||
var fixtures = entManager.System<FixtureSystem>();
|
||||
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
|
||||
var mapUid = entManager.System<SharedMapSystem>().CreateMap(out var mapId);
|
||||
SetupTumbler(entManager, mapId);
|
||||
|
||||
for (var i = 0; i < 800; i++)
|
||||
for (var i = 0; i < 300; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
var boxUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId));
|
||||
@@ -42,6 +42,9 @@ public class PhysicsTumblerBenchmark
|
||||
physics.WakeBody(boxUid, body: box);
|
||||
physics.SetSleepingAllowed(boxUid, box, false);
|
||||
}
|
||||
|
||||
if (entManager.TryGetComponent(mapUid, out BroadphaseComponent? mapBroadphase))
|
||||
entManager.System<SharedBroadphaseSystem>().RebuildBottomUp(mapBroadphase);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
@@ -49,7 +52,7 @@ public class PhysicsTumblerBenchmark
|
||||
{
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
|
||||
for (var i = 0; i < 5000; i++)
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
}
|
||||
|
||||
@@ -40,11 +40,7 @@ namespace Robust.Client.Animations
|
||||
var keyFrame = KeyFrames[keyFrameIndex];
|
||||
|
||||
var audioParams = keyFrame.AudioParamsFunc.Invoke();
|
||||
var audio = new SoundPathSpecifier(keyFrame.Resource)
|
||||
{
|
||||
Params = audioParams
|
||||
};
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(audio, Filter.Local(), entity, true);
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(keyFrame.Specifier, Filter.Local(), entity, true, audioParams);
|
||||
}
|
||||
|
||||
return (keyFrameIndex, playingTime);
|
||||
@@ -55,7 +51,7 @@ namespace Robust.Client.Animations
|
||||
/// <summary>
|
||||
/// The RSI state to play when this keyframe gets triggered.
|
||||
/// </summary>
|
||||
public readonly string Resource;
|
||||
public readonly ResolvedSoundSpecifier Specifier;
|
||||
|
||||
/// <summary>
|
||||
/// A function that returns the audio parameter to be used.
|
||||
@@ -69,9 +65,9 @@ namespace Robust.Client.Animations
|
||||
/// </summary>
|
||||
public readonly float KeyTime;
|
||||
|
||||
public KeyFrame(string resource, float keyTime, Func<AudioParams>? audioParams = null)
|
||||
public KeyFrame(ResolvedSoundSpecifier specifier, float keyTime, Func<AudioParams>? audioParams = null)
|
||||
{
|
||||
Resource = resource;
|
||||
Specifier = specifier;
|
||||
KeyTime = keyTime;
|
||||
AudioParamsFunc = audioParams ?? (() => AudioParams.Default);
|
||||
}
|
||||
|
||||
@@ -84,6 +84,19 @@ internal partial class AudioManager
|
||||
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
|
||||
}
|
||||
|
||||
void IAudioInternal.Remove(AudioStream stream)
|
||||
{
|
||||
if (stream.ClydeHandle == null)
|
||||
return;
|
||||
|
||||
if (!_audioSampleBuffers.Remove(stream.BufferId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AL.DeleteBuffer(stream.BufferId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
@@ -120,9 +133,9 @@ internal partial class AudioManager
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
|
||||
return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
|
||||
return new AudioStream(this, buffer, handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -179,9 +192,9 @@ internal partial class AudioManager
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
|
||||
return new AudioStream(handle, length, wav.NumChannels, name);
|
||||
return new AudioStream(this, buffer, handle, length, wav.NumChannels, name);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -210,8 +223,8 @@ internal partial class AudioManager
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
return new AudioStream(handle, length, channels, name);
|
||||
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
|
||||
return new AudioStream(this, buffer, handle, length, channels, name);
|
||||
}
|
||||
|
||||
public void SetMasterGain(float newGain)
|
||||
@@ -293,7 +306,7 @@ internal partial class AudioManager
|
||||
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
// TODO: This really shouldn't be indexing based on the ClydeHandle...
|
||||
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value].BufferHandle);
|
||||
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[stream.BufferId].BufferHandle);
|
||||
|
||||
var audioSource = new AudioSource(this, source, stream);
|
||||
_audioSources.Add(source, new WeakReference<BaseAudioSource>(audioSource));
|
||||
@@ -370,5 +383,12 @@ internal partial class AudioManager
|
||||
}
|
||||
|
||||
_bufferedAudioSources.Clear();
|
||||
|
||||
foreach (var buffer in _audioSampleBuffers.Values)
|
||||
{
|
||||
DeleteAudioBufferOnMainThread(buffer.BufferHandle);
|
||||
}
|
||||
|
||||
_audioSampleBuffers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Client.Audio.Sources;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -17,13 +18,15 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
{
|
||||
[Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Shared.IoC.Dependency] private readonly IReloadManager _reload = default!;
|
||||
[Shared.IoC.Dependency] private readonly IResourceCache _cache = default!;
|
||||
|
||||
private Thread? _gameThread;
|
||||
|
||||
private ALDevice _openALDevice;
|
||||
private ALContext _openALContext;
|
||||
|
||||
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
|
||||
private readonly Dictionary<int, LoadedAudioSample> _audioSampleBuffers = new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<BaseAudioSource>> _audioSources =
|
||||
new();
|
||||
@@ -116,6 +119,22 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
|
||||
|
||||
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true);
|
||||
|
||||
_reload.Register("/Audio", "*.ogg");
|
||||
_reload.Register("/Audio", "*.wav");
|
||||
|
||||
_reload.OnChanged += OnReload;
|
||||
}
|
||||
|
||||
private void OnReload(ResPath args)
|
||||
{
|
||||
if (args.Extension != "ogg" &&
|
||||
args.Extension != "wav")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cache.ReloadResource<AudioResource>(args);
|
||||
}
|
||||
|
||||
internal bool IsMainThread()
|
||||
@@ -140,6 +159,11 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
}
|
||||
}
|
||||
|
||||
internal void LogError(string message)
|
||||
{
|
||||
OpenALSawmill.Error(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Like _checkAlError but allows custom data to be passed in as relevant.
|
||||
/// </summary>
|
||||
|
||||
@@ -6,8 +6,15 @@ namespace Robust.Client.Audio;
|
||||
/// <summary>
|
||||
/// Has the metadata for a particular audio stream as well as the relevant internal handle to it.
|
||||
/// </summary>
|
||||
public sealed class AudioStream
|
||||
public sealed class AudioStream : IDisposable
|
||||
{
|
||||
private IAudioInternal _audio;
|
||||
|
||||
/// <summary>
|
||||
/// Buffer ID for this audio in AL.
|
||||
/// </summary>
|
||||
internal int BufferId { get; }
|
||||
|
||||
public TimeSpan Length { get; }
|
||||
internal IClydeHandle? ClydeHandle { get; }
|
||||
public string? Name { get; }
|
||||
@@ -15,8 +22,10 @@ public sealed class AudioStream
|
||||
public string? Artist { get; }
|
||||
public int ChannelCount { get; }
|
||||
|
||||
internal AudioStream(IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
|
||||
internal AudioStream(IAudioInternal internalAudio, int bufferId, IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
|
||||
{
|
||||
_audio = internalAudio;
|
||||
BufferId = bufferId;
|
||||
ClydeHandle = handle;
|
||||
Length = length;
|
||||
ChannelCount = channelCount;
|
||||
@@ -24,4 +33,9 @@ public sealed class AudioStream
|
||||
Title = title;
|
||||
Artist = artist;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_audio.Remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
@@ -56,6 +57,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
|
||||
private float _maxRayLength;
|
||||
private float _zOffset;
|
||||
private float _audioEndBuffer;
|
||||
|
||||
public override float ZOffset
|
||||
{
|
||||
@@ -79,8 +82,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
}
|
||||
|
||||
private float _zOffset;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -108,20 +109,31 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
SubscribeNetworkEvent<PlayAudioEntityMessage>(OnEntityAudio);
|
||||
SubscribeNetworkEvent<PlayAudioPositionalMessage>(OnEntityCoordinates);
|
||||
|
||||
Subs.CVar(CfgManager, CVars.AudioEndBuffer, OnAudioBuffer, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioTickRate, OnAudioTickRate, true);
|
||||
InitializeLimit();
|
||||
}
|
||||
|
||||
private void OnAudioBuffer(float value)
|
||||
{
|
||||
_audioEndBuffer = value;
|
||||
}
|
||||
|
||||
private void OnAudioTickRate(int obj)
|
||||
{
|
||||
_audioFrameTime = 1f / obj;
|
||||
_audioFrameTimeRemaining = MathF.Min(_audioFrameTimeRemaining, _audioFrameTime);
|
||||
}
|
||||
|
||||
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
|
||||
private void OnAudioState(Entity<AudioComponent> entity, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
var component = entity.Comp;
|
||||
|
||||
if (component.LifeStage < ComponentLifeStage.Initialized)
|
||||
return;
|
||||
|
||||
ApplyAudioParams(component.Params, component);
|
||||
component.Source.Global = component.Global;
|
||||
|
||||
@@ -145,21 +157,29 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
case AudioState.Stopped:
|
||||
component.StopPlaying();
|
||||
component.PlaybackPosition = 0f;
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
// If playback position changed then update it.
|
||||
if (!string.IsNullOrEmpty(component.FileName))
|
||||
{
|
||||
var position = (float) ((component.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
|
||||
var currentPosition = component.Source.PlaybackPosition;
|
||||
var diff = Math.Abs(position - currentPosition);
|
||||
var position = (float) ((entity.Comp.PauseTime ?? Timing.CurTime) - entity.Comp.AudioStart).TotalSeconds;
|
||||
var currentPosition = entity.Comp.Source.PlaybackPosition;
|
||||
var diff = Math.Abs(position - currentPosition);
|
||||
|
||||
if (diff > 0.1f)
|
||||
// Don't try to set the audio too far ahead.
|
||||
if (!string.IsNullOrEmpty(entity.Comp.FileName))
|
||||
{
|
||||
if (position > GetAudioLengthImpl(entity.Comp.FileName).TotalSeconds - _audioEndBuffer)
|
||||
{
|
||||
component.PlaybackPosition = position;
|
||||
entity.Comp.StopPlaying();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the difference is minor then we'll just keep playing it.
|
||||
if (diff > 0.1f)
|
||||
{
|
||||
entity.Comp.PlaybackPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -207,6 +227,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
private void SetupSource(Entity<AudioComponent> entity, AudioResource audioResource, TimeSpan? length = null)
|
||||
{
|
||||
var component = entity.Comp;
|
||||
length ??= GetAudioLength(component.FileName);
|
||||
|
||||
// If audio came into range then start playback at the correct position.
|
||||
var offset = ((entity.Comp.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
|
||||
|
||||
if (TryAudioLimit(component.FileName))
|
||||
{
|
||||
@@ -230,10 +254,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
// Don't play until first frame so occlusion etc. are correct.
|
||||
component.Gain = 0f;
|
||||
|
||||
length ??= GetAudioLength(component.FileName);
|
||||
|
||||
// If audio came into range then start playback at the correct position.
|
||||
var offset = (Timing.CurTime - component.AudioStart).TotalSeconds % length.Value.TotalSeconds;
|
||||
// If the offset < buffer than just play it from the start.
|
||||
if (offset < AudioDespawnBuffer)
|
||||
{
|
||||
offset = 0;
|
||||
}
|
||||
// Not enough audio to play
|
||||
else if (offset > length.Value.TotalSeconds - _audioEndBuffer)
|
||||
{
|
||||
component.StopPlaying();
|
||||
return;
|
||||
}
|
||||
|
||||
if (offset > 0)
|
||||
{
|
||||
@@ -415,6 +446,16 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return occlusion;
|
||||
}
|
||||
|
||||
private bool TryGetAudio(ResolvedSoundSpecifier specifier, [NotNullWhen(true)] out AudioResource? audio)
|
||||
{
|
||||
var filename = GetAudioPath(specifier);
|
||||
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
|
||||
return true;
|
||||
|
||||
Log.Error($"Server tried to play audio file {filename} which does not exist.");
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
|
||||
{
|
||||
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
|
||||
@@ -433,15 +474,15 @@ 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(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
|
||||
return PlayStatic(specifier, 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(ResolvedSoundSpecifier? specifier, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
|
||||
return PlayEntity(specifier, Filter.Local(), uid, true, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -477,21 +518,21 @@ 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(ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
if (specifier is null)
|
||||
return null;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
|
||||
{
|
||||
FileName = filename,
|
||||
Specifier = specifier,
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(filename, out var audio) ? PlayGlobal(audio, audioParams) : default;
|
||||
return TryGetAudio(specifier, out var audio) ? PlayGlobal(audio, specifier, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -499,9 +540,9 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// </summary>
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, AudioParams? audioParams = null)
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
|
||||
{
|
||||
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
|
||||
var (entity, component) = CreateAndStartPlayingStream(audioParams, specifier, stream);
|
||||
component.Global = true;
|
||||
component.Source.Global = true;
|
||||
DirtyField(entity, component, nameof(AudioComponent.Global));
|
||||
@@ -513,22 +554,22 @@ 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(ResolvedSoundSpecifier? specifier, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
if (specifier is null)
|
||||
return null;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
|
||||
{
|
||||
FileName = filename,
|
||||
Specifier = specifier,
|
||||
NetEntity = GetNetEntity(entity),
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(filename, out var audio) ? PlayEntity(audio, entity, audioParams) : default;
|
||||
return TryGetAudio(specifier, out var audio) ? PlayEntity(audio, entity, specifier, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -537,7 +578,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = null)
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TerminatingOrDeleted(entity))
|
||||
{
|
||||
@@ -545,7 +586,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return null;
|
||||
}
|
||||
|
||||
var playing = CreateAndStartPlayingStream(audioParams, stream);
|
||||
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
|
||||
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
|
||||
|
||||
return playing;
|
||||
@@ -557,22 +598,22 @@ 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(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
if (specifier is null)
|
||||
return null;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
|
||||
{
|
||||
FileName = filename,
|
||||
Specifier = specifier,
|
||||
Coordinates = GetNetCoordinates(coordinates),
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(filename, out var audio) ? PlayStatic(audio, coordinates, audioParams) : default;
|
||||
return TryGetAudio(specifier, out var audio) ? PlayStatic(audio, coordinates, specifier, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -581,7 +622,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TerminatingOrDeleted(coordinates.EntityId))
|
||||
{
|
||||
@@ -589,33 +630,33 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return null;
|
||||
}
|
||||
|
||||
var playing = CreateAndStartPlayingStream(audioParams, stream);
|
||||
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
|
||||
_xformSys.SetCoordinates(playing.Entity, coordinates);
|
||||
return playing;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, audioParams);
|
||||
return PlayGlobal(specifier, 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(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, entity, audioParams);
|
||||
return PlayEntity(specifier, 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(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
return PlayStatic(specifier, coordinates, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, audioParams);
|
||||
return PlayGlobal(specifier, audioParams);
|
||||
}
|
||||
|
||||
public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
|
||||
@@ -629,39 +670,39 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, EntityUid recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(filename, audioParams);
|
||||
return PlayGlobal(specifier, 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(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, uid, audioParams);
|
||||
return PlayEntity(specifier, 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(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(filename, uid, audioParams);
|
||||
return PlayEntity(specifier, 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(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
return PlayStatic(specifier, 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(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
return PlayStatic(specifier, coordinates, audioParams);
|
||||
}
|
||||
|
||||
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
|
||||
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, ResolvedSoundSpecifier? specifier, AudioStream stream)
|
||||
{
|
||||
var audioP = audioParams ?? AudioParams.Default;
|
||||
var entity = SetupAudio(null, audioP, initialize: false, length: stream.Length);
|
||||
var entity = SetupAudio(specifier, audioP, initialize: false, length: stream.Length);
|
||||
LoadStream(entity, stream);
|
||||
EntityManager.InitializeAndStartEntity(entity);
|
||||
var comp = entity.Comp;
|
||||
@@ -694,17 +735,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
|
||||
private void OnEntityCoordinates(PlayAudioPositionalMessage ev)
|
||||
{
|
||||
PlayStatic(ev.FileName, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
|
||||
PlayStatic(ev.Specifier, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
|
||||
}
|
||||
|
||||
private void OnEntityAudio(PlayAudioEntityMessage ev)
|
||||
{
|
||||
PlayEntity(ev.FileName, GetEntity(ev.NetEntity), ev.AudioParams, false);
|
||||
PlayEntity(ev.Specifier, GetEntity(ev.NetEntity), ev.AudioParams, false);
|
||||
}
|
||||
|
||||
private void OnGlobalAudio(PlayAudioGlobalMessage ev)
|
||||
{
|
||||
PlayGlobal(ev.FileName, ev.AudioParams, false);
|
||||
PlayGlobal(ev.Specifier, ev.AudioParams, false);
|
||||
}
|
||||
|
||||
protected override TimeSpan GetAudioLengthImpl(string filename)
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace Robust.Client.Audio;
|
||||
/// </summary>
|
||||
internal sealed class HeadlessAudioManager : IAudioInternal
|
||||
{
|
||||
private int _audioBuffer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void InitializePostWindowing()
|
||||
{
|
||||
@@ -65,6 +67,11 @@ internal sealed class HeadlessAudioManager : IAudioInternal
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Remove(AudioStream stream)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopAllAudio()
|
||||
{
|
||||
@@ -101,11 +108,11 @@ internal sealed class HeadlessAudioManager : IAudioInternal
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
return new AudioStream(null, length, channels, name);
|
||||
return new AudioStream(this, _audioBuffer++, null, length, channels, name);
|
||||
}
|
||||
|
||||
private static AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
|
||||
private AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
|
||||
{
|
||||
return new AudioStream(null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
|
||||
return new AudioStream(this, _audioBuffer++, null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ internal interface IAudioInternal : IAudioManager
|
||||
|
||||
void SetAttenuation(Attenuation attenuation);
|
||||
|
||||
void Remove(AudioStream stream);
|
||||
|
||||
/// <summary>
|
||||
/// Stops all audio from playing.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Robust.Client.Audio.Sources;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
@@ -11,7 +10,7 @@ namespace Robust.Client.Audio;
|
||||
public interface IAudioManager
|
||||
{
|
||||
IAudioSource? CreateAudioSource(AudioStream stream);
|
||||
|
||||
|
||||
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
|
||||
|
||||
AudioStream LoadAudioWav(Stream stream, string? name = null);
|
||||
|
||||
@@ -13,7 +13,7 @@ internal sealed class AudioSource : BaseAudioSource
|
||||
/// <summary>
|
||||
/// Underlying stream to the audio.
|
||||
/// </summary>
|
||||
private readonly AudioStream _sourceStream;
|
||||
internal readonly AudioStream SourceStream;
|
||||
|
||||
#if DEBUG
|
||||
private bool _didPositionWarning;
|
||||
@@ -21,7 +21,7 @@ internal sealed class AudioSource : BaseAudioSource
|
||||
|
||||
public AudioSource(AudioManager master, int sourceHandle, AudioStream sourceStream) : base(master, sourceHandle)
|
||||
{
|
||||
_sourceStream = sourceStream;
|
||||
SourceStream = sourceStream;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -47,13 +47,13 @@ internal sealed class AudioSource : BaseAudioSource
|
||||
#if DEBUG
|
||||
// OpenAL doesn't seem to want to play stereo positionally.
|
||||
// Log a warning if people try to.
|
||||
if (_sourceStream.ChannelCount > 1 && !_didPositionWarning)
|
||||
if (SourceStream.ChannelCount > 1 && !_didPositionWarning)
|
||||
{
|
||||
_didPositionWarning = true;
|
||||
Master.OpenALSawmill.Warning("Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
|
||||
_sourceStream.Name);
|
||||
SourceStream.Name);
|
||||
// warning isn't enough, people just ignore it :(
|
||||
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
|
||||
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{SourceStream.Name}'. Make sure the audio is MONO, not stereo.");
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -208,6 +208,12 @@ public abstract class BaseAudioSource : IAudioSource
|
||||
}
|
||||
set
|
||||
{
|
||||
if (float.IsNaN(value))
|
||||
{
|
||||
Master.LogError($"Tried to set NaN gain, setting audio source to 0f: {Environment.StackTrace}");
|
||||
value = 0f;
|
||||
}
|
||||
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
|
||||
@@ -115,10 +115,6 @@ namespace Robust.Client
|
||||
/// <inheritdoc />
|
||||
public void DisconnectFromServer(string reason)
|
||||
{
|
||||
DebugTools.Assert(RunLevel > ClientRunLevel.Initialize);
|
||||
DebugTools.Assert(_net.IsConnected);
|
||||
// run level changed in OnNetDisconnect()
|
||||
// are both of these *really* needed?
|
||||
_net.ClientDisconnect(reason);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ using Robust.Shared.Replays;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Upload;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client
|
||||
@@ -102,6 +103,7 @@ namespace Robust.Client
|
||||
deps.Register<ProfViewManager>();
|
||||
deps.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
|
||||
deps.Register<NetworkResourceManager>();
|
||||
deps.Register<IReloadManager, ReloadManager>();
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
|
||||
@@ -93,6 +93,7 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
|
||||
[Dependency] private readonly IReplayRecordingManagerInternal _replayRecording = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
|
||||
private IWebViewManagerHook? _webViewHook;
|
||||
|
||||
@@ -185,6 +186,7 @@ namespace Robust.Client
|
||||
// before prototype load.
|
||||
ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions);
|
||||
|
||||
_reload.Initialize();
|
||||
_reflectionManager.Initialize();
|
||||
_prototypeManager.Initialize();
|
||||
_prototypeManager.LoadDefaultPrototypes();
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class MapSystem : SharedMapSystem
|
||||
{
|
||||
// Client-side map entities use negative map Ids to avoid conflict with server-side maps.
|
||||
var id = new MapId(--LastMapId);
|
||||
while (MapManager.MapExists(id))
|
||||
while (MapExists(id) || UsedIds.Contains(id))
|
||||
{
|
||||
id = new MapId(--LastMapId);
|
||||
}
|
||||
|
||||
@@ -25,12 +25,6 @@ public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
|
||||
ProtoManager.PrototypesReloaded -= OnProtoReload;
|
||||
}
|
||||
|
||||
protected override void OnUserInterfaceShutdown(Entity<UserInterfaceComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
base.OnUserInterfaceShutdown(ent, ref args);
|
||||
_savedPositions.Remove(ent.Owner);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OpenUi(Entity<UserInterfaceComponent?> entity, Enum key, bool predicted = false)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class VisibilitySystem : SharedVisibilitySystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -126,7 +126,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
|
||||
|
||||
var mapId = vp.Eye!.Position.MapId;
|
||||
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldBox, worldBounds);
|
||||
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapSystem.GetMapOrInvalid(mapId), mapId, worldBox, worldBounds);
|
||||
|
||||
if (!overlay.BeforeDraw(args))
|
||||
return;
|
||||
@@ -178,7 +178,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var worldAABB = worldBounds.CalcBoundingBox();
|
||||
var mapId = vp.Eye!.Position.MapId;
|
||||
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldAABB, worldBounds);
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, _mapSystem.GetMapOrInvalid(mapId), mapId, worldAABB, worldBounds);
|
||||
|
||||
foreach (var overlay in list)
|
||||
{
|
||||
|
||||
@@ -523,6 +523,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
case Color color:
|
||||
program.SetUniform(name, color);
|
||||
break;
|
||||
case Color[] colorArr:
|
||||
program.SetUniform(name, colorArr);
|
||||
break;
|
||||
case int i:
|
||||
program.SetUniform(name, i);
|
||||
break;
|
||||
|
||||
@@ -485,6 +485,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
data.Parameters[name] = value;
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, Color[] value)
|
||||
{
|
||||
var data = Parent._shaderInstances[Handle];
|
||||
data.ParametersDirty = true;
|
||||
data.Parameters[name] = value;
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, int value)
|
||||
{
|
||||
var data = Parent._shaderInstances[Handle];
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Client.Input;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
@@ -23,6 +24,7 @@ using Robust.Shared.Maths;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
@@ -48,6 +50,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly ClientEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IReloadManager _reloads = default!;
|
||||
|
||||
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
|
||||
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
|
||||
@@ -99,6 +103,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_sawmillOgl = _logManager.GetSawmill("clyde.ogl");
|
||||
_sawmillWin = _logManager.GetSawmill("clyde.win");
|
||||
|
||||
_reloads.Register("/Shaders", "*.swsl");
|
||||
_reloads.Register("/Textures/Shaders", "*.swsl");
|
||||
_reloads.Register("/Textures", "*.jpg");
|
||||
_reloads.Register("/Textures", "*.jpeg");
|
||||
_reloads.Register("/Textures", "*.png");
|
||||
_reloads.Register("/Textures", "*.webp");
|
||||
|
||||
_reloads.OnChanged += OnChange;
|
||||
_proto.PrototypesReloaded += OnProtoReload;
|
||||
|
||||
_cfg.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayVSync, VSyncChanged, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayWindowMode, WindowModeChanged, true);
|
||||
@@ -122,6 +136,38 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return InitWindowing();
|
||||
}
|
||||
|
||||
private void OnProtoReload(PrototypesReloadedEventArgs obj)
|
||||
{
|
||||
if (!obj.WasModified<ShaderPrototype>())
|
||||
return;
|
||||
|
||||
foreach (var shader in obj.ByType[typeof(ShaderPrototype)].Modified.Keys)
|
||||
{
|
||||
_resourceCache.ReloadResource<ShaderSourceResource>(shader);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChange(ResPath obj)
|
||||
{
|
||||
if ((obj.TryRelativeTo(new ResPath("/Shaders"), out _) || obj.TryRelativeTo(new ResPath("/Textures/Shaders"), out _)) && obj.Extension == "swsl")
|
||||
{
|
||||
_resourceCache.ReloadResource<ShaderSourceResource>(obj);
|
||||
}
|
||||
|
||||
if (obj.TryRelativeTo(new ResPath("/Textures"), out _) && !obj.TryRelativeTo(new ResPath("/Textures/Tiles"), out _))
|
||||
{
|
||||
if (obj.Extension == "jpg" || obj.Extension == "jpeg" || obj.Extension == "webp")
|
||||
{
|
||||
_resourceCache.ReloadResource<TextureResource>(obj);
|
||||
}
|
||||
|
||||
if (obj.Extension == "png")
|
||||
{
|
||||
_resourceCache.ReloadResource<TextureResource>(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool InitializePostWindowing()
|
||||
{
|
||||
_gameThread = Thread.CurrentThread;
|
||||
|
||||
@@ -369,6 +369,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, Color[] value)
|
||||
{
|
||||
}
|
||||
|
||||
private protected override void SetParameterImpl(string name, int value)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetUniformDirect(int slot, in Color color, bool convertToLinear=true)
|
||||
private void SetUniformDirect(int slot, in Color color, bool convertToLinear = true)
|
||||
{
|
||||
var converted = color;
|
||||
if (convertToLinear)
|
||||
@@ -349,6 +349,39 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUniform(string uniformName, Color[] colors)
|
||||
{
|
||||
var uniformId = GetUniform(uniformName);
|
||||
SetUniformDirect(uniformId, colors);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetUniformDirect(int slot, Color[] colors, bool convertToLinear = true)
|
||||
{
|
||||
scoped Span<Color> colorsToPass;
|
||||
if (convertToLinear)
|
||||
{
|
||||
colorsToPass = stackalloc Color[colors.Length];
|
||||
for (int i = 0; i < colors.Length; i++)
|
||||
{
|
||||
colorsToPass[i] = Color.FromSrgb(colors[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
colorsToPass = colors;
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (Color* ptr = &colorsToPass[0])
|
||||
{
|
||||
GL.Uniform4(slot, colorsToPass.Length, (float*)ptr);
|
||||
_clyde.CheckGlError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUniform(string uniformName, in Vector3 vector)
|
||||
{
|
||||
var uniformId = GetUniform(uniformName);
|
||||
|
||||
@@ -221,11 +221,12 @@ namespace Robust.Client.Graphics
|
||||
|
||||
public static bool TypeSupportsArrays(this ShaderDataType type)
|
||||
{
|
||||
// TODO: add support for int, and vec3/4 arrays
|
||||
// TODO: add support for int, and vec3 arrays
|
||||
return
|
||||
(type == ShaderDataType.Float) ||
|
||||
(type == ShaderDataType.Vec2) ||
|
||||
(type == ShaderDataType.Bool);
|
||||
(type == ShaderDataType.Bool) ||
|
||||
(type == ShaderDataType.Vec4);
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
||||
|
||||
@@ -113,6 +113,13 @@ namespace Robust.Client.Graphics
|
||||
SetParameterImpl(name, value);
|
||||
}
|
||||
|
||||
public void SetParameter(string name, Color[] value)
|
||||
{
|
||||
EnsureAlive();
|
||||
EnsureMutable();
|
||||
SetParameterImpl(name, value);
|
||||
}
|
||||
|
||||
public void SetParameter(string name, Vector4 value)
|
||||
{
|
||||
EnsureAlive();
|
||||
@@ -223,6 +230,7 @@ namespace Robust.Client.Graphics
|
||||
private protected abstract void SetParameterImpl(string name, Vector3 value);
|
||||
private protected abstract void SetParameterImpl(string name, Vector4 value);
|
||||
private protected abstract void SetParameterImpl(string name, Color value);
|
||||
private protected abstract void SetParameterImpl(string name, Color[] value);
|
||||
private protected abstract void SetParameterImpl(string name, int value);
|
||||
private protected abstract void SetParameterImpl(string name, Vector2i value);
|
||||
private protected abstract void SetParameterImpl(string name, bool value);
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
@@ -23,6 +18,8 @@ namespace Robust.Client.Map
|
||||
{
|
||||
internal sealed class ClydeTileDefinitionManager : TileDefinitionManager, IClydeTileDefinitionManager, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
[Dependency] private readonly IResourceManager _manager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
@@ -57,6 +54,30 @@ namespace Robust.Client.Map
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_protoManager.PrototypesReloaded += OnProtoReload;
|
||||
|
||||
_reload.Register("/Textures/Tiles", "*.png");
|
||||
_reload.OnChanged += OnReload;
|
||||
|
||||
_genTextureAtlas();
|
||||
}
|
||||
|
||||
private void OnProtoReload(PrototypesReloadedEventArgs obj)
|
||||
{
|
||||
if (!obj.WasModified<ITileDefinition>())
|
||||
return;
|
||||
|
||||
_genTextureAtlas();
|
||||
}
|
||||
|
||||
private void OnReload(ResPath obj)
|
||||
{
|
||||
if (obj.Extension != "png")
|
||||
return;
|
||||
|
||||
if (!obj.TryRelativeTo(new ResPath("/Textures/Tiles"), out _))
|
||||
return;
|
||||
|
||||
_genTextureAtlas();
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ namespace Robust.Client.Placement
|
||||
if (mousePos.MapId == MapId.Nullspace)
|
||||
yield break;
|
||||
|
||||
var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
|
||||
var (_, (x, y)) = transformSys.ToCoordinates(pManager.StartPoint.EntityId, mousePos) - pManager.StartPoint;
|
||||
float iterations;
|
||||
Vector2 distance;
|
||||
if (Math.Abs(x) > Math.Abs(y))
|
||||
@@ -176,7 +176,7 @@ namespace Robust.Client.Placement
|
||||
if (mousePos.MapId == MapId.Nullspace)
|
||||
yield break;
|
||||
|
||||
var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
|
||||
var placementdiff = transformSys.ToCoordinates(pManager.StartPoint.EntityId, mousePos) - pManager.StartPoint;
|
||||
|
||||
var xSign = Math.Sign(placementdiff.X);
|
||||
var ySign = Math.Sign(placementdiff.Y);
|
||||
@@ -264,13 +264,15 @@ namespace Robust.Client.Placement
|
||||
protected EntityCoordinates ScreenToCursorGrid(ScreenCoordinates coords)
|
||||
{
|
||||
var mapCoords = pManager.EyeManager.PixelToMap(coords.Position);
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
|
||||
if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid))
|
||||
{
|
||||
return EntityCoordinates.FromMap(pManager.MapManager, mapCoords);
|
||||
|
||||
return transformSys.ToCoordinates(mapCoords);
|
||||
}
|
||||
|
||||
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
|
||||
return EntityCoordinates.FromMap(gridUid, mapCoords, transformSys, pManager.EntityManager);
|
||||
return transformSys.ToCoordinates(gridUid, mapCoords);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Robust.Client.Prototypes
|
||||
{
|
||||
public sealed class ClientPrototypeManager : PrototypeManager
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameControllerInternal _controller = default!;
|
||||
|
||||
private readonly List<FileSystemWatcher> _watchers = new();
|
||||
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
|
||||
private CancellationTokenSource _reloadToken = new();
|
||||
private readonly HashSet<ResPath> _reloadQueue = new();
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -37,9 +25,8 @@ namespace Robust.Client.Prototypes
|
||||
|
||||
_netManager.RegisterNetMessage<MsgReloadPrototypes>(accept: NetMessageAccept.Server);
|
||||
|
||||
_clyde.OnWindowFocused += WindowFocusedChanged;
|
||||
|
||||
WatchResources();
|
||||
_reload.Register("/Prototypes", "*.yml");
|
||||
_reload.OnChanged += ReloadPrototypeQueue;
|
||||
}
|
||||
|
||||
public override void LoadDefaultPrototypes(Dictionary<Type, HashSet<string>>? changed = null)
|
||||
@@ -49,99 +36,27 @@ namespace Robust.Client.Prototypes
|
||||
ResolveResults();
|
||||
}
|
||||
|
||||
private void WindowFocusedChanged(WindowFocusedEventArgs args)
|
||||
private void ReloadPrototypeQueue(ResPath file)
|
||||
{
|
||||
#if TOOLS
|
||||
if (args.Focused && _reloadQueue.Count > 0)
|
||||
{
|
||||
Timer.Spawn(_reloadDelay, ReloadPrototypeQueue, _reloadToken.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
_reloadToken.Cancel();
|
||||
_reloadToken = new CancellationTokenSource();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (file.Extension != "yml")
|
||||
return;
|
||||
|
||||
private void ReloadPrototypeQueue()
|
||||
{
|
||||
#if TOOLS
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var msg = new MsgReloadPrototypes();
|
||||
msg.Paths = _reloadQueue.ToArray();
|
||||
var msg = new MsgReloadPrototypes
|
||||
{
|
||||
Paths = [file]
|
||||
};
|
||||
_netManager.ClientSendMessage(msg);
|
||||
|
||||
// Reloading prototypes modifies entities. This currently causes some state management debug asserts to
|
||||
// fail. To avoid this, we set `IGameTiming.ApplyingState` to true, even though this isn't really applying a
|
||||
// server state.
|
||||
using var _ = _timing.StartStateApplicationArea();
|
||||
ReloadPrototypes(_reloadQueue);
|
||||
|
||||
_reloadQueue.Clear();
|
||||
ReloadPrototypes([file]);
|
||||
|
||||
Logger.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
|
||||
#endif
|
||||
}
|
||||
|
||||
private void WatchResources()
|
||||
{
|
||||
if (!_cfg.GetCVar(CVars.ResPrototypeReloadWatch))
|
||||
return;
|
||||
|
||||
#if TOOLS
|
||||
foreach (var path in Resources.GetContentRoots().Select(r => r.ToString())
|
||||
.Where(r => Directory.Exists(r + "/Prototypes")).Select(p => p + "/Prototypes"))
|
||||
{
|
||||
var watcher = new FileSystemWatcher(path, "*.yml")
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.LastWrite
|
||||
};
|
||||
|
||||
watcher.Changed += (_, args) =>
|
||||
{
|
||||
switch (args.ChangeType)
|
||||
{
|
||||
case WatcherChangeTypes.Renamed:
|
||||
case WatcherChangeTypes.Deleted:
|
||||
return;
|
||||
case WatcherChangeTypes.Created:
|
||||
// case WatcherChangeTypes.Deleted:
|
||||
case WatcherChangeTypes.Changed:
|
||||
case WatcherChangeTypes.All:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
TaskManager.RunOnMainThread(() =>
|
||||
{
|
||||
var file = new ResPath(args.FullPath);
|
||||
|
||||
foreach (var root in Resources.GetContentRoots())
|
||||
{
|
||||
if (!file.TryRelativeTo(root, out var relative))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_reloadQueue.Add(relative.Value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
watcher.EnableRaisingEvents = true;
|
||||
_watchers.Add(watcher);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.Error($"Watching resources in path {path} threw an exception:\n{ex}");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
@@ -26,22 +27,26 @@ public sealed class AudioResource : BaseResource
|
||||
throw new FileNotFoundException("Content file does not exist for audio sample.");
|
||||
}
|
||||
|
||||
using (var fileStream = cache.ContentFileRead(path))
|
||||
using var fileStream = cache.ContentFileRead(path);
|
||||
var audioManager = dependencies.Resolve<IAudioInternal>();
|
||||
if (path.Extension == "ogg")
|
||||
{
|
||||
var audioManager = dependencies.Resolve<IAudioInternal>();
|
||||
if (path.Extension == "ogg")
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
|
||||
}
|
||||
else if (path.Extension == "wav")
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Unable to load audio files outside of ogg Vorbis or PCM wav");
|
||||
}
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
|
||||
}
|
||||
else if (path.Extension == "wav")
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Unable to load audio files outside of ogg Vorbis or PCM wav");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
|
||||
{
|
||||
dependencies.Resolve<IAudioInternal>().Remove(AudioStream);
|
||||
Load(dependencies, path);
|
||||
}
|
||||
|
||||
public AudioResource(AudioStream stream) : base()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
@@ -176,6 +176,71 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_scrollBar.MoveToEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the current list of items with the items in newItems.
|
||||
/// newItems should be in the order which they should appear in the list,
|
||||
/// and items are considered equal if the Item text is equal in each item.
|
||||
///
|
||||
/// Provided the existing items have not been re-ordered relative to each
|
||||
/// other, any items which already exist in the list are not destroyed,
|
||||
/// which maintains consistency of scrollbars, selected items, etc.
|
||||
/// </summary>
|
||||
/// <param name="newItems">The list of items to update this list to</param>
|
||||
public void SetItems(List<Item> newItems)
|
||||
{
|
||||
SetItems(newItems, (a,b) => string.Compare(a.Text, b.Text));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// This variant allows for a custom equality operator to compare items, when
|
||||
/// comparing the Item text is not desired.
|
||||
/// </summary>
|
||||
/// <param name="itemCmp">Comparison function to compare existing to new items.</param>
|
||||
public void SetItems(List<Item> newItems, Comparison<Item> itemCmp)
|
||||
{
|
||||
// Walk through the existing items in this list and in newItems
|
||||
// in parallel to synchronize our items with those in newItems.
|
||||
int i = this.Count - 1;
|
||||
int j = newItems.Count - 1;
|
||||
while(i >= 0 && j >= 0)
|
||||
{
|
||||
var cmpResult = itemCmp(this[i], newItems[j]);
|
||||
if (cmpResult == 0)
|
||||
{
|
||||
// This item exists in both our list and `newItems`. Nothing to do.
|
||||
i--;
|
||||
j--;
|
||||
}
|
||||
else if (cmpResult > 0)
|
||||
{
|
||||
// Item exists in our list, but not in `newItems`. Remove it.
|
||||
RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
else if (cmpResult < 0)
|
||||
{
|
||||
// A new entry which doesn't exist in our list. Insert it.
|
||||
Insert(i + 1, newItems[j]);
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining items in our list don't exist in `newItems` so remove them
|
||||
while (i >= 0)
|
||||
{
|
||||
RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
|
||||
// And finally, any remaining items in `newItems` don't exist in our list. Create them.
|
||||
while (j >= 0)
|
||||
{
|
||||
Insert(0, newItems[j]);
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
// Without this attribute, this would compile into a property called "Item", causing problems with the Item class.
|
||||
[System.Runtime.CompilerServices.IndexerName("IndexItem")]
|
||||
public Item this[int index]
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace Robust.Client.UserInterface.Controls
|
||||
[Virtual]
|
||||
public class LineEdit : Control
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
|
||||
@@ -28,6 +28,30 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public int ScrollSpeedX { get; set; } = 50;
|
||||
public int ScrollSpeedY { get; set; } = 50;
|
||||
|
||||
public float VScroll
|
||||
{
|
||||
get => _vScrollBar.Value;
|
||||
set => _vScrollBar.Value = value;
|
||||
}
|
||||
|
||||
public float VScrollTarget
|
||||
{
|
||||
get => _vScrollBar.ValueTarget;
|
||||
set => _vScrollBar.ValueTarget = value;
|
||||
}
|
||||
|
||||
public float HScroll
|
||||
{
|
||||
get => _hScrollBar.Value;
|
||||
set => _hScrollBar.Value = value;
|
||||
}
|
||||
|
||||
public float HScrollTarget
|
||||
{
|
||||
get => _hScrollBar.ValueTarget;
|
||||
set => _hScrollBar.ValueTarget = value;
|
||||
}
|
||||
|
||||
private bool _reserveScrollbarSpace;
|
||||
public bool ReserveScrollbarSpace
|
||||
{
|
||||
|
||||
@@ -38,7 +38,6 @@ namespace Robust.Client.UserInterface.Controls;
|
||||
public sealed class TextEdit : Control
|
||||
{
|
||||
[Dependency] private readonly IClipboardManager _clipboard = null!;
|
||||
[Dependency] private readonly IClyde _clyde = null!;
|
||||
|
||||
// @formatter:off
|
||||
public const string StylePropertyCursorColor = "cursor-color";
|
||||
|
||||
141
Robust.Client/Utility/ReloadManager.cs
Normal file
141
Robust.Client/Utility/ReloadManager.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Robust.Client.Utility;
|
||||
|
||||
internal sealed class ReloadManager : IReloadManager
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
[Dependency] private readonly ITaskManager _tasks = default!;
|
||||
|
||||
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
|
||||
private CancellationTokenSource _reloadToken = new();
|
||||
private readonly HashSet<ResPath> _reloadQueue = new();
|
||||
private List<FileSystemWatcher> _watchers = new();
|
||||
|
||||
public event Action<ResPath>? OnChanged;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logMan.GetSawmill("reload");
|
||||
_clyde.OnWindowFocused += WindowFocusedChanged;
|
||||
}
|
||||
|
||||
private void WindowFocusedChanged(WindowFocusedEventArgs args)
|
||||
{
|
||||
#if TOOLS
|
||||
if (args.Focused && _reloadQueue.Count > 0)
|
||||
{
|
||||
Timer.Spawn(_reloadDelay, ReloadFiles, _reloadToken.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
_reloadToken.Cancel();
|
||||
_reloadToken = new CancellationTokenSource();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void ReloadFiles()
|
||||
{
|
||||
foreach (var file in _reloadQueue)
|
||||
{
|
||||
var rootedFile = file.ToRootedPath();
|
||||
|
||||
if (!_res.ContentFileExists(rootedFile))
|
||||
continue;
|
||||
|
||||
_sawmill.Info($"Reloading {rootedFile}");
|
||||
OnChanged?.Invoke(rootedFile);
|
||||
}
|
||||
|
||||
_reloadQueue.Clear();
|
||||
}
|
||||
|
||||
public void Register(string directory, string filter)
|
||||
{
|
||||
if (!_cfg.GetCVar(CVars.ResPrototypeReloadWatch))
|
||||
return;
|
||||
|
||||
#if TOOLS
|
||||
foreach (var root in _res.GetContentRoots())
|
||||
{
|
||||
var path = root + directory;
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var watcher = new FileSystemWatcher(path, filter)
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.LastWrite
|
||||
};
|
||||
|
||||
_watchers.Add(watcher);
|
||||
|
||||
watcher.Changed += OnWatch;
|
||||
|
||||
try
|
||||
{
|
||||
watcher.EnableRaisingEvents = true;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.Error($"Watching resources in path {path} threw an exception:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
void OnWatch(object sender, FileSystemEventArgs args)
|
||||
{
|
||||
switch (args.ChangeType)
|
||||
{
|
||||
case WatcherChangeTypes.Renamed:
|
||||
case WatcherChangeTypes.Deleted:
|
||||
return;
|
||||
case WatcherChangeTypes.Created:
|
||||
// case WatcherChangeTypes.Deleted:
|
||||
case WatcherChangeTypes.Changed:
|
||||
case WatcherChangeTypes.All:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
_tasks.RunOnMainThread(() =>
|
||||
{
|
||||
var fullPath = args.FullPath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
var file = new ResPath(fullPath);
|
||||
|
||||
foreach (var rootIter in _res.GetContentRoots())
|
||||
{
|
||||
if (!file.TryRelativeTo(rootIter, out var relative))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_reloadQueue.Add(relative.Value);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -81,53 +81,58 @@ 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(ResolvedSoundSpecifier? specifier, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
if (specifier is null)
|
||||
return null;
|
||||
|
||||
var entity = SetupAudio(filename, audioParams);
|
||||
var entity = SetupAudio(specifier, audioParams);
|
||||
AddAudioFilter(entity, entity.Comp, playerFilter);
|
||||
entity.Comp.Global = true;
|
||||
return (entity, entity.Comp);
|
||||
}
|
||||
|
||||
/// <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(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
if (specifier is null)
|
||||
return null;
|
||||
|
||||
if (TerminatingOrDeleted(uid))
|
||||
return null;
|
||||
|
||||
var entity = SetupAudio(filename, audioParams);
|
||||
var entity = SetupAudio(specifier, audioParams);
|
||||
// Move it after setting it up
|
||||
XformSystem.SetCoordinates(entity, new EntityCoordinates(uid, Vector2.Zero));
|
||||
|
||||
// TODO AUDIO
|
||||
// Add methods that allow for custom audio range.
|
||||
// Some methods try to reduce the audio range, resulting in a custom filter which then unnecessarily has to
|
||||
// use PVS overrides. PlayEntity with a reduced range shouldn't need PVS overrides at all.
|
||||
AddAudioFilter(entity, entity.Comp, playerFilter);
|
||||
|
||||
return (entity, entity.Comp);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
if (specifier is null)
|
||||
return null;
|
||||
|
||||
if (TerminatingOrDeleted(uid))
|
||||
return null;
|
||||
|
||||
var entity = SetupAudio(filename, audioParams);
|
||||
var entity = SetupAudio(specifier, audioParams);
|
||||
XformSystem.SetCoordinates(entity, new EntityCoordinates(uid, Vector2.Zero));
|
||||
|
||||
return (entity, entity.Comp);
|
||||
}
|
||||
|
||||
/// <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(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
if (specifier is null)
|
||||
return null;
|
||||
|
||||
if (TerminatingOrDeleted(coordinates.EntityId))
|
||||
@@ -139,7 +144,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
if (!coordinates.IsValid(EntityManager))
|
||||
return null;
|
||||
|
||||
var entity = SetupAudio(filename, audioParams);
|
||||
var entity = SetupAudio(specifier, audioParams);
|
||||
XformSystem.SetCoordinates(entity, coordinates);
|
||||
AddAudioFilter(entity, entity.Comp, playerFilter);
|
||||
|
||||
@@ -147,10 +152,10 @@ 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(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
if (specifier is null)
|
||||
return null;
|
||||
|
||||
if (TerminatingOrDeleted(coordinates.EntityId))
|
||||
@@ -163,7 +168,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return null;
|
||||
|
||||
// TODO: Transform TryFindGridAt mess + optimisation required.
|
||||
var entity = SetupAudio(filename, audioParams);
|
||||
var entity = SetupAudio(specifier, audioParams);
|
||||
XformSystem.SetCoordinates(entity, coordinates);
|
||||
|
||||
return (entity, entity.Comp);
|
||||
@@ -186,7 +191,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
if (sound == null)
|
||||
return null;
|
||||
|
||||
var audio = PlayPvs(GetSound(sound), source, audioParams ?? sound.Params);
|
||||
var audio = PlayPvs(ResolveSound(sound), source, audioParams ?? sound.Params);
|
||||
|
||||
if (audio == null)
|
||||
return null;
|
||||
@@ -201,7 +206,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
if (sound == null)
|
||||
return null;
|
||||
|
||||
var audio = PlayPvs(GetSound(sound), coordinates, audioParams ?? sound.Params);
|
||||
var audio = PlayPvs(ResolveSound(sound), coordinates, audioParams ?? sound.Params);
|
||||
|
||||
if (audio == null)
|
||||
return null;
|
||||
@@ -210,12 +215,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(ResolvedSoundSpecifier? 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(ResolvedSoundSpecifier? filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayGlobal(filename, actor.PlayerSession, audioParams);
|
||||
@@ -223,12 +228,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(ResolvedSoundSpecifier? 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(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayEntity(filename, actor.PlayerSession, uid, audioParams);
|
||||
@@ -236,12 +241,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(ResolvedSoundSpecifier? 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(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TryComp(recipient, out ActorComponent? actor))
|
||||
return PlayStatic(filename, actor.PlayerSession, coordinates, audioParams);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
@@ -42,8 +43,15 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
_ent.System<MapLoaderSystem>().Save(uid, args[1]);
|
||||
shell.WriteLine("Save successful. Look in the user data directory.");
|
||||
bool saveSuccess = _ent.System<MapLoaderSystem>().TrySaveGrid(uid, new ResPath(args[1]));
|
||||
if(saveSuccess)
|
||||
{
|
||||
shell.WriteLine("Save successful. Look in the user data directory.");
|
||||
}
|
||||
else
|
||||
{
|
||||
shell.WriteError("Save unsuccessful!");
|
||||
}
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
@@ -63,7 +71,6 @@ namespace Robust.Server.Console.Commands
|
||||
public sealed class LoadGridCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _system = default!;
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
[Dependency] private readonly IResourceManager _resource = default!;
|
||||
|
||||
public override string Command => "loadgrid";
|
||||
@@ -91,13 +98,14 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_map.MapExists(mapId))
|
||||
var sys = _system.GetEntitySystem<SharedMapSystem>();
|
||||
if (!sys.MapExists(mapId))
|
||||
{
|
||||
shell.WriteError("Target map does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
var loadOptions = new MapLoadOptions();
|
||||
Vector2 offset = default;
|
||||
if (args.Length >= 4)
|
||||
{
|
||||
if (!float.TryParse(args[2], out var x))
|
||||
@@ -112,9 +120,10 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
loadOptions.Offset = new Vector2(x, y);
|
||||
offset = new Vector2(x, y);
|
||||
}
|
||||
|
||||
Angle rot = default;
|
||||
if (args.Length >= 5)
|
||||
{
|
||||
if (!float.TryParse(args[4], out var rotation))
|
||||
@@ -123,9 +132,10 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
loadOptions.Rotation = Angle.FromDegrees(rotation);
|
||||
rot = Angle.FromDegrees(rotation);
|
||||
}
|
||||
|
||||
var opts = DeserializationOptions.Default;
|
||||
if (args.Length >= 6)
|
||||
{
|
||||
if (!bool.TryParse(args[5], out var storeUids))
|
||||
@@ -134,10 +144,11 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
loadOptions.StoreMapUids = storeUids;
|
||||
opts.StoreYamlUids = storeUids;
|
||||
}
|
||||
|
||||
_system.GetEntitySystem<MapLoaderSystem>().Load(mapId, args[1], loadOptions);
|
||||
var path = new ResPath(args[1]);
|
||||
_system.GetEntitySystem<MapLoaderSystem>().TryLoadGrid(mapId, path, out _, opts, offset, rot);
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
@@ -149,7 +160,6 @@ namespace Robust.Server.Console.Commands
|
||||
public sealed class SaveMap : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _system = default!;
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
[Dependency] private readonly IResourceManager _resource = default!;
|
||||
|
||||
public override string Command => "savemap";
|
||||
@@ -189,13 +199,14 @@ namespace Robust.Server.Console.Commands
|
||||
if (mapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
if (!_map.MapExists(mapId))
|
||||
var sys = _system.GetEntitySystem<SharedMapSystem>();
|
||||
if (!sys.MapExists(mapId))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-savemap-not-exist"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_map.IsMapInitialized(mapId) &&
|
||||
if (sys.IsInitialized(mapId) &&
|
||||
( args.Length < 3 || !bool.TryParse(args[2], out var force) || !force))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-savemap-init-warning"));
|
||||
@@ -203,15 +214,21 @@ namespace Robust.Server.Console.Commands
|
||||
}
|
||||
|
||||
shell.WriteLine(Loc.GetString("cmd-savemap-attempt", ("mapId", mapId), ("path", args[1])));
|
||||
_system.GetEntitySystem<MapLoaderSystem>().SaveMap(mapId, args[1]);
|
||||
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
|
||||
bool saveSuccess = _system.GetEntitySystem<MapLoaderSystem>().TrySaveMap(mapId, new ResPath(args[1]));
|
||||
if(saveSuccess)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
|
||||
}
|
||||
else
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-savemap-error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LoadMap : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _system = default!;
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
[Dependency] private readonly IResourceManager _resource = default!;
|
||||
|
||||
public override string Command => "loadmap";
|
||||
@@ -267,61 +284,49 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
if (_map.MapExists(mapId))
|
||||
var sys = _system.GetEntitySystem<SharedMapSystem>();
|
||||
if (sys.MapExists(mapId))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-loadmap-exists", ("mapId", mapId)));
|
||||
return;
|
||||
}
|
||||
|
||||
var loadOptions = new MapLoadOptions();
|
||||
|
||||
float x = 0, y = 0;
|
||||
if (args.Length >= 3)
|
||||
float x = 0;
|
||||
if (args.Length >= 3 && !float.TryParse(args[2], out x))
|
||||
{
|
||||
if (!float.TryParse(args[2], out x))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[2])));
|
||||
return;
|
||||
}
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[2])));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length >= 4)
|
||||
float y = 0;
|
||||
if (args.Length >= 4 && !float.TryParse(args[3], out y))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[3])));
|
||||
return;
|
||||
}
|
||||
var offset = new Vector2(x, y);
|
||||
|
||||
if (!float.TryParse(args[3], out y))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[3])));
|
||||
return;
|
||||
}
|
||||
float rotation = 0;
|
||||
if (args.Length >= 5 && !float.TryParse(args[4], out rotation))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[4])));
|
||||
return;
|
||||
}
|
||||
var rot = new Angle(rotation);
|
||||
|
||||
bool storeUids = false;
|
||||
if (args.Length >= 6 && !bool.TryParse(args[5], out storeUids))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-bool", ("arg", args[5])));
|
||||
return;
|
||||
}
|
||||
|
||||
loadOptions.Offset = new Vector2(x, y);
|
||||
var opts = new DeserializationOptions {StoreYamlUids = storeUids};
|
||||
|
||||
if (args.Length >= 5)
|
||||
{
|
||||
if (!float.TryParse(args[4], out var rotation))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[4])));
|
||||
return;
|
||||
}
|
||||
var path = new ResPath(args[1]);
|
||||
_system.GetEntitySystem<MapLoaderSystem>().TryLoadMapWithId(mapId, path, out _, out _, opts, offset, rot);
|
||||
|
||||
loadOptions.Rotation = new Angle(rotation);
|
||||
}
|
||||
|
||||
if (args.Length >= 6)
|
||||
{
|
||||
if (!bool.TryParse(args[5], out var storeUids))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-bool", ("arg", args[5])));
|
||||
return;
|
||||
}
|
||||
|
||||
loadOptions.StoreMapUids = storeUids;
|
||||
}
|
||||
|
||||
_system.GetEntitySystem<MapLoaderSystem>().TryLoad(mapId, args[1], out _, loadOptions);
|
||||
|
||||
if (_map.MapExists(mapId))
|
||||
if (sys.MapExists(mapId))
|
||||
shell.WriteLine(Loc.GetString("cmd-loadmap-success", ("mapId", mapId), ("path", args[1])));
|
||||
else
|
||||
shell.WriteLine(Loc.GetString("cmd-loadmap-error", ("path", args[1])));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ namespace Robust.Server.GameObjects
|
||||
protected override MapId GetNextMapId()
|
||||
{
|
||||
var id = new MapId(++LastMapId);
|
||||
while (MapManager.MapExists(id))
|
||||
while (MapExists(id) || UsedIds.Contains(id))
|
||||
{
|
||||
id = new MapId(++LastMapId);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
public sealed class VisibilitySystem : EntitySystem
|
||||
public sealed class VisibilitySystem : SharedVisibilitySystem
|
||||
{
|
||||
[Dependency] private readonly PvsSystem _pvs = default!;
|
||||
[Dependency] private readonly IViewVariablesManager _vvManager = default!;
|
||||
@@ -40,7 +40,7 @@ namespace Robust.Server.GameObjects
|
||||
EntityManager.EntityInitialized -= OnEntityInit;
|
||||
}
|
||||
|
||||
public void AddLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
|
||||
public override void AddLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
|
||||
{
|
||||
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Robust.Server.GameObjects
|
||||
RefreshVisibility(ent);
|
||||
}
|
||||
|
||||
public void RemoveLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
|
||||
public override void RemoveLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
|
||||
{
|
||||
if (!_visibilityQuery.Resolve(ent.Owner, ref ent.Comp, false))
|
||||
return;
|
||||
@@ -67,7 +67,7 @@ namespace Robust.Server.GameObjects
|
||||
RefreshVisibility(ent);
|
||||
}
|
||||
|
||||
public void SetLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
|
||||
public override void SetLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
|
||||
{
|
||||
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);
|
||||
|
||||
@@ -90,14 +90,14 @@ namespace Robust.Server.GameObjects
|
||||
RefreshVisibility(ent.Owner, null, ent.Comp);
|
||||
}
|
||||
|
||||
public void RefreshVisibility(EntityUid uid,
|
||||
public override void RefreshVisibility(EntityUid uid,
|
||||
VisibilityComponent? visibilityComponent = null,
|
||||
MetaDataComponent? meta = null)
|
||||
{
|
||||
RefreshVisibility((uid, visibilityComponent, meta));
|
||||
}
|
||||
|
||||
public void RefreshVisibility(Entity<VisibilityComponent?, MetaDataComponent?> ent)
|
||||
public override void RefreshVisibility(Entity<VisibilityComponent?, MetaDataComponent?> ent)
|
||||
{
|
||||
if (!_metaQuery.Resolve(ent, ref ent.Comp2, false))
|
||||
return;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
internal interface IServerEntityManagerInternal : IServerEntityManager
|
||||
{
|
||||
// These methods are used by the map loader to do multi-stage entity construction during map load.
|
||||
// I would recommend you refer to the MapLoader for usage.
|
||||
|
||||
EntityUid AllocEntity(EntityPrototype? prototype);
|
||||
|
||||
void FinishEntityLoad(EntityUid entity, IEntityLoadContext? context = null);
|
||||
|
||||
void FinishEntityLoad(EntityUid entity, EntityPrototype? prototype, IEntityLoadContext? context = null);
|
||||
|
||||
void FinishEntityInitialization(EntityUid entity, MetaDataComponent? meta = null);
|
||||
|
||||
void FinishEntityStartup(EntityUid entity);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata component used to keep consistent UIDs inside map files cross saving.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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, UnsavedComponent]
|
||||
public sealed partial class MapSaveIdComponent : Component
|
||||
{
|
||||
public int Uid { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ namespace Robust.Server.GameObjects
|
||||
/// Manager for entities -- controls things like template loading and instantiation
|
||||
/// </summary>
|
||||
[UsedImplicitly] // DI Container
|
||||
public sealed class ServerEntityManager : EntityManager, IServerEntityManagerInternal
|
||||
public sealed class ServerEntityManager : EntityManager, IServerEntityManager
|
||||
{
|
||||
private static readonly Gauge EntitiesCount = Metrics.CreateGauge(
|
||||
"robust_entities_count",
|
||||
@@ -61,32 +61,6 @@ namespace Robust.Server.GameObjects
|
||||
_pvs = System<PvsSystem>();
|
||||
}
|
||||
|
||||
EntityUid IServerEntityManagerInternal.AllocEntity(EntityPrototype? prototype)
|
||||
{
|
||||
return AllocEntity(prototype, out _);
|
||||
}
|
||||
|
||||
void IServerEntityManagerInternal.FinishEntityLoad(EntityUid entity, IEntityLoadContext? context)
|
||||
{
|
||||
LoadEntity(entity, context);
|
||||
}
|
||||
|
||||
void IServerEntityManagerInternal.FinishEntityLoad(EntityUid entity, EntityPrototype? prototype, IEntityLoadContext? context)
|
||||
{
|
||||
LoadEntity(entity, context, prototype);
|
||||
}
|
||||
|
||||
void IServerEntityManagerInternal.FinishEntityInitialization(EntityUid entity, MetaDataComponent? meta)
|
||||
{
|
||||
InitializeEntity(entity, meta);
|
||||
}
|
||||
|
||||
[Obsolete("Use StartEntity")]
|
||||
void IServerEntityManagerInternal.FinishEntityStartup(EntityUid entity)
|
||||
{
|
||||
StartEntity(entity);
|
||||
}
|
||||
|
||||
internal override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
|
||||
{
|
||||
if (prototypeName == null)
|
||||
|
||||
@@ -184,6 +184,9 @@ internal struct PvsMetadata
|
||||
public NetEntity NetEntity;
|
||||
|
||||
public GameTick LastModifiedTick;
|
||||
|
||||
// TODO PVS maybe store as int?
|
||||
// Theres extra space anyways, and the mask checks always need to convert to an int first, so it'd probably be faster too.
|
||||
public ushort VisMask;
|
||||
public EntityLifeStage LifeStage;
|
||||
#if DEBUG
|
||||
|
||||
@@ -29,7 +29,8 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
base.Initialize();
|
||||
EntityManager.EntityDeleted += OnDeleted;
|
||||
_player.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
|
||||
SubscribeLocalEvent<MapRemovedEvent>(OnMapRemoved);
|
||||
SubscribeLocalEvent<MapCreatedEvent>(OnMapCreated);
|
||||
SubscribeLocalEvent<GridInitializeEvent>(OnGridCreated);
|
||||
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
|
||||
|
||||
@@ -133,7 +134,7 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
|
||||
/// <summary>
|
||||
/// Forces the entity, all of its parents, and all of its children to ignore normal PVS range limitations,
|
||||
/// causing them to always be sent to all clients.
|
||||
/// causing them to be sent to all clients. This will still respect visibility masks, it only overrides the range.
|
||||
/// </summary>
|
||||
public override void AddGlobalOverride(EntityUid uid)
|
||||
{
|
||||
@@ -159,8 +160,9 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
/// This causes an entity and all of its parents to always be sent to all players.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This differs from <see cref="AddGlobalOverride"/> as it does not send children, and will ignore a players usual
|
||||
/// PVS budget. You generally shouldn't use this unless an entity absolutely always needs to be sent to all clients.
|
||||
/// This differs from <see cref="AddGlobalOverride"/> as it does not send children, will ignore a players usual
|
||||
/// PVS budget, and ignores visibility masks. You generally shouldn't use this unless an entity absolutely always
|
||||
/// needs to be sent to all clients.
|
||||
/// </remarks>
|
||||
public void AddForceSend(EntityUid uid)
|
||||
{
|
||||
@@ -176,11 +178,12 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This causes an entity and all of its parents to always be sent to a player..
|
||||
/// This causes an entity and all of its parents to always be sent to a player.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This differs from <see cref="AddSessionOverride"/> as it does not send children, and will ignore a players usual
|
||||
/// PVS budget. You generally shouldn't use this unless an entity absolutely always needs to be sent to a client.
|
||||
/// This differs from <see cref="AddSessionOverride"/> as it does not send children, will ignore a players usual
|
||||
/// PVS budget, and ignores visibility masks. You generally shouldn't use this unless an entity absolutely always
|
||||
/// needs to be sent to a client.
|
||||
/// </remarks>
|
||||
public void AddForceSend(EntityUid uid, ICommonSession session)
|
||||
{
|
||||
@@ -206,7 +209,7 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
|
||||
/// <summary>
|
||||
/// Forces the entity, all of its parents, and all of its children to ignore normal PVS range limitations for a
|
||||
/// specific session.
|
||||
/// specific session. This will still respect visibility masks, it only overrides the range.
|
||||
/// </summary>
|
||||
public override void AddSessionOverride(EntityUid uid, ICommonSession session)
|
||||
{
|
||||
@@ -235,15 +238,17 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
|
||||
/// <summary>
|
||||
/// Forces the entity, all of its parents, and all of its children to ignore normal PVS range limitations,
|
||||
/// causing them to always be sent to all clients.
|
||||
/// causing them to always be sent to the specified clients. This will still respect visibility masks, it only
|
||||
/// overrides the range.
|
||||
/// </summary>
|
||||
public override void AddSessionOverrides(EntityUid uid, Filter filter)
|
||||
{
|
||||
_hasOverride.Add(uid);
|
||||
base.AddSessionOverrides(uid, filter);
|
||||
|
||||
foreach (var session in filter.Recipients)
|
||||
{
|
||||
AddSessionOverride(uid, session);
|
||||
SessionOverrides.GetOrNew(session).Add(uid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,14 +275,6 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
|
||||
#region Map/Grid Events
|
||||
|
||||
private void OnMapChanged(MapChangedEvent ev)
|
||||
{
|
||||
if (ev.Created)
|
||||
OnMapCreated(ev);
|
||||
else
|
||||
OnMapDestroyed(ev);
|
||||
}
|
||||
|
||||
private void OnGridRemoved(GridRemovalEvent ev)
|
||||
{
|
||||
RemoveForceSend(ev.EntityUid);
|
||||
@@ -290,12 +287,12 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
AddForceSend(ev.EntityUid);
|
||||
}
|
||||
|
||||
private void OnMapDestroyed(MapChangedEvent ev)
|
||||
private void OnMapRemoved(MapRemovedEvent ev)
|
||||
{
|
||||
RemoveForceSend(ev.Uid);
|
||||
}
|
||||
|
||||
private void OnMapCreated(MapChangedEvent ev)
|
||||
private void OnMapCreated(MapCreatedEvent ev)
|
||||
{
|
||||
// TODO PVS remove this requirement.
|
||||
// I think this just required refactoring client game state logic so it doesn't sending maps/grids to nullspace.
|
||||
|
||||
@@ -303,11 +303,8 @@ internal sealed partial class PvsSystem
|
||||
RemoveRoot(ev.EntityUid);
|
||||
}
|
||||
|
||||
private void OnMapChanged(MapChangedEvent ev)
|
||||
private void OnMapChanged(MapRemovedEvent ev)
|
||||
{
|
||||
if (!ev.Destroyed)
|
||||
return;
|
||||
|
||||
RemoveRoot(ev.Uid);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,14 +19,15 @@ internal sealed partial class PvsSystem
|
||||
|
||||
private void AddAllOverrides(PvsSession session)
|
||||
{
|
||||
var mask = session.VisMask;
|
||||
var fromTick = session.FromTick;
|
||||
RaiseExpandEvent(session, fromTick);
|
||||
var mask = RaiseExpandEvent(session, fromTick);
|
||||
|
||||
foreach (ref var ent in CollectionsMarshal.AsSpan(_cachedGlobalOverride))
|
||||
{
|
||||
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
|
||||
meta.Validate(ent.Meta);
|
||||
|
||||
// PVS overrides still respect visibility masks
|
||||
if ((mask & meta.VisMask) == meta.VisMask)
|
||||
AddEntity(session, ref ent, ref meta, fromTick);
|
||||
}
|
||||
@@ -36,7 +37,7 @@ internal sealed partial class PvsSystem
|
||||
|
||||
foreach (var uid in sessionOverrides)
|
||||
{
|
||||
RecursivelyAddOverride(session, uid, fromTick, addChildren: true);
|
||||
RecursivelyAddOverride(session, uid, fromTick, addChildren: true, mask);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,22 +46,23 @@ internal sealed partial class PvsSystem
|
||||
/// </summary>
|
||||
private void AddForcedEntities(PvsSession session)
|
||||
{
|
||||
// Forced overrides do not respect visibility masks, so we set all bits.
|
||||
var mask = -1;
|
||||
|
||||
// Ignore PVS budgets
|
||||
session.Budget = new() {NewLimit = int.MaxValue, EnterLimit = int.MaxValue};
|
||||
|
||||
var mask = session.VisMask;
|
||||
var fromTick = session.FromTick;
|
||||
foreach (ref var ent in CollectionsMarshal.AsSpan(_cachedForceOverride))
|
||||
{
|
||||
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
|
||||
meta.Validate(ent.Meta);
|
||||
if ((mask & meta.VisMask) == meta.VisMask)
|
||||
AddEntity(session, ref ent, ref meta, fromTick);
|
||||
AddEntity(session, ref ent, ref meta, fromTick);
|
||||
}
|
||||
|
||||
foreach (var uid in session.Viewers)
|
||||
{
|
||||
RecursivelyAddOverride(session, uid, fromTick, addChildren: false);
|
||||
RecursivelyAddOverride(session, uid, fromTick, addChildren: false, mask);
|
||||
}
|
||||
|
||||
if (!_pvsOverride.SessionForceSend.TryGetValue(session.Session, out var sessionForce))
|
||||
@@ -68,13 +70,13 @@ internal sealed partial class PvsSystem
|
||||
|
||||
foreach (var uid in sessionForce)
|
||||
{
|
||||
RecursivelyAddOverride(session, uid, fromTick, addChildren: false);
|
||||
RecursivelyAddOverride(session, uid, fromTick, addChildren: false, mask);
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseExpandEvent(PvsSession session, GameTick fromTick)
|
||||
private int RaiseExpandEvent(PvsSession session, GameTick fromTick)
|
||||
{
|
||||
var expandEvent = new ExpandPvsEvent(session.Session);
|
||||
var expandEvent = new ExpandPvsEvent(session.Session, session.VisMask);
|
||||
|
||||
if (session.Session.AttachedEntity != null)
|
||||
RaiseLocalEvent(session.Session.AttachedEntity.Value, ref expandEvent, true);
|
||||
@@ -85,23 +87,25 @@ internal sealed partial class PvsSystem
|
||||
{
|
||||
foreach (var uid in expandEvent.Entities)
|
||||
{
|
||||
RecursivelyAddOverride(session, uid, fromTick, addChildren: false);
|
||||
RecursivelyAddOverride(session, uid, fromTick, addChildren: false, expandEvent.VisMask);
|
||||
}
|
||||
}
|
||||
|
||||
if (expandEvent.RecursiveEntities == null)
|
||||
return;
|
||||
return expandEvent.VisMask;
|
||||
|
||||
foreach (var uid in expandEvent.RecursiveEntities)
|
||||
{
|
||||
RecursivelyAddOverride(session, uid, fromTick, addChildren: true);
|
||||
RecursivelyAddOverride(session, uid, fromTick, addChildren: true, expandEvent.VisMask);
|
||||
}
|
||||
|
||||
return expandEvent.VisMask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively add an entity and all of its parents to the to-send set. This optionally also adds all children.
|
||||
/// </summary>
|
||||
private bool RecursivelyAddOverride(PvsSession session, EntityUid uid, GameTick fromTick, bool addChildren)
|
||||
private bool RecursivelyAddOverride(PvsSession session, EntityUid uid, GameTick fromTick, bool addChildren, int mask)
|
||||
{
|
||||
if (!_xformQuery.TryGetComponent(uid, out var xform))
|
||||
{
|
||||
@@ -116,17 +120,20 @@ internal sealed partial class PvsSystem
|
||||
// to the toSend set, it doesn't guarantee that its parents have been. E.g., if a player ghost just teleported
|
||||
// to follow a far away entity, the player's own entity is still being sent, but we need to ensure that we also
|
||||
// send the new parents, which may otherwise be delayed because of the PVS budget.
|
||||
if (parent.IsValid() && !RecursivelyAddOverride(session, parent, fromTick, false))
|
||||
if (parent.IsValid() && !RecursivelyAddOverride(session, parent, fromTick, false, mask))
|
||||
return false;
|
||||
|
||||
if (!_metaQuery.TryGetComponent(uid, out var meta))
|
||||
return false;
|
||||
|
||||
if ((mask & meta.VisibilityMask) != meta.VisibilityMask)
|
||||
return false;
|
||||
|
||||
if (!AddEntity(session, (uid, meta), fromTick))
|
||||
return false;
|
||||
|
||||
if (addChildren)
|
||||
RecursivelyAddChildren(session, xform, fromTick);
|
||||
RecursivelyAddChildren(session, xform, fromTick, mask);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -134,7 +141,7 @@ internal sealed partial class PvsSystem
|
||||
/// <summary>
|
||||
/// Recursively add an entity and all of its children to the to-send set.
|
||||
/// </summary>
|
||||
private void RecursivelyAddChildren(PvsSession session, TransformComponent xform, GameTick fromTick)
|
||||
private void RecursivelyAddChildren(PvsSession session, TransformComponent xform, GameTick fromTick, int mask)
|
||||
{
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
@@ -145,10 +152,14 @@ internal sealed partial class PvsSystem
|
||||
}
|
||||
|
||||
var metadata = _metaQuery.GetComponent(child);
|
||||
if (!AddEntity(session, (child, metadata), fromTick))
|
||||
return;
|
||||
|
||||
RecursivelyAddChildren(session, childXform, fromTick);
|
||||
if ((mask & metadata.VisibilityMask) != metadata.VisibilityMask)
|
||||
continue;
|
||||
|
||||
if (!AddEntity(session, (child, metadata), fromTick))
|
||||
return; // Budget was exceeded (or some error occurred) -> return instead of continue.
|
||||
|
||||
RecursivelyAddChildren(session, childXform, fromTick, mask);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,18 +142,32 @@ internal sealed partial class PvsSystem
|
||||
|
||||
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
|
||||
{
|
||||
var rep = new EntityStringRepresentation(entity);
|
||||
Log.Error($"Attempted to add a deleted entity to PVS send set: '{rep}'. Deletion queued: {EntityManager.IsQueuedForDeletion(uid)}. Trace:\n{Environment.StackTrace}");
|
||||
|
||||
// This can happen if some entity was some removed from it's parent while that parent was being deleted.
|
||||
// As a result the entity was marked for deletion but was never actually properly deleted.
|
||||
EntityManager.QueueDeleteEntity(uid);
|
||||
|
||||
bool queued;
|
||||
lock (_toDelete)
|
||||
{
|
||||
queued = EntityManager.IsQueuedForDeletion(uid) || _toDelete.Contains(uid);
|
||||
if (!queued)
|
||||
_toDelete.Add(uid);
|
||||
}
|
||||
|
||||
var rep = new EntityStringRepresentation(entity);
|
||||
Log.Error($"Attempted to add a deleted entity to PVS send set: '{rep}'. Deletion queued: {queued}. Trace:\n{Environment.StackTrace}");
|
||||
return false;
|
||||
}
|
||||
|
||||
data.LastSeen = _gameTiming.CurTick;
|
||||
session.ToSend!.Add(entity.Comp.PvsData);
|
||||
|
||||
// TODO PVS PERFORMANCE
|
||||
// Investigate whether its better to defer actually creating the entity state & populating session.States here?
|
||||
// I.e., should be be constructing the to-send list & to-get-states lists, and then separately getting all states
|
||||
// after we have gotten all entities? If the CPU can focus on only processing data in session.DataMemory without
|
||||
// having to access miscellaneous component info, maybe it will be faster?
|
||||
// Though for that to work I guess it also has to avoid accessing the metadata component's lifestage?
|
||||
|
||||
if (session.RequestedFull)
|
||||
{
|
||||
var state = GetFullEntityState(session.Session, uid, meta);
|
||||
|
||||
@@ -94,6 +94,8 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
/// </summary>
|
||||
private readonly List<GameTick> _deletedTick = new();
|
||||
|
||||
private readonly HashSet<EntityUid> _toDelete = new();
|
||||
|
||||
/// <summary>
|
||||
/// The sessions that are currently being processed. Note that this is in general used by parallel & async tasks.
|
||||
/// Hence player disconnection processing is deferred and only run via <see cref="ProcessDisconnections"/>.
|
||||
@@ -127,7 +129,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
_metaQuery = GetEntityQuery<MetaDataComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
|
||||
SubscribeLocalEvent<MapRemovedEvent>(OnMapChanged);
|
||||
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
|
||||
SubscribeLocalEvent<TransformComponent, TransformStartupEvent>(OnTransformStartup);
|
||||
|
||||
@@ -195,6 +197,12 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
// Construct & serialize the game state for each player (and for the replay).
|
||||
SerializeStates();
|
||||
|
||||
foreach (var uid in _toDelete)
|
||||
{
|
||||
EntityManager.QueueDeleteEntity(uid);
|
||||
}
|
||||
_toDelete.Clear();
|
||||
|
||||
// Compress & send the states.
|
||||
SendStates();
|
||||
|
||||
@@ -465,18 +473,27 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public struct ExpandPvsEvent(ICommonSession session)
|
||||
public struct ExpandPvsEvent(ICommonSession session, int mask)
|
||||
{
|
||||
public readonly ICommonSession Session = session;
|
||||
|
||||
/// <summary>
|
||||
/// List of entities that will get added to this session's PVS set.
|
||||
/// List of entities that will get added to this session's PVS set. This will still respect visibility masks.
|
||||
/// </summary>
|
||||
public List<EntityUid>? Entities;
|
||||
|
||||
/// <summary>
|
||||
/// List of entities that will get added to this session's PVS set. Unlike <see cref="Entities"/> this will also
|
||||
/// recursively add all children of the given entity.
|
||||
/// recursively add all children of the given entity. This will still respect visibility masks.
|
||||
/// </summary>
|
||||
public List<EntityUid>? RecursiveEntities;
|
||||
|
||||
/// <summary>
|
||||
/// Visibility mask to use when adding entities. Defaults to the usual visibility mask for that client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this mask will affect all global & session overrides from <see cref="PvsOverrideSystem"/> for this
|
||||
/// client, not just the entities in <see cref="Entities"/> and <see cref="RecursiveEntities"/>.
|
||||
/// </remarks>
|
||||
public int VisMask = mask;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.Maps;
|
||||
|
||||
/// <summary>
|
||||
/// Added to Maps that were loaded by MapLoaderSystem. If not present then this map was created externally.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class LoadedMapComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Server.Maps
|
||||
{
|
||||
[PublicAPI]
|
||||
public sealed class MapLoadOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, UID components will be created for loaded entities
|
||||
/// to maintain consistency upon subsequent savings.
|
||||
/// </summary>
|
||||
public bool StoreMapUids { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Offset to apply to the loaded objects.
|
||||
/// </summary>
|
||||
public Vector2 Offset
|
||||
{
|
||||
get => _offset;
|
||||
set
|
||||
{
|
||||
TransformMatrix = Matrix3Helpers.CreateTransform(value, Rotation);
|
||||
_offset = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 _offset = Vector2.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Rotation to apply to the loaded objects as a collective, around 0, 0.
|
||||
/// </summary>
|
||||
/// <remarks>Setting this overrides </remarks>
|
||||
public Angle Rotation
|
||||
{
|
||||
get => _rotation;
|
||||
set
|
||||
{
|
||||
TransformMatrix = Matrix3Helpers.CreateTransform(Offset, value);
|
||||
_rotation = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Angle _rotation = Angle.Zero;
|
||||
|
||||
public Matrix3x2 TransformMatrix { get; set; } = Matrix3x2.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// If there is a map entity serialized should we also load it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should be set to false if you want to load a map file onto an existing map and do not wish to overwrite the existing entity.
|
||||
/// </remarks>
|
||||
public bool LoadMap { get; set; } = true;
|
||||
|
||||
public bool DoMapInit = false;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,6 @@ namespace Robust.Server
|
||||
deps.Register<IResourceManagerInternal, ResourceManager>();
|
||||
deps.Register<EntityManager, ServerEntityManager>();
|
||||
deps.Register<IServerEntityManager, ServerEntityManager>();
|
||||
deps.Register<IServerEntityManagerInternal, ServerEntityManager>();
|
||||
deps.Register<IServerGameStateManager, ServerGameStateManager>();
|
||||
deps.Register<IReplayRecordingManager, ReplayRecordingManager>();
|
||||
deps.Register<IReplayRecordingManagerInternal, ReplayRecordingManager>();
|
||||
|
||||
@@ -29,12 +29,12 @@ namespace Robust.Server
|
||||
/// <summary>
|
||||
/// Directory to load all assemblies from.
|
||||
/// </summary>
|
||||
public ResPath AssemblyDirectory { get; init; } = new(@"/Assemblies");
|
||||
public ResPath AssemblyDirectory { get; init; } = new(@"/Assemblies/");
|
||||
|
||||
/// <summary>
|
||||
/// Directory to load all prototypes from.
|
||||
/// </summary>
|
||||
public ResPath PrototypeDirectory { get; init; } = new(@"/Prototypes");
|
||||
public ResPath PrototypeDirectory { get; init; } = new(@"/Prototypes/");
|
||||
|
||||
/// <summary>
|
||||
/// Whether to disable mounting the "Resources/" folder on FULL_RELEASE.
|
||||
|
||||
@@ -212,6 +212,48 @@ namespace Robust.Shared.Maths
|
||||
return surfaceIntersect / (Area(this) + Area(other) - surfaceIntersect);
|
||||
}
|
||||
|
||||
public readonly bool IsValid()
|
||||
{
|
||||
var d = Vector2.Subtract(TopRight, BottomLeft);
|
||||
bool valid = d.X >= 0.0f && d.Y >= 0.0f;
|
||||
valid = valid && BottomLeft.IsValid() && TopRight.IsValid();
|
||||
return valid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enlarges this box to contain another box.
|
||||
/// </summary>
|
||||
public bool EnlargeAabb(Box2 other)
|
||||
{
|
||||
var changed = false;
|
||||
|
||||
if (other.Left < Left)
|
||||
{
|
||||
Left = other.Left;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (other.Bottom < Bottom)
|
||||
{
|
||||
Bottom = other.Bottom;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (Right < other.Right)
|
||||
{
|
||||
Right = other.Right;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (other.Top < Top)
|
||||
{
|
||||
Top = other.Top;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the smallest rectangle that contains both of the rectangles.
|
||||
/// </summary>
|
||||
@@ -401,6 +443,15 @@ namespace Robust.Shared.Maths
|
||||
public static float Perimeter(in Box2 box)
|
||||
=> (box.Width + box.Height) * 2;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public static Box2 Union(Box2 a, Box2 b)
|
||||
{
|
||||
return new Box2(
|
||||
Vector2.Min(a.BottomLeft, b.BottomLeft),
|
||||
Vector2.Max(a.TopRight, b.TopRight));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public static Box2 Union(in Vector2 a, in Vector2 b)
|
||||
|
||||
@@ -15,6 +15,7 @@ public static class Vector2Helpers
|
||||
/// </summary>
|
||||
public static readonly Vector2 Half = new(0.5f, 0.5f);
|
||||
|
||||
[Pure]
|
||||
public static bool IsValid(this Vector2 v)
|
||||
{
|
||||
if (float.IsNaN(v.X) || float.IsNaN(v.Y))
|
||||
@@ -30,6 +31,13 @@ public static class Vector2Helpers
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public static Vector2 MulAdd(Vector2 a, float s, Vector2 b)
|
||||
{
|
||||
return new Vector2(a.X + s * b.X, a.Y + s * b.Y);
|
||||
}
|
||||
|
||||
public static Vector2 GetLengthAndNormalize(this Vector2 v, ref float length)
|
||||
{
|
||||
length = v.Length();
|
||||
|
||||
@@ -5,6 +5,9 @@ using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Audio
|
||||
{
|
||||
@@ -29,11 +32,25 @@ namespace Robust.Shared.Audio
|
||||
[DataDefinition]
|
||||
public partial struct AudioParams
|
||||
{
|
||||
private float _volume = Default.Volume;
|
||||
|
||||
/// <summary>
|
||||
/// Base volume to play the audio at, in dB.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Volume { get; set; } = Default.Volume;
|
||||
public float Volume
|
||||
{
|
||||
get => _volume;
|
||||
set
|
||||
{
|
||||
if (float.IsNaN(value))
|
||||
{
|
||||
value = float.NegativeInfinity;
|
||||
}
|
||||
|
||||
_volume = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scale for the audio pitch.
|
||||
|
||||
90
Robust.Shared/Audio/ResolvedSoundSpecifier.cs
Normal file
90
Robust.Shared/Audio/ResolvedSoundSpecifier.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Shared.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a path to a sound resource, either as a literal path or as a collection ID and index.
|
||||
/// </summary>
|
||||
/// <seealso cref="ResolvedPathSpecifier"/>
|
||||
/// <seealso cref="ResolvedCollectionSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public abstract partial class ResolvedSoundSpecifier {
|
||||
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
|
||||
public static implicit operator ResolvedSoundSpecifier(string s) => new ResolvedPathSpecifier(s);
|
||||
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
|
||||
public static implicit operator ResolvedSoundSpecifier(ResPath s) => new ResolvedPathSpecifier(s);
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether <c>s</c> is null, or if it contains an empty path/collection ID.
|
||||
/// </summary>
|
||||
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s) {
|
||||
return s switch {
|
||||
null => true,
|
||||
ResolvedPathSpecifier path => path.Path.ToString() == "",
|
||||
ResolvedCollectionSpecifier collection => string.IsNullOrEmpty(collection.Collection),
|
||||
_ => throw new ArgumentOutOfRangeException("s", s, "argument is not a ResolvedPathSpecifier or a ResolvedCollectionSpecifier"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a path to a sound resource as a literal path.
|
||||
/// </summary>
|
||||
/// <seealso cref="ResolvedCollectionSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
|
||||
/// <summary>
|
||||
/// The resource path of the sound.
|
||||
/// </summary>
|
||||
public ResPath Path { get; private set; }
|
||||
|
||||
override public string ToString() =>
|
||||
$"ResolvedPathSpecifier({Path})";
|
||||
|
||||
[UsedImplicitly]
|
||||
private ResolvedPathSpecifier()
|
||||
{
|
||||
}
|
||||
public ResolvedPathSpecifier(ResPath path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
public ResolvedPathSpecifier(string path) : this(new ResPath(path))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a path to a sound resource as a collection ID and index.
|
||||
/// </summary>
|
||||
/// <seealso cref="ResolvedPathSpecifier"/>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier {
|
||||
/// <summary>
|
||||
/// The ID of the <see cref="SoundCollectionPrototype">sound collection</see> to look up.
|
||||
/// </summary>
|
||||
public ProtoId<SoundCollectionPrototype>? Collection { get; private set; }
|
||||
/// <summary>
|
||||
/// The index of the file in the associated sound collection to play.
|
||||
/// </summary>
|
||||
public int Index { get; private set; }
|
||||
|
||||
override public string ToString() =>
|
||||
$"ResolvedCollectionSpecifier({Collection}, {Index})";
|
||||
|
||||
[UsedImplicitly]
|
||||
private ResolvedCollectionSpecifier()
|
||||
{
|
||||
}
|
||||
|
||||
public ResolvedCollectionSpecifier(string collection, int index)
|
||||
{
|
||||
Collection = collection;
|
||||
Index = index;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,9 @@ public sealed partial class SoundPathSpecifier : SoundSpecifier
|
||||
[DataField(Node, customTypeSerializer: typeof(ResPathSerializer), required: true)]
|
||||
public ResPath Path { get; private set; }
|
||||
|
||||
override public string ToString() =>
|
||||
$"SoundPathSpecifier({Path})";
|
||||
|
||||
[UsedImplicitly]
|
||||
private SoundPathSpecifier()
|
||||
{
|
||||
@@ -52,6 +55,9 @@ public sealed partial class SoundCollectionSpecifier : SoundSpecifier
|
||||
[DataField(Node, customTypeSerializer: typeof(PrototypeIdSerializer<SoundCollectionPrototype>), required: true)]
|
||||
public string? Collection { get; private set; }
|
||||
|
||||
override public string ToString() =>
|
||||
$"SoundCollectionSpecifier({Collection})";
|
||||
|
||||
[UsedImplicitly]
|
||||
public SoundCollectionSpecifier() { }
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
[Dependency] protected readonly MetaDataSystem MetadataSys = default!;
|
||||
[Dependency] protected readonly SharedTransformSystem XformSystem = default!;
|
||||
|
||||
private const float AudioDespawnBuffer = 1f;
|
||||
public const float AudioDespawnBuffer = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Default max range at which the sound can be heard.
|
||||
@@ -283,33 +283,60 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the filepath to a sound file.
|
||||
/// Resolve a sound specifier so it can be consistently played back on all clients.
|
||||
/// </summary>
|
||||
public string GetSound(SoundSpecifier specifier)
|
||||
public ResolvedSoundSpecifier ResolveSound(SoundSpecifier specifier)
|
||||
{
|
||||
switch (specifier)
|
||||
{
|
||||
case SoundPathSpecifier path:
|
||||
return path.Path == default ? string.Empty : path.Path.ToString();
|
||||
return new ResolvedPathSpecifier(path.Path == default ? string.Empty : path.Path.ToString());
|
||||
|
||||
case SoundCollectionSpecifier collection:
|
||||
{
|
||||
if (collection.Collection == null)
|
||||
return string.Empty;
|
||||
return new ResolvedPathSpecifier(string.Empty);
|
||||
|
||||
var soundCollection = ProtoMan.Index<SoundCollectionPrototype>(collection.Collection);
|
||||
return RandMan.Pick(soundCollection.PickFiles).ToString();
|
||||
var index = RandMan.Next(soundCollection.PickFiles.Count);
|
||||
return new ResolvedCollectionSpecifier(collection.Collection, index);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
return new ResolvedPathSpecifier(string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the filepath to a sound file.
|
||||
/// </summary>
|
||||
[Obsolete("Use ResolveSound() and pass around resolved sound specifiers instead.")]
|
||||
public string GetSound(SoundSpecifier specifier)
|
||||
{
|
||||
var resolved = ResolveSound(specifier);
|
||||
return GetAudioPath(resolved);
|
||||
}
|
||||
|
||||
#region AudioParams
|
||||
|
||||
protected Entity<AudioComponent> SetupAudio(string? fileName, AudioParams? audioParams, bool initialize = true, TimeSpan? length = null)
|
||||
[return: NotNullIfNotNull(nameof(specifier))]
|
||||
public string? GetAudioPath(ResolvedSoundSpecifier? specifier)
|
||||
{
|
||||
return specifier switch {
|
||||
ResolvedPathSpecifier path =>
|
||||
path.Path.ToString(),
|
||||
ResolvedCollectionSpecifier collection =>
|
||||
collection.Collection is null ?
|
||||
string.Empty :
|
||||
ProtoMan.Index<SoundCollectionPrototype>(collection.Collection).PickFiles[collection.Index].ToString(),
|
||||
null => null,
|
||||
_ => throw new ArgumentOutOfRangeException("specifier", specifier, "argument is not a ResolvedPathSpecifier or a ResolvedCollectionSpecifier"),
|
||||
};
|
||||
}
|
||||
|
||||
protected Entity<AudioComponent> SetupAudio(ResolvedSoundSpecifier? specifier, AudioParams? audioParams, bool initialize = true, TimeSpan? length = null)
|
||||
{
|
||||
var uid = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
|
||||
var fileName = GetAudioPath(specifier);
|
||||
DebugTools.Assert(!string.IsNullOrEmpty(fileName) || length is not null);
|
||||
MetadataSys.SetEntityName(uid, $"Audio ({fileName})", raiseEvents: false);
|
||||
audioParams ??= AudioParams.Default;
|
||||
@@ -385,6 +412,13 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
if (component.Params.Volume.Equals(value))
|
||||
return;
|
||||
|
||||
// Not a log error for now because if something has a negative infinity volume (i.e. 0 gain) then subtracting from it can
|
||||
// easily cause this and making callers deal with it everywhere is quite annoying.
|
||||
if (float.IsNaN(value))
|
||||
{
|
||||
value = float.NegativeInfinity;
|
||||
}
|
||||
|
||||
component.Params.Volume = value;
|
||||
component.Volume = value;
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.Params));
|
||||
@@ -395,8 +429,9 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Gets the timespan of the specified audio.
|
||||
/// </summary>
|
||||
public TimeSpan GetAudioLength(string filename)
|
||||
public TimeSpan GetAudioLength(ResolvedSoundSpecifier specifier)
|
||||
{
|
||||
var filename = GetAudioPath(specifier) ?? string.Empty;
|
||||
if (!filename.StartsWith("/"))
|
||||
throw new ArgumentException("Path must be rooted");
|
||||
|
||||
@@ -429,7 +464,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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(ResolvedSoundSpecifier? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file globally, without position.
|
||||
@@ -438,7 +473,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="playerFilter">The set of players that will hear the sound.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayGlobal(GetSound(sound), playerFilter, recordReplay, audioParams ?? sound.Params);
|
||||
return sound == null ? null : PlayGlobal(ResolveSound(sound), playerFilter, recordReplay, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -446,7 +481,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, ICommonSession recipient, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file globally, without position.
|
||||
@@ -455,7 +490,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="recipient">The player that will hear the sound.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayGlobal(GetSound(sound), recipient, audioParams ?? sound.Params);
|
||||
return sound == null ? null : PlayGlobal(ResolveSound(sound), recipient, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
public abstract void LoadStream<T>(Entity<AudioComponent> entity, T stream);
|
||||
@@ -465,7 +500,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null);
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, EntityUid recipient, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file globally, without position.
|
||||
@@ -474,7 +509,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="recipient">The player that will hear the sound.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, EntityUid recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayGlobal(GetSound(sound), recipient, audioParams ?? sound.Params);
|
||||
return sound == null ? null : PlayGlobal(ResolveSound(sound), recipient, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -483,7 +518,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>
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
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(ResolvedSoundSpecifier? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file following an entity.
|
||||
@@ -491,7 +526,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>
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
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(ResolvedSoundSpecifier? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file following an entity.
|
||||
@@ -499,7 +534,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>
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
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(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file following an entity.
|
||||
@@ -509,7 +544,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayEntity(GetSound(sound), playerFilter, uid, recordReplay, audioParams ?? sound.Params);
|
||||
return sound == null ? null : PlayEntity(ResolveSound(sound), playerFilter, uid, recordReplay, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -520,7 +555,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayEntity(GetSound(sound), recipient, uid, audioParams ?? sound.Params);
|
||||
return sound == null ? null : PlayEntity(ResolveSound(sound), recipient, uid, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -531,7 +566,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayEntity(GetSound(sound), recipient, uid, audioParams ?? sound.Params);
|
||||
return sound == null ? null : PlayEntity(ResolveSound(sound), recipient, uid, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -541,7 +576,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(SoundSpecifier? sound, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayPvs(GetSound(sound), uid, audioParams ?? sound.Params);
|
||||
return sound == null ? null : PlayPvs(ResolveSound(sound), uid, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -551,7 +586,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="coordinates">The EntityCoordinates to attach the audio source to.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(SoundSpecifier? sound, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayPvs(GetSound(sound), coordinates, audioParams ?? sound.Params);
|
||||
return sound == null ? null : PlayPvs(ResolveSound(sound), coordinates, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -559,7 +594,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string? filename,
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? filename,
|
||||
EntityCoordinates coordinates, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
@@ -567,7 +602,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string? filename, EntityUid uid,
|
||||
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? filename, EntityUid uid,
|
||||
AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
@@ -604,7 +639,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>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
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(ResolvedSoundSpecifier? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file at a static position.
|
||||
@@ -612,7 +647,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>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
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(ResolvedSoundSpecifier? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file at a static position.
|
||||
@@ -620,7 +655,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>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
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(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
|
||||
|
||||
/// <summary>
|
||||
/// Play an audio file at a static position.
|
||||
@@ -630,7 +665,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayStatic(GetSound(sound), playerFilter, coordinates, recordReplay, audioParams);
|
||||
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -641,7 +676,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayStatic(GetSound(sound), recipient, coordinates, audioParams ?? sound.Params);
|
||||
return sound == null ? null : PlayStatic(ResolveSound(sound), recipient, coordinates, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -652,7 +687,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return sound == null ? null : PlayStatic(GetSound(sound), recipient, coordinates, audioParams ?? sound.Params);
|
||||
return sound == null ? null : PlayStatic(ResolveSound(sound), recipient, coordinates, audioParams ?? sound.Params);
|
||||
}
|
||||
|
||||
// These are just here for replays now.
|
||||
@@ -665,7 +700,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
[NetSerializable, Serializable]
|
||||
protected abstract class AudioMessage : EntityEventArgs
|
||||
{
|
||||
public string FileName = string.Empty;
|
||||
public ResolvedSoundSpecifier Specifier = new ResolvedPathSpecifier(string.Empty);
|
||||
public AudioParams AudioParams;
|
||||
}
|
||||
|
||||
|
||||
@@ -1233,6 +1233,12 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<float> AudioRaycastLength =
|
||||
CVarDef.Create("audio.raycast_length", SharedAudioSystem.DefaultSoundRange, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum offset for audio to be played at from its full duration. If it's past this then the audio won't be played.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> AudioEndBuffer =
|
||||
CVarDef.Create("audio.end_buffer", 0.01f, CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Tickrate for audio calculations.
|
||||
/// OpenAL recommends 30TPS. This is to avoid running raycasts every frame especially for high-refresh rate monitors.
|
||||
|
||||
@@ -59,7 +59,7 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
|
||||
UpdatesAfter.Add(typeof(SharedTransformSystem));
|
||||
UpdatesAfter.Add(typeof(SharedPhysicsSystem));
|
||||
|
||||
SubscribeLocalEvent<MapChangedEvent>(MapManagerOnMapCreated);
|
||||
SubscribeLocalEvent<MapCreatedEvent>(MapManagerOnMapCreated);
|
||||
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
|
||||
|
||||
SubscribeLocalEvent<TComp, ComponentStartup>(OnCompStartup);
|
||||
@@ -143,11 +143,8 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
|
||||
RemComp(uid, component);
|
||||
}
|
||||
|
||||
private void MapManagerOnMapCreated(MapChangedEvent e)
|
||||
private void MapManagerOnMapCreated(MapCreatedEvent e)
|
||||
{
|
||||
if (e.Destroyed || e.Map == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
EnsureComp<TTreeComp>(e.Uid);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ internal sealed class ListMapsCommand : LocalizedEntityCommands
|
||||
{
|
||||
var msg = new StringBuilder();
|
||||
|
||||
foreach (var mapId in _map.GetAllMapIds().OrderBy(id => id.Value))
|
||||
foreach (var mapId in _mapSystem.GetAllMapIds().OrderBy(id => id.Value))
|
||||
{
|
||||
if (!_mapSystem.TryGetMap(mapId, out var mapUid))
|
||||
continue;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
@@ -186,7 +186,9 @@ public static class CompletionHelper
|
||||
|
||||
public static IEnumerable<CompletionOption> MapUids(IEntityManager? entManager = null)
|
||||
{
|
||||
return Components<MapComponent>(string.Empty, entManager);
|
||||
IoCManager.Resolve(ref entManager);
|
||||
|
||||
return Components<MapComponent>(string.Empty, entManager, limit: 128);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -194,7 +196,7 @@ public static class CompletionHelper
|
||||
/// </summary>
|
||||
public static IEnumerable<CompletionOption> NetEntities(string text, IEntityManager? entManager = null, int limit = 20)
|
||||
{
|
||||
if (!NetEntity.TryParse(text, out _))
|
||||
if (text != string.Empty && !NetEntity.TryParse(text, out _))
|
||||
yield break;
|
||||
|
||||
IoCManager.Resolve(ref entManager);
|
||||
@@ -214,7 +216,7 @@ public static class CompletionHelper
|
||||
|
||||
public static IEnumerable<CompletionOption> Components<T>(string text, IEntityManager? entManager = null, int limit = 20) where T : IComponent
|
||||
{
|
||||
if (!NetEntity.TryParse(text, out _))
|
||||
if (text != string.Empty && !NetEntity.TryParse(text, out _))
|
||||
yield break;
|
||||
|
||||
IoCManager.Resolve(ref entManager);
|
||||
|
||||
@@ -60,7 +60,9 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
internal string GetPath(ResPath relPath)
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()));
|
||||
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
|
||||
// Sanitise platform-specific path and standardize it for engine use.
|
||||
.Replace(Path.DirectorySeparatorChar, '/');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Robust.Shared.ContentPack
|
||||
var paths = new List<ResPath>();
|
||||
|
||||
foreach (var filePath in _res.ContentFindRelativeFiles(mountPath)
|
||||
.Where(p => !p.ToString().Contains('/') && p.Filename.StartsWith(filterPrefix) &&
|
||||
.Where(p => p.Filename.StartsWith(filterPrefix) &&
|
||||
p.Extension == "dll"))
|
||||
{
|
||||
var fullPath = mountPath / filePath;
|
||||
|
||||
@@ -379,7 +379,9 @@ namespace Robust.Shared.ContentPack
|
||||
{
|
||||
if (root is DirLoader loader)
|
||||
{
|
||||
yield return new ResPath(loader.GetPath(new ResPath(@"/")));
|
||||
var rootDir = loader.GetPath(new ResPath(@"/"));
|
||||
|
||||
yield return new ResPath(rootDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,7 +696,7 @@ Types:
|
||||
- "bool IsMatch(string, string, System.Text.RegularExpressions.RegexOptions, System.TimeSpan)"
|
||||
- "int GroupNumberFromName(string)"
|
||||
- "int[] GetGroupNumbers()"
|
||||
- "string Escape()"
|
||||
- "string Escape(string)"
|
||||
- "string GroupNameFromNumber(int)"
|
||||
- "string Replace(string, string)"
|
||||
- "string Replace(string, string, int)"
|
||||
@@ -736,6 +736,15 @@ Types:
|
||||
- "void .ctor(string)"
|
||||
- "void .ctor(string, System.Text.RegularExpressions.RegexOptions)"
|
||||
- "void .ctor(string, System.Text.RegularExpressions.RegexOptions, System.TimeSpan)"
|
||||
- "int Count(string)"
|
||||
- "int Count(System.ReadOnlySpan`1<char>)"
|
||||
- "int Count(System.ReadOnlySpan`1<char>, int)"
|
||||
- "int Count(string, string)"
|
||||
- "int Count(string, string, System.Text.RegularExpressions.RegexOptions)"
|
||||
- "int Count(string, string, System.Text.RegularExpressions.RegexOptions, System.TimeSpan)"
|
||||
- "int Count(System.ReadOnlySpan`1<char>, string)"
|
||||
- "int Count(System.ReadOnlySpan`1<char>, string, System.Text.RegularExpressions.RegexOptions)"
|
||||
- "int Count(System.ReadOnlySpan`1<char>, string, System.Text.RegularExpressions.RegexOptions, System.TimeSpan)"
|
||||
RegexMatchTimeoutException: { All: True }
|
||||
RegexOptions: { } # Enum
|
||||
RegexParseError: { }
|
||||
@@ -789,6 +798,7 @@ Types:
|
||||
- "bool Equals(System.ReadOnlySpan`1<char>)"
|
||||
- "bool Equals(System.Text.StringBuilder)"
|
||||
- "char get_Chars(int)"
|
||||
- "void set_Chars(int, char)"
|
||||
- "int EnsureCapacity(int)"
|
||||
- "int get_Capacity()"
|
||||
- "int get_Length()"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Shared.EntitySerialization.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Added to Maps that were loaded by <see cref="MapLoaderSystem"/>. If not present then this map was created externally.
|
||||
/// </summary>
|
||||
[RegisterComponent, UnsavedComponent]
|
||||
public sealed partial class LoadedMapComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.GameObjects;
|
||||
namespace Robust.Shared.EntitySerialization.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Used by <see cref="MapLoaderSystem"/> to track the original tile map from when a map was loaded.
|
||||
@@ -0,0 +1,20 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Shared.EntitySerialization.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This component is optionally added to entities that get loaded from yaml files. It stores the UID that the entity
|
||||
/// had within the yaml file. This is used when saving the entity back to a yaml file so that it re-uses the same UID.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is primarily intended to reduce the diff sizes when modifying yaml maps. Note that there is no guarantee that
|
||||
/// the given uid will be used when writing the entity. E.g., if more than one entity have this component with the
|
||||
/// same uid, only one of those entities will be saved with the requested id.
|
||||
/// </remarks>
|
||||
[RegisterComponent, UnsavedComponent]
|
||||
public sealed partial class YamlUidComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Uid;
|
||||
}
|
||||
1221
Robust.Shared/EntitySerialization/EntityDeserializer.cs
Normal file
1221
Robust.Shared/EntitySerialization/EntityDeserializer.cs
Normal file
File diff suppressed because it is too large
Load Diff
988
Robust.Shared/EntitySerialization/EntitySerializer.cs
Normal file
988
Robust.Shared/EntitySerialization/EntitySerializer.cs
Normal file
@@ -0,0 +1,988 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.EntitySerialization.Components;
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.EntitySerialization;
|
||||
|
||||
/// <summary>
|
||||
/// This class provides methods for serializing entities into yaml. It provides some more control over
|
||||
/// serialization than the methods provided by <see cref="MapLoaderSystem"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There are several methods (e.g., <see cref="SerializeEntityRecursive"/> that serialize entities into a
|
||||
/// per-entity <see cref="MappingDataNode"/> stored in the <see cref="EntityData"/> dictionary, which is indexed by the
|
||||
/// entity's assigned yaml id (see <see cref="GetYamlUid"/>. The generated data can then be written to a larger yaml
|
||||
/// document using the various "Write" methods. (e.g., <see cref="WriteEntitySection"/>). After a one has finished using
|
||||
/// the generated data, the serializer needs to be reset (<see cref="Reset"/>) using it again to serialize other entities.
|
||||
/// </remarks>
|
||||
public sealed class EntitySerializer : ISerializationContext,
|
||||
ITypeSerializer<EntityUid, ValueDataNode>,
|
||||
ITypeSerializer<NetEntity, ValueDataNode>
|
||||
{
|
||||
public const int MapFormatVersion = 7;
|
||||
// v6->v7: PR #5572 - Added more metadata, List maps/grids/orphans, include some life-stage information
|
||||
// v5->v6: PR #4307 - Converted Tile.TypeId from ushort to int
|
||||
// v4->v5: PR #3992 - Removed name & author fields
|
||||
// v3->v4: PR #3913 - Grouped entities by prototype
|
||||
// v2->v3: PR #3468
|
||||
|
||||
public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
|
||||
|
||||
[Dependency] public readonly EntityManager EntMan = default!;
|
||||
[Dependency] public readonly IGameTiming Timing = default!;
|
||||
[Dependency] private readonly IComponentFactory _factory = default!;
|
||||
[Dependency] private readonly ISerializationManager _serialization = default!;
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDef = default!;
|
||||
[Dependency] private readonly IConfigurationManager _conf = default!;
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
|
||||
private readonly ISawmill _log;
|
||||
public readonly Dictionary<EntityUid, int> YamlUidMap = new();
|
||||
public readonly HashSet<int> YamlIds = new();
|
||||
|
||||
|
||||
public string? CurrentComponent { get; private set; }
|
||||
public Entity<MetaDataComponent>? CurrentEntity { get; private set; }
|
||||
public int CurrentEntityYamlUid { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tile ID -> yaml tile ID mapping.
|
||||
/// </summary>
|
||||
private readonly Dictionary<int, int> _tileMap = new();
|
||||
private readonly HashSet<int> _yamlTileIds = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool WritingReadingPrototypes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, the serializer will refuse to serialize the given entity and will orphan any entity that is parented to
|
||||
/// it. This is useful for serializing things like a grid (or multiple grids & entities) that are parented to a map
|
||||
/// without actually serializing the map itself.
|
||||
/// </summary>
|
||||
public EntityUid Truncate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of all entities that have previously been ignored via <see cref="Truncate"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is tracked in case somebody does something weird, like trying to save a grid w/o its map, and then later on
|
||||
/// including the map in the file. AFAIK, that should work in principle, though it would lead to a weird file where
|
||||
/// the grid is orphaned and not on the map where it should be.
|
||||
/// </remarks>
|
||||
public readonly HashSet<EntityUid> Truncated = new();
|
||||
|
||||
public readonly SerializationOptions Options;
|
||||
|
||||
/// <summary>
|
||||
/// Cached prototype data. This is used to avoid writing redundant data that is already specified in an entity's
|
||||
/// prototype.
|
||||
/// </summary>
|
||||
public readonly Dictionary<string, Dictionary<string, MappingDataNode>> PrototypeCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// The serialized entity data.
|
||||
/// </summary>
|
||||
public readonly Dictionary<int, (EntityUid Uid, MappingDataNode Node)> EntityData = new();
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="EntityData"/> indices grouped by their entity prototype ids.
|
||||
/// </summary>
|
||||
public readonly Dictionary<string, List<int>> Prototypes = new();
|
||||
|
||||
/// <summary>
|
||||
/// Yaml ids of all serialized map entities.
|
||||
/// </summary>
|
||||
public readonly List<int> Maps = new();
|
||||
|
||||
/// <summary>
|
||||
/// Yaml ids of all serialized null-space entities.
|
||||
/// This only includes entities that were initially in null-space, it does not include entities that were
|
||||
/// serialized without their parents. Those are in <see cref="Orphans"/>.
|
||||
/// </summary>
|
||||
public readonly List<int> Nullspace = new();
|
||||
|
||||
/// <summary>
|
||||
/// Yaml ids of all serialized grid entities.
|
||||
/// </summary>
|
||||
public readonly List<int> Grids = new();
|
||||
|
||||
/// <summary>
|
||||
/// Yaml ids of all serialized entities in the file whose parents were not serialized. This does not include
|
||||
/// entities that did not have a parent (e.g., maps or null-space entities). I.e., these are the entities that
|
||||
/// need to be attached to a new parent when loading the file, unless you want to load them into null-space.
|
||||
/// </summary>
|
||||
public readonly List<int> Orphans = new();
|
||||
|
||||
private readonly string _metaName;
|
||||
private readonly string _xformName;
|
||||
private readonly MappingDataNode _emptyMetaNode;
|
||||
private readonly MappingDataNode _emptyXformNode;
|
||||
private int _nextYamlUid = 1;
|
||||
private int _nextYamlTileId;
|
||||
|
||||
private readonly List<EntityUid> _autoInclude = new();
|
||||
private readonly EntityQuery<YamlUidComponent> _yamlQuery;
|
||||
private readonly EntityQuery<MapGridComponent> _gridQuery;
|
||||
private readonly EntityQuery<MapComponent> _mapQuery;
|
||||
private readonly EntityQuery<MetaDataComponent> _metaQuery;
|
||||
private readonly EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
/// <summary>
|
||||
/// C# event for checking whether an entity is serializable. Can be used by content to prevent specific entities
|
||||
/// from getting serialized.
|
||||
/// </summary>
|
||||
public event IsSerializableDelegate? OnIsSerializeable;
|
||||
public delegate void IsSerializableDelegate(Entity<MetaDataComponent> ent, ref bool serializable);
|
||||
|
||||
public EntitySerializer(IDependencyCollection _dependency, SerializationOptions options)
|
||||
{
|
||||
_dependency.InjectDependencies(this);
|
||||
|
||||
_log = _logMan.GetSawmill("entity_serializer");
|
||||
SerializerProvider.RegisterSerializer(this);
|
||||
|
||||
_metaName = _factory.GetComponentName(typeof(MetaDataComponent));
|
||||
_xformName = _factory.GetComponentName(typeof(TransformComponent));
|
||||
_emptyMetaNode = _serialization.WriteValueAs<MappingDataNode>(typeof(MetaDataComponent), new MetaDataComponent(), alwaysWrite: true, context: this);
|
||||
|
||||
CurrentComponent = _xformName;
|
||||
_emptyXformNode = _serialization.WriteValueAs<MappingDataNode>(typeof(TransformComponent), new TransformComponent(), alwaysWrite: true, context: this);
|
||||
CurrentComponent = null;
|
||||
|
||||
_yamlQuery = EntMan.GetEntityQuery<YamlUidComponent>();
|
||||
_gridQuery = EntMan.GetEntityQuery<MapGridComponent>();
|
||||
_mapQuery = EntMan.GetEntityQuery<MapComponent>();
|
||||
_metaQuery = EntMan.GetEntityQuery<MetaDataComponent>();
|
||||
_xformQuery = EntMan.GetEntityQuery<TransformComponent>();
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public bool IsSerializable(Entity<MetaDataComponent?> ent)
|
||||
{
|
||||
if (ent.Comp == null && !EntMan.TryGetComponent(ent.Owner, out ent.Comp))
|
||||
return false;
|
||||
|
||||
if (ent.Comp.EntityPrototype?.MapSavable == false)
|
||||
return false;
|
||||
|
||||
bool serializable = true;
|
||||
OnIsSerializeable?.Invoke(ent!, ref serializable);
|
||||
return serializable;
|
||||
}
|
||||
|
||||
#region Serialize API
|
||||
|
||||
/// <summary>
|
||||
/// Serialize a single entity. This does not automatically include
|
||||
/// children, though depending on the setting of <see cref="SerializationOptions.MissingEntityBehaviour"/> it may
|
||||
/// auto-include additional entities aside from the one provided.
|
||||
/// </summary>
|
||||
public void SerializeEntity(EntityUid uid)
|
||||
{
|
||||
if (!IsSerializable(uid))
|
||||
throw new Exception($"{EntMan.ToPrettyString(uid)} is not serializable");
|
||||
|
||||
DebugTools.AssertNull(CurrentEntity);
|
||||
ReserveYamlId(uid);
|
||||
SerializeEntityInternal(uid);
|
||||
DebugTools.AssertNull(CurrentEntity);
|
||||
if (_autoInclude.Count != 0)
|
||||
ProcessAutoInclude();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize a set of entities. This does not automatically include children or parents, though depending on the
|
||||
/// setting of <see cref="SerializationOptions.MissingEntityBehaviour"/> it may auto-include additional entities
|
||||
/// aside from the one provided.
|
||||
/// </summary>
|
||||
public void SerializeEntities(HashSet<EntityUid> entities)
|
||||
{
|
||||
foreach (var uid in entities)
|
||||
{
|
||||
if (!IsSerializable(uid))
|
||||
throw new Exception($"{EntMan.ToPrettyString(uid)} is not serializable");
|
||||
}
|
||||
|
||||
ReserveYamlIds(entities);
|
||||
SerializeEntitiesInternal(entities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes an entity and all of its serializable children. Note that this will not automatically serialize the
|
||||
/// entity's parents.
|
||||
/// </summary>
|
||||
public void SerializeEntityRecursive(EntityUid root)
|
||||
{
|
||||
if (!IsSerializable(root))
|
||||
throw new Exception($"{EntMan.ToPrettyString(root)} is not serializable");
|
||||
|
||||
Truncate = _xformQuery.GetComponent(root).ParentUid;
|
||||
Truncated.Add(Truncate);
|
||||
InitializeTileMap(root);
|
||||
HashSet<EntityUid> entities = new();
|
||||
RecursivelyIncludeChildren(root, entities);
|
||||
ReserveYamlIds(entities);
|
||||
SerializeEntitiesInternal(entities);
|
||||
Truncate = EntityUid.Invalid;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the <see cref="_tileMap"/> that is used to serialize grid chunks using
|
||||
/// <see cref="MapChunkSerializer"/>. This initialization just involves checking to see if any of the entities being
|
||||
/// serialized were previously deserialized. If they were, it will re-use the old tile map. This is not actually required,
|
||||
/// and is just meant to prevent large map file diffs when the internal tile ids change. I.e., you can serialize entities
|
||||
/// without initializing the tile map.
|
||||
/// </summary>
|
||||
private void InitializeTileMap(EntityUid root)
|
||||
{
|
||||
if (!FindSavedTileMap(root, out var savedMap))
|
||||
return;
|
||||
|
||||
// Note: some old maps were saved with duplicate id strings.
|
||||
// I.e, multiple integers that correspond to the same prototype id.
|
||||
// Hence the TryAdd()
|
||||
//
|
||||
// Though now we also need to use TryAdd in case InitializeTileMap() is called multiple times.
|
||||
// E.g., if different grids get added separately to a single save file, in which case the
|
||||
// tile map may already be partially populated.
|
||||
foreach (var (origId, prototypeId) in savedMap)
|
||||
{
|
||||
if (_tileDef.TryGetDefinition(prototypeId, out var definition))
|
||||
{
|
||||
_tileMap.TryAdd(definition.TileId, origId);
|
||||
_yamlTileIds.Add(origId); // Make sure we record the IDs we're using so when we need to reserve new ones we can
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool FindSavedTileMap(EntityUid root, [NotNullWhen(true)] out Dictionary<int, string>? map)
|
||||
{
|
||||
// Try and fetch the mapping directly
|
||||
if (EntMan.TryGetComponent(root, out MapSaveTileMapComponent? comp))
|
||||
{
|
||||
map = comp.TileMap;
|
||||
return true;
|
||||
}
|
||||
|
||||
// iterate over all of its children and grab the first grid with a mapping
|
||||
var xform = _xformQuery.GetComponent(root);
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
if (!EntMan.TryGetComponent(child, out MapSaveTileMapComponent? cComp))
|
||||
continue;
|
||||
map = cComp.TileMap;
|
||||
return true;
|
||||
}
|
||||
|
||||
map = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
#region AutoInclude
|
||||
|
||||
private void ProcessAutoInclude()
|
||||
{
|
||||
DebugTools.AssertEqual(_autoInclude.ToHashSet().Count, _autoInclude.Count);
|
||||
|
||||
var ents = new HashSet<EntityUid>();
|
||||
|
||||
switch (Options.MissingEntityBehaviour)
|
||||
{
|
||||
case MissingEntityBehaviour.PartialInclude:
|
||||
// Include the entity and any of its direct parents
|
||||
foreach (var uid in _autoInclude)
|
||||
{
|
||||
RecursivelyIncludeParents(uid, ents);
|
||||
}
|
||||
break;
|
||||
case MissingEntityBehaviour.IncludeNullspace:
|
||||
case MissingEntityBehaviour.AutoInclude:
|
||||
// Find the root transform of all the included entities
|
||||
var roots = new HashSet<EntityUid>();
|
||||
foreach (var uid in _autoInclude)
|
||||
{
|
||||
GetRootNode(uid, roots);
|
||||
}
|
||||
|
||||
// Recursively include all children of these root nodes.
|
||||
foreach (var root in roots)
|
||||
{
|
||||
RecursivelyIncludeChildren(root, ents);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
_autoInclude.Clear();
|
||||
SerializeEntitiesInternal(ents);
|
||||
}
|
||||
|
||||
private void RecursivelyIncludeChildren(EntityUid uid, HashSet<EntityUid> ents)
|
||||
{
|
||||
if (!IsSerializable(uid))
|
||||
return;
|
||||
|
||||
ents.Add(uid);
|
||||
var xform = _xformQuery.GetComponent(uid);
|
||||
foreach (var child in xform._children)
|
||||
{
|
||||
RecursivelyIncludeChildren(child, ents);
|
||||
}
|
||||
}
|
||||
|
||||
private void GetRootNode(EntityUid uid, HashSet<EntityUid> ents)
|
||||
{
|
||||
if (!IsSerializable(uid))
|
||||
throw new NotSupportedException($"Attempted to auto-include an unserializable entity: {EntMan.ToPrettyString(uid)}");
|
||||
|
||||
var xform = _xformQuery.GetComponent(uid);
|
||||
while (xform.ParentUid.IsValid() && xform.ParentUid != Truncate)
|
||||
{
|
||||
uid = xform.ParentUid;
|
||||
xform = _xformQuery.GetComponent(uid);
|
||||
|
||||
if (!IsSerializable(uid))
|
||||
throw new NotSupportedException($"Encountered an un-serializable parent entity: {EntMan.ToPrettyString(uid)}");
|
||||
}
|
||||
|
||||
ents.Add(uid);
|
||||
}
|
||||
|
||||
private void RecursivelyIncludeParents(EntityUid uid, HashSet<EntityUid> ents)
|
||||
{
|
||||
while (uid.IsValid() && uid != Truncate)
|
||||
{
|
||||
if (!ents.Add(uid))
|
||||
break;
|
||||
|
||||
if (!IsSerializable(uid))
|
||||
throw new NotSupportedException($"Encountered an un-serializable parent entity: {EntMan.ToPrettyString(uid)}");
|
||||
|
||||
uid = _xformQuery.GetComponent(uid).ParentUid;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void SerializeEntitiesInternal(HashSet<EntityUid> entities)
|
||||
{
|
||||
foreach (var uid in entities)
|
||||
{
|
||||
DebugTools.AssertNull(CurrentEntity);
|
||||
SerializeEntityInternal(uid);
|
||||
}
|
||||
|
||||
DebugTools.AssertNull(CurrentEntity);
|
||||
if (_autoInclude.Count != 0)
|
||||
ProcessAutoInclude();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize a single entity, and store the results in <see cref="EntityData"/>.
|
||||
/// </summary>
|
||||
private void SerializeEntityInternal(EntityUid uid)
|
||||
{
|
||||
var saveId = GetYamlUid(uid);
|
||||
DebugTools.Assert(!EntityData.ContainsKey(saveId));
|
||||
|
||||
// It might be possible that something could cause an entity to be included twice.
|
||||
// E.g., if someone serializes a grid w/o its map, and then tries to separately include the map and all its children.
|
||||
// In that case, the grid would already have been serialized as a orphan.
|
||||
// uhhh.... I guess its fine?
|
||||
if (EntityData.ContainsKey(saveId))
|
||||
return;
|
||||
|
||||
var meta = _metaQuery.GetComponent(uid);
|
||||
var protoId = meta.EntityPrototype?.ID ?? string.Empty;
|
||||
|
||||
switch (meta.EntityLifeStage)
|
||||
{
|
||||
case <= EntityLifeStage.Initializing:
|
||||
_log.Error($"Encountered an uninitialized entity: {EntMan.ToPrettyString(uid)}");
|
||||
break;
|
||||
case >= EntityLifeStage.Terminating:
|
||||
_log.Error($"Encountered terminating or deleted entity: {EntMan.ToPrettyString(uid)}");
|
||||
break;
|
||||
}
|
||||
|
||||
CurrentEntityYamlUid = saveId;
|
||||
CurrentEntity = (uid, meta);
|
||||
|
||||
Prototypes.GetOrNew(protoId).Add(saveId);
|
||||
var xform = _xformQuery.GetComponent(uid);
|
||||
|
||||
if (_mapQuery.HasComp(uid))
|
||||
Maps.Add(saveId);
|
||||
else if (xform.ParentUid == EntityUid.Invalid)
|
||||
Nullspace.Add(saveId);
|
||||
|
||||
if (_gridQuery.HasComp(uid))
|
||||
{
|
||||
// The current assumption is that grids cannot be in null-space, because the rest of the code
|
||||
// (broadphase, etc) don't support grids without maps.
|
||||
DebugTools.Assert(xform.ParentUid != EntityUid.Invalid || _mapQuery.HasComp(uid));
|
||||
Grids.Add(saveId);
|
||||
}
|
||||
|
||||
var entData = new MappingDataNode
|
||||
{
|
||||
{"uid", saveId.ToString(CultureInfo.InvariantCulture)}
|
||||
};
|
||||
|
||||
EntityData[saveId] = (uid, entData);
|
||||
var cache = GetProtoCache(meta.EntityPrototype);
|
||||
|
||||
// Store information about whether a given entity has been map-initialized.
|
||||
// In principle, if a map has been map-initialized, then all entities on that map should also be map-initialized.
|
||||
// But technically there is nothing that prevents someone from moving a post-init entity onto a pre-init map and vice-versa.
|
||||
// Also, we need to record this information even if the map is not being serialized.
|
||||
// In 99% of cases, this data is probably redundant and just bloats the file, but I can't think of a better way of handling it.
|
||||
// At least it should only bloat post-init maps, which aren't really getting used so far.
|
||||
if (meta.EntityLifeStage == EntityLifeStage.MapInitialized)
|
||||
{
|
||||
if (Options.ExpectPreInit)
|
||||
_log.Error($"Expected all entities to be pre-mapinit, but encountered post-init entity: {EntMan.ToPrettyString(uid)}");
|
||||
entData.Add("mapInit", "true");
|
||||
|
||||
// If an entity has been map-initialized, we assume it is un-paused.
|
||||
// If it is paused, we have to specify it.
|
||||
if (meta.EntityPaused)
|
||||
entData.Add("paused", "true");
|
||||
}
|
||||
else
|
||||
{
|
||||
// If an entity has not yet been map-initialized, we assume it is paused.
|
||||
// I don't know in what situations it wouldn't be, but might as well future proof this.
|
||||
if (!meta.EntityPaused)
|
||||
entData.Add("paused", "false");
|
||||
}
|
||||
|
||||
var components = new SequenceDataNode();
|
||||
if (xform.NoLocalRotation && xform.LocalRotation != 0)
|
||||
{
|
||||
_log.Error($"Encountered a no-rotation entity with non-zero local rotation: {EntMan.ToPrettyString(uid)}");
|
||||
xform._localRotation = 0;
|
||||
}
|
||||
|
||||
foreach (var component in EntMan.GetComponentsInternal(uid))
|
||||
{
|
||||
var compType = component.GetType();
|
||||
|
||||
var reg = _factory.GetRegistration(compType);
|
||||
if (reg.Unsaved)
|
||||
continue;
|
||||
|
||||
CurrentComponent = reg.Name;
|
||||
MappingDataNode? compMapping;
|
||||
MappingDataNode? protoMapping = null;
|
||||
if (cache != null && cache.TryGetValue(reg.Name, out protoMapping))
|
||||
{
|
||||
// If this has a prototype, we need to use alwaysWrite: true.
|
||||
// E.g., an anchored prototype might have anchored: true. If we we are saving an un-anchored
|
||||
// instance of this entity, and if we have alwaysWrite: false, then compMapping would not include
|
||||
// the anchored data-field (as false is the default for this bool data field), so the entity would
|
||||
// implicitly be saved as anchored.
|
||||
compMapping = _serialization.WriteValueAs<MappingDataNode>(compType, component, alwaysWrite: true, context: this);
|
||||
|
||||
// This will not recursively call Except() on the values of the mapping. It will only remove
|
||||
// key-value pairs if both the keys and values are equal.
|
||||
compMapping = compMapping.Except(protoMapping);
|
||||
if(compMapping == null)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
compMapping = _serialization.WriteValueAs<MappingDataNode>(compType, component, alwaysWrite: false, context: this);
|
||||
}
|
||||
|
||||
// Don't need to write it if nothing was written! Note that if this entity has no associated
|
||||
// prototype, we ALWAYS want to write the component, because merely the fact that it exists is
|
||||
// information that needs to be written.
|
||||
if (compMapping.Children.Count != 0 || protoMapping == null)
|
||||
{
|
||||
compMapping.InsertAt(0, "type", new ValueDataNode(reg.Name));
|
||||
components.Add(compMapping);
|
||||
}
|
||||
}
|
||||
|
||||
CurrentComponent = null;
|
||||
if (components.Count != 0)
|
||||
entData.Add("components", components);
|
||||
|
||||
// TODO ENTITY SERIALIZATION
|
||||
// Consider adding a Action<EntityUid, MappingDataNode>? OnEntitySerialized
|
||||
// I.e., allow content to modify the per-entity data? I don't know if that would actually be useful, as content
|
||||
// could just as easily append a separate entity dictionary to the output that has the extra per-entity data they
|
||||
// want to serialize.
|
||||
|
||||
if (meta.EntityPrototype == null)
|
||||
{
|
||||
CurrentEntityYamlUid = 0;
|
||||
CurrentEntity = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// an entity may have less components than the original prototype, so we need to check if any are missing.
|
||||
SequenceDataNode? missingComponents = null;
|
||||
foreach (var (name, comp) in meta.EntityPrototype.Components)
|
||||
{
|
||||
// try comp instead of has-comp as it checks whether the component is supposed to have been
|
||||
// deleted.
|
||||
if (EntMan.TryGetComponent(uid, comp.Component.GetType(), out _))
|
||||
continue;
|
||||
|
||||
missingComponents ??= new();
|
||||
missingComponents.Add(new ValueDataNode(name));
|
||||
}
|
||||
|
||||
if (missingComponents != null)
|
||||
entData.Add("missingComponents", missingComponents);
|
||||
|
||||
CurrentEntityYamlUid = 0;
|
||||
CurrentEntity = null;
|
||||
}
|
||||
|
||||
private Dictionary<string, MappingDataNode>? GetProtoCache(EntityPrototype? proto)
|
||||
{
|
||||
if (proto == null)
|
||||
return null;
|
||||
|
||||
if (PrototypeCache.TryGetValue(proto.ID, out var cache))
|
||||
return cache;
|
||||
|
||||
PrototypeCache[proto.ID] = cache = new(proto.Components.Count);
|
||||
WritingReadingPrototypes = true;
|
||||
|
||||
foreach (var (compName, comp) in proto.Components)
|
||||
{
|
||||
CurrentComponent = compName;
|
||||
cache.Add(compName, _serialization.WriteValueAs<MappingDataNode>(comp.Component.GetType(), comp.Component, alwaysWrite: true, context: this));
|
||||
}
|
||||
|
||||
CurrentComponent = null;
|
||||
WritingReadingPrototypes = false;
|
||||
cache.TryAdd(_metaName, _emptyMetaNode);
|
||||
cache.TryAdd(_xformName, _emptyXformNode);
|
||||
return cache;
|
||||
}
|
||||
|
||||
#region Write
|
||||
|
||||
public MappingDataNode Write()
|
||||
{
|
||||
DebugTools.AssertEqual(Maps.ToHashSet().Count, Maps.Count, "Duplicate maps?");
|
||||
DebugTools.AssertEqual(Grids.ToHashSet().Count, Grids.Count, "Duplicate grids?");
|
||||
DebugTools.AssertEqual(Orphans.ToHashSet().Count, Orphans.Count, "Duplicate orphans?");
|
||||
DebugTools.AssertEqual(Nullspace.ToHashSet().Count, Nullspace.Count, "Duplicate nullspace?");
|
||||
|
||||
return new MappingDataNode
|
||||
{
|
||||
{"meta", WriteMetadata()},
|
||||
{"maps", WriteIds(Maps)},
|
||||
{"grids", WriteIds(Grids)},
|
||||
{"orphans", WriteIds(Orphans)},
|
||||
{"nullspace", WriteIds(Nullspace)},
|
||||
{"tilemap", WriteTileMap()},
|
||||
{"entities", WriteEntitySection()},
|
||||
};
|
||||
}
|
||||
|
||||
public MappingDataNode WriteMetadata()
|
||||
{
|
||||
return new MappingDataNode
|
||||
{
|
||||
{"format", MapFormatVersion.ToString(CultureInfo.InvariantCulture)},
|
||||
{"category", GetCategory().ToString()},
|
||||
{"engineVersion", _conf.GetCVar(CVars.BuildEngineVersion) },
|
||||
{"forkId", _conf.GetCVar(CVars.BuildForkId)},
|
||||
{"forkVersion", _conf.GetCVar(CVars.BuildVersion)},
|
||||
{"time", DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)},
|
||||
{"entityCount", EntityData.Count.ToString(CultureInfo.InvariantCulture)}
|
||||
};
|
||||
}
|
||||
|
||||
public SequenceDataNode WriteIds(List<int> ids)
|
||||
{
|
||||
var result = new SequenceDataNode();
|
||||
foreach (var id in ids)
|
||||
{
|
||||
result.Add(new ValueDataNode(id.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the <see cref="_tileMap"/> to yaml. This data is required to deserialize any serialized grid chunks using <see cref="MapChunkSerializer"/>.
|
||||
/// </summary>
|
||||
public MappingDataNode WriteTileMap()
|
||||
{
|
||||
var map = new MappingDataNode();
|
||||
foreach (var (tileId, yamlTileId) in _tileMap.OrderBy(x => x.Key))
|
||||
{
|
||||
// This can come up if tests try to serialize test maps with custom / placeholder tile ids without registering them with the tile def manager..
|
||||
if (!_tileDef.TryGetDefinition(tileId, out var def))
|
||||
throw new Exception($"Attempting to serialize a tile {tileId} with no valid tile definition.");
|
||||
|
||||
var yamlId = yamlTileId.ToString(CultureInfo.InvariantCulture);
|
||||
map.Add(yamlId, def.ID);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public SequenceDataNode WriteEntitySection()
|
||||
{
|
||||
if (YamlIds.Count != YamlUidMap.Count || YamlIds.Count != EntityData.Count)
|
||||
{
|
||||
// Maybe someone reserved a yaml id with ReserveYamlId() or implicitly with GetId() without actually
|
||||
// ever serializing the entity, This can lead to references to non-existent entities.
|
||||
throw new Exception($"Entity count mismatch");
|
||||
}
|
||||
|
||||
var prototypes = new SequenceDataNode();
|
||||
var protos = Prototypes.Keys.ToList();
|
||||
protos.Sort(StringComparer.InvariantCulture);
|
||||
|
||||
foreach (var protoId in protos)
|
||||
{
|
||||
var entities = new SequenceDataNode();
|
||||
var node = new MappingDataNode
|
||||
{
|
||||
{ "proto", protoId },
|
||||
{ "entities", entities},
|
||||
};
|
||||
|
||||
prototypes.Add(node);
|
||||
|
||||
var saveIds = Prototypes[protoId];
|
||||
saveIds.Sort();
|
||||
foreach (var saveId in saveIds)
|
||||
{
|
||||
var entData = EntityData[saveId].Node;
|
||||
entities.Add(entData);
|
||||
}
|
||||
}
|
||||
|
||||
return prototypes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the category that the serialized data belongs to. If one was specified in the
|
||||
/// <see cref="SerializationOptions"/> it will use that after validating it, otherwise it will attempt to infer a
|
||||
/// category.
|
||||
/// </summary>
|
||||
public FileCategory GetCategory()
|
||||
{
|
||||
switch (Options.Category)
|
||||
{
|
||||
case FileCategory.Save:
|
||||
return FileCategory.Save;
|
||||
|
||||
case FileCategory.Map:
|
||||
return Maps.Count == 1 ? FileCategory.Map : FileCategory.Unknown;
|
||||
|
||||
case FileCategory.Grid:
|
||||
if (Maps.Count > 0 || Grids.Count != 1)
|
||||
return FileCategory.Unknown;
|
||||
return FileCategory.Grid;
|
||||
|
||||
case FileCategory.Entity:
|
||||
if (Maps.Count > 0 || Grids.Count > 0 || Orphans.Count != 1)
|
||||
return FileCategory.Unknown;
|
||||
return FileCategory.Entity;
|
||||
|
||||
default:
|
||||
if (Maps.Count == 1)
|
||||
{
|
||||
// Contains a single map, and no orphaned entities that need reparenting.
|
||||
if (Orphans.Count == 0)
|
||||
return FileCategory.Map;
|
||||
}
|
||||
else if (Grids.Count == 1)
|
||||
{
|
||||
// Contains a single orphaned grid.
|
||||
if (Orphans.Count == 1 && Grids[0] == Orphans[0])
|
||||
return FileCategory.Grid;
|
||||
}
|
||||
else if (Orphans.Count == 1)
|
||||
{
|
||||
// A lone orphaned entity.
|
||||
return FileCategory.Entity;
|
||||
}
|
||||
|
||||
return FileCategory.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region YamlIds
|
||||
|
||||
/// <summary>
|
||||
/// Get (or allocate) the integer id that will be used in the serialized file to refer to the given entity.
|
||||
/// </summary>
|
||||
public int GetYamlUid(EntityUid uid)
|
||||
{
|
||||
return !YamlUidMap.TryGetValue(uid, out var id) ? AllocateYamlUid(uid) : id;
|
||||
}
|
||||
|
||||
private int AllocateYamlUid(EntityUid uid)
|
||||
{
|
||||
if (Truncated.Contains(uid))
|
||||
{
|
||||
_log.Error(
|
||||
"Including a previously truncated entity within the serialization process? Something probably wrong");
|
||||
}
|
||||
|
||||
DebugTools.Assert(!YamlUidMap.ContainsKey(uid));
|
||||
while (!YamlIds.Add(_nextYamlUid))
|
||||
{
|
||||
_nextYamlUid++;
|
||||
}
|
||||
|
||||
YamlUidMap.Add(uid, _nextYamlUid);
|
||||
return _nextYamlUid++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get (or allocate) the integer id that will be used in the serialized file to refer to the given grid tile id.
|
||||
/// </summary>
|
||||
public int GetYamlTileId(int tileId)
|
||||
{
|
||||
if (_tileMap.TryGetValue(tileId, out var yamlId))
|
||||
return yamlId;
|
||||
|
||||
return AllocateYamlTileId(tileId);
|
||||
}
|
||||
|
||||
private int AllocateYamlTileId(int tileId)
|
||||
{
|
||||
while (!_yamlTileIds.Add(_nextYamlTileId))
|
||||
{
|
||||
_nextYamlTileId++;
|
||||
}
|
||||
|
||||
_tileMap[tileId] = _nextYamlTileId;
|
||||
return _nextYamlTileId++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method ensures that the given entities have a yaml ids assigned. If the entities have a
|
||||
/// <see cref="YamlUidComponent"/>, they will attempt to use that id, which exists to prevent large map file diffs
|
||||
/// due to changing yaml ids.
|
||||
/// </summary>
|
||||
public void ReserveYamlIds(HashSet<EntityUid> entities)
|
||||
{
|
||||
List<EntityUid> needIds = new();
|
||||
foreach (var uid in entities)
|
||||
{
|
||||
if (YamlUidMap.ContainsKey(uid))
|
||||
continue;
|
||||
|
||||
if (_yamlQuery.TryGetComponent(uid, out var comp) && comp.Uid > 0 && YamlIds.Add(comp.Uid))
|
||||
{
|
||||
if (Truncated.Contains(uid))
|
||||
{
|
||||
_log.Error(
|
||||
"Including a previously truncated entity within the serialization process? Something probably wrong");
|
||||
}
|
||||
|
||||
YamlUidMap.Add(uid, comp.Uid);
|
||||
}
|
||||
else
|
||||
{
|
||||
needIds.Add(uid);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var uid in needIds)
|
||||
{
|
||||
AllocateYamlUid(uid);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method ensures that the given entity has a yaml id assigned to it. If the entity has a
|
||||
/// <see cref="YamlUidComponent"/>, it will attempt to use that id, which exists to prevent large map file diffs due
|
||||
/// to changing yaml ids.
|
||||
/// </summary>
|
||||
public void ReserveYamlId(EntityUid uid)
|
||||
{
|
||||
if (YamlUidMap.ContainsKey(uid))
|
||||
return;
|
||||
|
||||
if (_yamlQuery.TryGetComponent(uid, out var comp) && comp.Uid > 0 && YamlIds.Add(comp.Uid))
|
||||
{
|
||||
if (Truncated.Contains(uid))
|
||||
{
|
||||
_log.Error(
|
||||
"Including a previously truncated entity within the serialization process? Something probably wrong");
|
||||
}
|
||||
|
||||
YamlUidMap.Add(uid, comp.Uid);
|
||||
}
|
||||
else
|
||||
AllocateYamlUid(uid);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ITypeSerializer
|
||||
|
||||
ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(
|
||||
ISerializationManager serializationManager,
|
||||
ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
ISerializationContext? context)
|
||||
{
|
||||
if (node.Value == "invalid")
|
||||
return new ValidatedValueNode(node);
|
||||
|
||||
if (!int.TryParse(node.Value, out _))
|
||||
return new ErrorNode(node, "Invalid EntityUid");
|
||||
|
||||
return new ValidatedValueNode(node);
|
||||
}
|
||||
|
||||
public DataNode Write(
|
||||
ISerializationManager serializationManager,
|
||||
EntityUid value,
|
||||
IDependencyCollection dependencies,
|
||||
bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
if (YamlUidMap.TryGetValue(value, out var yamlId))
|
||||
return new ValueDataNode(yamlId.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
if (CurrentComponent == _xformName)
|
||||
{
|
||||
if (value == EntityUid.Invalid)
|
||||
return new ValueDataNode("invalid");
|
||||
|
||||
DebugTools.Assert(!Orphans.Contains(CurrentEntityYamlUid));
|
||||
Orphans.Add(CurrentEntityYamlUid);
|
||||
|
||||
if (Options.ErrorOnOrphan && CurrentEntity != null && value != Truncate)
|
||||
_log.Error($"Serializing entity {EntMan.ToPrettyString(CurrentEntity)} without including its parent {EntMan.ToPrettyString(value)}");
|
||||
|
||||
return new ValueDataNode("invalid");
|
||||
}
|
||||
|
||||
if (value == EntityUid.Invalid)
|
||||
{
|
||||
if (Options.MissingEntityBehaviour != MissingEntityBehaviour.Ignore)
|
||||
_log.Error($"Encountered an invalid entityUid reference.");
|
||||
|
||||
return new ValueDataNode("invalid");
|
||||
}
|
||||
|
||||
if (value == Truncate)
|
||||
{
|
||||
_log.Error(
|
||||
$"{EntMan.ToPrettyString(CurrentEntity)}:{CurrentComponent} is attempting to serialize references to a truncated entity {EntMan.ToPrettyString(Truncate)}.");
|
||||
}
|
||||
|
||||
switch (Options.MissingEntityBehaviour)
|
||||
{
|
||||
case MissingEntityBehaviour.Error:
|
||||
_log.Error(EntMan.Deleted(value)
|
||||
? $"Encountered a reference to a deleted entity {value} while serializing {EntMan.ToPrettyString(CurrentEntity)}."
|
||||
: $"Encountered a reference to a missing entity: {value} while serializing {EntMan.ToPrettyString(CurrentEntity)}.");
|
||||
return new ValueDataNode("invalid");
|
||||
case MissingEntityBehaviour.Ignore:
|
||||
return new ValueDataNode("invalid");
|
||||
case MissingEntityBehaviour.IncludeNullspace:
|
||||
if (!EntMan.TryGetComponent(value, out TransformComponent? xform)
|
||||
|| xform.ParentUid != EntityUid.Invalid
|
||||
|| _gridQuery.HasComp(value)
|
||||
|| _mapQuery.HasComp(value))
|
||||
{
|
||||
goto case MissingEntityBehaviour.Error;
|
||||
}
|
||||
goto case MissingEntityBehaviour.AutoInclude;
|
||||
case MissingEntityBehaviour.PartialInclude:
|
||||
case MissingEntityBehaviour.AutoInclude:
|
||||
if (Options.LogAutoInclude is {} level)
|
||||
_log.Log(level, $"Auto-including entity {EntMan.ToPrettyString(value)} referenced by {EntMan.ToPrettyString(CurrentEntity)}");
|
||||
_autoInclude.Add(value);
|
||||
var id = GetYamlUid(value);
|
||||
return new ValueDataNode(id.ToString(CultureInfo.InvariantCulture));
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
EntityUid ITypeReader<EntityUid, ValueDataNode>.Read(
|
||||
ISerializationManager serializationManager,
|
||||
ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context,
|
||||
ISerializationManager.InstantiationDelegate<EntityUid>? _)
|
||||
{
|
||||
return node.Value == "invalid" ? EntityUid.Invalid : EntityUid.Parse(node.Value);
|
||||
}
|
||||
|
||||
public ValidationNode Validate(
|
||||
ISerializationManager serializationManager,
|
||||
ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
if (node.Value == "invalid")
|
||||
return new ValidatedValueNode(node);
|
||||
|
||||
if (!int.TryParse(node.Value, out _))
|
||||
return new ErrorNode(node, "Invalid NetEntity");
|
||||
|
||||
return new ValidatedValueNode(node);
|
||||
}
|
||||
|
||||
public NetEntity Read(
|
||||
ISerializationManager serializationManager,
|
||||
ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context = null,
|
||||
ISerializationManager.InstantiationDelegate<NetEntity>? instanceProvider = null)
|
||||
{
|
||||
return node.Value == "invalid" ? NetEntity.Invalid : NetEntity.Parse(node.Value);
|
||||
}
|
||||
|
||||
public DataNode Write(
|
||||
ISerializationManager serializationManager,
|
||||
NetEntity value,
|
||||
IDependencyCollection dependencies,
|
||||
bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
var uid = EntMan.GetEntity(value);
|
||||
return serializationManager.WriteValue(uid, alwaysWrite, context);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
71
Robust.Shared/EntitySerialization/LoadResult.cs
Normal file
71
Robust.Shared/EntitySerialization/LoadResult.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Robust.Shared.EntitySerialization;
|
||||
|
||||
/// <summary>
|
||||
/// Class containing information about entities that were loaded from a yaml file.
|
||||
/// </summary>
|
||||
public sealed class LoadResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The file format version.
|
||||
/// </summary>
|
||||
public int Version;
|
||||
|
||||
/// <summary>
|
||||
/// The category of the file that was loaded in.
|
||||
/// This might not match the actual final result. E.g., when loading in a grid file, a map may automatically gets
|
||||
/// generated for it via <see cref="EntityDeserializer.AdoptGrids"/>.
|
||||
/// </summary>
|
||||
public FileCategory Category = FileCategory.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// The engine version that was used to write the file. See <see cref="CVars.BuildEngineVersion"/>.
|
||||
/// </summary>
|
||||
public string? EngineVersion;
|
||||
|
||||
/// <summary>
|
||||
/// The fork that was used to write the file. See <see cref="CVars.BuildForkId"/>.
|
||||
/// </summary>
|
||||
public string? ForkId;
|
||||
|
||||
/// <summary>
|
||||
/// The fork version that was used to write the file. See <see cref="CVars.BuildVersion"/>.
|
||||
/// </summary>
|
||||
public string? ForkVersion;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DateTime.UtcNow"/> when the file was created.
|
||||
/// </summary>
|
||||
public DateTime? Time;
|
||||
|
||||
/// <summary>
|
||||
/// Set of all entities that were created while the file was being loaded.
|
||||
/// </summary>
|
||||
public readonly HashSet<EntityUid> Entities = new();
|
||||
|
||||
/// <summary>
|
||||
/// Set of entities that are not parented to other entities. This will be a combination of <see cref="Maps"/>,
|
||||
/// <see cref="Orphans"/>, and <see cref="NullspaceEntities"/>.
|
||||
/// </summary>
|
||||
public readonly HashSet<EntityUid> RootNodes = new();
|
||||
|
||||
public readonly HashSet<Entity<MapComponent>> Maps = new();
|
||||
|
||||
public readonly HashSet<Entity<MapGridComponent>> Grids = new();
|
||||
|
||||
/// <summary>
|
||||
/// Deserialized entities that need to be assigned a new parent. These differ from "true" null-space entities.
|
||||
/// E,g, saving a grid without saving the map would make the grid an "orphan".
|
||||
/// </summary>
|
||||
public readonly HashSet<EntityUid> Orphans = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of null-space entities. This contains all entities without a parent that don't have a
|
||||
/// <see cref="MapComponent"/>, and were not listed as orphans
|
||||
/// </summary>
|
||||
public readonly HashSet<EntityUid> NullspaceEntities = new();
|
||||
}
|
||||
@@ -14,19 +14,25 @@ using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.Maps;
|
||||
namespace Robust.Shared.EntitySerialization;
|
||||
|
||||
[TypeSerializer]
|
||||
internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingDataNode>, ITypeCopyCreator<MapChunk>
|
||||
{
|
||||
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,
|
||||
IDependencyCollection dependencies, ISerializationContext? context = null)
|
||||
public ValidationNode Validate(
|
||||
ISerializationManager serializationManager,
|
||||
MappingDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public MapChunk Read(ISerializationManager serializationManager, MappingDataNode node,
|
||||
IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null, ISerializationManager.InstantiationDelegate<MapChunk>? instantiationDelegate = null)
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context = null,
|
||||
ISerializationManager.InstantiationDelegate<MapChunk>? instantiationDelegate = null)
|
||||
{
|
||||
var ind = (Vector2i) serializationManager.Read(typeof(Vector2i), node["ind"], hookCtx, context)!;
|
||||
var tileNode = (ValueDataNode)node["tiles"];
|
||||
@@ -50,10 +56,8 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
|
||||
IReadOnlyDictionary<int, string>? tileMap = null;
|
||||
|
||||
if (context is MapSerializationContext serContext)
|
||||
{
|
||||
if (context is EntityDeserializer serContext)
|
||||
tileMap = serContext.TileMap;
|
||||
}
|
||||
|
||||
if (tileMap == null)
|
||||
{
|
||||
@@ -104,16 +108,12 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
|
||||
root.Add("version", new ValueDataNode("6"));
|
||||
|
||||
Dictionary<int, int>? tileWriteMap = null;
|
||||
if (context is MapSerializationContext mapContext)
|
||||
tileWriteMap = mapContext.TileWriteMap;
|
||||
|
||||
gridNode.Value = SerializeTiles(value, tileWriteMap);
|
||||
gridNode.Value = SerializeTiles(value, context as EntitySerializer);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static string SerializeTiles(MapChunk chunk, Dictionary<int, int>? tileWriteMap)
|
||||
private static string SerializeTiles(MapChunk chunk, EntitySerializer? serializer)
|
||||
{
|
||||
// number of bytes written per tile, because sizeof(Tile) is useless.
|
||||
const int structSize = 6;
|
||||
@@ -124,17 +124,34 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
using (var stream = new MemoryStream(barr))
|
||||
using (var writer = new BinaryWriter(stream))
|
||||
{
|
||||
if (serializer == null)
|
||||
{
|
||||
for (ushort y = 0; y < chunk.ChunkSize; y++)
|
||||
{
|
||||
for (ushort x = 0; x < chunk.ChunkSize; x++)
|
||||
{
|
||||
var tile = chunk.GetTile(x, y);
|
||||
writer.Write(tile.TypeId);
|
||||
writer.Write((byte) tile.Flags);
|
||||
writer.Write(tile.Variant);
|
||||
}
|
||||
}
|
||||
return Convert.ToBase64String(barr);
|
||||
}
|
||||
|
||||
var lastTile = -1;
|
||||
var yamlId = -1;
|
||||
for (ushort y = 0; y < chunk.ChunkSize; y++)
|
||||
{
|
||||
for (ushort x = 0; x < chunk.ChunkSize; x++)
|
||||
{
|
||||
var tile = chunk.GetTile(x, y);
|
||||
var typeId = tile.TypeId;
|
||||
if (tileWriteMap != null)
|
||||
typeId = tileWriteMap[typeId];
|
||||
if (tile.TypeId != lastTile)
|
||||
yamlId = serializer.GetYamlTileId(tile.TypeId);
|
||||
|
||||
writer.Write(typeId);
|
||||
writer.Write((byte)tile.Flags);
|
||||
lastTile = tile.TypeId;
|
||||
writer.Write(yamlId);
|
||||
writer.Write((byte) tile.Flags);
|
||||
writer.Write(tile.Variant);
|
||||
}
|
||||
}
|
||||
@@ -143,8 +160,12 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
return Convert.ToBase64String(barr);
|
||||
}
|
||||
|
||||
public MapChunk CreateCopy(ISerializationManager serializationManager, MapChunk source,
|
||||
IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null)
|
||||
public MapChunk CreateCopy(
|
||||
ISerializationManager serializationManager,
|
||||
MapChunk source,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
var mapManager = dependencies.Resolve<IMapManager>();
|
||||
mapManager.SuppressOnTileChanged = true;
|
||||
139
Robust.Shared/EntitySerialization/Options.cs
Normal file
139
Robust.Shared/EntitySerialization/Options.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.EntitySerialization.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Shared.EntitySerialization;
|
||||
|
||||
public record struct SerializationOptions
|
||||
{
|
||||
public static readonly SerializationOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// What to do when serializing the EntityUid of an entity that is not one of entities currently being serialized.
|
||||
/// I.e., What should happen when serializing a map that has entities with components that store references to a
|
||||
/// null-space entity? Note that this does not affect the treatment of <see cref="TransformComponent.ParentUid"/>,
|
||||
/// which will never auto-include parents.
|
||||
/// </summary>
|
||||
public MissingEntityBehaviour MissingEntityBehaviour = MissingEntityBehaviour.IncludeNullspace;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to log an error when serializing an entity without its parent.
|
||||
/// </summary>
|
||||
public bool ErrorOnOrphan = true;
|
||||
|
||||
/// <summary>
|
||||
/// Log level to use when auto-including entities while serializing. Null implies no logs.
|
||||
/// See <see cref="MissingEntityBehaviour"/>.
|
||||
/// </summary>
|
||||
public LogLevel? LogAutoInclude = LogLevel.Info;
|
||||
|
||||
/// <summary>
|
||||
/// If true, the serializer will log an error if it encounters a post map-init entity.
|
||||
/// </summary>
|
||||
public bool ExpectPreInit;
|
||||
|
||||
public FileCategory Category;
|
||||
|
||||
public SerializationOptions()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public record struct DeserializationOptions()
|
||||
{
|
||||
public static readonly DeserializationOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// If true, each loaded entity will get a <see cref="YamlUidComponent"/> that stores the uid that the entity
|
||||
/// had in the yaml file. This is used to maintain consistent entity labelling on subsequent saves.
|
||||
/// </summary>
|
||||
public bool StoreYamlUids = false;
|
||||
|
||||
/// <summary>
|
||||
/// If true, all maps that get created while loading this file will get map-initialized.
|
||||
/// </summary>
|
||||
public bool InitializeMaps = false;
|
||||
|
||||
/// <summary>
|
||||
/// If true, all maps that get created while loading this file will get paused.
|
||||
/// Note that the converse is not true, paused maps will not get un-paused if this is false.
|
||||
/// Pre-mapinit maps are assumed to be paused.
|
||||
/// </summary>
|
||||
public bool PauseMaps = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to log an error when starting up a grid entity that has no map.
|
||||
/// This usually indicates that someone is attempting to load an incorrect file type (e.g. loading a grid as a map).
|
||||
/// </summary>
|
||||
public bool LogOrphanedGrids = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to log an error when encountering an yaml entity id.
|
||||
/// <see cref="TransformComponent.ParentUid"/> is exempt from this.
|
||||
/// </summary>
|
||||
public bool LogInvalidEntities = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to automatically assign map ids to any deserialized map entities.
|
||||
/// If false, maps need to be manually given ids before entities are initialized.
|
||||
/// </summary>
|
||||
public bool AssignMapids = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Superset of <see cref="EntitySerialization.DeserializationOptions"/> that contain information relevant to loading
|
||||
/// maps & grids, potentially onto other existing maps.
|
||||
/// </summary>
|
||||
public struct MapLoadOptions()
|
||||
{
|
||||
public static readonly MapLoadOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// If specified, all orphaned entities and the children of all loaded maps will be re-parented onto this map.
|
||||
/// I.e., this will merge map contents onto an existing map. This will also cause any maps that get loaded to
|
||||
/// delete themselves after their children have been moved.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this option effectively causes <see cref="DeserializationOptions.InitializeMaps"/> and
|
||||
/// <see cref="DeserializationOptions.PauseMaps"/> to have no effect, as the target map is not a map that was
|
||||
/// created by the deserialization.
|
||||
/// </remarks>
|
||||
public MapId? MergeMap = null;
|
||||
|
||||
/// <summary>
|
||||
/// Offset to apply to the position of any loaded entities that are directly parented to a map.
|
||||
/// </summary>
|
||||
public Vector2 Offset;
|
||||
|
||||
/// <summary>
|
||||
/// Rotation to apply to the position & local rotation of any loaded entities that are directly parented to a map.
|
||||
/// </summary>
|
||||
public Angle Rotation;
|
||||
|
||||
/// <summary>
|
||||
/// Options to use when deserializing entities.
|
||||
/// </summary>
|
||||
public DeserializationOptions DeserializationOptions = DeserializationOptions.Default;
|
||||
|
||||
/// <summary>
|
||||
/// When loading a single map, this will attempt to force the map to use the given map id. Generally, it is better
|
||||
/// to allow the map system to auto-allocate a map id, to avoid accidentally re-using an old id.
|
||||
/// </summary>
|
||||
public MapId? ForceMapId;
|
||||
|
||||
/// <summary>
|
||||
/// The expected <see cref="LoadResult.Category"/> for the file currently being read in, at the end of the entity
|
||||
/// creation step. Will log errors if the category doesn't match the expected one (e.g., trying to load a "map" from a file
|
||||
/// that doesn't contain any map entities).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that the effective final category may change by the time the file has fully loaded. E.g., when loading a
|
||||
/// file containing an orphaned grid, a map may be automatically created for the grid, but the category will still
|
||||
/// be <see cref="FileCategory.Grid"/>
|
||||
/// </remarks>
|
||||
public FileCategory? ExpectedCategory;
|
||||
}
|
||||
88
Robust.Shared/EntitySerialization/SerializationEnums.cs
Normal file
88
Robust.Shared/EntitySerialization/SerializationEnums.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Upload;
|
||||
|
||||
namespace Robust.Shared.EntitySerialization;
|
||||
|
||||
/// <summary>
|
||||
/// This enum is used to indicate the type of entity data that was written to a file. The actual format of the file does
|
||||
/// not change, but it helps avoid mistakes like accidentally using a map file when trying to load a single grid.
|
||||
/// </summary>
|
||||
public enum FileCategory : byte
|
||||
{
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// File should contain a single orphaned entity, its children, and maybe some null-space entities.
|
||||
/// </summary>
|
||||
Entity,
|
||||
|
||||
/// <summary>
|
||||
/// File should contain a single grid, its children, and maybe some null-space entities.
|
||||
/// </summary>
|
||||
Grid,
|
||||
|
||||
/// <summary>
|
||||
/// File should contain a single map, its children, and maybe some null-space entities.
|
||||
/// </summary>
|
||||
Map,
|
||||
|
||||
/// <summary>
|
||||
/// File is a full game save, and will likely contain at least one map and a few null-space entities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The file might also contain additional yaml entries for things like prototypes uploaded via
|
||||
/// <see cref="IGamePrototypeLoadManager"/>, and might contain references to additional resources that need to be
|
||||
/// loaded (e.g., files uploaded using <see cref="SharedNetworkResourceManager"/>).
|
||||
/// </remarks>
|
||||
Save,
|
||||
}
|
||||
|
||||
public enum MissingEntityBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Log an error and replace the reference with <see cref="EntityUid.Invalid"/>
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// Ignore the reference, replace it with <see cref="EntityUid.Invalid"/>
|
||||
/// </summary>
|
||||
Ignore,
|
||||
|
||||
/// <summary>
|
||||
/// Automatically include & serialize any referenced null-space entities and their children.
|
||||
/// I.e., entities that are not attached to any parent and are not maps. Any non-nullspace entities will result in
|
||||
/// an error.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is primarily intended to make it easy to auto-include information carrying null-space entities. E.g., the
|
||||
/// "minds" of players, or entities that represent power or gas networks on a grid. Note that a full game save
|
||||
/// should still try to explicitly include all relevant entities, as this could still easily fail to auto-include
|
||||
/// relevant entities if they are not explicitly referenced in a data-field by some other entity.
|
||||
/// </remarks>
|
||||
IncludeNullspace,
|
||||
|
||||
/// <summary>
|
||||
/// Automatically include & serialize any referenced entity. Note that this means that the missing entity's
|
||||
/// parents will (generally) also be included, however this will not include other children. E.g., if serializing a
|
||||
/// grid that references an entity on the map, this will also cause the map to get serialized, but will not necessarily
|
||||
/// serialize everything on the map.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If trying to serialize an entity without its parent (i.e., its parent is truncated via
|
||||
/// <see cref="EntitySerializer.Truncate"/>), this will try to respect that. E.g., if a referenced entity is on the
|
||||
/// same map as a grid that is getting serialized, it should include the entity without including the map.
|
||||
/// </remarks>
|
||||
/// <remarks>
|
||||
/// Note that this might unexpectedly change the <see cref="FileCategory"/>. I.e., trying to serialize a grid might
|
||||
/// accidentally lead to serializing a (partial?) map file.
|
||||
/// </remarks>
|
||||
PartialInclude,
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PartialInclude"/> that will also automatically include the children of any entities that
|
||||
/// that are automatically included. Note that because auto-inclusion generally needs to include an entity's
|
||||
/// parents, this will include more than just the missing entity's direct children.
|
||||
/// </summary>
|
||||
AutoInclude,
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Map.Events;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Utility;
|
||||
using Vector2 = System.Numerics.Vector2;
|
||||
|
||||
namespace Robust.Shared.EntitySerialization.Systems;
|
||||
|
||||
// This partial class file contains methods for loading generic entities and grids. Map specific methods are in another
|
||||
// file
|
||||
public sealed partial class MapLoaderSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to load entities from a yaml file. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
|
||||
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
|
||||
/// </summary>
|
||||
public bool TryLoadGeneric(
|
||||
ResPath file,
|
||||
[NotNullWhen(true)] out HashSet<Entity<MapComponent>>? maps,
|
||||
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
|
||||
MapLoadOptions? options = null)
|
||||
{
|
||||
grids = null;
|
||||
maps = null;
|
||||
if (!TryLoadGeneric(file, out var data, options))
|
||||
return false;
|
||||
|
||||
maps = data.Maps;
|
||||
grids = data.Grids;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load entities from a yaml file. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
|
||||
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
|
||||
/// </summary>
|
||||
/// <param name="file">The file to load.</param>
|
||||
/// <param name="result">Data class containing information about the loaded entities</param>
|
||||
/// <param name="options">Optional Options for configuring loading behaviour.</param>
|
||||
public bool TryLoadGeneric(ResPath file, [NotNullWhen(true)] out LoadResult? result, MapLoadOptions? options = null)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (!TryReadFile(file, out var data))
|
||||
return false;
|
||||
|
||||
_stopwatch.Restart();
|
||||
var ev = new BeforeEntityReadEvent();
|
||||
RaiseLocalEvent(ev);
|
||||
|
||||
var opts = options ?? MapLoadOptions.Default;
|
||||
|
||||
// If we are forcing a map id, we cannot auto-assign ids.
|
||||
opts.DeserializationOptions.AssignMapids = opts.ForceMapId == null;
|
||||
|
||||
if (opts.MergeMap is { } targetId && !_mapSystem.MapExists(targetId))
|
||||
throw new Exception($"Target map {targetId} does not exist");
|
||||
|
||||
if (opts.MergeMap != null && opts.ForceMapId != null)
|
||||
throw new Exception($"Invalid combination of MapLoadOptions");
|
||||
|
||||
if (_mapSystem.MapExists(opts.ForceMapId))
|
||||
throw new Exception($"Target map already exists");
|
||||
|
||||
// Using a local deserializer instead of a cached value, both to ensure that we don't accidentally carry over
|
||||
// data from a previous serializations, and because some entities cause other maps/grids to be loaded during
|
||||
// during mapinit.
|
||||
var deserializer = new EntityDeserializer(
|
||||
_dependency,
|
||||
data,
|
||||
opts.DeserializationOptions,
|
||||
ev.RenamedPrototypes,
|
||||
ev.DeletedPrototypes);
|
||||
|
||||
if (!deserializer.TryProcessData())
|
||||
{
|
||||
Log.Debug($"Failed to process entity data in {file}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
deserializer.CreateEntities();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Caught exception while creating entities for map {file}: {e}");
|
||||
Delete(deserializer.Result);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (opts.ExpectedCategory is { } exp && exp != deserializer.Result.Category)
|
||||
{
|
||||
// Did someone try to load a map file as a grid or vice versa?
|
||||
Log.Error($"Map {file} does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
|
||||
Delete(deserializer.Result);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reparent entities if loading entities onto an existing map.
|
||||
var merged = new HashSet<EntityUid>();
|
||||
MergeMaps(deserializer, opts, merged);
|
||||
|
||||
if (!SetMapId(deserializer, opts))
|
||||
return false;
|
||||
|
||||
// Apply any offsets & rotations specified by the load options
|
||||
ApplyTransform(deserializer, opts);
|
||||
|
||||
try
|
||||
{
|
||||
deserializer.StartEntities();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Caught exception while starting entities: {e}");
|
||||
Delete(deserializer.Result);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (opts.MergeMap is {} map)
|
||||
MapInitalizeMerged(merged, map);
|
||||
|
||||
result = deserializer.Result;
|
||||
Log.Debug($"Loaded map in {_stopwatch.Elapsed}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load a regular (non-map, non-grid) entity from a file.
|
||||
/// The loaded entity will initially be in null-space.
|
||||
/// If the file does not contain exactly one orphaned entity, this will return false and delete loaded entities.
|
||||
/// </summary>
|
||||
public bool TryLoadEntity(
|
||||
ResPath path,
|
||||
[NotNullWhen(true)] out Entity<TransformComponent>? entity,
|
||||
DeserializationOptions? options = null)
|
||||
{
|
||||
var opts = new MapLoadOptions
|
||||
{
|
||||
DeserializationOptions = options ?? DeserializationOptions.Default,
|
||||
ExpectedCategory = FileCategory.Entity
|
||||
};
|
||||
|
||||
entity = null;
|
||||
if (!TryLoadGeneric(path, out var result, opts))
|
||||
return false;
|
||||
|
||||
if (result.Orphans.Count == 1)
|
||||
{
|
||||
var uid = result.Orphans.Single();
|
||||
entity = (uid, Transform(uid));
|
||||
return true;
|
||||
}
|
||||
|
||||
Delete(result);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load a grid entity from a file and parent it to the given map.
|
||||
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
|
||||
/// </summary>
|
||||
public bool TryLoadGrid(
|
||||
MapId map,
|
||||
ResPath path,
|
||||
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
|
||||
DeserializationOptions? options = null,
|
||||
Vector2 offset = default,
|
||||
Angle rot = default)
|
||||
{
|
||||
var opts = new MapLoadOptions
|
||||
{
|
||||
MergeMap = map,
|
||||
Offset = offset,
|
||||
Rotation = rot,
|
||||
DeserializationOptions = options ?? DeserializationOptions.Default,
|
||||
ExpectedCategory = FileCategory.Grid
|
||||
};
|
||||
|
||||
grid = null;
|
||||
if (!TryLoadGeneric(path, out var result, opts))
|
||||
return false;
|
||||
|
||||
if (result.Grids.Count == 1)
|
||||
{
|
||||
grid = result.Grids.Single();
|
||||
return true;
|
||||
}
|
||||
|
||||
Delete(result);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load a grid entity from a file and parent it to a newly created map.
|
||||
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
|
||||
/// </summary>
|
||||
public bool TryLoadGrid(
|
||||
ResPath path,
|
||||
[NotNullWhen(true)] out Entity<MapComponent>? map,
|
||||
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
|
||||
DeserializationOptions? options = null,
|
||||
Vector2 offset = default,
|
||||
Angle rot = default)
|
||||
{
|
||||
var opts = options ?? DeserializationOptions.Default;
|
||||
|
||||
var mapUid = _mapSystem.CreateMap(out var mapId, runMapInit: opts.InitializeMaps);
|
||||
if (opts.PauseMaps)
|
||||
_mapSystem.SetPaused(mapUid, true);
|
||||
|
||||
if (!TryLoadGrid(mapId, path, out grid, options, offset, rot))
|
||||
{
|
||||
Del(mapUid);
|
||||
map = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
map = new(mapUid, Comp<MapComponent>(mapUid));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ApplyTransform(EntityDeserializer deserializer, MapLoadOptions opts)
|
||||
{
|
||||
if (opts.Rotation == Angle.Zero && opts.Offset == Vector2.Zero)
|
||||
return;
|
||||
|
||||
// If merging onto a single map, the transformation was already applied by SwapRootNode()
|
||||
if (opts.MergeMap != null)
|
||||
return;
|
||||
|
||||
var matrix = Matrix3Helpers.CreateTransform(opts.Offset, opts.Rotation);
|
||||
|
||||
// We want to apply the transforms to all children of any loaded maps. However, we can't just iterate over the
|
||||
// children of loaded maps, as transform component has not yet been initialized. I.e. xform.Children is empty.
|
||||
// Hence we iterate over all entities and check which ones are attached to maps.
|
||||
foreach (var uid in deserializer.Result.Entities)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
|
||||
if (!_mapQuery.HasComp(xform.ParentUid))
|
||||
continue;
|
||||
|
||||
// The original comment around this bit of logic was just:
|
||||
// > Smelly
|
||||
// I don't know what sloth meant by that, but I guess applying transforms to grid-maps is a no-no?
|
||||
// Or more generally, loading a mapgrid onto another (potentially non-mapgrid) map is just generally kind of weird.
|
||||
if (_gridQuery.HasComponent(xform.ParentUid))
|
||||
continue;
|
||||
|
||||
var rot = xform.LocalRotation + opts.Rotation;
|
||||
var pos = Vector2.Transform(xform.LocalPosition, matrix);
|
||||
_xform.SetLocalPositionRotation(uid, pos, rot, xform);
|
||||
DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Vector2 = System.Numerics.Vector2;
|
||||
|
||||
namespace Robust.Shared.EntitySerialization.Systems;
|
||||
|
||||
// This partial class file contains methods specific to loading maps
|
||||
public sealed partial class MapLoaderSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to load a file containing a single map.
|
||||
/// If the file does not contain exactly one map, this will return false and delete all loaded entities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this will not automatically initialize the map, unless specified via the <see cref="DeserializationOptions"/>.
|
||||
/// </remarks>
|
||||
public bool TryLoadMap(
|
||||
ResPath path,
|
||||
[NotNullWhen(true)] out Entity<MapComponent>? map,
|
||||
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
|
||||
DeserializationOptions? options = null,
|
||||
Vector2 offset = default,
|
||||
Angle rot = default)
|
||||
{
|
||||
var opts = new MapLoadOptions
|
||||
{
|
||||
Offset = offset,
|
||||
Rotation = rot,
|
||||
DeserializationOptions = options ?? DeserializationOptions.Default,
|
||||
ExpectedCategory = FileCategory.Map
|
||||
};
|
||||
|
||||
map = null;
|
||||
grids = null;
|
||||
if (!TryLoadGeneric(path, out var result, opts))
|
||||
return false;
|
||||
|
||||
if (result.Maps.Count == 1)
|
||||
{
|
||||
map = result.Maps.First();
|
||||
grids = result.Grids;
|
||||
return true;
|
||||
}
|
||||
|
||||
Delete(result);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to load a file containing a single map, assign it the given map id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If possible, it is better to use <see cref="TryLoadMap"/> which automatically assigns a <see cref="MapId"/>.
|
||||
/// </remarks>
|
||||
/// <remarks>
|
||||
/// Note that this will not automatically initialize the map, unless specified via the <see cref="DeserializationOptions"/>.
|
||||
/// </remarks>
|
||||
public bool TryLoadMapWithId(
|
||||
MapId mapId,
|
||||
ResPath path,
|
||||
[NotNullWhen(true)] out Entity<MapComponent>? map,
|
||||
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
|
||||
DeserializationOptions? options = null,
|
||||
Vector2 offset = default,
|
||||
Angle rot = default)
|
||||
{
|
||||
map = null;
|
||||
grids = null;
|
||||
|
||||
var opts = new MapLoadOptions
|
||||
{
|
||||
Offset = offset,
|
||||
Rotation = rot,
|
||||
DeserializationOptions = options ?? DeserializationOptions.Default,
|
||||
ExpectedCategory = FileCategory.Map
|
||||
};
|
||||
|
||||
if (_mapSystem.MapExists(mapId))
|
||||
throw new Exception($"Target map already exists");
|
||||
|
||||
opts.ForceMapId = mapId;
|
||||
if (!TryLoadGeneric(path, out var result, opts))
|
||||
return false;
|
||||
|
||||
if (!_mapSystem.TryGetMap(mapId, out var uid) || !TryComp(uid, out MapComponent? comp))
|
||||
return false;
|
||||
|
||||
map = new(uid.Value, comp);
|
||||
grids = result.Grids;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to load a file containing a single map, and merge its children onto another map. After which the
|
||||
/// loaded map gets deleted.
|
||||
/// </summary>
|
||||
public bool TryMergeMap(
|
||||
MapId mapId,
|
||||
ResPath path,
|
||||
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
|
||||
DeserializationOptions? options = null,
|
||||
Vector2 offset = default,
|
||||
Angle rot = default)
|
||||
{
|
||||
grids = null;
|
||||
|
||||
var opts = new MapLoadOptions
|
||||
{
|
||||
Offset = offset,
|
||||
Rotation = rot,
|
||||
DeserializationOptions = options ?? DeserializationOptions.Default,
|
||||
ExpectedCategory = FileCategory.Map
|
||||
};
|
||||
|
||||
if (!_mapSystem.MapExists(mapId))
|
||||
throw new Exception($"Target map {mapId} does not exist");
|
||||
|
||||
opts.MergeMap = mapId;
|
||||
if (!TryLoadGeneric(path, out var result, opts))
|
||||
return false;
|
||||
|
||||
if (!_mapSystem.TryGetMap(mapId, out var uid) || !TryComp(uid, out MapComponent? comp))
|
||||
return false;
|
||||
|
||||
grids = result.Grids;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void MergeMaps(EntityDeserializer deserializer, MapLoadOptions opts, HashSet<EntityUid> merged)
|
||||
{
|
||||
if (opts.MergeMap is not {} targetId)
|
||||
return;
|
||||
|
||||
if (!_mapSystem.TryGetMap(targetId, out var targetUid))
|
||||
throw new Exception($"Target map {targetId} does not exist");
|
||||
|
||||
deserializer.Result.Category = FileCategory.Unknown;
|
||||
var rotation = opts.Rotation;
|
||||
var matrix = Matrix3Helpers.CreateTransform(opts.Offset, rotation);
|
||||
var target = new Entity<TransformComponent>(targetUid.Value, Transform(targetUid.Value));
|
||||
|
||||
// We want to apply the transforms to all children of any loaded maps. However, we can't just iterate over the
|
||||
// children of loaded maps, as transform component has not yet been initialized. I.e. xform.Children is empty.
|
||||
// Hence we iterate over all entities and check which ones are attached to maps.
|
||||
HashSet<EntityUid> maps = new();
|
||||
HashSet<EntityUid> logged = new();
|
||||
foreach (var uid in deserializer.Result.Entities)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
if (!_mapQuery.HasComp(xform.ParentUid))
|
||||
continue;
|
||||
|
||||
if (_gridQuery.HasComponent(xform.ParentUid) && logged.Add(xform.ParentUid))
|
||||
{
|
||||
Log.Error($"Merging a grid-map onto another map is not supported.");
|
||||
continue;
|
||||
}
|
||||
|
||||
maps.Add(xform.ParentUid);
|
||||
Merge(merged, uid, target, matrix, rotation);
|
||||
}
|
||||
|
||||
deserializer.ToDelete.UnionWith(maps);
|
||||
deserializer.Result.Maps.RemoveWhere(x => maps.Contains(x.Owner));
|
||||
|
||||
foreach (var uid in deserializer.Result.Orphans)
|
||||
{
|
||||
Merge(merged, uid, target, matrix, rotation);
|
||||
}
|
||||
|
||||
deserializer.Result.Orphans.Clear();
|
||||
}
|
||||
|
||||
private void Merge(
|
||||
HashSet<EntityUid> merged,
|
||||
EntityUid uid,
|
||||
Entity<TransformComponent> target,
|
||||
in Matrix3x2 matrix,
|
||||
Angle rotation)
|
||||
{
|
||||
merged.Add(uid);
|
||||
var xform = Transform(uid);
|
||||
var angle = xform.LocalRotation + rotation;
|
||||
var pos = Vector2.Transform(xform.LocalPosition, matrix);
|
||||
var coords = new EntityCoordinates(target.Owner, pos);
|
||||
_xform.SetCoordinates((uid, xform, MetaData(uid)), coords, rotation: angle, newParent: target.Comp);
|
||||
}
|
||||
|
||||
private void MapInitalizeMerged(HashSet<EntityUid> merged, MapId targetId)
|
||||
{
|
||||
// fuck me I hate this map merging bullshit.
|
||||
// loading a map "onto" another map shouldn't need to be supported by the generic map loading methods.
|
||||
// If something needs to do that, it should implement it itself.
|
||||
// AFAIK this only exists for the loadgamemap command?
|
||||
|
||||
if (!_mapSystem.TryGetMap(targetId, out var targetUid))
|
||||
throw new Exception($"Target map {targetId} does not exist");
|
||||
|
||||
if (_mapSystem.IsInitialized(targetUid.Value))
|
||||
{
|
||||
foreach (var uid in merged)
|
||||
{
|
||||
_mapSystem.RecursiveMapInit(uid);
|
||||
}
|
||||
}
|
||||
|
||||
var paused = _mapSystem.IsPaused(targetUid.Value);
|
||||
foreach (var uid in merged)
|
||||
{
|
||||
_mapSystem.RecursiveSetPaused(uid, paused);
|
||||
}
|
||||
}
|
||||
|
||||
private bool SetMapId(EntityDeserializer deserializer, MapLoadOptions opts)
|
||||
{
|
||||
if (opts.ForceMapId is not { } id)
|
||||
return true;
|
||||
|
||||
if (deserializer.Result.Maps.Count != 1)
|
||||
{
|
||||
Log.Error(
|
||||
$"The {nameof(MapLoadOptions.ForceMapId)} option is only supported when loading a file containing a single map.");
|
||||
Delete(deserializer.Result);
|
||||
return false;
|
||||
}
|
||||
|
||||
var map = deserializer.Result.Maps.Single();
|
||||
_mapSystem.AssignMapId(map, id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Events;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.EntitySerialization.Systems;
|
||||
|
||||
// This partial class file contains methods for serializing and saving entities, grids, and maps.
|
||||
public sealed partial class MapLoaderSystem
|
||||
{
|
||||
/// <inheritdoc cref="EntitySerializer.OnIsSerializeable"/>
|
||||
public event EntitySerializer.IsSerializableDelegate? OnIsSerializable;
|
||||
|
||||
/// <summary>
|
||||
/// Recursively serialize the given entity and its children.
|
||||
/// </summary>
|
||||
public (MappingDataNode Node, FileCategory Category) SerializeEntitiesRecursive(
|
||||
HashSet<EntityUid> entities,
|
||||
SerializationOptions? options = null)
|
||||
{
|
||||
_stopwatch.Restart();
|
||||
if (!entities.All(Exists))
|
||||
throw new Exception($"Cannot serialize deleted entities");
|
||||
|
||||
Log.Info($"Serializing entities: {string.Join(", ", entities.Select(x => ToPrettyString(x).ToString()))}");
|
||||
|
||||
var maps = entities.Select(x => Transform(x).MapID).ToHashSet();
|
||||
var ev = new BeforeSerializationEvent(entities, maps);
|
||||
RaiseLocalEvent(ev);
|
||||
|
||||
// In case no options were provided, we assume that if all of the starting entities are pre-init, we should
|
||||
// expect that **all** entities that get serialized should be pre-init.
|
||||
var opts = options ?? SerializationOptions.Default with
|
||||
{
|
||||
ExpectPreInit = (entities.All(x => LifeStage(x) < EntityLifeStage.MapInitialized))
|
||||
};
|
||||
|
||||
var serializer = new EntitySerializer(_dependency, opts);
|
||||
serializer.OnIsSerializeable += OnIsSerializable;
|
||||
|
||||
foreach (var ent in entities)
|
||||
{
|
||||
serializer.SerializeEntityRecursive(ent);
|
||||
}
|
||||
|
||||
var data = serializer.Write();
|
||||
var cat = serializer.GetCategory();
|
||||
|
||||
var ev2 = new AfterSerializationEvent(entities, data, cat);
|
||||
RaiseLocalEvent(ev2);
|
||||
|
||||
Log.Debug($"Serialized {serializer.EntityData.Count} entities in {_stopwatch.Elapsed}");
|
||||
return (data, cat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize a standard (non-grid, non-map) entity and all of its children and write the result to a
|
||||
/// yaml file.
|
||||
/// </summary>
|
||||
public bool TrySaveEntity(EntityUid entity, ResPath path, SerializationOptions? options = null)
|
||||
{
|
||||
if (_mapQuery.HasComp(entity))
|
||||
{
|
||||
Log.Error($"{ToPrettyString(entity)} is a map. Use {nameof(TrySaveMap)}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_gridQuery.HasComp(entity))
|
||||
{
|
||||
Log.Error($"{ToPrettyString(entity)} is a grid. Use {nameof(TrySaveGrid)}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var opts = options ?? SerializationOptions.Default;
|
||||
opts.Category = FileCategory.Entity;
|
||||
|
||||
MappingDataNode data;
|
||||
FileCategory cat;
|
||||
try
|
||||
{
|
||||
(data, cat) = SerializeEntitiesRecursive([entity], opts);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Caught exception while trying to serialize entity {ToPrettyString(entity)}:\n{e}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cat != FileCategory.Entity)
|
||||
{
|
||||
Log.Error($"Failed to save {ToPrettyString(entity)} as a singular entity. Output: {cat}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Write(path, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize a map and all of its children and write the result to a yaml file.
|
||||
/// </summary>
|
||||
public bool TrySaveMap(MapId mapId, ResPath path, SerializationOptions? options = null)
|
||||
{
|
||||
if (_mapSystem.TryGetMap(mapId, out var mapUid))
|
||||
return TrySaveMap(mapUid.Value, path, options);
|
||||
|
||||
Log.Error($"Unable to find map {mapId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize a map and all of its children and write the result to a yaml file.
|
||||
/// </summary>
|
||||
public bool TrySaveMap(EntityUid map, ResPath path, SerializationOptions? options = null)
|
||||
{
|
||||
if (!_mapQuery.HasComp(map))
|
||||
{
|
||||
Log.Error($"{ToPrettyString(map)} is not a map.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var opts = options ?? SerializationOptions.Default;
|
||||
opts.Category = FileCategory.Map;
|
||||
|
||||
MappingDataNode data;
|
||||
FileCategory cat;
|
||||
try
|
||||
{
|
||||
(data, cat) = SerializeEntitiesRecursive([map], opts);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Caught exception while trying to serialize map {ToPrettyString(map)}:\n{e}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cat != FileCategory.Map)
|
||||
{
|
||||
Log.Error($"Failed to save {ToPrettyString(map)} as a map. Output: {cat}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Write(path, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize a grid and all of its children and write the result to a yaml file.
|
||||
/// </summary>
|
||||
public bool TrySaveGrid(EntityUid grid, ResPath path, SerializationOptions? options = null)
|
||||
{
|
||||
if (!_gridQuery.HasComp(grid))
|
||||
{
|
||||
Log.Error($"{ToPrettyString(grid)} is not a grid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_mapQuery.HasComp(grid))
|
||||
{
|
||||
Log.Error($"{ToPrettyString(grid)} is a map, not (just) a grid. Use {nameof(TrySaveMap)}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var opts = options ?? SerializationOptions.Default;
|
||||
opts.Category = FileCategory.Grid;
|
||||
|
||||
MappingDataNode data;
|
||||
FileCategory cat;
|
||||
try
|
||||
{
|
||||
(data, cat) = SerializeEntitiesRecursive([grid], opts);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Caught exception while trying to serialize grid {ToPrettyString(grid)}:\n{e}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cat != FileCategory.Grid)
|
||||
{
|
||||
Log.Error($"Failed to save {ToPrettyString(grid)} as a grid. Output: {cat}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Write(path, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize an entities and all of their children to a yaml file.
|
||||
/// This makes no assumptions about the expected entity or resulting file category.
|
||||
/// If possible, use the map/grid specific variants instead.
|
||||
/// </summary>
|
||||
public bool TrySaveGeneric(
|
||||
EntityUid uid,
|
||||
ResPath path,
|
||||
out FileCategory category,
|
||||
SerializationOptions? options = null)
|
||||
{
|
||||
return TrySaveGeneric([uid], path, out category, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize one or more entities and all of their children to a yaml file.
|
||||
/// This makes no assumptions about the expected entity or resulting file category.
|
||||
/// If possible, use the map/grid specific variants instead.
|
||||
/// </summary>
|
||||
public bool TrySaveGeneric(
|
||||
HashSet<EntityUid> entities,
|
||||
ResPath path,
|
||||
out FileCategory category,
|
||||
SerializationOptions? options = null)
|
||||
{
|
||||
category = FileCategory.Unknown;
|
||||
if (entities.Count == 0)
|
||||
return false;
|
||||
|
||||
var opts = options ?? SerializationOptions.Default;
|
||||
|
||||
MappingDataNode data;
|
||||
try
|
||||
{
|
||||
(data, category) = SerializeEntitiesRecursive(entities, opts);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Caught exception while trying to serialize entities:\n{e}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Write(path, data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
132
Robust.Shared/EntitySerialization/Systems/MapLoaderSystem.cs
Normal file
132
Robust.Shared/EntitySerialization/Systems/MapLoaderSystem.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Robust.Shared.EntitySerialization.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// This class provides methods for saving and loading maps and grids.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The save & load methods are basically wrappers around <see cref="EntitySerializer"/> and
|
||||
/// <see cref="EntityDeserializer"/>, which can be used for more control over serialization.
|
||||
/// </remarks>
|
||||
public sealed partial class MapLoaderSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xform = default!;
|
||||
[Dependency] private readonly IDependencyCollection _dependency = default!;
|
||||
|
||||
private Stopwatch _stopwatch = new();
|
||||
|
||||
private EntityQuery<MapComponent> _mapQuery;
|
||||
private EntityQuery<MapGridComponent> _gridQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_gridQuery = GetEntityQuery<MapGridComponent>();
|
||||
_mapQuery = GetEntityQuery<MapComponent>();
|
||||
_gridQuery = GetEntityQuery<MapGridComponent>();
|
||||
}
|
||||
|
||||
private void Write(ResPath path, MappingDataNode data)
|
||||
{
|
||||
Log.Info($"Saving serialized results to {path}");
|
||||
path = path.ToRootedPath();
|
||||
var document = new YamlDocument(data.ToYaml());
|
||||
using var writer = _resourceManager.UserData.OpenWriteText(path);
|
||||
{
|
||||
var stream = new YamlStream {document};
|
||||
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReadFile(ResPath file, [NotNullWhen(true)] out MappingDataNode? data)
|
||||
{
|
||||
var resPath = file.ToRootedPath();
|
||||
data = null;
|
||||
|
||||
if (!TryGetReader(resPath, out var reader))
|
||||
return false;
|
||||
|
||||
Log.Info($"Loading file: {resPath}");
|
||||
_stopwatch.Restart();
|
||||
|
||||
using var textReader = reader;
|
||||
var documents = DataNodeParser.ParseYamlStream(reader).ToArray();
|
||||
Log.Debug($"Loaded yml stream in {_stopwatch.Elapsed}");
|
||||
|
||||
// Yes, logging errors in a "try" method is kinda shit, but it was throwing exceptions when I found it and it does
|
||||
// make sense to at least provide some kind of feedback for why it failed.
|
||||
switch (documents.Length)
|
||||
{
|
||||
case < 1:
|
||||
Log.Error("Stream has no YAML documents.");
|
||||
return false;
|
||||
case > 1:
|
||||
Log.Error("Stream too many YAML documents. Map files store exactly one.");
|
||||
return false;
|
||||
default:
|
||||
data = (MappingDataNode) documents[0].Root;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetReader(ResPath resPath, [NotNullWhen(true)] out TextReader? reader)
|
||||
{
|
||||
if (_resourceManager.UserData.Exists(resPath))
|
||||
{
|
||||
// Log warning if file exists in both user and content data.
|
||||
if (_resourceManager.ContentFileExists(resPath))
|
||||
Log.Warning("Reading map user data instead of content");
|
||||
|
||||
reader = _resourceManager.UserData.OpenText(resPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_resourceManager.TryContentFileRead(resPath, out var contentReader))
|
||||
{
|
||||
reader = new StreamReader(contentReader);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.Error($"File not found: {resPath}");
|
||||
reader = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for deleting all loaded entities.
|
||||
/// </summary>
|
||||
public void Delete(LoadResult result)
|
||||
{
|
||||
foreach (var uid in result.Maps)
|
||||
{
|
||||
Del(uid);
|
||||
}
|
||||
|
||||
foreach (var uid in result.Orphans)
|
||||
{
|
||||
Del(uid);
|
||||
}
|
||||
|
||||
foreach (var uid in result.Entities)
|
||||
{
|
||||
Del(uid);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls PVS visibility of entities. THIS COMPONENT CONTROLS WHETHER ENTITIES ARE NETWORKED TO PLAYERS
|
||||
/// AND SHOULD NOT BE USED AS THE SOLE WAY TO HIDE AN ENTITY FROM A PLAYER.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(VisibilitySystem))]
|
||||
[Access(typeof(SharedVisibilitySystem))]
|
||||
public sealed partial class VisibilityComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
@@ -1075,6 +1075,97 @@ namespace Robust.Shared.GameObjects
|
||||
return TryGetComponent(uid.Value, netId, out component, meta);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryCopyComponent<T>(EntityUid source, EntityUid target, ref T? sourceComponent, [NotNullWhen(true)] out T? targetComp, MetaDataComponent? meta = null) where T : IComponent
|
||||
{
|
||||
if (!MetaQuery.Resolve(target, ref meta))
|
||||
{
|
||||
targetComp = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceComponent == null && !TryGetComponent(source, out sourceComponent))
|
||||
{
|
||||
targetComp = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
targetComp = CopyComponentInternal(source, target, sourceComponent, meta);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryCopyComponents(
|
||||
EntityUid source,
|
||||
EntityUid target,
|
||||
MetaDataComponent? meta = null,
|
||||
params Type[] sourceComponents)
|
||||
{
|
||||
if (!MetaQuery.TryGetComponent(target, out meta))
|
||||
return false;
|
||||
|
||||
var allCopied = true;
|
||||
|
||||
foreach (var type in sourceComponents)
|
||||
{
|
||||
if (!TryGetComponent(source, type, out var srcComp))
|
||||
{
|
||||
allCopied = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
CopyComponent(source, target, srcComp, meta: meta);
|
||||
}
|
||||
|
||||
return allCopied;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IComponent CopyComponent(EntityUid source, EntityUid target, IComponent sourceComponent, MetaDataComponent? meta = null)
|
||||
{
|
||||
if (!MetaQuery.Resolve(target, ref meta))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
return CopyComponentInternal(source, target, sourceComponent, meta);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T CopyComponent<T>(EntityUid source, EntityUid target, T sourceComponent,MetaDataComponent? meta = null) where T : IComponent
|
||||
{
|
||||
if (!MetaQuery.Resolve(target, ref meta))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
return CopyComponentInternal(source, target, sourceComponent, meta);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CopyComponents(EntityUid source, EntityUid target, MetaDataComponent? meta = null, params IComponent[] sourceComponents)
|
||||
{
|
||||
if (!MetaQuery.Resolve(target, ref meta))
|
||||
return;
|
||||
|
||||
foreach (var comp in sourceComponents)
|
||||
{
|
||||
CopyComponentInternal(source, target, comp, meta);
|
||||
}
|
||||
}
|
||||
|
||||
private T CopyComponentInternal<T>(EntityUid source, EntityUid target, T sourceComponent, MetaDataComponent meta) where T : IComponent
|
||||
{
|
||||
var compReg = ComponentFactory.GetRegistration(sourceComponent.GetType());
|
||||
var component = (T)ComponentFactory.GetComponent(compReg);
|
||||
|
||||
_serManager.CopyTo(sourceComponent, ref component, notNullableOverride: true);
|
||||
component.Owner = target;
|
||||
|
||||
AddComponentInternal(target, component, compReg, true, false, meta);
|
||||
return component;
|
||||
}
|
||||
|
||||
public EntityQuery<TComp1> GetEntityQuery<TComp1>() where TComp1 : IComponent
|
||||
{
|
||||
var comps = _entTraitArray[CompIdx.ArrayIndex<TComp1>()];
|
||||
|
||||
@@ -189,10 +189,12 @@ namespace Robust.Shared.GameObjects
|
||||
return false;
|
||||
|
||||
var compType = component.GetType();
|
||||
var compName = _componentFactory.GetComponentName(compType);
|
||||
if (compName == _xformName || compName == _metaReg.Name)
|
||||
|
||||
if (compType == typeof(TransformComponent) || compType == typeof(MetaDataComponent))
|
||||
continue;
|
||||
|
||||
var compName = _componentFactory.GetComponentName(compType);
|
||||
|
||||
// If the component isn't on the prototype then it's custom.
|
||||
if (!protoData.TryGetValue(compName, out var protoMapping))
|
||||
return false;
|
||||
@@ -208,9 +210,7 @@ namespace Robust.Shared.GameObjects
|
||||
return false;
|
||||
}
|
||||
|
||||
var diff = compMapping.Except(protoMapping);
|
||||
|
||||
if (diff != null && diff.Children.Count != 0)
|
||||
if (compMapping.AnyExcept(protoMapping))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -311,11 +311,16 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual EntityUid CreateEntityUninitialized(string? prototypeName, ComponentRegistry? overrides = null)
|
||||
public EntityUid CreateEntityUninitialized(string? prototypeName, ComponentRegistry? overrides = null)
|
||||
{
|
||||
return CreateEntity(prototypeName, out _, overrides);
|
||||
}
|
||||
|
||||
public EntityUid CreateEntityUninitialized(string? prototypeName, out MetaDataComponent meta, ComponentRegistry? overrides = null)
|
||||
{
|
||||
return CreateEntity(prototypeName, out meta, overrides);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual EntityUid CreateEntityUninitialized(string? prototypeName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
|
||||
{
|
||||
@@ -548,7 +553,28 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
TransformComponent? parentXform = null;
|
||||
if (xform.ParentUid.IsValid())
|
||||
TransformQuery.Resolve(xform.ParentUid, ref parentXform);
|
||||
{
|
||||
if (xform.LifeStage < ComponentLifeStage.Initialized)
|
||||
{
|
||||
// Entity is being deleted before initialization ever finished.
|
||||
// The entity will not yet have been added to the parent's transform component.
|
||||
// This is seemingly pretty error prone ATM, and I'm not even sure if it should be supported?
|
||||
|
||||
// Just in case it HAS somehow been added, make sure we remove it.
|
||||
if (TransformQuery.TryComp(xform.ParentUid, out parentXform) && parentXform._children.Remove(e))
|
||||
DebugTools.Assert($"Child entity {ToPrettyString(e)} was added to the parent's child set prior to being initialized?");
|
||||
|
||||
parentXform = null;
|
||||
xform._parent = EntityUid.Invalid;
|
||||
xform._anchored = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use resolve for automatic error logging.
|
||||
// ReSharper disable once ReturnValueOfPureMethodIsNotUsed
|
||||
TransformQuery.Resolve(xform.ParentUid, ref parentXform);
|
||||
}
|
||||
}
|
||||
|
||||
// Then actually delete them
|
||||
RecursiveDeleteEntity(e, meta, xform, parentXform);
|
||||
@@ -783,7 +809,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// <summary>
|
||||
/// Allocates an entity and stores it but does not load components or do initialization.
|
||||
/// </summary>
|
||||
private protected EntityUid AllocEntity(
|
||||
protected internal EntityUid AllocEntity(
|
||||
EntityPrototype? prototype,
|
||||
out MetaDataComponent metadata)
|
||||
{
|
||||
@@ -793,6 +819,9 @@ namespace Robust.Shared.GameObjects
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AllocEntity(Robust.Shared.Prototypes.EntityPrototype?,out Robust.Shared.GameObjects.MetaDataComponent)"/>
|
||||
internal EntityUid AllocEntity(EntityPrototype? prototype) => AllocEntity(prototype, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Allocates an entity and stores it but does not load components or do initialization.
|
||||
/// </summary>
|
||||
@@ -872,21 +901,9 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private protected void LoadEntity(EntityUid entity, IEntityLoadContext? context)
|
||||
{
|
||||
EntityPrototype.LoadEntity((entity, MetaQuery.GetComponent(entity)), ComponentFactory, this, _serManager, context);
|
||||
}
|
||||
|
||||
private protected void LoadEntity(EntityUid entity, IEntityLoadContext? context, EntityPrototype? prototype)
|
||||
{
|
||||
var meta = MetaQuery.GetComponent(entity);
|
||||
DebugTools.Assert(meta.EntityPrototype == prototype);
|
||||
EntityPrototype.LoadEntity((entity, meta), ComponentFactory, this, _serManager, context);
|
||||
}
|
||||
|
||||
public void InitializeAndStartEntity(EntityUid entity, MapId? mapId = null)
|
||||
{
|
||||
var doMapInit = _mapManager.IsMapInitialized(mapId ?? TransformQuery.GetComponent(entity).MapID);
|
||||
var doMapInit = _mapSystem.IsInitialized(mapId ?? TransformQuery.GetComponent(entity).MapID);
|
||||
InitializeAndStartEntity(entity, doMapInit);
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ public partial class EntitySystem
|
||||
protected void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
|
||||
where T : IComponentDelta
|
||||
{
|
||||
EntityManager.DirtyFields(uid, comp, meta);
|
||||
EntityManager.DirtyFields(uid, comp, meta, fields);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -565,6 +565,52 @@ public partial class EntitySystem
|
||||
|
||||
#endregion
|
||||
|
||||
#region Component Copy
|
||||
|
||||
/// <inheritdoc cref="IEntityManager.TryCopyComponent"/>
|
||||
protected bool TryCopyComponent<T>(
|
||||
EntityUid source,
|
||||
EntityUid target,
|
||||
ref T? sourceComponent,
|
||||
[NotNullWhen(true)] out T? targetComp,
|
||||
MetaDataComponent? meta = null) where T : IComponent
|
||||
{
|
||||
return EntityManager.TryCopyComponent(source, target, ref sourceComponent, out targetComp, meta);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEntityManager.TryCopyComponents"/>
|
||||
protected bool TryCopyComponents(
|
||||
EntityUid source,
|
||||
EntityUid target,
|
||||
MetaDataComponent? meta = null,
|
||||
params Type[] sourceComponents)
|
||||
{
|
||||
return EntityManager.TryCopyComponents(source, target, meta, sourceComponents);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEntityManager.CopyComponent"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected IComponent CopyComp(EntityUid source, EntityUid target, IComponent sourceComponent, MetaDataComponent? meta = null)
|
||||
{
|
||||
return EntityManager.CopyComponent(source, target, sourceComponent, meta);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEntityManager.CopyComponent{T}"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected T CopyComp<T>(EntityUid source, EntityUid target, T sourceComponent, MetaDataComponent? meta = null) where T : IComponent
|
||||
{
|
||||
return EntityManager.CopyComponent(source, target, sourceComponent, meta);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEntityManager.CopyComponents"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected void CopyComps(EntityUid source, EntityUid target, MetaDataComponent? meta = null, params IComponent[] sourceComponents)
|
||||
{
|
||||
EntityManager.CopyComponents(source, target, meta, sourceComponents);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Component Has
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -22,11 +22,11 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
public sealed class EntitySystemManager : IEntitySystemManager, IPostInjectInit
|
||||
{
|
||||
[IoC.Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[IoC.Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[IoC.Dependency] private readonly ProfManager _profManager = default!;
|
||||
[IoC.Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
[IoC.Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly ProfManager _profManager = default!;
|
||||
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
@@ -35,6 +35,18 @@ namespace Robust.Shared.GameObjects
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
internal DependencyCollection SystemDependencyCollection = default!;
|
||||
|
||||
public IDependencyCollection DependencyCollection
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_initialized)
|
||||
return SystemDependencyCollection;
|
||||
|
||||
throw new InvalidOperationException($"{nameof(EntitySystemManager)} has not been initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<Type> _systemTypes = new();
|
||||
|
||||
private static readonly Histogram _tickUsageHistogram = Metrics.CreateHistogram("robust_entity_systems_update_usage",
|
||||
|
||||
@@ -358,6 +358,51 @@ namespace Robust.Shared.GameObjects
|
||||
/// <returns>If the component existed in the entity.</returns>
|
||||
bool TryGetComponent([NotNullWhen(true)] EntityUid? uid, ushort netId, [NotNullWhen(true)] out IComponent? component, MetaDataComponent? meta = null);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to run <see cref="CopyComponents"/> without throwing if the component doesn't exist.
|
||||
/// </summary>
|
||||
bool TryCopyComponent<T>(
|
||||
EntityUid source,
|
||||
EntityUid target,
|
||||
ref T? sourceComponent,
|
||||
[NotNullWhen(true)] out T? targetComp,
|
||||
MetaDataComponent? meta = null) where T : IComponent;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to run <see cref="CopyComponents"/> without throwing if the components don't exist.
|
||||
/// </summary>
|
||||
bool TryCopyComponents(EntityUid source, EntityUid target, MetaDataComponent? meta = null, params Type[] sourceComponents);
|
||||
|
||||
/// <summary>
|
||||
/// Copy a single component from source to target entity.
|
||||
/// </summary>
|
||||
/// <param name="source">The source entity to copy from.</param>
|
||||
/// <param name="target">The target entity to copy to.</param>
|
||||
/// <param name="sourceComponent">The source component instance to copy.</param>
|
||||
/// <param name="component">The copied component if successful.</param>
|
||||
/// <param name="meta">Optional metadata of the target entity.</param>
|
||||
IComponent CopyComponent(EntityUid source, EntityUid target, IComponent sourceComponent, MetaDataComponent? meta = null);
|
||||
|
||||
/// <summary>
|
||||
/// Copy a single component from source to target entity.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of component to copy.</typeparam>
|
||||
/// <param name="source">The source entity to copy from.</param>
|
||||
/// <param name="target">The target entity to copy to.</param>
|
||||
/// <param name="sourceComponent">The source component instance to copy.</param>
|
||||
/// <param name="component">The copied component if successful.</param>
|
||||
/// <param name="meta">Optional metadata of the target entity.</param>
|
||||
T CopyComponent<T>(EntityUid source, EntityUid target, T sourceComponent, MetaDataComponent? meta = null) where T : IComponent;
|
||||
|
||||
/// <summary>
|
||||
/// Copy multiple components from source to target entity using existing component instances.
|
||||
/// </summary>
|
||||
/// <param name="source">The source entity to copy from.</param>
|
||||
/// <param name="target">The target entity to copy to.</param>
|
||||
/// <param name="meta">Optional metadata of the target entity.</param>
|
||||
/// <param name="sourceComponents">Array of component instances to copy.</param>
|
||||
void CopyComponents(EntityUid source, EntityUid target, MetaDataComponent? meta = null, params IComponent[] sourceComponents);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a cached struct enumerator with the specified component.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.IoC.Exceptions;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
@@ -131,5 +132,10 @@ namespace Robust.Shared.GameObjects
|
||||
IEnumerable<Type> GetEntitySystemTypes();
|
||||
bool TryGetEntitySystem(Type sysType, [NotNullWhen(true)] out object? system);
|
||||
object GetEntitySystem(Type sysType);
|
||||
|
||||
/// <summary>
|
||||
/// Dependency collection that contains all the loaded systems.
|
||||
/// </summary>
|
||||
public IDependencyCollection DependencyCollection { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -12,7 +13,7 @@ namespace Robust.Shared.GameObjects;
|
||||
/// <summary>
|
||||
/// Network identifier for entities; used by client and server to refer to the same entity where their local <see cref="EntityUid"/> may differ.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[Serializable, NetSerializable, CopyByRef]
|
||||
public readonly struct NetEntity : IEquatable<NetEntity>, IComparable<NetEntity>, ISpanFormattable
|
||||
{
|
||||
public readonly int Id;
|
||||
|
||||
@@ -125,7 +125,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
|
||||
SubscribeLocalEvent<BroadphaseComponent, ComponentAdd>(OnBroadphaseAdd);
|
||||
SubscribeLocalEvent<BroadphaseComponent, ComponentInit>(OnBroadphaseInit);
|
||||
SubscribeLocalEvent<GridAddEvent>(OnGridAdd);
|
||||
SubscribeLocalEvent<MapChangedEvent>(OnMapChange);
|
||||
SubscribeLocalEvent<MapCreatedEvent>(OnMapChange);
|
||||
|
||||
_transform.OnBeforeMoveEvent += OnMove;
|
||||
EntityManager.EntityInitialized += OnEntityInit;
|
||||
@@ -194,9 +194,9 @@ public sealed partial class EntityLookupSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMapChange(MapChangedEvent ev)
|
||||
private void OnMapChange(MapCreatedEvent ev)
|
||||
{
|
||||
if (ev.Created && ev.Map != MapId.Nullspace)
|
||||
if (ev.MapId != MapId.Nullspace)
|
||||
{
|
||||
EnsureComp<BroadphaseComponent>(ev.Uid);
|
||||
}
|
||||
@@ -490,7 +490,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
|
||||
{
|
||||
var bounds = fixture.Shape.ComputeAABB(broadphaseTransform, i);
|
||||
var proxy = fixture.Proxies[i];
|
||||
tree.MoveProxy(proxy.ProxyId, bounds, Vector2.Zero);
|
||||
tree.MoveProxy(proxy.ProxyId, bounds);
|
||||
proxy.AABB = bounds;
|
||||
moveBuffer[proxy] = fixture.Shape.ComputeAABB(mapTransform, i);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,9 @@ internal sealed class PrototypeReloadSystem : EntitySystem
|
||||
{
|
||||
var data = newPrototype.Components[name];
|
||||
var component = _componentFactory.GetComponent(name);
|
||||
EntityManager.AddComponent(entity, component);
|
||||
|
||||
if (!EntityManager.HasComponent(entity, component.GetType()))
|
||||
EntityManager.AddComponent(entity, component);
|
||||
}
|
||||
|
||||
// Update entity metadata
|
||||
|
||||
@@ -162,7 +162,7 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
if (TryComp<GridTreeComponent>(xform.MapUid, out var gridTree))
|
||||
{
|
||||
gridTree.Tree.MoveProxy(component.MapProxy, in aabb, Vector2.Zero);
|
||||
gridTree.Tree.MoveProxy(component.MapProxy, in aabb);
|
||||
}
|
||||
|
||||
if (TryComp<MovedGridsComponent>(xform.MapUid, out var movedGrids))
|
||||
@@ -187,7 +187,7 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
if (TryComp<GridTreeComponent>(xform.MapUid, out var gridTree))
|
||||
{
|
||||
gridTree.Tree.MoveProxy(component.MapProxy, in aabb, Vector2.Zero);
|
||||
gridTree.Tree.MoveProxy(component.MapProxy, in aabb);
|
||||
}
|
||||
|
||||
if (TryComp<MovedGridsComponent>(xform.MapUid, out var movedGrids))
|
||||
@@ -309,6 +309,7 @@ public abstract partial class SharedMapSystem
|
||||
ChunkDatum data)
|
||||
{
|
||||
var counter = 0;
|
||||
var gridEnt = new Entity<MapGridComponent>(uid, component);
|
||||
|
||||
if (data.IsDeleted())
|
||||
{
|
||||
@@ -326,10 +327,12 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
var gridIndices = deletedChunk.ChunkTileToGridTile((x, y));
|
||||
var newTileRef = new TileRef(uid, gridIndices, Tile.Empty);
|
||||
_mapInternal.RaiseOnTileChanged(newTileRef, oldTile, index);
|
||||
_mapInternal.RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index);
|
||||
}
|
||||
}
|
||||
|
||||
deletedChunk.CachedBounds = Box2i.Empty;
|
||||
deletedChunk.SuppressCollisionRegeneration = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -350,10 +353,12 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
var gridIndices = chunk.ChunkTileToGridTile((x, y));
|
||||
var newTileRef = new TileRef(uid, gridIndices, tile);
|
||||
_mapInternal.RaiseOnTileChanged(newTileRef, oldTile, index);
|
||||
_mapInternal.RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index);
|
||||
}
|
||||
}
|
||||
|
||||
DebugTools.Assert(chunk.Fixtures.SetEquals(data.Fixtures));
|
||||
|
||||
// These should never refer to the same object
|
||||
DebugTools.AssertNotEqual(chunk.Fixtures, data.Fixtures);
|
||||
|
||||
@@ -508,7 +513,7 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
if (TryComp<GridTreeComponent>(xform.MapUid, out var gridTree))
|
||||
{
|
||||
var proxy = gridTree.Tree.CreateProxy(in aabb, (uid, _fixturesQuery.Comp(uid), component));
|
||||
var proxy = gridTree.Tree.CreateProxy(in aabb, uint.MaxValue, (uid, _fixturesQuery.Comp(uid), component));
|
||||
DebugTools.Assert(component.MapProxy == DynamicTree.Proxy.Free);
|
||||
component.MapProxy = proxy;
|
||||
}
|
||||
@@ -562,7 +567,7 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
if (TryComp<GridTreeComponent>(xform.MapUid, out var gridTree))
|
||||
{
|
||||
var proxy = gridTree.Tree.CreateProxy(in aabb, (uid, _fixturesQuery.Comp(uid), grid));
|
||||
var proxy = gridTree.Tree.CreateProxy(in aabb, uint.MaxValue, (uid, _fixturesQuery.Comp(uid), grid));
|
||||
DebugTools.Assert(grid.MapProxy == DynamicTree.Proxy.Free);
|
||||
grid.MapProxy = proxy;
|
||||
}
|
||||
@@ -1614,7 +1619,7 @@ public abstract partial class SharedMapSystem
|
||||
if (!MapManager.SuppressOnTileChanged)
|
||||
{
|
||||
var newTileRef = new TileRef(uid, gridTile, newTile);
|
||||
_mapInternal.RaiseOnTileChanged(newTileRef, oldTile, mapChunk.Indices);
|
||||
_mapInternal.RaiseOnTileChanged((uid, grid), newTileRef, oldTile, mapChunk.Indices);
|
||||
}
|
||||
|
||||
if (shapeChanged && !mapChunk.SuppressCollisionRegeneration)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
@@ -70,14 +71,15 @@ public abstract partial class SharedMapSystem
|
||||
if (component.MapId == MapId.Nullspace)
|
||||
{
|
||||
if (state.MapId == MapId.Nullspace)
|
||||
throw new Exception($"Received invalid map state? {ToPrettyString(uid)}");
|
||||
throw new Exception($"Received invalid map state for {ToPrettyString(uid)}");
|
||||
|
||||
component.MapId = state.MapId;
|
||||
Maps.Add(component.MapId, uid);
|
||||
AssignMapId((uid, component), state.MapId);
|
||||
RecursiveMapIdUpdate(uid, uid, component.MapId);
|
||||
}
|
||||
|
||||
DebugTools.AssertEqual(component.MapId, state.MapId);
|
||||
if (component.MapId != state.MapId)
|
||||
throw new Exception($"Received invalid map state for {ToPrettyString(uid)}");
|
||||
|
||||
component.LightingEnabled = state.LightingEnabled;
|
||||
component.MapInitialized = state.Initialized;
|
||||
|
||||
@@ -119,26 +121,75 @@ public abstract partial class SharedMapSystem
|
||||
EnsureComp<MovedGridsComponent>(uid);
|
||||
}
|
||||
|
||||
private void OnCompInit(EntityUid uid, MapComponent component, ComponentInit args)
|
||||
internal void AssignMapId(Entity<MapComponent> map, MapId? id = null)
|
||||
{
|
||||
if (component.MapId == MapId.Nullspace)
|
||||
component.MapId = GetNextMapId();
|
||||
|
||||
DebugTools.AssertEqual(component.MapId.IsClientSide, IsClientSide(uid));
|
||||
if (!Maps.TryAdd(component.MapId, uid))
|
||||
if (map.Comp.MapId != MapId.Nullspace)
|
||||
{
|
||||
if (Maps[component.MapId] != uid)
|
||||
throw new Exception($"Attempted to initialize a map {ToPrettyString(uid)} with a duplicate map id {component.MapId}");
|
||||
if (id != null && map.Comp.MapId != id)
|
||||
{
|
||||
QueueDel(map.Owner);
|
||||
throw new Exception($"Map entity {ToPrettyString(map.Owner)} has already been assigned an id");
|
||||
}
|
||||
|
||||
if (!Maps.TryGetValue(map.Comp.MapId, out var existing) || existing != map.Owner)
|
||||
{
|
||||
QueueDel(map.Owner);
|
||||
throw new Exception($"Map entity {ToPrettyString(map.Owner)} was improperly assigned a map id?");
|
||||
}
|
||||
|
||||
DebugTools.Assert(UsedIds.Contains(map.Comp.MapId));
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = new MapChangedEvent(uid, component.MapId, true);
|
||||
RaiseLocalEvent(uid, msg, true);
|
||||
map.Comp.MapId = id ?? GetNextMapId();
|
||||
|
||||
if (IsClientSide(map) != map.Comp.MapId.IsClientSide)
|
||||
throw new Exception($"Attempting to assign a client-side map id to a networked entity or vice-versa");
|
||||
|
||||
if (!UsedIds.Add(map.Comp.MapId))
|
||||
Log.Warning($"Re-using a previously used map id ({map.Comp.MapId}) for map entity {ToPrettyString(map)}");
|
||||
|
||||
if (Maps.TryAdd(map.Comp.MapId, map.Owner))
|
||||
return;
|
||||
|
||||
if (Maps[map.Comp.MapId] == map.Owner)
|
||||
return;
|
||||
|
||||
QueueDel(map);
|
||||
throw new Exception(
|
||||
$"Attempted to assign an existing mapId {map.Comp} to a map entity {ToPrettyString(map.Owner)}");
|
||||
}
|
||||
|
||||
private void OnCompInit(Entity<MapComponent> map, ref ComponentInit args)
|
||||
{
|
||||
AssignMapId(map);
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var msg = new MapChangedEvent(map, map.Comp.MapId, true);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
RaiseLocalEvent(map, msg, true);
|
||||
var ev = new MapCreatedEvent(map, map.Comp.MapId);
|
||||
RaiseLocalEvent(map, ev, true);
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, MapComponent component, MapInitEvent args)
|
||||
{
|
||||
DebugTools.Assert(!component.MapInitialized);
|
||||
component.MapInitialized = true;
|
||||
Dirty(uid, component);
|
||||
}
|
||||
|
||||
private void OnCompStartup(EntityUid uid, MapComponent component, ComponentStartup args)
|
||||
{
|
||||
if (component.MapPaused)
|
||||
RecursiveSetPaused(uid, true);
|
||||
// un-initialized maps are always paused.
|
||||
component.MapPaused |= !component.MapInitialized;
|
||||
|
||||
if (!component.MapPaused)
|
||||
return;
|
||||
|
||||
// Recursively pause all entities on the map
|
||||
component.MapPaused = false;
|
||||
SetPaused(uid, true);
|
||||
}
|
||||
|
||||
private void OnMapRemoved(EntityUid uid, MapComponent component, ComponentShutdown args)
|
||||
@@ -146,8 +197,13 @@ public abstract partial class SharedMapSystem
|
||||
DebugTools.Assert(component.MapId != MapId.Nullspace);
|
||||
Maps.Remove(component.MapId);
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var msg = new MapChangedEvent(uid, component.MapId, false);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
RaiseLocalEvent(uid, msg, true);
|
||||
|
||||
var ev = new MapRemovedEvent(uid, component.MapId);
|
||||
RaiseLocalEvent(uid, ev, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -181,19 +237,17 @@ public abstract partial class SharedMapSystem
|
||||
if (_netManager.IsClient && _netManager.IsConnected && !mapId.IsClientSide)
|
||||
throw new ArgumentException($"Attempted to create a client-side map entity with a non client-side map ID?");
|
||||
|
||||
var uid = EntityManager.CreateEntityUninitialized(null);
|
||||
var map = _factory.GetComponent<MapComponent>();
|
||||
map.MapId = mapId;
|
||||
AddComp(uid, map);
|
||||
if (UsedIds.Contains(mapId))
|
||||
Log.Warning($"Re-using MapId: {mapId}");
|
||||
|
||||
// Give the entity a name, mainly for debugging. Content can always override this with a localized name.
|
||||
var meta = MetaData(uid);
|
||||
_meta.SetEntityName(uid, $"Map Entity", meta);
|
||||
var (uid, map, meta) = CreateUninitializedMap();
|
||||
DebugTools.AssertEqual(map.MapId, MapId.Nullspace);
|
||||
AssignMapId((uid, map), mapId);
|
||||
|
||||
// Initialize components. this should add the map id to the collections.
|
||||
EntityManager.InitializeComponents(uid, meta);
|
||||
EntityManager.StartComponents(uid);
|
||||
DebugTools.Assert(Maps[mapId] == uid);
|
||||
EntityManager.InitializeEntity(uid, meta);
|
||||
EntityManager.StartEntity(uid);
|
||||
DebugTools.AssertEqual(Maps[mapId], uid);
|
||||
|
||||
if (runMapInit)
|
||||
InitializeMap((uid, map));
|
||||
@@ -202,4 +256,22 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
return uid;
|
||||
}
|
||||
|
||||
public Entity<MapComponent, MetaDataComponent> CreateUninitializedMap()
|
||||
{
|
||||
var uid = EntityManager.CreateEntityUninitialized(null, out var meta);
|
||||
_meta.SetEntityName(uid, $"Map Entity", meta);
|
||||
return (uid, AddComp<MapComponent>(uid), meta);
|
||||
}
|
||||
|
||||
public void DeleteMap(MapId mapId)
|
||||
{
|
||||
if (TryGetMap(mapId, out var uid))
|
||||
Del(uid);
|
||||
}
|
||||
|
||||
public IEnumerable<MapId> GetAllMapIds()
|
||||
{
|
||||
return Maps.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,6 @@ public abstract partial class SharedMapSystem
|
||||
return map.Comp.MapInitialized;
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, MapComponent component, MapInitEvent args)
|
||||
{
|
||||
DebugTools.Assert(!component.MapInitialized);
|
||||
component.MapInitialized = true;
|
||||
EntityManager.Dirty(uid, component);
|
||||
}
|
||||
|
||||
public void InitializeMap(MapId mapId, bool unpause = true)
|
||||
{
|
||||
if(!Maps.TryGetValue(mapId, out var uid))
|
||||
@@ -63,7 +56,7 @@ public abstract partial class SharedMapSystem
|
||||
SetPaused(map, false);
|
||||
}
|
||||
|
||||
private void RecursiveMapInit(EntityUid entity)
|
||||
internal void RecursiveMapInit(EntityUid entity)
|
||||
{
|
||||
var toInitialize = new List<EntityUid> {entity};
|
||||
for (var i = 0; i < toInitialize.Count; i++)
|
||||
|
||||
@@ -22,7 +22,7 @@ public abstract partial class SharedMapSystem
|
||||
if (!_mapQuery.Resolve(map, ref map.Comp))
|
||||
return false;
|
||||
|
||||
return map.Comp.MapPaused;
|
||||
return map.Comp.MapPaused || !map.Comp.MapInitialized;
|
||||
}
|
||||
|
||||
public void SetPaused(MapId mapId, bool paused)
|
||||
@@ -49,7 +49,7 @@ public abstract partial class SharedMapSystem
|
||||
RecursiveSetPaused(map, paused);
|
||||
}
|
||||
|
||||
private void RecursiveSetPaused(EntityUid entity, bool paused)
|
||||
internal void RecursiveSetPaused(EntityUid entity, bool paused)
|
||||
{
|
||||
_meta.SetEntityPaused(entity, paused);
|
||||
foreach (var child in Transform(entity)._children)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user