mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a17df38342 | ||
|
|
85871525cf | ||
|
|
97204cc445 | ||
|
|
cc73138a93 | ||
|
|
8f75560ec4 | ||
|
|
b323c8bd1e | ||
|
|
faef44daaa | ||
|
|
fbc706f37b | ||
|
|
9d1b15ab4b | ||
|
|
ea1cc5e446 | ||
|
|
bcb5c2d35d | ||
|
|
c011eff80e | ||
|
|
e163c496c3 | ||
|
|
fec81bc2a1 | ||
|
|
7016facb9a | ||
|
|
0c41a041e3 | ||
|
|
55571ef5b1 | ||
|
|
afaef645b0 | ||
|
|
d442d90d60 |
@@ -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 -->
|
||||
|
||||
|
||||
102
RELEASE-NOTES.md
102
RELEASE-NOTES.md
@@ -54,6 +54,108 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 247.0.2
|
||||
|
||||
|
||||
## 247.0.1
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
* The fixes to renderer state may have inadvertantly broken some rendering code that relied upon the old behavior.
|
||||
* TileRenderFlag has been removed and now it's just a byte flag on the tile for content usage.
|
||||
|
||||
### New features
|
||||
|
||||
* Add BeforeLighting overlay draw space for overlays that need to draw directly to lighting and want to do it immediately beforehand.
|
||||
* Change BlurLights to BlurRenderTarget and make it public for content usage.
|
||||
* Add ContentFlag to tiles for content-flag usage.
|
||||
* Add a basic mix shader for doing canvas blends.
|
||||
* Add GetClearColorEvent for content to override the clear color behavior.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix pushing renderer state not restoring stencil status, blend status, queued shader instance scissor state.
|
||||
|
||||
|
||||
## 245.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add more info to the AnchorEntity debug message.
|
||||
* Make ParseObject public where it will parse a supplied Type and string into the specified object.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix EntityPrototypeView not always updating the entity correctly.
|
||||
* Tweak BUI shutdown to potentially avoid skipping closing.
|
||||
|
||||
### Other
|
||||
|
||||
* Increase Audio entity despawn buffer to avoid clipping.
|
||||
|
||||
|
||||
## 245.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `BoundUserInterface.Open()` now has the `MustCallBase` attribute
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed an error in `MappingDataNode.TryAddCopy()`, which was causing yaml inheritance/deserialization bugs.
|
||||
|
||||
|
||||
## 244.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
kind: canvas
|
||||
light_mode: unshaded
|
||||
|
||||
# Simple mix blend
|
||||
- type: shader
|
||||
id: Mix
|
||||
kind: canvas
|
||||
blend_mode: Mix
|
||||
|
||||
- type: shader
|
||||
id: shaded
|
||||
kind: canvas
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
|
||||
|
||||
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), vp.Eye!.Position.MapId, worldBox, worldBounds);
|
||||
var mapId = vp.Eye!.Position.MapId;
|
||||
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldBox, worldBounds);
|
||||
|
||||
if (!overlay.BeforeDraw(args))
|
||||
return;
|
||||
@@ -175,8 +176,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var worldBounds = CalcWorldBounds(vp);
|
||||
var worldAABB = worldBounds.CalcBoundingBox();
|
||||
var mapId = vp.Eye!.Position.MapId;
|
||||
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, vp.Eye!.Position.MapId, worldAABB, worldBounds);
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldAABB, worldBounds);
|
||||
|
||||
foreach (var overlay in list)
|
||||
{
|
||||
@@ -421,12 +423,19 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var oldTransform = _currentMatrixModel;
|
||||
var oldScissor = _currentScissorState;
|
||||
var oldMatrixProj = _currentMatrixProj;
|
||||
var oldMatrixView = _currentMatrixView;
|
||||
var oldBoundTarget = _currentBoundRenderTarget;
|
||||
var oldRenderTarget = _currentRenderTarget;
|
||||
var oldShader = _queuedShaderInstance;
|
||||
var oldCaps = _glCaps;
|
||||
|
||||
// Need to get state before flushing render queue in case they modify the original state.
|
||||
var state = PushRenderStateFull();
|
||||
|
||||
// Have to flush the render queue so that all commands finish rendering to the previous framebuffer.
|
||||
FlushRenderQueue();
|
||||
|
||||
var state = PushRenderStateFull();
|
||||
|
||||
{
|
||||
BindRenderTargetFull(RtToLoaded(rt));
|
||||
if (clearColor is not null)
|
||||
@@ -448,8 +457,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
PopRenderStateFull(state);
|
||||
_updateUniformConstants(_currentRenderTarget.Size);
|
||||
|
||||
SetScissorFull(oldScissor);
|
||||
_currentMatrixModel = oldTransform;
|
||||
|
||||
DebugTools.Assert(oldCaps.Equals(_glCaps));
|
||||
DebugTools.Assert(_currentMatrixModel.Equals(oldTransform));
|
||||
DebugTools.Assert(_currentScissorState.Equals(oldScissor));
|
||||
DebugTools.Assert(_currentMatrixProj.Equals(oldMatrixProj));
|
||||
DebugTools.Assert(oldMatrixView.Equals(_currentMatrixView));
|
||||
DebugTools.Assert(oldRenderTarget.Equals(_currentRenderTarget));
|
||||
DebugTools.Assert(oldBoundTarget.Equals(_currentBoundRenderTarget));
|
||||
DebugTools.Assert(oldShader.Equals(_queuedShaderInstance));
|
||||
}
|
||||
|
||||
private void RenderViewport(Viewport viewport)
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
|
||||
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Client.ComponentTrees;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Graphics;
|
||||
using static Robust.Shared.GameObjects.OccluderComponent;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -279,8 +277,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
const float arbitraryDistanceMax = 1234;
|
||||
|
||||
GL.Disable(EnableCap.Blend);
|
||||
CheckGlError();
|
||||
IsBlending = false;
|
||||
|
||||
GL.Enable(EnableCap.DepthTest);
|
||||
CheckGlError();
|
||||
@@ -329,8 +326,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GL.Disable(EnableCap.DepthTest);
|
||||
CheckGlError();
|
||||
|
||||
GL.Enable(EnableCap.Blend);
|
||||
CheckGlError();
|
||||
IsBlending = true;
|
||||
}
|
||||
|
||||
private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye)
|
||||
@@ -394,21 +390,43 @@ namespace Robust.Client.Graphics.Clyde
|
||||
FinalizeDepthDraw();
|
||||
}
|
||||
|
||||
GL.Enable(EnableCap.StencilTest);
|
||||
_isStencilling = true;
|
||||
IsStencilling = true;
|
||||
|
||||
var (lightW, lightH) = GetLightMapSize(viewport.Size);
|
||||
GL.Viewport(0, 0, lightW, lightH);
|
||||
CheckGlError();
|
||||
|
||||
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
|
||||
DebugTools.Assert(_currentBoundRenderTarget.TextureHandle.Equals(viewport.LightRenderTarget.Texture.TextureId));
|
||||
CheckGlError();
|
||||
GLClearColor(_entityManager.GetComponentOrNull<MapLightComponent>(mapUid)?.AmbientLightColor ?? MapLightComponent.DefaultColor);
|
||||
|
||||
var clearEv = new GetClearColorEvent();
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local, ref clearEv);
|
||||
|
||||
var clearColor = clearEv.Color ?? GetClearColor(mapUid);
|
||||
GLClearColor(clearColor);
|
||||
GL.ClearStencil(0xFF);
|
||||
GL.StencilMask(0xFF);
|
||||
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
|
||||
CheckGlError();
|
||||
|
||||
var oldTarget = _currentRenderTarget;
|
||||
var oldProj = _currentMatrixProj;
|
||||
var oldShader = _queuedShaderInstance;
|
||||
var oldModel = _currentMatrixModel;
|
||||
var oldScissor = _currentScissorState;
|
||||
var state = PushRenderStateFull();
|
||||
|
||||
RenderOverlays(viewport, OverlaySpace.BeforeLighting, worldAABB, worldBounds);
|
||||
PopRenderStateFull(state);
|
||||
|
||||
DebugTools.Assert(oldScissor.Equals(_currentScissorState));
|
||||
DebugTools.Assert(oldModel.Equals(_currentMatrixModel));
|
||||
DebugTools.Assert(oldShader.Equals(_queuedShaderInstance));
|
||||
DebugTools.Assert(oldProj.Equals(_currentMatrixProj));
|
||||
DebugTools.Assert(oldTarget.Equals(_currentRenderTarget));
|
||||
DebugTools.Assert(_currentBoundRenderTarget.TextureHandle.Equals(viewport.LightRenderTarget.Texture.TextureId));
|
||||
|
||||
ApplyLightingFovToBuffer(viewport, eye);
|
||||
|
||||
var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle]
|
||||
@@ -509,13 +527,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
ResetBlendFunc();
|
||||
GL.Disable(EnableCap.StencilTest);
|
||||
_isStencilling = false;
|
||||
IsStencilling = false;
|
||||
|
||||
CheckGlError();
|
||||
|
||||
if (_cfg.GetCVar(CVars.LightBlur))
|
||||
BlurLights(viewport, eye);
|
||||
BlurRenderTarget(viewport, viewport.LightRenderTarget, viewport.LightBlurTarget, eye, 14f);
|
||||
|
||||
using (_prof.Group("BlurOntoWalls"))
|
||||
{
|
||||
@@ -531,9 +548,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GL.Viewport(0, 0, viewport.Size.X, viewport.Size.Y);
|
||||
CheckGlError();
|
||||
|
||||
Array.Clear(_lightsToRenderList, 0, count);
|
||||
|
||||
_lightingReady = true;
|
||||
Array.Clear(_lightsToRenderList, 0, count);
|
||||
}
|
||||
|
||||
private static bool LightQuery(ref (
|
||||
@@ -643,21 +659,33 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return (state.count, expandedBounds);
|
||||
}
|
||||
|
||||
private void BlurLights(Viewport viewport, IEye eye)
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
public Color GetClearColor(EntityUid mapUid)
|
||||
{
|
||||
using var _ = DebugGroup(nameof(BlurLights));
|
||||
return _entityManager.GetComponentOrNull<MapLightComponent>(mapUid)?.AmbientLightColor ??
|
||||
MapLightComponent.DefaultColor;
|
||||
}
|
||||
|
||||
GL.Disable(EnableCap.Blend);
|
||||
CheckGlError();
|
||||
/// <inheritdoc/>
|
||||
public void BlurRenderTarget(IClydeViewport viewport, IRenderTarget target, IRenderTarget blurBuffer, IEye eye, float multiplier)
|
||||
{
|
||||
if (target is not RenderTexture rTexture || blurBuffer is not RenderTexture blurTexture)
|
||||
return;
|
||||
|
||||
using var _ = DebugGroup(nameof(BlurRenderTarget));
|
||||
|
||||
var state = PushRenderStateFull();
|
||||
IsBlending = false;
|
||||
CalcScreenMatrices(viewport.Size, out var proj, out var view);
|
||||
SetProjViewBuffer(proj, view);
|
||||
|
||||
var shader = _loadedShaders[_lightBlurShaderHandle].Program;
|
||||
shader.Use();
|
||||
|
||||
SetupGlobalUniformsImmediate(shader, viewport.LightRenderTarget.Texture);
|
||||
SetupGlobalUniformsImmediate(shader, rTexture.Texture);
|
||||
|
||||
var size = viewport.LightRenderTarget.Size;
|
||||
var size = target.Size;
|
||||
shader.SetUniformMaybe("size", (Vector2)size);
|
||||
shader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
|
||||
|
||||
@@ -667,14 +695,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Initially we're pulling from the light render target.
|
||||
// So we set it out of the loop so
|
||||
// _wallBleedIntermediateRenderTarget2 gets bound at the end of the loop body.
|
||||
SetTexture(TextureUnit.Texture0, viewport.LightRenderTarget.Texture);
|
||||
SetTexture(TextureUnit.Texture0, rTexture.Texture);
|
||||
|
||||
// Have to scale the blurring radius based on viewport size and camera zoom.
|
||||
const float refCameraHeight = 14;
|
||||
var facBase = _cfg.GetCVar(CVars.LightBlurFactor);
|
||||
var cameraSize = eye.Zoom.Y * viewport.Size.Y * (1 / viewport.RenderScale.Y) / EyeManager.PixelsPerMeter;
|
||||
// 7e-3f is just a magic factor that makes it look ok.
|
||||
var factor = facBase * (refCameraHeight / cameraSize);
|
||||
var factor = facBase * (multiplier / cameraSize);
|
||||
|
||||
// Multi-iteration gaussian blur.
|
||||
for (var i = 3; i > 0; i--)
|
||||
@@ -683,35 +710,31 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Set factor.
|
||||
shader.SetUniformMaybe("radius", scale);
|
||||
|
||||
BindRenderTargetFull(viewport.LightBlurTarget);
|
||||
BindRenderTargetImmediate(RtToLoaded(blurBuffer));
|
||||
|
||||
// Blur horizontally to _wallBleedIntermediateRenderTarget1.
|
||||
shader.SetUniformMaybe("direction", Vector2.UnitX);
|
||||
_drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader);
|
||||
|
||||
SetTexture(TextureUnit.Texture0, viewport.LightBlurTarget.Texture);
|
||||
SetTexture(TextureUnit.Texture0, blurTexture.Texture);
|
||||
|
||||
BindRenderTargetFull(viewport.LightRenderTarget);
|
||||
BindRenderTargetImmediate(RtToLoaded(rTexture));
|
||||
|
||||
// Blur vertically to _wallBleedIntermediateRenderTarget2.
|
||||
shader.SetUniformMaybe("direction", Vector2.UnitY);
|
||||
_drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader);
|
||||
|
||||
SetTexture(TextureUnit.Texture0, viewport.LightRenderTarget.Texture);
|
||||
SetTexture(TextureUnit.Texture0, rTexture.Texture);
|
||||
}
|
||||
|
||||
GL.Enable(EnableCap.Blend);
|
||||
CheckGlError();
|
||||
// We didn't trample over the old _currentMatrices so just roll it back.
|
||||
SetProjViewBuffer(_currentMatrixProj, _currentMatrixView);
|
||||
PopRenderStateFull(state);
|
||||
}
|
||||
|
||||
private void BlurOntoWalls(Viewport viewport, IEye eye)
|
||||
{
|
||||
using var _ = DebugGroup(nameof(BlurOntoWalls));
|
||||
|
||||
GL.Disable(EnableCap.Blend);
|
||||
CheckGlError();
|
||||
IsBlending = false;
|
||||
CalcScreenMatrices(viewport.Size, out var proj, out var view);
|
||||
SetProjViewBuffer(proj, view);
|
||||
|
||||
@@ -761,8 +784,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
SetTexture(TextureUnit.Texture0, viewport.WallBleedIntermediateRenderTarget2.Texture);
|
||||
}
|
||||
|
||||
GL.Enable(EnableCap.Blend);
|
||||
CheckGlError();
|
||||
IsBlending = true;
|
||||
// We didn't trample over the old _currentMatrices so just roll it back.
|
||||
SetProjViewBuffer(_currentMatrixProj, _currentMatrixView);
|
||||
}
|
||||
@@ -775,8 +797,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
GL.Viewport(0, 0, viewport.LightRenderTarget.Size.X, viewport.LightRenderTarget.Size.Y);
|
||||
CheckGlError();
|
||||
GL.Disable(EnableCap.Blend);
|
||||
CheckGlError();
|
||||
IsBlending = false;
|
||||
|
||||
var shader = _loadedShaders[_mergeWallLayerShaderHandle].Program;
|
||||
shader.Use();
|
||||
@@ -796,8 +817,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
IntPtr.Zero);
|
||||
CheckGlError();
|
||||
|
||||
GL.Enable(EnableCap.Blend);
|
||||
CheckGlError();
|
||||
IsBlending = true;
|
||||
}
|
||||
|
||||
private void ApplyFovToBuffer(Viewport viewport, IEye eye)
|
||||
@@ -827,8 +847,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
FovSetTransformAndBlit(viewport, eye.Position.Position, fovShader);
|
||||
|
||||
GL.StencilMask(0x00);
|
||||
GL.Disable(EnableCap.StencilTest);
|
||||
_isStencilling = false;
|
||||
IsStencilling = false;
|
||||
}
|
||||
|
||||
private void ApplyLightingFovToBuffer(Viewport viewport, IEye eye)
|
||||
@@ -1135,22 +1154,20 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var lightMapSize = GetLightMapSize(viewport.Size);
|
||||
var lightMapSizeQuart = GetLightMapSize(viewport.Size, true);
|
||||
var lightMapColorFormat = _hasGLFloatFramebuffers
|
||||
? RenderTargetColorFormat.R11FG11FB10F
|
||||
: RenderTargetColorFormat.Rgba8;
|
||||
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
|
||||
|
||||
viewport.LightRenderTarget?.Dispose();
|
||||
viewport.WallMaskRenderTarget?.Dispose();
|
||||
viewport.WallBleedIntermediateRenderTarget1?.Dispose();
|
||||
viewport.WallBleedIntermediateRenderTarget2?.Dispose();
|
||||
var lightMapColorFormat = _hasGLFloatFramebuffers
|
||||
? RenderTargetColorFormat.R11FG11FB10F
|
||||
: RenderTargetColorFormat.Rgba8;
|
||||
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
|
||||
|
||||
viewport.WallMaskRenderTarget = CreateRenderTarget(viewport.Size, RenderTargetColorFormat.R8,
|
||||
name: $"{viewport.Name}-{nameof(viewport.WallMaskRenderTarget)}");
|
||||
|
||||
viewport.LightRenderTarget = CreateRenderTarget(lightMapSize,
|
||||
new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: true),
|
||||
lightMapSampleParameters,
|
||||
viewport.LightRenderTarget = (RenderTexture) CreateLightRenderTarget(lightMapSize,
|
||||
$"{viewport.Name}-{nameof(viewport.LightRenderTarget)}");
|
||||
|
||||
viewport.LightBlurTarget = CreateRenderTarget(lightMapSize,
|
||||
@@ -1158,11 +1175,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
lightMapSampleParameters,
|
||||
$"{viewport.Name}-{nameof(viewport.LightBlurTarget)}");
|
||||
|
||||
viewport.WallBleedIntermediateRenderTarget1 = CreateRenderTarget(lightMapSizeQuart, lightMapColorFormat,
|
||||
viewport.WallBleedIntermediateRenderTarget1 = CreateRenderTarget(lightMapSizeQuart,
|
||||
new RenderTargetFormatParameters(lightMapColorFormat),
|
||||
lightMapSampleParameters,
|
||||
$"{viewport.Name}-{nameof(viewport.WallBleedIntermediateRenderTarget1)}");
|
||||
|
||||
viewport.WallBleedIntermediateRenderTarget2 = CreateRenderTarget(lightMapSizeQuart, lightMapColorFormat,
|
||||
viewport.WallBleedIntermediateRenderTarget2 = CreateRenderTarget(lightMapSizeQuart,
|
||||
new RenderTargetFormatParameters(lightMapColorFormat),
|
||||
lightMapSampleParameters,
|
||||
$"{viewport.Name}-{nameof(viewport.WallBleedIntermediateRenderTarget2)}");
|
||||
}
|
||||
|
||||
@@ -30,6 +30,20 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// It, like _mainWindowRenderTarget, is initialized in Clyde's constructor
|
||||
private LoadedRenderTarget _currentBoundRenderTarget;
|
||||
|
||||
|
||||
public IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true)
|
||||
{
|
||||
var lightMapColorFormat = _hasGLFloatFramebuffers
|
||||
? RTCF.R11FG11FB10F
|
||||
: RTCF.Rgba8;
|
||||
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
|
||||
|
||||
return CreateRenderTarget(size,
|
||||
new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: depthStencil),
|
||||
lightMapSampleParameters,
|
||||
name: name);
|
||||
}
|
||||
|
||||
IRenderTexture IClyde.CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
|
||||
TextureSampleParameters? sampleParameters, string? name)
|
||||
{
|
||||
@@ -204,7 +218,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
Size = size,
|
||||
TextureHandle = textureObject.TextureId,
|
||||
MemoryPressure = pressure,
|
||||
ColorFormat = format.ColorFormat
|
||||
ColorFormat = format.ColorFormat,
|
||||
SampleParameters = sampleParameters,
|
||||
};
|
||||
|
||||
//GC.AddMemoryPressure(pressure);
|
||||
@@ -251,9 +266,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private LoadedRenderTarget RtToLoaded(RenderTargetBase rt)
|
||||
private LoadedRenderTarget RtToLoaded(IRenderTarget rt)
|
||||
{
|
||||
return _renderTargets[rt.Handle];
|
||||
switch (rt)
|
||||
{
|
||||
case RenderTargetBase based:
|
||||
return _renderTargets[based.Handle];
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -302,6 +323,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Renderbuffer handle
|
||||
public GLHandle DepthStencilHandle;
|
||||
public long MemoryPressure;
|
||||
|
||||
public TextureSampleParameters? SampleParameters;
|
||||
}
|
||||
|
||||
private abstract class RenderTargetBase : IRenderTarget
|
||||
|
||||
@@ -90,9 +90,61 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// (queue) and (misc), current state of the scissor test. Null if disabled.
|
||||
private UIBox2i? _currentScissorState;
|
||||
|
||||
// Some simple flags that basically just tracks the current state of glEnable(GL_STENCIL/GL_SCISSOR_TEST)
|
||||
private bool _isScissoring;
|
||||
private bool _isStencilling;
|
||||
/// <summary>
|
||||
/// Tracks enabled GL capabilities for renderer state.
|
||||
/// </summary>
|
||||
private GLCaps _glCaps = GLCaps.None;
|
||||
|
||||
private bool IsStencilling
|
||||
{
|
||||
get => (_glCaps & GLCaps.Stencilling) == GLCaps.Stencilling;
|
||||
set
|
||||
{
|
||||
if (value == IsStencilling)
|
||||
return;
|
||||
|
||||
if (value)
|
||||
{
|
||||
_glCaps |= GLCaps.Stencilling;
|
||||
GL.Enable(EnableCap.StencilTest);
|
||||
}
|
||||
else
|
||||
{
|
||||
_glCaps &= ~GLCaps.Stencilling;
|
||||
GL.Disable(EnableCap.StencilTest);
|
||||
}
|
||||
|
||||
CheckGlError();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsBlending
|
||||
{
|
||||
get => (_glCaps & GLCaps.Blending) == GLCaps.Blending;
|
||||
set
|
||||
{
|
||||
if (value == IsBlending)
|
||||
return;
|
||||
|
||||
if (value)
|
||||
{
|
||||
_glCaps |= GLCaps.Blending;
|
||||
GL.Enable(EnableCap.Blend);
|
||||
}
|
||||
else
|
||||
{
|
||||
_glCaps &= ~GLCaps.Blending;
|
||||
GL.Disable(EnableCap.Blend);
|
||||
}
|
||||
|
||||
CheckGlError();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsScissoring
|
||||
{
|
||||
get => _currentScissorState != null;
|
||||
}
|
||||
|
||||
private readonly RefList<RenderCommand> _queuedRenderCommands = new RefList<RenderCommand>();
|
||||
|
||||
@@ -364,16 +416,17 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void SetScissorImmediate(bool enable, in UIBox2i box)
|
||||
{
|
||||
var oldIsScissoring = _isScissoring;
|
||||
_isScissoring = enable;
|
||||
if (_isScissoring)
|
||||
if (enable)
|
||||
{
|
||||
if (!oldIsScissoring)
|
||||
{
|
||||
GL.Enable(EnableCap.ScissorTest);
|
||||
CheckGlError();
|
||||
}
|
||||
GL.Enable(EnableCap.ScissorTest);
|
||||
}
|
||||
else
|
||||
{
|
||||
GL.Disable(EnableCap.ScissorTest);
|
||||
}
|
||||
|
||||
if (enable)
|
||||
{
|
||||
// Don't forget to flip it, these coordinates have bottom left as origin.
|
||||
// TODO: Broken when rendering to non-screen render targets.
|
||||
|
||||
@@ -387,11 +440,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
CheckGlError();
|
||||
}
|
||||
else if (oldIsScissoring)
|
||||
{
|
||||
GL.Disable(EnableCap.ScissorTest);
|
||||
CheckGlError();
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: sRGB IS IN LINEAR IF FRAMEBUFFER_SRGB IS ACTIVE.
|
||||
@@ -420,17 +468,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var program = shader.Program;
|
||||
|
||||
program.Use();
|
||||
IsStencilling = instance.Stencil.Enabled;
|
||||
|
||||
// Handle stencil parameters.
|
||||
if (instance.Stencil.Enabled)
|
||||
{
|
||||
if (!_isStencilling)
|
||||
{
|
||||
GL.Enable(EnableCap.StencilTest);
|
||||
CheckGlError();
|
||||
_isStencilling = true;
|
||||
}
|
||||
|
||||
GL.StencilMask(instance.Stencil.WriteMask);
|
||||
CheckGlError();
|
||||
GL.StencilFunc(ToGLStencilFunc(instance.Stencil.Func), instance.Stencil.Ref, instance.Stencil.ReadMask);
|
||||
@@ -438,12 +480,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, ToGLStencilOp(instance.Stencil.Op));
|
||||
CheckGlError();
|
||||
}
|
||||
else if (_isStencilling)
|
||||
{
|
||||
GL.Disable(EnableCap.StencilTest);
|
||||
CheckGlError();
|
||||
_isStencilling = false;
|
||||
}
|
||||
|
||||
if (instance.Parameters.Count == 0)
|
||||
return (program, instance);
|
||||
@@ -859,17 +895,34 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private FullStoredRendererState PushRenderStateFull()
|
||||
{
|
||||
return new FullStoredRendererState(_currentMatrixProj, _currentMatrixView, _currentRenderTarget);
|
||||
return new FullStoredRendererState(
|
||||
_currentMatrixProj,
|
||||
_currentMatrixView,
|
||||
_currentBoundRenderTarget,
|
||||
_currentRenderTarget,
|
||||
_queuedShaderInstance,
|
||||
_currentScissorState,
|
||||
_glCaps);
|
||||
}
|
||||
|
||||
private void PopRenderStateFull(in FullStoredRendererState state)
|
||||
{
|
||||
SetProjViewFull(state.ProjMatrix, state.ViewMatrix);
|
||||
BindRenderTargetFull(state.RenderTarget);
|
||||
BindRenderTargetImmediate(state.BoundRenderTarget);
|
||||
|
||||
var (width, height) = state.RenderTarget.Size;
|
||||
_queuedShaderInstance = state.QueuedShaderInstance;
|
||||
_currentRenderTarget = state.RenderTarget;
|
||||
var (width, height) = state.BoundRenderTarget.Size;
|
||||
GL.Viewport(0, 0, width, height);
|
||||
CheckGlError();
|
||||
|
||||
IsStencilling = (state.GLCaps & GLCaps.Stencilling) == GLCaps.Stencilling;
|
||||
IsBlending = (state.GLCaps & GLCaps.Blending) == GLCaps.Blending;
|
||||
|
||||
SetScissorFull(state.ScissorState);
|
||||
|
||||
GL.ClearStencil(0xFF);
|
||||
GL.StencilMask(0xFF);
|
||||
GL.Clear(ClearBufferMask.StencilBufferBit);
|
||||
}
|
||||
|
||||
private void SetViewportImmediate(Box2i box)
|
||||
@@ -1061,15 +1114,44 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
public readonly Matrix3x2 ProjMatrix;
|
||||
public readonly Matrix3x2 ViewMatrix;
|
||||
public readonly LoadedRenderTarget BoundRenderTarget;
|
||||
public readonly LoadedRenderTarget RenderTarget;
|
||||
public readonly ClydeShaderInstance QueuedShaderInstance;
|
||||
|
||||
public FullStoredRendererState(in Matrix3x2 projMatrix, in Matrix3x2 viewMatrix,
|
||||
LoadedRenderTarget renderTarget)
|
||||
public readonly UIBox2i? ScissorState;
|
||||
|
||||
public readonly GLCaps GLCaps;
|
||||
|
||||
public FullStoredRendererState(
|
||||
in Matrix3x2 projMatrix,
|
||||
in Matrix3x2 viewMatrix,
|
||||
LoadedRenderTarget boundRenderTarget,
|
||||
LoadedRenderTarget renderTarget,
|
||||
ClydeShaderInstance queuedShaderInstance,
|
||||
UIBox2i? scissorState,
|
||||
GLCaps glcaps
|
||||
)
|
||||
{
|
||||
ProjMatrix = projMatrix;
|
||||
ViewMatrix = viewMatrix;
|
||||
BoundRenderTarget = boundRenderTarget;
|
||||
RenderTarget = renderTarget;
|
||||
QueuedShaderInstance = queuedShaderInstance;
|
||||
|
||||
ScissorState = scissorState;
|
||||
GLCaps = glcaps;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum GLCaps : ushort
|
||||
{
|
||||
// If you add flags here make sure to update PopRenderState!
|
||||
None = 0,
|
||||
|
||||
Blending = 1 << 0,
|
||||
|
||||
Stencilling = 1 << 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
|
||||
|
||||
@@ -245,7 +246,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
overrideVersion != null,
|
||||
_windowing!.GetDescription());
|
||||
|
||||
GL.Enable(EnableCap.Blend);
|
||||
IsBlending = true;
|
||||
if (_hasGLSrgb && !_isGLES)
|
||||
{
|
||||
GL.Enable(EnableCap.FramebufferSrgb);
|
||||
|
||||
@@ -9,6 +9,7 @@ using Robust.Client.Input;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -189,6 +190,22 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return new DummyTexture(size);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Color GetClearColor(EntityUid mapUid)
|
||||
{
|
||||
return Color.Transparent;
|
||||
}
|
||||
|
||||
public void BlurRenderTarget(IClydeViewport viewport, IRenderTarget target, IRenderTarget blurBuffer, IEye eye, float multiplier)
|
||||
{
|
||||
// NOOP
|
||||
}
|
||||
|
||||
public IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true)
|
||||
{
|
||||
return CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.R8, hasDepthStencil: depthStencil), null, name: name);
|
||||
}
|
||||
|
||||
public IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
|
||||
TextureSampleParameters? sampleParameters = null, string? name = null)
|
||||
{
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Maths;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
@@ -71,6 +74,24 @@ namespace Robust.Client.Graphics
|
||||
in TextureLoadParameters? loadParams = null)
|
||||
where T : unmanaged, IPixel<T>;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the clear color for the specified map viewport.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
Color GetClearColor(EntityUid mapUid);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a blur to the specified render target. Requires a separate buffer with similar properties to draw intermediate steps into.
|
||||
/// </summary>
|
||||
/// <param name="viewport">The viewport being used for drawing.</param>
|
||||
/// <param name="target">The blur target.</param>
|
||||
/// <param name="blurBuffer">The separate buffer to draw into.</param>
|
||||
/// <param name="eye">The eye being drawn with.</param>
|
||||
/// <param name="multiplier">Scale of how much blur to blur by.</param>
|
||||
void BlurRenderTarget(IClydeViewport viewport, IRenderTarget target, IRenderTarget blurBuffer, IEye eye, float multiplier);
|
||||
|
||||
IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true);
|
||||
|
||||
IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
|
||||
TextureSampleParameters? sampleParameters = null, string? name = null);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
IRenderTexture RenderTarget { get; }
|
||||
IRenderTexture LightRenderTarget { get; }
|
||||
|
||||
IEye? Eye { get; set; }
|
||||
Vector2i Size { get; }
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Maths;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
@@ -15,5 +17,42 @@ namespace Robust.Client.Graphics
|
||||
Vector2i Size { get; }
|
||||
|
||||
void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion = null) where T : unmanaged, IPixel<T>;
|
||||
|
||||
public Vector2 LocalToWorld(IEye eye, Vector2 point, Vector2 scale)
|
||||
{
|
||||
var newPoint = point;
|
||||
|
||||
// (inlined version of UiProjMatrix^-1)
|
||||
newPoint -= Size / 2f;
|
||||
newPoint *= new Vector2(1, -1) / EyeManager.PixelsPerMeter;
|
||||
|
||||
// view matrix
|
||||
eye.GetViewMatrixInv(out var viewMatrixInv, scale);
|
||||
newPoint = Vector2.Transform(newPoint, viewMatrixInv);
|
||||
|
||||
return newPoint;
|
||||
}
|
||||
|
||||
public Vector2 WorldToLocal(Vector2 point, IEye eye, Vector2 scale)
|
||||
{
|
||||
var newPoint = point;
|
||||
|
||||
eye.GetViewMatrix(out var viewMatrix, scale);
|
||||
newPoint = Vector2.Transform(newPoint, viewMatrix);
|
||||
|
||||
// (inlined version of UiProjMatrix)
|
||||
newPoint *= new Vector2(1, -1) * EyeManager.PixelsPerMeter;
|
||||
newPoint += Size / 2f;
|
||||
|
||||
return newPoint;
|
||||
}
|
||||
|
||||
public Matrix3x2 GetWorldToLocalMatrix(IEye eye, Vector2 scale)
|
||||
{
|
||||
eye.GetViewMatrix(out var viewMatrix, scale * new Vector2(EyeManager.PixelsPerMeter, -EyeManager.PixelsPerMeter));
|
||||
viewMatrix.M31 += Size.X / 2f;
|
||||
viewMatrix.M32 += Size.Y / 2f;
|
||||
return viewMatrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
Robust.Client/Graphics/Lighting/GetClearColorEvent.cs
Normal file
13
Robust.Client/Graphics/Lighting/GetClearColorEvent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Raised by the engine if content wishes to override the default clear color.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct GetClearColorEvent
|
||||
{
|
||||
public Color? Color;
|
||||
}
|
||||
@@ -9,6 +9,5 @@ namespace Robust.Client.Graphics
|
||||
public bool DrawHardFov { get; set; } = true;
|
||||
public bool DrawLighting { get; set; } = true;
|
||||
public bool LockConsoleAccess { get; set; } = false;
|
||||
public Color AmbientLightColor { get; set; } = Color.FromSrgb(Color.Black);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
@@ -39,6 +40,8 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
public readonly UIBox2i ViewportBounds;
|
||||
|
||||
public readonly EntityUid MapUid;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="MapId"/> of the viewport's eye.
|
||||
/// </summary>
|
||||
@@ -65,6 +68,7 @@ namespace Robust.Client.Graphics
|
||||
IClydeViewport viewport,
|
||||
IRenderHandle renderHandle,
|
||||
in UIBox2i viewportBounds,
|
||||
in EntityUid mapUid,
|
||||
in MapId mapId,
|
||||
in Box2 worldAabb,
|
||||
in Box2Rotated worldBounds)
|
||||
@@ -78,6 +82,7 @@ namespace Robust.Client.Graphics
|
||||
Viewport = viewport;
|
||||
RenderHandle = renderHandle;
|
||||
ViewportBounds = viewportBounds;
|
||||
MapUid = mapUid;
|
||||
MapId = mapId;
|
||||
WorldAABB = worldAabb;
|
||||
WorldBounds = worldBounds;
|
||||
|
||||
@@ -9,6 +9,7 @@ public class EntityPrototypeView : SpriteView
|
||||
{
|
||||
private string? _currentPrototype;
|
||||
private EntityUid? _ourEntity;
|
||||
private bool _isShowing;
|
||||
|
||||
public EntityPrototypeView()
|
||||
{
|
||||
@@ -31,7 +32,7 @@ public class EntityPrototypeView : SpriteView
|
||||
|
||||
_currentPrototype = entProto;
|
||||
|
||||
if (_ourEntity != null)
|
||||
if (_ourEntity != null || _isShowing)
|
||||
{
|
||||
UpdateEntity();
|
||||
}
|
||||
@@ -45,6 +46,8 @@ public class EntityPrototypeView : SpriteView
|
||||
{
|
||||
UpdateEntity();
|
||||
}
|
||||
|
||||
_isShowing = true;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
@@ -52,6 +55,8 @@ public class EntityPrototypeView : SpriteView
|
||||
base.ExitedTree();
|
||||
EntMan.TryQueueDeleteEntity(_ourEntity);
|
||||
_ourEntity = null;
|
||||
|
||||
_isShowing = false;
|
||||
}
|
||||
|
||||
private void UpdateEntity()
|
||||
|
||||
@@ -104,6 +104,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
var entity = SetupAudio(filename, 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -50,6 +50,9 @@ public sealed class GamePrototypeLoadManager : SharedPrototypeLoadManager
|
||||
|
||||
internal void SendToNewUser(INetChannel channel)
|
||||
{
|
||||
if (LoadedPrototypes.Count == 0)
|
||||
return;
|
||||
|
||||
// Just dump all the prototypes on connect, before them missing could be an issue.
|
||||
var msg = new GamePrototypeLoadMessage
|
||||
{
|
||||
|
||||
@@ -37,6 +37,8 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
[Dependency] protected readonly MetaDataSystem MetadataSys = default!;
|
||||
[Dependency] protected readonly SharedTransformSystem XformSystem = default!;
|
||||
|
||||
private const float AudioDespawnBuffer = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Default max range at which the sound can be heard.
|
||||
/// </summary>
|
||||
@@ -234,7 +236,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
{
|
||||
var timed = EnsureComp<TimedDespawnComponent>(entity.Value);
|
||||
var audioLength = GetAudioLength(component.FileName);
|
||||
timed.Lifetime = (float) audioLength.TotalSeconds + 0.01f;
|
||||
timed.Lifetime = (float) audioLength.TotalSeconds + AudioDespawnBuffer;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -322,7 +324,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
|
||||
var despawn = AddComp<TimedDespawnComponent>(uid);
|
||||
// Don't want to clip audio too short due to imprecision.
|
||||
despawn.Lifetime = (float) length.Value.TotalSeconds + 0.01f;
|
||||
despawn.Lifetime = (float) length.Value.TotalSeconds + AudioDespawnBuffer;
|
||||
}
|
||||
|
||||
if (comp.Params.Variation != null && comp.Params.Variation.Value != 0f)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,58 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Configuration
|
||||
{
|
||||
public static class CVarCommandUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a string into an object of the given type.
|
||||
/// </summary>
|
||||
/// <exception cref="FormatException">Thrown if the string could not be parsed into the given type.</exception>
|
||||
/// <exception cref="NotSupportedException">Thrown if the type is not supported.</exception>
|
||||
public static object ParseObject(Type type, string input)
|
||||
{
|
||||
if (type == typeof(bool))
|
||||
{
|
||||
if (bool.TryParse(input, out var val))
|
||||
return val;
|
||||
|
||||
if (Parse.TryInt32(input, out var intVal))
|
||||
{
|
||||
if (intVal == 0) return false;
|
||||
if (intVal == 1) return true;
|
||||
}
|
||||
|
||||
throw new FormatException($"Could not parse bool value: {input}");
|
||||
}
|
||||
|
||||
if (type == typeof(string))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
if (type == typeof(int))
|
||||
{
|
||||
return Parse.Int32(input);
|
||||
}
|
||||
|
||||
if (type == typeof(float))
|
||||
{
|
||||
return Parse.Float(input);
|
||||
}
|
||||
|
||||
if (type == typeof(long))
|
||||
{
|
||||
return long.Parse(input);
|
||||
}
|
||||
|
||||
if (type == typeof(ushort))
|
||||
{
|
||||
return ushort.Parse(input);
|
||||
}
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
||||
internal sealed class CVarCommand : LocalizedCommands
|
||||
{
|
||||
@@ -51,7 +103,7 @@ namespace Robust.Shared.Configuration
|
||||
var type = _cfg.GetCVarType(name);
|
||||
try
|
||||
{
|
||||
var parsed = ParseObject(type, value);
|
||||
var parsed = CVarCommandUtil.ParseObject(type, value);
|
||||
_cfg.SetCVar(name, parsed);
|
||||
}
|
||||
catch (FormatException)
|
||||
@@ -95,50 +147,6 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static object ParseObject(Type type, string input)
|
||||
{
|
||||
if (type == typeof(bool))
|
||||
{
|
||||
if (bool.TryParse(input, out var val))
|
||||
return val;
|
||||
|
||||
if (Parse.TryInt32(input, out var intVal))
|
||||
{
|
||||
if (intVal == 0) return false;
|
||||
if (intVal == 1) return true;
|
||||
}
|
||||
|
||||
throw new FormatException($"Could not parse bool value: {input}");
|
||||
}
|
||||
|
||||
if (type == typeof(string))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
if (type == typeof(int))
|
||||
{
|
||||
return Parse.Int32(input);
|
||||
}
|
||||
|
||||
if (type == typeof(float))
|
||||
{
|
||||
return Parse.Float(input);
|
||||
}
|
||||
|
||||
if (type == typeof(long))
|
||||
{
|
||||
return long.Parse(input);
|
||||
}
|
||||
|
||||
if (type == typeof(ushort))
|
||||
{
|
||||
return ushort.Parse(input);
|
||||
}
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CVarSubsCommand : LocalizedCommands
|
||||
|
||||
@@ -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(Path.DirectorySeparatorChar))
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
985
Robust.Shared/EntitySerialization/EntitySerializer.cs
Normal file
985
Robust.Shared/EntitySerialization/EntitySerializer.cs
Normal file
@@ -0,0 +1,985 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 frids?");
|
||||
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)
|
||||
{
|
||||
@@ -73,7 +77,7 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
for (ushort x = 0; x < chunk.ChunkSize; x++)
|
||||
{
|
||||
var id = version < 6 ? reader.ReadUInt16() : reader.ReadInt32();
|
||||
var flags = (TileRenderFlag)reader.ReadByte();
|
||||
var flags = reader.ReadByte();
|
||||
var variant = reader.ReadByte();
|
||||
|
||||
var defName = tileMap[id];
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -54,6 +54,11 @@ namespace Robust.Shared.Enums
|
||||
/// <summary>
|
||||
/// Overlay will be rendered below grids, entities, and everything else. In world space.
|
||||
/// </summary>
|
||||
WorldSpaceBelowWorld = 1 << 8
|
||||
WorldSpaceBelowWorld = 1 << 8,
|
||||
|
||||
/// <summary>
|
||||
/// Called after GLClear but before FOV applied to the lighting buffer.
|
||||
/// </summary>
|
||||
BeforeLighting = 1 << 9,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// Invoked when the UI is opened.
|
||||
/// Do all creation and opening of things like windows in here.
|
||||
/// </summary>
|
||||
[MustCallBase]
|
||||
protected internal virtual void Open()
|
||||
{
|
||||
if (IsOpened)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -119,7 +119,8 @@ public abstract partial class SharedTransformSystem
|
||||
|
||||
public bool AnchorEntity(Entity<TransformComponent> entity, Entity<MapGridComponent>? grid = null)
|
||||
{
|
||||
DebugTools.Assert(grid == null || grid.Value.Owner == entity.Comp.GridUid);
|
||||
DebugTools.Assert(grid == null || grid.Value.Owner == entity.Comp.GridUid,
|
||||
$"Tried to anchor entity {Name(entity)} to a grid ({grid!.Value.Owner}) different from its GridUid ({entity.Comp.GridUid})");
|
||||
|
||||
if (grid == null)
|
||||
{
|
||||
@@ -213,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);
|
||||
|
||||
|
||||
@@ -41,13 +41,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
/// </summary>
|
||||
private readonly List<(BoundUserInterface Bui, bool value)> _queuedBuis = new();
|
||||
|
||||
/// <summary>
|
||||
/// Temporary storage for BUI keys
|
||||
/// </summary>
|
||||
private ValueList<Enum> _keys = new();
|
||||
|
||||
private ValueList<EntityUid> _entList = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -288,11 +281,12 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
|
||||
protected virtual void OnUserInterfaceShutdown(Entity<UserInterfaceComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
var ents = new ValueList<EntityUid>();
|
||||
foreach (var (key, acts) in ent.Comp.Actors)
|
||||
{
|
||||
_entList.Clear();
|
||||
_entList.AddRange(acts);
|
||||
foreach (var actor in _entList)
|
||||
ents.Clear();
|
||||
ents.AddRange(acts);
|
||||
foreach (var actor in ents)
|
||||
{
|
||||
CloseUiInternal(ent!, key, actor);
|
||||
DebugTools.Assert(!acts.Contains(actor));
|
||||
@@ -897,12 +891,13 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (actor.Comp.OpenInterfaces.Count == 0)
|
||||
return;
|
||||
|
||||
var keys = new ValueList<Enum>();
|
||||
foreach (var (uid, enums) in actor.Comp.OpenInterfaces)
|
||||
{
|
||||
_keys.Clear();
|
||||
_keys.AddRange(enums);
|
||||
keys.Clear();
|
||||
keys.AddRange(enums);
|
||||
|
||||
foreach (var weh in _keys)
|
||||
foreach (var weh in keys)
|
||||
{
|
||||
if (weh is not T)
|
||||
continue;
|
||||
@@ -923,12 +918,14 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (actor.Comp.OpenInterfaces.Count == 0)
|
||||
return;
|
||||
|
||||
var keys = new ValueList<Enum>();
|
||||
|
||||
foreach (var (uid, enums) in actor.Comp.OpenInterfaces)
|
||||
{
|
||||
_keys.Clear();
|
||||
_keys.AddRange(enums);
|
||||
keys.Clear();
|
||||
keys.AddRange(enums);
|
||||
|
||||
foreach (var key in _keys)
|
||||
foreach (var key in keys)
|
||||
{
|
||||
CloseUiInternal(uid, key, actor.Owner);
|
||||
}
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -17,7 +17,6 @@ public sealed partial class MapLightComponent : Component
|
||||
/// <summary>
|
||||
/// Ambient light. This is in linear-light, i.e. when providing a fixed colour, you must use Color.FromSrgb(Color.Black)!
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("ambientLightColor")]
|
||||
public Color AmbientLightColor { get; set; } = Color.FromSrgb(Color.Black);
|
||||
[DataField]
|
||||
public Color AmbientLightColor = Color.FromSrgb(Color.Black);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Map;
|
||||
@@ -16,9 +17,9 @@ public readonly struct Tile : IEquatable<Tile>, ISpanFormattable
|
||||
public readonly int TypeId;
|
||||
|
||||
/// <summary>
|
||||
/// Rendering flags.
|
||||
/// Custom flags for additional tile-data.
|
||||
/// </summary>
|
||||
public readonly TileRenderFlag Flags;
|
||||
public readonly byte Flags;
|
||||
|
||||
/// <summary>
|
||||
/// Variant of this tile to render.
|
||||
@@ -39,9 +40,9 @@ public readonly struct Tile : IEquatable<Tile>, ISpanFormattable
|
||||
/// Creates a new instance of a grid tile.
|
||||
/// </summary>
|
||||
/// <param name="typeId">Internal type ID.</param>
|
||||
/// <param name="flags">Flags used by toolbox's rendering.</param>
|
||||
/// <param name="flags">Custom tile flags for usage.</param>
|
||||
/// <param name="variant">The visual variant this tile is using.</param>
|
||||
public Tile(int typeId, TileRenderFlag flags = 0, byte variant = 0)
|
||||
public Tile(int typeId, byte flags = 0, byte variant = 0)
|
||||
{
|
||||
TypeId = typeId;
|
||||
Flags = flags;
|
||||
@@ -112,9 +113,19 @@ public readonly struct Tile : IEquatable<Tile>, ISpanFormattable
|
||||
return (TypeId.GetHashCode() * 397) ^ Flags.GetHashCode() ^ Variant.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public Tile WithVariant(byte variant)
|
||||
{
|
||||
return new Tile(TypeId, Flags, variant);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public Tile WithFlag(byte flag)
|
||||
{
|
||||
return new Tile(TypeId, flags: flag, variant: Variant);
|
||||
}
|
||||
}
|
||||
|
||||
public enum TileRenderFlag : byte
|
||||
{
|
||||
public sealed class TileFlagLayer {}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,8 +4,7 @@ using Robust.Shared.ViewVariables;
|
||||
namespace Robust.Shared.Prototypes;
|
||||
|
||||
/// <summary>
|
||||
/// Prototype that represents an alias from one tile ID to another.
|
||||
/// Tile alias prototypes, unlike tile prototypes, are implemented here, as they're really just fed to TileDefinitionManager.
|
||||
/// Prototype that represents an alias from one tile ID to another. These are used when deserializing entities from yaml.
|
||||
/// </summary>
|
||||
[Prototype("tileAlias")]
|
||||
public sealed partial class TileAliasPrototype : IPrototype
|
||||
@@ -13,13 +12,13 @@ public sealed partial class TileAliasPrototype : IPrototype
|
||||
/// <summary>
|
||||
/// The target tile ID to alias to.
|
||||
/// </summary>
|
||||
[DataField("target")]
|
||||
[DataField]
|
||||
public string Target { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The source tile ID (and the ID of this tile alias).
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[IdDataFieldAttribute]
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
|
||||
namespace Robust.Shared.Prototypes;
|
||||
|
||||
internal sealed class YamlValidationContext : ISerializationContext, ITypeSerializer<EntityUid, ValueDataNode>
|
||||
internal sealed class YamlValidationContext :
|
||||
ISerializationContext,
|
||||
ITypeSerializer<EntityUid, ValueDataNode>,
|
||||
ITypeSerializer<NetEntity, ValueDataNode>
|
||||
{
|
||||
public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
|
||||
public bool WritingReadingPrototypes => true;
|
||||
@@ -24,7 +27,7 @@ internal sealed class YamlValidationContext : ISerializationContext, ITypeSerial
|
||||
ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(ISerializationManager serializationManager,
|
||||
ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context)
|
||||
{
|
||||
if (node.Value == "null" || node.Value == "invalid")
|
||||
if (node.Value == "invalid")
|
||||
return new ValidatedValueNode(node);
|
||||
|
||||
return new ErrorNode(node, "Prototypes should not contain EntityUids", true);
|
||||
@@ -52,11 +55,42 @@ internal sealed class YamlValidationContext : ISerializationContext, ITypeSerial
|
||||
return EntityUid.Parse(node.Value);
|
||||
}
|
||||
|
||||
[MustUseReturnValue]
|
||||
public EntityUid Copy(ISerializationManager serializationManager, EntityUid source, EntityUid target,
|
||||
bool skipHook,
|
||||
public ValidationNode Validate(
|
||||
ISerializationManager serializationManager,
|
||||
ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
return new((int)source);
|
||||
if (node.Value == "invalid")
|
||||
return new ValidatedValueNode(node);
|
||||
|
||||
return new ErrorNode(node, "Prototypes should not contain NetEntities");
|
||||
}
|
||||
|
||||
public NetEntity Read(
|
||||
ISerializationManager serializationManager,
|
||||
ValueDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context = null,
|
||||
ISerializationManager.InstantiationDelegate<NetEntity>? instanceProvider = null)
|
||||
{
|
||||
if (node.Value == "invalid")
|
||||
return NetEntity.Invalid;
|
||||
|
||||
return NetEntity.Parse(node.Value);
|
||||
}
|
||||
|
||||
public DataNode Write(
|
||||
ISerializationManager serializationManager,
|
||||
NetEntity value,
|
||||
IDependencyCollection dependencies,
|
||||
bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
if (!value.Valid)
|
||||
return new ValueDataNode("invalid");
|
||||
|
||||
return new ValueDataNode(value.Id.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
|
||||
@@ -440,6 +441,12 @@ namespace Robust.Shared.Serialization.Manager
|
||||
return (TNode) PushComposition(type, parents, child, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple <see cref="MappingDataNode"/> inheritance pusher clones data and overrides a parent's values with
|
||||
/// the child's.
|
||||
/// </summary>
|
||||
MappingDataNode CombineMappings(MappingDataNode child, MappingDataNode parent);
|
||||
|
||||
#endregion
|
||||
|
||||
public bool TryGetVariableType(Type type, string variableName, [NotNullWhen(true)] out Type? variableType);
|
||||
|
||||
@@ -26,14 +26,23 @@ public partial class SerializationManager
|
||||
|
||||
public DataNode PushComposition(Type type, DataNode[] parents, DataNode child, ISerializationContext? context = null)
|
||||
{
|
||||
// TODO SERIALIZATION
|
||||
// Add variant that doesn't require a parent array.
|
||||
|
||||
// TODO SERIALIZATION
|
||||
// Change inheritance pushing so that it modifies the passed in child. This avoids re-creating the child
|
||||
// multiple times when there are multiple children.
|
||||
//
|
||||
// I.e., change the PushCompositionDelegate signature to not have a return value, and also add an override
|
||||
// of this method that modified the given child.
|
||||
|
||||
if (parents.Length == 0)
|
||||
return child.Copy();
|
||||
|
||||
DebugTools.Assert(parents.All(x => x.GetType() == child.GetType()));
|
||||
|
||||
// TODO SERIALIZATION
|
||||
// Change inheritance pushing so that it modifies the passed in child.
|
||||
// I.e., move the child.Clone() statement to the beginning here, then make the delegate modify the clone.
|
||||
|
||||
// the child.Clone() statement to the beginning here, then make the delegate modify the clone.
|
||||
// Currently pusing more than one parent requires multiple unnecessary clones.
|
||||
|
||||
var pusher = GetOrCreatePushCompositionDelegate(type, child);
|
||||
|
||||
@@ -57,13 +57,11 @@ namespace Robust.Shared.Serialization.Manager
|
||||
|
||||
foreach (var flagType in bitflagType.GetCustomAttributes<FlagsForAttribute>(true))
|
||||
{
|
||||
if (_flagsMapping.ContainsKey(flagType.Tag))
|
||||
if (!_flagsMapping.TryAdd(flagType.Tag, bitflagType))
|
||||
{
|
||||
throw new NotSupportedException($"Multiple bitflag enums declared for the tag {flagType.Tag}.");
|
||||
}
|
||||
|
||||
_flagsMapping.Add(flagType.Tag, bitflagType);
|
||||
|
||||
var highestBit = bitflagType
|
||||
.GetEnumValues()
|
||||
.Cast<int>()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user