mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a604e4a04 | ||
|
|
6218ef6e3f | ||
|
|
76b46479b6 | ||
|
|
de9a8d286a | ||
|
|
a1df0fb4af | ||
|
|
e6bc5a1057 | ||
|
|
11b24579a2 | ||
|
|
685d002bb7 | ||
|
|
2e0d18aeaf | ||
|
|
06dbff0429 | ||
|
|
15958a9447 | ||
|
|
fd5a4d9b8a | ||
|
|
6d958847cb | ||
|
|
8a04a4f3a5 | ||
|
|
7104a4f459 | ||
|
|
f29949a32c | ||
|
|
3bbbabf238 | ||
|
|
d95aca3d9e | ||
|
|
e14537074e | ||
|
|
af2d01981f | ||
|
|
7df23e047c | ||
|
|
5c7ab43049 | ||
|
|
8f75560ec4 | ||
|
|
b323c8bd1e | ||
|
|
faef44daaa | ||
|
|
fbc706f37b |
@@ -57,7 +57,7 @@
|
||||
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
|
||||
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -54,6 +54,96 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 247.2.1
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -6,7 +6,7 @@ using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
public static class Program
|
||||
internal static class Program
|
||||
{
|
||||
// This was supposed to be the main entry for the subprocess program... It doesn't work.
|
||||
public static int Main(string[] args)
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IGameControllerInternal _gameController = default!;
|
||||
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
var cachePath = "";
|
||||
if (_resourceManager.UserData is WritableDirProvider userData)
|
||||
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
|
||||
{
|
||||
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
|
||||
cachePath = Path.Combine(rootDir, "cef_cache", "0");
|
||||
}
|
||||
|
||||
var settings = new CefSettings()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -415,6 +415,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 +443,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 +487,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 +509,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 +523,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 +547,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 +555,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 +567,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 +591,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 +599,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 +639,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 +704,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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ namespace Robust.Client
|
||||
|
||||
_prof.Initialize();
|
||||
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
|
||||
|
||||
var mountOptions = _commandLineArgs != null
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class VisibilitySystem : SharedVisibilitySystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -297,7 +297,7 @@ namespace Robust.Server
|
||||
: null;
|
||||
|
||||
// Set up the VFS
|
||||
_resources.Initialize(dataDir);
|
||||
_resources.Initialize(dataDir, hideUserDataDir: false);
|
||||
|
||||
var mountOptions = _commandLineArgs != null
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;
|
||||
|
||||
@@ -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,7 +43,7 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
_ent.System<MapLoaderSystem>().Save(uid, args[1]);
|
||||
_ent.System<MapLoaderSystem>().TrySaveGrid(uid, new ResPath(args[1]));
|
||||
shell.WriteLine("Save successful. Look in the user data directory.");
|
||||
}
|
||||
|
||||
@@ -63,7 +64,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 +91,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 +113,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 +125,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 +137,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 +153,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 +192,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,7 +207,7 @@ namespace Robust.Server.Console.Commands
|
||||
}
|
||||
|
||||
shell.WriteLine(Loc.GetString("cmd-savemap-attempt", ("mapId", mapId), ("path", args[1])));
|
||||
_system.GetEntitySystem<MapLoaderSystem>().SaveMap(mapId, args[1]);
|
||||
_system.GetEntitySystem<MapLoaderSystem>().TrySaveMap(mapId, new ResPath(args[1]));
|
||||
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
|
||||
}
|
||||
}
|
||||
@@ -211,7 +215,6 @@ namespace Robust.Server.Console.Commands
|
||||
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 +270,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>();
|
||||
|
||||
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() { }
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -395,8 +422,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 +457,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 +466,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 +474,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 +483,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 +493,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 +502,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 +511,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 +519,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 +527,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 +537,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 +548,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 +559,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 +569,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 +579,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 +587,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 +595,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 +632,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 +640,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 +648,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 +658,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 +669,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 +680,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 +693,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
internal string GetPath(ResPath relPath)
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()));
|
||||
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -14,7 +14,11 @@ namespace Robust.Shared.ContentPack
|
||||
/// The directory to use for user data.
|
||||
/// If null, a virtual temporary file system is used instead.
|
||||
/// </param>
|
||||
void Initialize(string? userData);
|
||||
/// <param name="hideUserDataDir">
|
||||
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
|
||||
/// <see cref="IResourceManager.UserData"/>.
|
||||
/// </param>
|
||||
void Initialize(string? userData, bool hideUserDataDir);
|
||||
|
||||
/// <summary>
|
||||
/// Mounts a single stream as a content file. Useful for unit testing.
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
|
||||
{
|
||||
/// <summary>
|
||||
/// The root path of this provider.
|
||||
/// Can be null if it's a virtual provider.
|
||||
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
|
||||
/// </summary>
|
||||
string? RootDir { get; }
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.ContentPack
|
||||
{
|
||||
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
|
||||
!OperatingSystem.IsWindows()
|
||||
&& !OperatingSystem.IsMacOS();
|
||||
|
||||
|
||||
internal static string SafeGetResourcePath(string baseDir, ResPath path)
|
||||
{
|
||||
var relSysPath = path.ToRelativeSystemPath();
|
||||
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
|
||||
{
|
||||
// Hard cap on any exploit smuggling a .. in there.
|
||||
// Since that could allow leaving sandbox.
|
||||
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
|
||||
}
|
||||
|
||||
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
|
||||
// better safe than sorry check
|
||||
if (!retPath.StartsWith(baseDir))
|
||||
{
|
||||
// Allow path to match if it's just missing the directory separator at the end.
|
||||
if (retPath != baseDir.TrimEnd('\\'))
|
||||
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
|
||||
}
|
||||
|
||||
return retPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
|
||||
public IWritableDirProvider UserData { get; private set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Initialize(string? userData)
|
||||
public virtual void Initialize(string? userData, bool hideRootDir)
|
||||
{
|
||||
Sawmill = _logManager.GetSawmill("res");
|
||||
|
||||
if (userData != null)
|
||||
{
|
||||
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
|
||||
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -379,7 +379,13 @@ namespace Robust.Shared.ContentPack
|
||||
{
|
||||
if (root is DirLoader loader)
|
||||
{
|
||||
yield return new ResPath(loader.GetPath(new ResPath(@"/")));
|
||||
var rootDir = loader.GetPath(new ResPath(@"/"));
|
||||
|
||||
// TODO: GET RID OF THIS.
|
||||
// This code shouldn't be passing OS disk paths through ResPath.
|
||||
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
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)"
|
||||
|
||||
@@ -10,17 +10,22 @@ namespace Robust.Shared.ContentPack
|
||||
/// <inheritdoc />
|
||||
internal sealed class WritableDirProvider : IWritableDirProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
private readonly bool _hideRootDir;
|
||||
|
||||
public string RootDir { get; }
|
||||
|
||||
string? IWritableDirProvider.RootDir => _hideRootDir ? null : RootDir;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="WritableDirProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="rootDir">Root file system directory to allow writing.</param>
|
||||
public WritableDirProvider(DirectoryInfo rootDir)
|
||||
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
|
||||
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
|
||||
{
|
||||
// FullName does not have a trailing separator, and we MUST have a separator.
|
||||
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
|
||||
_hideRootDir = hideRootDir;
|
||||
}
|
||||
|
||||
#region File Access
|
||||
@@ -119,7 +124,7 @@ namespace Robust.Shared.ContentPack
|
||||
throw new FileNotFoundException();
|
||||
|
||||
var dirInfo = new DirectoryInfo(GetFullPath(path));
|
||||
return new WritableDirProvider(dirInfo);
|
||||
return new WritableDirProvider(dirInfo, _hideRootDir);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -180,20 +185,7 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
path = path.Clean();
|
||||
|
||||
return GetFullPath(RootDir, path);
|
||||
}
|
||||
|
||||
private static string GetFullPath(string root, ResPath path)
|
||||
{
|
||||
var relPath = path.ToRelativeSystemPath();
|
||||
if (relPath.Contains("\\..") || relPath.Contains("/.."))
|
||||
{
|
||||
// Hard cap on any exploit smuggling a .. in there.
|
||||
// Since that could allow leaving sandbox.
|
||||
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Path.Combine(root, relPath));
|
||||
return PathHelpers.SafeGetResourcePath(RootDir, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,240 @@
|
||||
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: {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($"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;
|
||||
}
|
||||
|
||||
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(source, 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>()];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -32,6 +33,13 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
internal Dictionary<MapId, EntityUid> Maps { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// This hashset is used to try prevent MapId re-use. This is mainly for auto-assigned map ids.
|
||||
/// Loading a map with a specific id (e.g., the various mapping commands) may still result in an id being
|
||||
/// reused.
|
||||
/// </summary>
|
||||
protected HashSet<MapId> UsedIds = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -53,6 +61,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// <summary>
|
||||
/// Arguments for when a map is created or deleted.
|
||||
/// </summary>
|
||||
[Obsolete("Use map creation or deletion events")]
|
||||
public sealed class MapChangedEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Uid;
|
||||
@@ -83,6 +92,16 @@ namespace Robust.Shared.GameObjects
|
||||
public bool Destroyed => !Created;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised whenever a map is created.
|
||||
/// </summary>
|
||||
public readonly record struct MapCreatedEvent(EntityUid Uid, MapId MapId);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised whenever a map is removed.
|
||||
/// </summary>
|
||||
public readonly record struct MapRemovedEvent(EntityUid Uid, MapId MapId);
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public sealed class GridStartupEvent : EntityEventArgs
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.Shared.Map.Components;
|
||||
@@ -214,7 +214,15 @@ public abstract partial class SharedTransformSystem
|
||||
}
|
||||
else if (_mapQuery.TryComp(uid, out var mapComp))
|
||||
{
|
||||
DebugTools.AssertNotEqual(mapComp.MapId, MapId.Nullspace);
|
||||
if (mapComp.MapId == MapId.Nullspace)
|
||||
{
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw new Exception("Transform is initialising before map ids have been assigned?");
|
||||
#endif
|
||||
Log.Error($"Transform is initialising before map ids have been assigned?");
|
||||
_map.AssignMapId((uid, mapComp));
|
||||
}
|
||||
|
||||
xform.MapUid = uid;
|
||||
xform.MapID = mapComp.MapId;
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace Robust.Shared.GameObjects
|
||||
if (xform.GridUid == xform.ParentUid)
|
||||
return (xform.Coordinates, GetWorldRotation(xform, XformQuery));
|
||||
|
||||
DebugTools.Assert(!_mapManager.IsGrid(uid) && !_mapManager.IsMap(uid));
|
||||
DebugTools.Assert(!HasComp<MapComponent>(uid) && !HasComp<MapComponent>(uid));
|
||||
|
||||
var (pos, worldRot) = GetWorldPositionRotation(xform, XformQuery);
|
||||
|
||||
|
||||
26
Robust.Shared/GameObjects/Systems/SharedVisibilitySystem.cs
Normal file
26
Robust.Shared/GameObjects/Systems/SharedVisibilitySystem.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
public abstract class SharedVisibilitySystem : EntitySystem
|
||||
{
|
||||
public virtual void AddLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void RemoveLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void SetLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void RefreshVisibility(EntityUid uid,
|
||||
VisibilityComponent? visibilityComponent = null,
|
||||
MetaDataComponent? meta = null)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void RefreshVisibility(Entity<VisibilityComponent?, MetaDataComponent?> ent)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Robust.Shared.Map.Components
|
||||
[DataField]
|
||||
public bool LightingEnabled { get; set; } = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[ViewVariables(VVAccess.ReadOnly), Access(typeof(SharedMapSystem), Other = AccessPermissions.ReadExecute)]
|
||||
public MapId MapId { get; internal set; } = MapId.Nullspace;
|
||||
|
||||
[DataField, Access(typeof(SharedMapSystem), typeof(MapManager))]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
|
||||
namespace Robust.Shared.Map.Events;
|
||||
|
||||
@@ -28,17 +30,13 @@ public sealed class BeforeEntityReadEvent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This event is broadcast just before an entity gets serialized.
|
||||
/// This event is broadcast just before the given entities (and their children) are serialized.
|
||||
/// For convenience, the event also contains a set with all the maps that the entities are on. This does not
|
||||
/// necessarily mean that the maps are themselves getting serialized.
|
||||
/// </summary>
|
||||
public sealed class BeforeSaveEvent(EntityUid entity, EntityUid? map)
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity that is going to be saved. usually a map or grid.
|
||||
/// </summary>
|
||||
public EntityUid Entity = entity;
|
||||
public readonly record struct BeforeSerializationEvent(HashSet<EntityUid> Entities, HashSet<MapId> MapIds);
|
||||
|
||||
/// <summary>
|
||||
/// The map that the <see cref="Entity"/> is on.
|
||||
/// </summary>
|
||||
public EntityUid? Map = map;
|
||||
}
|
||||
/// <summary>
|
||||
/// This event is broadcast just after entities (and their children) have been serialized, but before it gets written to a yaml file.
|
||||
/// </summary>
|
||||
public readonly record struct AfterSerializationEvent(HashSet<EntityUid> Entities, MappingDataNode Node, FileCategory Category);
|
||||
|
||||
@@ -47,21 +47,25 @@ namespace Robust.Shared.Map
|
||||
/// </summary>
|
||||
/// <param name="mapId">The map ID to check existence of.</param>
|
||||
/// <returns>True if the map exists, false otherwise.</returns>
|
||||
[Obsolete("Use MapSystem")]
|
||||
bool MapExists([NotNullWhen(true)] MapId? mapId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the map entity ID for a given map, or an invalid entity Id if the map does not exist.
|
||||
/// </summary>
|
||||
[Obsolete("Use TryGetMap")]
|
||||
[Obsolete("Use MapSystem")]
|
||||
EntityUid GetMapEntityId(MapId mapId);
|
||||
|
||||
/// <summary>
|
||||
/// Replaces GetMapEntity()'s throw-on-failure semantics.
|
||||
/// </summary>
|
||||
[Obsolete("Use MapSystem")]
|
||||
EntityUid GetMapEntityIdOrThrow(MapId mapId);
|
||||
|
||||
[Obsolete("Use MapSystem")]
|
||||
IEnumerable<MapId> GetAllMapIds();
|
||||
|
||||
[Obsolete("Use MapSystem")]
|
||||
void DeleteMap(MapId mapId);
|
||||
|
||||
// ReSharper disable once MethodOverloadWithOptionalParameter
|
||||
@@ -205,20 +209,22 @@ namespace Robust.Shared.Map
|
||||
[Obsolete("Just delete the grid entity")]
|
||||
void DeleteGrid(EntityUid euid);
|
||||
|
||||
[Obsolete("Use HasComp")]
|
||||
bool IsGrid(EntityUid uid);
|
||||
|
||||
[Obsolete("Use HasComp")]
|
||||
bool IsMap(EntityUid uid);
|
||||
|
||||
//
|
||||
// Pausing functions
|
||||
//
|
||||
|
||||
[Obsolete("Use MapSystem")]
|
||||
void SetMapPaused(MapId mapId, bool paused);
|
||||
|
||||
[Obsolete("Use MapSystem")]
|
||||
void DoMapInitialize(MapId mapId);
|
||||
|
||||
[Obsolete("Use CreateMap's runMapInit argument")]
|
||||
void AddUninitializedMap(MapId mapId);
|
||||
|
||||
[Obsolete("Use MapSystem")]
|
||||
bool IsMapPaused(MapId mapId);
|
||||
|
||||
|
||||
@@ -67,15 +67,5 @@ namespace Robust.Shared.Map
|
||||
/// </summary>
|
||||
/// <param name="tileDef">THe definition to register.</param>
|
||||
void Register(ITileDefinition tileDef);
|
||||
|
||||
/// <summary>
|
||||
/// Register a tile alias with this manager.
|
||||
/// The tile need not exist yet - the alias's creation will be deferred until it exists.
|
||||
/// Tile aliases do not have IDs of their own and do not show up in enumeration.
|
||||
/// Their main utility is for easier map migration.
|
||||
/// </summary>
|
||||
/// <param name="src">The source tile (i.e. name of the alias).</param>
|
||||
/// <param name="dst">The destination tile (i.e. the actual concrete tile).</param>
|
||||
void AssignAlias(string src, string dst);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Robust.Shared.Map
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value.ToString();
|
||||
return IsClientSide ? $"c{-Value}" : Value.ToString();
|
||||
}
|
||||
|
||||
public bool IsClientSide => Value < 0;
|
||||
|
||||
@@ -125,6 +125,9 @@ internal partial class MapManager
|
||||
EntityManager.System<MetaDataSystem>().SetEntityName(gridEnt, $"grid", meta);
|
||||
EntityManager.InitializeComponents(gridEnt, meta);
|
||||
EntityManager.StartComponents(gridEnt);
|
||||
// Note that this does not actually map-initialize the grid entity, even if the map its being spawn on has already been initialized.
|
||||
// I don't know whether that is intentional or not.
|
||||
|
||||
return (gridEnt, grid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Map;
|
||||
|
||||
@@ -28,16 +27,10 @@ public sealed class MapEventArgs : EventArgs
|
||||
|
||||
internal partial class MapManager
|
||||
{
|
||||
private Dictionary<MapId, EntityUid> _mapEntities => _mapSystem.Maps;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void DeleteMap(MapId mapId)
|
||||
{
|
||||
if (!_mapEntities.TryGetValue(mapId, out var ent) || !ent.IsValid())
|
||||
throw new InvalidOperationException($"Attempted to delete nonexistent map '{mapId}'");
|
||||
|
||||
EntityManager.DeleteEntity(ent);
|
||||
DebugTools.Assert(!_mapEntities.ContainsKey(mapId));
|
||||
_mapSystem.DeleteMap(mapId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -81,7 +74,7 @@ internal partial class MapManager
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<MapId> GetAllMapIds()
|
||||
{
|
||||
return _mapEntities.Keys;
|
||||
return _mapSystem.GetAllMapIds();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Robust.Shared.Map
|
||||
{
|
||||
@@ -27,14 +25,6 @@ namespace Robust.Shared.Map
|
||||
return _mapSystem.IsInitialized(mapId);
|
||||
}
|
||||
|
||||
public void AddUninitializedMap(MapId mapId)
|
||||
{
|
||||
var ent = GetMapEntityId(mapId);
|
||||
EntityManager.GetComponent<MapComponent>(ent).MapInitialized = false;
|
||||
var meta = EntityManager.GetComponent<MetaDataComponent>(ent);
|
||||
((EntityManager)EntityManager).SetLifeStage(meta, EntityLifeStage.Initialized);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsMapPaused(MapId mapId)
|
||||
{
|
||||
@@ -87,7 +77,7 @@ namespace Robust.Shared.Map
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteLine(IsMapPaused(mapId).ToString());
|
||||
shell.WriteLine(_mapSystem.IsPaused(mapId).ToString());
|
||||
});
|
||||
|
||||
_conhost.RegisterCommand("unpausemap",
|
||||
|
||||
@@ -45,54 +45,54 @@ internal partial class MapManager
|
||||
public void FindGridsIntersecting<T>(MapId mapId, T shape, Transform transform,
|
||||
ref List<Entity<MapGridComponent>> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt, shape, transform, ref grids, approx, includeMap);
|
||||
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt.Value, shape, transform, ref grids, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting<T>(MapId mapId, T shape, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt, shape, transform, callback, includeMap, approx);
|
||||
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt.Value, shape, transform, callback, includeMap, approx);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt, worldAABB, callback, approx, includeMap);
|
||||
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt.Value, worldAABB, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting<TState>(MapId mapId, Box2 worldAABB, ref TState state, GridCallback<TState> callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var map))
|
||||
FindGridsIntersecting(map, worldAABB, ref state, callback, approx, includeMap);
|
||||
if (_mapSystem.TryGetMap(mapId, out var map))
|
||||
FindGridsIntersecting(map.Value, worldAABB, ref state, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref List<Entity<MapGridComponent>> grids,
|
||||
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var map))
|
||||
FindGridsIntersecting(map, worldAABB, ref grids, approx, includeMap);
|
||||
if (_mapSystem.TryGetMap(mapId, out var map))
|
||||
FindGridsIntersecting(map.Value, worldAABB, ref grids, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate,
|
||||
bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt, worldBounds, callback, approx, includeMap);
|
||||
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt.Value, worldBounds, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting<TState>(MapId mapId, Box2Rotated worldBounds, ref TState state, GridCallback<TState> callback,
|
||||
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt, worldBounds, ref state, callback, approx, includeMap);
|
||||
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt.Value, worldBounds, ref state, callback, approx, includeMap);
|
||||
}
|
||||
|
||||
public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref List<Entity<MapGridComponent>> grids,
|
||||
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt, worldBounds, ref grids, approx, includeMap);
|
||||
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
|
||||
FindGridsIntersecting(mapEnt.Value, worldBounds, ref grids, approx, includeMap);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -338,8 +338,8 @@ internal partial class MapManager
|
||||
/// </summary>
|
||||
public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid)
|
||||
{
|
||||
if (_mapEntities.TryGetValue(mapId, out var map))
|
||||
return TryFindGridAt(map, worldPos, out uid, out grid);
|
||||
if (_mapSystem.TryGetMap(mapId, out var map))
|
||||
return TryFindGridAt(map.Value, worldPos, out uid, out grid);
|
||||
|
||||
uid = default;
|
||||
grid = null;
|
||||
|
||||
@@ -11,10 +11,10 @@ namespace Robust.Shared.Map;
|
||||
|
||||
/// <inheritdoc cref="IMapManager" />
|
||||
[Virtual]
|
||||
internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber, IPostInjectInit
|
||||
internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber
|
||||
{
|
||||
[field: Dependency] public IGameTiming GameTiming { get; } = default!;
|
||||
[field: Dependency] public IEntityManager EntityManager { get; } = default!;
|
||||
[Dependency] public readonly IGameTiming GameTiming = default!;
|
||||
[Dependency] public readonly IEntityManager EntityManager = default!;
|
||||
[Dependency] private readonly IManifoldManager _manifolds = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConsoleHost _conhost = default!;
|
||||
@@ -34,6 +34,7 @@ internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber,
|
||||
_gridTreeQuery = EntityManager.GetEntityQuery<GridTreeComponent>();
|
||||
_gridQuery = EntityManager.GetEntityQuery<MapGridComponent>();
|
||||
InitializeMapPausing();
|
||||
_sawmill = _logManager.GetSawmill("system.map");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -74,9 +75,4 @@ internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber,
|
||||
EntityManager.DeleteEntity(uid);
|
||||
}
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("system.map");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Shared.Map;
|
||||
|
||||
internal sealed class MapSerializationContext : ISerializationContext, IEntityLoadContext,
|
||||
ITypeSerializer<EntityUid, ValueDataNode>
|
||||
{
|
||||
public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
|
||||
|
||||
// Run-specific data
|
||||
public Dictionary<int, string>? TileMap;
|
||||
public readonly Dictionary<string, IComponent> CurrentReadingEntityComponents = new();
|
||||
public HashSet<string> CurrentlyIgnoredComponents = new();
|
||||
public string? CurrentComponent;
|
||||
public EntityUid? CurrentWritingEntity;
|
||||
public IEntityManager EntityManager;
|
||||
public IGameTiming Timing;
|
||||
|
||||
private Dictionary<int, EntityUid> _uidEntityMap = new();
|
||||
private Dictionary<EntityUid, int> _entityUidMap = new();
|
||||
|
||||
// Native tile ID -> map tile ID map for writing maps.
|
||||
public Dictionary<int, int> TileWriteMap = [];
|
||||
|
||||
/// <summary>
|
||||
/// Are we currently iterating prototypes or entities for writing.
|
||||
/// </summary>
|
||||
public bool WritingReadingPrototypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the map has been MapInitialized or not.
|
||||
/// </summary>
|
||||
public bool MapInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// How long the target map has been paused. Used for time offsets.
|
||||
/// </summary>
|
||||
public TimeSpan PauseTime;
|
||||
|
||||
/// <summary>
|
||||
/// The parent of the entity being saved, This entity is not itself getting saved.
|
||||
/// </summary>
|
||||
private EntityUid? _parentUid;
|
||||
|
||||
public MapSerializationContext(IEntityManager entityManager, IGameTiming timing)
|
||||
{
|
||||
EntityManager = entityManager;
|
||||
Timing = timing;
|
||||
SerializerProvider.RegisterSerializer(this);
|
||||
}
|
||||
|
||||
public void Set(
|
||||
Dictionary<int, EntityUid> uidEntityMap,
|
||||
Dictionary<EntityUid, int> entityUidMap,
|
||||
bool mapPreInit,
|
||||
TimeSpan pauseTime,
|
||||
EntityUid? parentUid)
|
||||
{
|
||||
_uidEntityMap = uidEntityMap;
|
||||
_entityUidMap = entityUidMap;
|
||||
MapInitialized = mapPreInit;
|
||||
PauseTime = pauseTime;
|
||||
if (parentUid != null && parentUid.Value.IsValid())
|
||||
_parentUid = parentUid;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
CurrentReadingEntityComponents.Clear();
|
||||
CurrentlyIgnoredComponents.Clear();
|
||||
CurrentComponent = null;
|
||||
CurrentWritingEntity = null;
|
||||
PauseTime = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
// Create custom object serializers that will correctly allow data to be overriden by the map file.
|
||||
bool IEntityLoadContext.TryGetComponent(string componentName, [NotNullWhen(true)] out IComponent? component)
|
||||
{
|
||||
return CurrentReadingEntityComponents.TryGetValue(componentName, out component);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetExtraComponentTypes()
|
||||
{
|
||||
return CurrentReadingEntityComponents!.Keys;
|
||||
}
|
||||
|
||||
public bool ShouldSkipComponent(string compName)
|
||||
{
|
||||
return CurrentlyIgnoredComponents.Contains(compName);
|
||||
}
|
||||
|
||||
ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(ISerializationManager serializationManager,
|
||||
ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context)
|
||||
{
|
||||
if (node.Value == "null")
|
||||
{
|
||||
return new ValidatedValueNode(node);
|
||||
}
|
||||
|
||||
if (!int.TryParse(node.Value, out var val) || !_uidEntityMap.ContainsKey(val))
|
||||
{
|
||||
return new ErrorNode(node, "Invalid EntityUid", true);
|
||||
}
|
||||
|
||||
return new ValidatedValueNode(node);
|
||||
}
|
||||
|
||||
public DataNode Write(ISerializationManager serializationManager, EntityUid value,
|
||||
IDependencyCollection dependencies, bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
if (!_entityUidMap.TryGetValue(value, out var entityUidMapped))
|
||||
{
|
||||
if (CurrentComponent == "Transform")
|
||||
{
|
||||
if (!value.IsValid() || value == _parentUid)
|
||||
return new ValueDataNode("invalid");
|
||||
}
|
||||
|
||||
dependencies
|
||||
.Resolve<ILogManager>()
|
||||
.GetSawmill("map")
|
||||
.Error("Encountered an invalid entityUid '{0}' while serializing a map.", value);
|
||||
|
||||
return new ValueDataNode("invalid");
|
||||
}
|
||||
|
||||
return new ValueDataNode(entityUidMapped.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
EntityUid ITypeReader<EntityUid, ValueDataNode>.Read(ISerializationManager serializationManager,
|
||||
ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context, ISerializationManager.InstantiationDelegate<EntityUid>? _)
|
||||
{
|
||||
if (node.Value == "invalid" && CurrentComponent == "Transform")
|
||||
return EntityUid.Invalid;
|
||||
|
||||
if (int.TryParse(node.Value, out var val) && _uidEntityMap.TryGetValue(val, out var entity))
|
||||
return entity;
|
||||
|
||||
dependencies
|
||||
.Resolve<ILogManager>()
|
||||
.GetSawmill("map")
|
||||
.Error("Error in map file: found local entity UID '{0}' which does not exist.", val);
|
||||
|
||||
return EntityUid.Invalid;
|
||||
|
||||
}
|
||||
|
||||
[MustUseReturnValue]
|
||||
public EntityUid Copy(ISerializationManager serializationManager, EntityUid source, EntityUid target,
|
||||
bool skipHook,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
return new((int)source);
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,6 @@ namespace Robust.Shared.Map
|
||||
|
||||
public virtual void Initialize()
|
||||
{
|
||||
foreach (var prototype in _prototypeManager.EnumeratePrototypes<TileAliasPrototype>())
|
||||
{
|
||||
AssignAlias(prototype.ID, prototype.Target);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Register(ITileDefinition tileDef)
|
||||
@@ -45,46 +41,8 @@ namespace Robust.Shared.Map
|
||||
tileDef.AssignTileId(id);
|
||||
TileDefs.Add(tileDef);
|
||||
_tileNames[name] = tileDef;
|
||||
|
||||
AliasingHandleDeferred(name);
|
||||
}
|
||||
|
||||
private void AliasingHandleDeferred(string name)
|
||||
{
|
||||
// Aliases may have been held back due to tiles not being registered yet, handle this.
|
||||
if (_awaitingAliases.ContainsKey(name))
|
||||
{
|
||||
var list = _awaitingAliases[name];
|
||||
_awaitingAliases.Remove(name);
|
||||
foreach (var alias in list)
|
||||
{
|
||||
AssignAlias(alias, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public virtual void AssignAlias(string src, string dst)
|
||||
{
|
||||
if (_tileNames.ContainsKey(src))
|
||||
{
|
||||
throw new ArgumentException("Another tile definition or alias with the same name has already been registered.", nameof(src));
|
||||
}
|
||||
|
||||
if (_tileNames.ContainsKey(dst))
|
||||
{
|
||||
// Simple enough, source to destination.
|
||||
_tileNames[src] = _tileNames[dst];
|
||||
AliasingHandleDeferred(src);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Less simple - stash this alias for later so it appears when the target does.
|
||||
if (!_awaitingAliases.ContainsKey(dst))
|
||||
_awaitingAliases[dst] = new();
|
||||
_awaitingAliases[dst].Add(src);
|
||||
}
|
||||
}
|
||||
|
||||
public Tile GetVariantTile(string name, IRobustRandom random)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
@@ -11,7 +10,6 @@ using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Lidgren.Network;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network.Messages.Handshake;
|
||||
using Robust.Shared.Utility;
|
||||
using SpaceWizards.Sodium;
|
||||
@@ -98,7 +96,7 @@ namespace Robust.Shared.Network
|
||||
{
|
||||
await CCDoHandshake(winningPeer, winningConnection, userNameRequest, mainCancelToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
winningPeer.Peer.Shutdown("Cancelled");
|
||||
_toCleanNetPeers.Add(winningPeer.Peer);
|
||||
@@ -120,7 +118,10 @@ namespace Robust.Shared.Network
|
||||
_logger.Debug("Handshake completed, connection established.");
|
||||
}
|
||||
|
||||
private async Task CCDoHandshake(NetPeerData peer, NetConnection connection, string userNameRequest,
|
||||
private async Task CCDoHandshake(
|
||||
NetPeerData peer,
|
||||
NetConnection connection,
|
||||
string userNameRequest,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
var encrypt = _config.GetCVar(CVars.NetEncrypt);
|
||||
@@ -289,37 +290,51 @@ namespace Robust.Shared.Network
|
||||
private async Task<(NetPeerData winningPeer, NetConnection winningConnection)?>
|
||||
CCHappyEyeballs(int port, IPAddress first, IPAddress? second, CancellationToken mainCancelToken)
|
||||
{
|
||||
NetPeerData CreatePeerForIp(IPAddress address)
|
||||
// Try to establish a connection with an IP address and wait for it to either connect or fail
|
||||
// Returns a disposable wrapper around the peer/connection because ParallelTask
|
||||
async Task<ConnectionAttempt> AttemptConnection(IPAddress address, CancellationToken cancel)
|
||||
{
|
||||
var config = _getBaseNetPeerConfig();
|
||||
if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
config.LocalAddress = IPAddress.IPv6Any;
|
||||
}
|
||||
else
|
||||
{
|
||||
config.LocalAddress = IPAddress.Any;
|
||||
}
|
||||
config.LocalAddress = address.AddressFamily == AddressFamily.InterNetworkV6
|
||||
? IPAddress.IPv6Any
|
||||
: IPAddress.Any;
|
||||
|
||||
var peer = new NetPeer(config);
|
||||
peer.Start();
|
||||
var data = new NetPeerData(peer);
|
||||
_netPeers.Add(data);
|
||||
return data;
|
||||
var peerData = new NetPeerData(peer);
|
||||
_netPeers.Add(peerData);
|
||||
|
||||
var connection = peer.Connect(new IPEndPoint(address, port));
|
||||
|
||||
try
|
||||
{
|
||||
// We need AwaitNonInitStatusChange to properly handle connection state transitions
|
||||
var reason = await AwaitNonInitStatusChange(connection, cancel);
|
||||
|
||||
if (connection.Status != NetConnectionStatus.Connected)
|
||||
{
|
||||
// Connection failed, clean up and yeet an exception
|
||||
peer.Shutdown(reason);
|
||||
_toCleanNetPeers.Add(peer);
|
||||
throw new Exception($"Connection failed: {reason}");
|
||||
}
|
||||
|
||||
return new ConnectionAttempt(peerData, connection, this);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Something went wrong!
|
||||
peer.Shutdown("Connection attempt failed");
|
||||
_toCleanNetPeers.Add(peer);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Create first peer.
|
||||
var firstPeer = CreatePeerForIp(first);
|
||||
var firstConnection = firstPeer.Peer.Connect(new IPEndPoint(first, port));
|
||||
NetPeerData? secondPeer = null;
|
||||
NetConnection? secondConnection = null;
|
||||
string? secondReason = null;
|
||||
|
||||
// Waits for a connection's status to change from InitiatedConnect to anything else
|
||||
async Task<string> AwaitNonInitStatusChange(NetConnection connection, CancellationToken cancellationToken)
|
||||
{
|
||||
NetConnectionStatus status;
|
||||
string reason;
|
||||
|
||||
NetConnectionStatus status;
|
||||
do
|
||||
{
|
||||
reason = await AwaitStatusChange(connection, cancellationToken);
|
||||
@@ -329,124 +344,37 @@ namespace Robust.Shared.Network
|
||||
return reason;
|
||||
}
|
||||
|
||||
async Task ConnectSecondDelayed(CancellationToken cancellationToken)
|
||||
{
|
||||
DebugTools.AssertNotNull(second);
|
||||
// Connecting via second peer is delayed by 25ms to give an advantage to IPv6, if it works.
|
||||
var delay = TimeSpan.FromSeconds(_config.GetCVar(CVars.NetHappyEyeballsDelay));
|
||||
await Task.Delay(delay, cancellationToken);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
secondPeer = CreatePeerForIp(second);
|
||||
secondConnection = secondPeer.Peer.Connect(new IPEndPoint(second, port));
|
||||
|
||||
secondReason = await AwaitNonInitStatusChange(secondConnection, cancellationToken);
|
||||
}
|
||||
|
||||
NetPeerData? winningPeer;
|
||||
NetConnection? winningConnection;
|
||||
string? firstReason = null;
|
||||
try
|
||||
{
|
||||
if (second != null)
|
||||
{
|
||||
// We have two addresses to try.
|
||||
var cancellation = CancellationTokenSource.CreateLinkedTokenSource(mainCancelToken);
|
||||
var firstPeerChanged = AwaitNonInitStatusChange(firstConnection, cancellation.Token);
|
||||
var secondPeerChanged = ConnectSecondDelayed(cancellation.Token);
|
||||
// Create list of IPs to try
|
||||
var addresses = second != null
|
||||
? new[] { first, second }
|
||||
: new[] { first };
|
||||
|
||||
var firstChange = await Task.WhenAny(firstPeerChanged, secondPeerChanged);
|
||||
// Use ParallelTask to handle the connection attempts
|
||||
var delay = TimeSpan.FromSeconds(_config.GetCVar(CVars.NetHappyEyeballsDelay));
|
||||
var (result, _) = await HappyEyeballsHttp.ParallelTask(
|
||||
addresses.Length,
|
||||
(i, token) => AttemptConnection(addresses[i], token),
|
||||
delay,
|
||||
mainCancelToken);
|
||||
|
||||
if (firstChange == firstPeerChanged)
|
||||
{
|
||||
_logger.Debug("First peer status changed.");
|
||||
// First peer responded first.
|
||||
if (firstConnection.Status == NetConnectionStatus.Connected)
|
||||
{
|
||||
// First peer won!
|
||||
_logger.Debug("First peer succeeded.");
|
||||
cancellation.Cancel();
|
||||
if (secondPeer != null)
|
||||
{
|
||||
secondPeer.Peer.Shutdown("First connection attempt won.");
|
||||
_toCleanNetPeers.Add(secondPeer.Peer);
|
||||
}
|
||||
|
||||
winningPeer = firstPeer;
|
||||
winningConnection = firstConnection;
|
||||
}
|
||||
else
|
||||
{
|
||||
// First peer failed, try the second one I guess.
|
||||
_logger.Debug("First peer failed.");
|
||||
firstPeer.Peer.Shutdown("You failed.");
|
||||
_toCleanNetPeers.Add(firstPeer.Peer);
|
||||
firstReason = await firstPeerChanged;
|
||||
await secondPeerChanged;
|
||||
winningPeer = secondPeer;
|
||||
winningConnection = secondConnection;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (secondConnection!.Status == NetConnectionStatus.Connected)
|
||||
{
|
||||
// Second peer won!
|
||||
_logger.Debug("Second peer succeeded.");
|
||||
cancellation.Cancel();
|
||||
firstPeer.Peer.Shutdown("Second connection attempt won.");
|
||||
_toCleanNetPeers.Add(firstPeer.Peer);
|
||||
winningPeer = secondPeer;
|
||||
winningConnection = secondConnection;
|
||||
}
|
||||
else
|
||||
{
|
||||
// First peer failed, try the second one I guess.
|
||||
_logger.Debug("Second peer failed.");
|
||||
secondPeer!.Peer.Shutdown("You failed.");
|
||||
_toCleanNetPeers.Add(secondPeer.Peer);
|
||||
firstReason = await firstPeerChanged;
|
||||
winningPeer = firstPeer;
|
||||
winningConnection = firstConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only one address to try. Pretty straight forward.
|
||||
firstReason = await AwaitNonInitStatusChange(firstConnection, mainCancelToken);
|
||||
winningPeer = firstPeer;
|
||||
winningConnection = firstConnection;
|
||||
}
|
||||
return (result.Peer, result.Connection);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
firstPeer.Peer.Shutdown("Cancelled");
|
||||
_toCleanNetPeers.Add(firstPeer.Peer);
|
||||
if (secondPeer != null)
|
||||
{
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
secondPeer.Peer.Shutdown("Cancelled");
|
||||
_toCleanNetPeers.Add(secondPeer.Peer);
|
||||
}
|
||||
|
||||
// Connection attempt was cancelled, nothing to see here
|
||||
OnConnectFailed("Connection attempt cancelled.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// winningPeer can still be failed at this point.
|
||||
// If it is, neither succeeded. RIP.
|
||||
if (winningConnection!.Status != NetConnectionStatus.Connected)
|
||||
catch (AggregateException ae)
|
||||
{
|
||||
winningPeer!.Peer.Shutdown("You failed");
|
||||
_toCleanNetPeers.Add(winningPeer.Peer);
|
||||
OnConnectFailed((secondReason ?? firstReason)!);
|
||||
// ParallelTask throws AggregateException with all connection failures
|
||||
// We just take the first one
|
||||
var message = ae.InnerExceptions.First().Message;
|
||||
OnConnectFailed(message);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (winningPeer!, winningConnection);
|
||||
}
|
||||
|
||||
private Task<string> AwaitStatusChange(NetConnection connection, CancellationToken cancellationToken = default)
|
||||
@@ -471,7 +399,8 @@ namespace Robust.Shared.Network
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private Task<NetIncomingMessage> AwaitData(NetConnection connection,
|
||||
private Task<NetIncomingMessage> AwaitData(
|
||||
NetConnection connection,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_awaitingData.ContainsKey(connection))
|
||||
@@ -529,5 +458,17 @@ namespace Robust.Shared.Network
|
||||
}
|
||||
|
||||
private sealed record JoinRequest(string Hash, string? Hwid);
|
||||
|
||||
private sealed class ConnectionAttempt(NetPeerData peer, NetConnection connection, NetManager netManager) : IDisposable
|
||||
{
|
||||
public NetPeerData Peer { get; } = peer;
|
||||
public NetConnection Connection { get; } = connection;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Peer.Peer.Shutdown("Disposing unused connection attempt");
|
||||
netManager._toCleanNetPeers.Add(Peer.Peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,6 +587,14 @@ namespace Robust.Shared.Network
|
||||
public void ClientDisconnect(string reason)
|
||||
{
|
||||
DebugTools.Assert(IsClient, "Should never be called on the server.");
|
||||
|
||||
// First handle any in-progress connection attempt
|
||||
if (ClientConnectState != ClientConnectionState.NotConnecting)
|
||||
{
|
||||
_cancelConnectTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
// Then handle existing connection if any
|
||||
if (ServerChannel != null)
|
||||
{
|
||||
Disconnect?.Invoke(this, new NetDisconnectedArgs(ServerChannel, reason));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
@@ -78,13 +79,11 @@ public sealed class FixtureSerializer : ITypeSerializer<Dictionary<string, Fixtu
|
||||
if (value.Count == 0)
|
||||
return seq;
|
||||
|
||||
if (context is MapSerializationContext mapContext)
|
||||
if (context is EntitySerializer ctx)
|
||||
{
|
||||
// Don't serialize mapgrid fixtures because it's bloat and we'll just generate them at runtime.
|
||||
if (dependencies.Resolve<IEntityManager>().HasComponent<MapGridComponent>(mapContext.CurrentWritingEntity))
|
||||
{
|
||||
if (ctx.EntMan.HasComponent<MapGridComponent>(ctx.CurrentEntity))
|
||||
return seq;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (id, fixture) in value)
|
||||
|
||||
@@ -236,7 +236,8 @@ public abstract partial class SharedJointSystem : EntitySystem
|
||||
Vector2? anchorB = null,
|
||||
string? id = null,
|
||||
TransformComponent? xformA = null,
|
||||
TransformComponent? xformB = null)
|
||||
TransformComponent? xformB = null,
|
||||
int? minimumDistance = null)
|
||||
{
|
||||
if (!Resolve(bodyA, ref xformA) || !Resolve(bodyB, ref xformB))
|
||||
{
|
||||
@@ -246,9 +247,13 @@ public abstract partial class SharedJointSystem : EntitySystem
|
||||
anchorA ??= Vector2.Zero;
|
||||
anchorB ??= Vector2.Zero;
|
||||
|
||||
var length = Vector2.Transform(anchorA.Value, xformA.WorldMatrix) - Vector2.Transform(anchorB.Value, xformB.WorldMatrix);
|
||||
var vecA = Vector2.Transform(anchorA.Value, xformA.WorldMatrix);
|
||||
var vecB = Vector2.Transform(anchorB.Value, xformB.WorldMatrix);
|
||||
var length = (vecA - vecB).Length();
|
||||
if (minimumDistance != null)
|
||||
length = Math.Max(minimumDistance.Value, length);
|
||||
|
||||
var joint = new DistanceJoint(bodyA, bodyB, anchorA.Value, anchorB.Value, length.Length());
|
||||
var joint = new DistanceJoint(bodyA, bodyB, anchorA.Value, anchorB.Value, length);
|
||||
id ??= GetJointId(joint);
|
||||
joint.ID = id;
|
||||
AddJoint(joint);
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace Robust.Shared.Prototypes;
|
||||
/// </remarks>
|
||||
/// <remarks><seealso cref="ProtoId{T}"/> for a wrapper of other prototype kinds.</remarks>
|
||||
[Serializable, NetSerializable]
|
||||
public readonly record struct EntProtoId(string Id) : IEquatable<string>, IComparable<EntProtoId>, IAsType<string>
|
||||
public readonly record struct EntProtoId(string Id) : IEquatable<string>, IComparable<EntProtoId>, IAsType<string>,
|
||||
IAsType<ProtoId<EntityPrototype>>
|
||||
{
|
||||
public static implicit operator string(EntProtoId protoId)
|
||||
{
|
||||
@@ -49,7 +50,9 @@ public readonly record struct EntProtoId(string Id) : IEquatable<string>, ICompa
|
||||
return string.Compare(Id, other.Id, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public string AsType() => Id;
|
||||
string IAsType<string>.AsType() => Id;
|
||||
|
||||
ProtoId<EntityPrototype> IAsType<ProtoId<EntityPrototype>>.AsType() => new(Id);
|
||||
|
||||
public override string ToString() => Id ?? string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
@@ -268,7 +269,7 @@ namespace Robust.Shared.Prototypes
|
||||
component = newComponent;
|
||||
}
|
||||
|
||||
if (context is not MapSerializationContext map)
|
||||
if (context is not EntityDeserializer map)
|
||||
{
|
||||
serManager.CopyTo(data, ref component, context, notNullableOverride: true);
|
||||
return;
|
||||
|
||||
@@ -38,6 +38,9 @@ public sealed class MultiRootInheritanceGraph<T> where T : notnull
|
||||
//check for circular inheritance
|
||||
foreach (var parent in parents)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(parent, id))
|
||||
throw new InvalidOperationException($"Self Inheritance detected for id \"{id}\"!");
|
||||
|
||||
var parentsL1 = GetParents(parent);
|
||||
if(parentsL1 == null) continue;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user