Compare commits

...

13 Commits

Author SHA1 Message Date
Pieter-Jan Briers
f4edb591fe Version: 219.2.3 2024-08-11 19:54:46 +02:00
Pieter-Jan Briers
79b7dde452 Use absolute path for explorer.exe
frick me

(cherry picked from commit 0284eb0430)
2024-08-11 19:54:46 +02:00
Pieter-Jan Briers
ab99d5be07 Version: 219.2.2 2024-08-11 19:32:35 +02:00
Pieter-Jan Briers
11c05509cb Compile compat fixes
(cherry picked from commit 025d90d281)
(cherry picked from commit 799702b814)
(cherry picked from commit 4600ee8e5788891f1b610e2d5141fb4e1228d323)
2024-08-11 19:32:35 +02:00
Pieter-Jan Briers
e1120b0d4c Version: 219.2.1 2024-08-11 17:56:08 +02:00
Pieter-Jan Briers
73c22ff6ac Security updates (#5353)
* Fix security bug in WritableDirProvider.OpenOsWindow()

Reported by @NarryG and @nyeogmi

* Sandbox updates

* Update ImageSharp again

(cherry picked from commit 7d778248ee)
(cherry picked from commit f66cda74e95619ddba2221bda644bf4394619805)
(cherry picked from commit db8ba83866c523e08e4fba0b80cd954f4f190613)
2024-08-11 17:56:08 +02:00
metalgearsloth
eb63809999 Version: 219.2.0 2024-04-25 00:31:04 +10:00
Leon Friedrich
4c3c74865c Fix yaml linter & improve validation of static fields (#5056) 2024-04-25 00:27:02 +10:00
Nemanja
b624f5b70f Add SetMapCoordinates (#5065)
* add set map coordinates function

* mmmmmmm yes

* Update Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2024-04-24 22:55:58 +10:00
Pieter-Jan Briers
6566a7658a Fix DebugCoordsPanel freezing when hovering a UI control.
It would bail out of the entire update logic if you aren't hovering over a map position, which isn't great when the control displays far more than in-simulation mouse position info.
2024-04-23 20:34:29 +02:00
metalgearsloth
9e3e1cc929 Optimise physics networking a lot (#5064)
Avoids unnecessarily dirtying every single tick when a mob is moving. Also avoids the getcomponent every time which is nice.
2024-04-23 22:36:40 +10:00
ElectroJr
4e87d93009 Version: 219.1.3 2024-04-23 00:26:17 -04:00
Leon Friedrich
1031ae4cc5 Fix mapping not pausing maps (#5063) 2024-04-23 14:25:40 +10:00
22 changed files with 559 additions and 180 deletions

View File

@@ -33,10 +33,10 @@ jobs:
mkdir "release/${{ steps.parse_version.outputs.version }}"
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
- name: Upload files to centcomm
- name: Upload files to Suns
uses: appleboy/scp-action@master
with:
host: centcomm.spacestation14.io
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
source: "release/${{ steps.parse_version.outputs.version }}"
@@ -46,7 +46,7 @@ jobs:
- name: Update manifest JSON
uses: appleboy/ssh-action@master
with:
host: centcomm.spacestation14.io
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}

View File

@@ -1,4 +1,14 @@
<Project>
<PropertyGroup>
<!--
We actually set ManagePackageVersionsCentrally manually in another import file.
Since .NET SDK 8.0.300, ManagePackageVersionsCentrally is automatically set if Directory.Packages.props exists.
https://github.com/NuGet/NuGet.Client/pull/5572
We actively negate this here, as we have some packages in tree we don't want such automatic behavior for.
We use Directory.Build.props to get copy the state *after* our MSBuild config but before Nuget's config.
-->
<ManagePackageVersionsCentrally />
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
@@ -45,7 +55,7 @@
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,6 +54,38 @@ END TEMPLATE-->
*None yet*
## 219.2.3
## 219.2.2
## 219.2.1
## 219.2.0
### New features
* Add SetMapCoordinates to TransformSystem.
* Improve YAML Linter and validation of static fields.
### Bugfixes
* Fix DebugCoordsPanel freezing when hovering a control.
### Other
* Optimise physics networking to not dirty every tick of movement.
## 219.1.3
### Bugfixes
* Fix map-loader not pausing pre-init maps when not actively overwriting an existing map.
## 219.1.2
### Bugfixes

View File

@@ -285,6 +285,7 @@ namespace Robust.Client
/// <summary>
/// Enumeration of the run levels of the BaseClient.
/// </summary>
/// <seealso cref="ClientRunLevelExt"/>
public enum ClientRunLevel : byte
{
Error = 0,
@@ -315,6 +316,21 @@ namespace Robust.Client
SinglePlayerGame,
}
/// <summary>
/// Helper functions for working with <see cref="ClientRunLevel"/>.
/// </summary>
public static class ClientRunLevelExt
{
/// <summary>
/// Check if a <see cref="ClientRunLevel"/> is <see cref="ClientRunLevel.InGame"/>
/// or <see cref="ClientRunLevel.SinglePlayerGame"/>.
/// </summary>
public static bool IsInGameLike(this ClientRunLevel runLevel)
{
return runLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame;
}
}
/// <summary>
/// Event arguments for when something changed with the player.
/// </summary>

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var type = Type.GetType(args[0]);
var type = GetType(args[0]);
if (type == null)
{
@@ -25,6 +25,17 @@ namespace Robust.Client.Console.Commands
shell.WriteLine(sig);
}
}
private Type? GetType(string name)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.GetType(name) is { } type)
return type;
}
return null;
}
}
#endif
}

View File

@@ -20,6 +20,7 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClyde _displayManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IBaseClient _baseClient = default!;
private readonly StringBuilder _textBuilder = new();
private readonly char[] _textBuffer = new char[1024];
@@ -58,30 +59,36 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
_textBuilder.Clear();
var isInGame = _baseClient.RunLevel.IsInGameLike();
var mouseScreenPos = _inputManager.MouseScreenPosition;
var screenSize = _displayManager.ScreenSize;
var screenScale = _displayManager.MainWindow.ContentScale;
EntityCoordinates mouseGridPos;
TileRef tile;
EntityCoordinates mouseGridPos = default;
TileRef tile = default;
MapCoordinates mouseWorldMap = default;
var mouseWorldMap = _eyeManager.PixelToMap(mouseScreenPos);
if (mouseWorldMap == MapCoordinates.Nullspace)
return;
var mapSystem = _entityManager.System<SharedMapSystem>();
var xformSystem = _entityManager.System<SharedTransformSystem>();
if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
if (isInGame)
{
mouseGridPos = mapSystem.MapToGrid(mouseGridUid, mouseWorldMap);
tile = mapSystem.GetTileRef(mouseGridUid, mouseGrid, mouseGridPos);
}
else
{
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
mouseWorldMap.Position);
tile = new TileRef(EntityUid.Invalid, mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
mouseWorldMap = _eyeManager.PixelToMap(mouseScreenPos);
if (mouseWorldMap != MapCoordinates.Nullspace)
{
var mapSystem = _entityManager.System<SharedMapSystem>();
var xformSystem = _entityManager.System<SharedTransformSystem>();
if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
{
mouseGridPos = mapSystem.MapToGrid(mouseGridUid, mouseWorldMap);
tile = mapSystem.GetTileRef(mouseGridUid, mouseGrid, mouseGridPos);
}
else
{
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
mouseWorldMap.Position);
tile = new TileRef(EntityUid.Invalid,
mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
}
}
}
var controlHovered = UserInterfaceManager.CurrentlyHovered;
@@ -95,32 +102,37 @@ Mouse Pos:
{tile}
GUI: {controlHovered}");
_textBuilder.AppendLine("\nAttached NetEntity:");
var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
if (controlledEntity == EntityUid.Invalid)
if (isInGame)
{
_textBuilder.AppendLine("No attached netentity.");
}
else
{
var entityTransform = _entityManager.GetComponent<TransformComponent>(controlledEntity);
var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
var xformSystem = _entityManager.System<SharedTransformSystem>();
var playerCoordinates = entityTransform.Coordinates;
var playerRotation = xformSystem.GetWorldRotation(entityTransform);
var gridRotation = entityTransform.GridUid != null
? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
: Angle.Zero;
_textBuilder.AppendLine("\nAttached NetEntity:");
var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
_textBuilder.Append($@" Screen: {playerScreen}
if (controlledEntity == EntityUid.Invalid)
{
_textBuilder.AppendLine("No attached netentity.");
}
else
{
var entityTransform = _entityManager.GetComponent<TransformComponent>(controlledEntity);
var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
var playerCoordinates = entityTransform.Coordinates;
var playerRotation = xformSystem.GetWorldRotation(entityTransform);
var gridRotation = entityTransform.GridUid != null
? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
: Angle.Zero;
_textBuilder.Append($@" Screen: {playerScreen}
{playerWorldOffset}
{_entityManager.GetNetCoordinates(playerCoordinates)}
Rotation: {playerRotation.Degrees:F2}°
NEntId: {_entityManager.GetNetEntity(controlledEntity)}
Grid NEntId: {_entityManager.GetNetEntity(entityTransform.GridUid)}
Grid Rotation: {gridRotation.Degrees:F2}°");
}
}
_contents.TextMemory = FormatHelpers.BuilderToMemory(_textBuilder, _textBuffer);

View File

@@ -706,6 +706,7 @@ public sealed class MapLoaderSystem : EntitySystem
}
else
{
data.MapIsPaused = !data.MapIsPostInit;
mapComp.MapId = data.TargetMap;
DebugTools.Assert(mapComp.LifeStage < ComponentLifeStage.Initializing);
EnsureComp<LoadedMapComponent>(rootNode);

View File

@@ -32,7 +32,7 @@ namespace Robust.Shared.ContentPack
String("short").ThenReturn(PrimitiveTypeCode.Int16);
private static readonly Parser<char, PrimitiveTypeCode> UInt16TypeParser =
String("ushort").ThenReturn(PrimitiveTypeCode.UInt32);
String("ushort").ThenReturn(PrimitiveTypeCode.UInt16);
private static readonly Parser<char, PrimitiveTypeCode> Int32TypeParser =
String("int").ThenReturn(PrimitiveTypeCode.Int32);

View File

@@ -84,12 +84,146 @@ Types:
- "bool get_HasContents()"
Lidgren.Network:
NetBuffer:
All: True
Methods:
- "byte[] get_Data()"
- "void set_Data(byte[])"
- "int get_LengthBytes()"
- "void set_LengthBytes(int)"
- "int get_LengthBits()"
- "void set_LengthBits(int)"
- "long get_Position()"
- "void set_Position(long)"
- "int get_PositionInBytes()"
- "byte[] PeekDataBuffer()"
- "bool PeekBoolean()"
- "byte PeekByte()"
- "sbyte PeekSByte()"
- "byte PeekByte(int)"
- "System.Span`1<byte> PeekBytes(System.Span`1<byte>)"
- "byte[] PeekBytes(int)"
- "void PeekBytes(byte[], int, int)"
- "short PeekInt16()"
- "ushort PeekUInt16()"
- "int PeekInt32()"
- "int PeekInt32(int)"
- "uint PeekUInt32()"
- "uint PeekUInt32(int)"
- "ulong PeekUInt64()"
- "long PeekInt64()"
- "ulong PeekUInt64(int)"
- "long PeekInt64(int)"
- "float PeekFloat()"
- "System.Half PeekHalf()"
- "float PeekSingle()"
- "double PeekDouble()"
- "string PeekString()"
- "int PeekStringSize()"
- "bool ReadBoolean()"
- "byte ReadByte()"
- "bool ReadByte(ref byte)"
- "sbyte ReadSByte()"
- "byte ReadByte(int)"
- "System.Span`1<byte> ReadBytes(System.Span`1<byte>)"
- "byte[] ReadBytes(int)"
- "bool ReadBytes(int, ref byte[])"
- "bool TryReadBytes(System.Span`1<byte>)"
- "void ReadBytes(byte[], int, int)"
- "void ReadBits(System.Span`1<byte>, int)"
- "void ReadBits(byte[], int, int)"
- "short ReadInt16()"
- "ushort ReadUInt16()"
- "int ReadInt32()"
- "bool ReadInt32(ref int)"
- "int ReadInt32(int)"
- "uint ReadUInt32()"
- "bool ReadUInt32(ref uint)"
- "uint ReadUInt32(int)"
- "ulong ReadUInt64()"
- "long ReadInt64()"
- "ulong ReadUInt64(int)"
- "long ReadInt64(int)"
- "float ReadFloat()"
- "System.Half ReadHalf()"
- "float ReadSingle()"
- "bool ReadSingle(ref float)"
- "double ReadDouble()"
- "uint ReadVariableUInt32()"
- "bool ReadVariableUInt32(ref uint)"
- "int ReadVariableInt32()"
- "long ReadVariableInt64()"
- "ulong ReadVariableUInt64()"
- "float ReadSignedSingle(int)"
- "float ReadUnitSingle(int)"
- "float ReadRangedSingle(float, float, int)"
- "int ReadRangedInteger(int, int)"
- "long ReadRangedInteger(long, long)"
- "string ReadString()"
- "bool ReadString(ref string)"
- "double ReadTime(Lidgren.Network.NetConnection, bool)"
- "System.Net.IPEndPoint ReadIPEndPoint()"
- "void SkipPadBits()"
- "void ReadPadBits()"
- "void SkipPadBits(int)"
- "void EnsureBufferSize(int)"
- "void Write(bool)"
- "void Write(byte)"
- "void WriteAt(int, byte)"
- "void Write(sbyte)"
- "void Write(byte, int)"
- "void Write(byte[])"
- "void Write(System.ReadOnlySpan`1<byte>)"
- "void Write(byte[], int, int)"
- "void Write(ushort)"
- "void WriteAt(int, ushort)"
- "void Write(ushort, int)"
- "void Write(short)"
- "void WriteAt(int, short)"
- "void Write(int)"
- "void WriteAt(int, int)"
- "void Write(uint)"
- "void WriteAt(int, uint)"
- "void Write(uint, int)"
- "void Write(int, int)"
- "void Write(ulong)"
- "void WriteAt(int, ulong)"
- "void Write(ulong, int)"
- "void Write(long)"
- "void Write(long, int)"
- "void Write(System.Half)"
- "void Write(float)"
- "void Write(double)"
- "int WriteVariableUInt32(uint)"
- "int WriteVariableInt32(int)"
- "int WriteVariableInt64(long)"
- "int WriteVariableUInt64(ulong)"
- "void WriteSignedSingle(float, int)"
- "void WriteUnitSingle(float, int)"
- "void WriteRangedSingle(float, float, float, int)"
- "int WriteRangedInteger(int, int, int)"
- "int WriteRangedInteger(long, long, long)"
- "void Write(string)"
- "void Write(System.Net.IPEndPoint)"
- "void WriteTime(bool)"
- "void WriteTime(double, bool)"
- "void WritePadBits()"
- "void WritePadBits(int)"
- "void Write(Lidgren.Network.NetBuffer)"
- "void Zero(int)"
- "void .ctor()"
NetDeliveryMethod: { }
NetIncomingMessage:
All: True
Methods:
- "Lidgren.Network.NetIncomingMessageType get_MessageType()"
- "Lidgren.Network.NetDeliveryMethod get_DeliveryMethod()"
- "int get_SequenceChannel()"
- "System.Net.IPEndPoint get_SenderEndPoint()"
- "Lidgren.Network.NetConnection get_SenderConnection()"
- "double get_ReceiveTime()"
- "double ReadTime(bool)"
- "string ToString()"
NetOutgoingMessage:
All: True
Methods:
- "string ToString()"
Nett:
CommentLocation: { } # Enum
Toml:

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
@@ -135,11 +136,37 @@ namespace Robust.Shared.ContentPack
path = path.Directory;
var fullPath = GetFullPath(path);
Process.Start(new ProcessStartInfo
if (OperatingSystem.IsWindows())
{
UseShellExecute = true,
FileName = fullPath,
});
Process.Start(new ProcessStartInfo
{
FileName = $"{Environment.GetEnvironmentVariable("SystemRoot")}\\explorer.exe",
Arguments = ".",
WorkingDirectory = fullPath,
});
}
else if (OperatingSystem.IsMacOS())
{
Process.Start(new ProcessStartInfo
{
FileName = "open",
Arguments = ".",
WorkingDirectory = fullPath,
});
}
else if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD())
{
Process.Start(new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = ".",
WorkingDirectory = fullPath,
});
}
else
{
throw new NotSupportedException("Opening OS windows not supported on this OS");
}
}
#endregion

View File

@@ -870,6 +870,28 @@ public abstract partial class SharedTransformSystem
return GetMapCoordinates(entity.Comp);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetMapCoordinates(EntityUid entity, MapCoordinates coordinates)
{
var xform = XformQuery.GetComponent(entity);
SetMapCoordinates((entity, xform), coordinates);
}
public void SetMapCoordinates(Entity<TransformComponent> entity, MapCoordinates coordinates)
{
var mapUid = _map.GetMap(coordinates.MapId);
if (!_gridQuery.HasComponent(entity) &&
_mapManager.TryFindGridAt(mapUid, coordinates.Position, out var targetGrid, out _))
{
var invWorldMatrix = GetInvWorldMatrix(targetGrid);
SetCoordinates(entity, new EntityCoordinates(targetGrid, invWorldMatrix.Transform(coordinates.Position)));
}
else
{
SetCoordinates(entity, new EntityCoordinates(mapUid, coordinates.Position));
}
}
[Pure]
public (Vector2 WorldPosition, Angle WorldRotation) GetWorldPositionRotation(EntityUid uid)
{

View File

@@ -313,55 +313,60 @@ public partial class SharedPhysicsSystem
Dirty(uid, body);
}
public void SetAngularVelocity(EntityUid uid, float value, bool dirty = true, FixturesComponent? manager = null, PhysicsComponent? body = null)
public bool SetAngularVelocity(EntityUid uid, float value, bool dirty = true, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body))
return;
return false;
if (body.BodyType == BodyType.Static)
return;
return false;
if (value * value > 0.0f)
{
if (!WakeBody(uid, manager: manager, body: body))
return;
return false;
}
// CloseToPercent tolerance needs to be small enough such that an angular velocity just above
// sleep-tolerance can damp down to sleeping.
if (MathHelper.CloseToPercent(body.AngularVelocity, value, 0.00001f))
return;
return false;
body.AngularVelocity = value;
if (dirty)
Dirty(uid, body);
return true;
}
/// <summary>
/// Attempts to set the body to collidable, wake it, then move it.
/// </summary>
public void SetLinearVelocity(EntityUid uid, Vector2 velocity, bool dirty = true, bool wakeBody = true, FixturesComponent? manager = null, PhysicsComponent? body = null)
public bool SetLinearVelocity(EntityUid uid, Vector2 velocity, bool dirty = true, bool wakeBody = true, FixturesComponent? manager = null, PhysicsComponent? body = null)
{
if (!Resolve(uid, ref body))
return;
return false;
if (body.BodyType == BodyType.Static) return;
if (body.BodyType == BodyType.Static)
return false;
if (wakeBody && Vector2.Dot(velocity, velocity) > 0.0f)
{
if (!WakeBody(uid, manager: manager, body: body))
return;
return false;
}
if (body.LinearVelocity.EqualsApprox(velocity, 0.0000001f))
return;
return false;
body.LinearVelocity = velocity;
if (dirty)
Dirty(uid, body);
return true;
}
public void SetAngularDamping(EntityUid uid, PhysicsComponent body, float value, bool dirty = true)

View File

@@ -660,13 +660,11 @@ public abstract partial class SharedPhysicsSystem
});
// Update data sequentially
var metaQuery = GetEntityQuery<MetaDataComponent>();
for (var i = 0; i < actualIslands.Length; i++)
{
var island = actualIslands[i];
UpdateBodies(in island, solvedPositions, solvedAngles, linearVelocities, angularVelocities, xformQuery, metaQuery);
UpdateBodies(in island, solvedPositions, solvedAngles, linearVelocities, angularVelocities, xformQuery);
SleepBodies(in island, sleepStatus);
}
@@ -1001,8 +999,7 @@ public abstract partial class SharedPhysicsSystem
float[] angles,
Vector2[] linearVelocities,
float[] angularVelocities,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<MetaDataComponent> metaQuery)
EntityQuery<TransformComponent> xformQuery)
{
foreach (var (joint, error) in island.BrokenJoints)
{
@@ -1035,21 +1032,22 @@ public abstract partial class SharedPhysicsSystem
}
var linVelocity = linearVelocities[offset + i];
var physicsDirtied = false;
if (!float.IsNaN(linVelocity.X) && !float.IsNaN(linVelocity.Y))
{
SetLinearVelocity(uid, linVelocity, false, body: body);
physicsDirtied |= SetLinearVelocity(uid, linVelocity, false, body: body);
}
var angVelocity = angularVelocities[offset + i];
if (!float.IsNaN(angVelocity))
{
SetAngularVelocity(uid, angVelocity, false, body: body);
physicsDirtied |= SetAngularVelocity(uid, angVelocity, false, body: body);
}
// TODO: Should check if the values update.
Dirty(uid, body, metaQuery.GetComponent(uid));
if (physicsDirtied)
Dirty(uid, body);
}
}

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Robust.Shared.Random;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown;
@@ -272,7 +273,8 @@ public interface IPrototypeManager
out Dictionary<Type, HashSet<string>> prototypes);
/// <summary>
/// This method uses reflection to validate that prototype id fields correspond to valid prototypes.
/// This method uses reflection to validate that all static prototype id fields correspond to valid prototypes.
/// This will validate all known to <see cref="IReflectionManager"/>
/// </summary>
/// <remarks>
/// This will validate any field that has either a <see cref="ValidatePrototypeIdAttribute{T}"/> attribute, or a
@@ -280,7 +282,12 @@ public interface IPrototypeManager
/// </remarks>
/// <param name="prototypes">A collection prototypes to use for validation. Any prototype not in this collection
/// will be considered invalid.</param>
List<string> ValidateFields(Dictionary<Type, HashSet<string>> prototypes);
List<string> ValidateStaticFields(Dictionary<Type, HashSet<string>> prototypes);
/// <summary>
/// This is a variant of <see cref="ValidateStaticFields(System.Collections.Generic.Dictionary{System.Type,System.Collections.Generic.HashSet{string}})"/> that only validates a single type.
/// </summary>
List<string> ValidateStaticFields(Type type, Dictionary<Type, HashSet<string>> prototypes);
/// <summary>
/// This method will serialize all loaded prototypes into yaml and then validate them. This can be used to ensure

View File

@@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
using BindingFlags = System.Reflection.BindingFlags;
@@ -13,35 +12,41 @@ namespace Robust.Shared.Prototypes;
public partial class PrototypeManager
{
/// <inheritdoc/>
public List<string> ValidateFields(Dictionary<Type, HashSet<string>> prototypes)
public List<string> ValidateStaticFields(Dictionary<Type, HashSet<string>> prototypes)
{
var errors = new List<string>();
foreach (var type in _reflectionManager.FindAllTypes())
{
// TODO validate public static fields on abstract classes that have no implementations?
if (!type.IsAbstract)
ValidateType(type, errors, prototypes);
ValidateStaticFieldsInternal(type, errors, prototypes);
}
return errors;
}
/// <summary>
/// Validate all fields defined on this type and all base types.
/// </summary>
private void ValidateType(Type type, List<string> errors, Dictionary<Type, HashSet<string>> prototypes)
/// <inheritdoc/>
public List<string> ValidateStaticFields(Type type, Dictionary<Type, HashSet<string>> prototypes)
{
object? instance = null;
Type? baseType = type;
var errors = new List<string>();
ValidateStaticFieldsInternal(type, errors, prototypes);
return errors;
}
var flags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public |
BindingFlags.DeclaredOnly;
/// <summary>
/// Validate all static fields defined on this type and all base types.
/// </summary>
private void ValidateStaticFieldsInternal(Type type, List<string> errors, Dictionary<Type, HashSet<string>> prototypes)
{
var baseType = type;
var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly;
while (baseType != null)
{
foreach (var field in baseType.GetFields(flags))
{
ValidateField(field, type, ref instance, errors, prototypes);
DebugTools.Assert(field.IsStatic);
ValidateStaticField(field, type, errors, prototypes);
}
// We need to get the fields on the base type separately in order to get the private fields
@@ -49,92 +54,110 @@ public partial class PrototypeManager
}
}
private void ValidateField(
private void ValidateStaticField(
FieldInfo field,
Type type,
ref object? instance,
List<string> errors,
Dictionary<Type, HashSet<string>> prototypes)
{
DebugTools.Assert(field.IsStatic);
DebugTools.Assert(!field.HasCustomAttribute<DataFieldAttribute>(), "Datafields should not be static");
// Is this even a prototype id related field?
if (!TryGetFieldPrototype(field, out var proto, out var canBeNull, out var canBeEmpty))
if (!TryGetFieldPrototype(field, out var proto))
return;
if (!TryGetFieldValue(field, type, ref instance, errors, out var value))
return;
var id = value?.ToString();
if (id == null)
if (!prototypes.TryGetValue(proto, out var validIds))
{
if (!canBeNull)
errors.Add($"Prototype id field failed validation. Fields should not be null. Field: {field.Name} in {type.FullName}");
errors.Add($"Prototype id field failed validation. Unknown prototype kind {proto.Name}. Field: {field.Name} in {type.FullName}");
return;
}
if (string.IsNullOrWhiteSpace(id))
if (!TryGetIds(field, proto, out var ids))
{
if (!canBeEmpty)
errors.Add($"Prototype id field failed validation. Non-optional non-nullable data-fields must have a default value. Field: {field.Name} in {type.FullName}");
TryGetIds(field, proto, out _);
DebugTools.Assert($"Failed to get ids, despite resolving the field into a prototype kind?");
return;
}
if (!prototypes.TryGetValue(proto, out var ids))
foreach (var id in ids)
{
errors.Add($"Prototype id field failed validation. Unknown prototype kind. Field: {field.Name} in {type.FullName}");
return;
}
if (!ids.Contains(id))
{
errors.Add($"Prototype id field failed validation. Unknown prototype: {id}. Field: {field.Name} in {type.FullName}");
if (!validIds.Contains(id))
errors.Add($"Prototype id field failed validation. Unknown prototype: {id} of type {proto.Name}. Field: {field.Name} in {type.FullName}");
}
}
/// <summary>
/// Get the value of some field. If this is not a static field, this will create instance of the object in order to
/// validate default field values.
/// Extract prototype ids from a string, IEnumerable{string}, EntProtoId, IEnumerable{EntProtoId}, ProtoId{T}, or IEnumerable{ProtoId{T}} field.
/// </summary>
private bool TryGetFieldValue(FieldInfo field, Type type, ref object? instance, List<string> errors, out object? value)
private bool TryGetIds(FieldInfo field, Type proto, [NotNullWhen(true)] out string[]? ids)
{
value = null;
ids = null;
var value = field.GetValue(null);
if (value == null)
return false;
if (field.IsStatic || instance != null)
if (value is string str)
{
value = field.GetValue(instance);
ids = [str];
return true;
}
var constructor = type.GetConstructor(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
Type.EmptyTypes);
// TODO handle parameterless record constructors.
// Figure out how ISerializationManager does it, or just re-use that code somehow.
// In the meantime, record data fields need an explicit parameterless ctor.
if (constructor == null)
if (value is IEnumerable<string> strEnum)
{
errors.Add($"Prototype id field failed validation. Could not create instance to validate default value. Field: {field.Name} in {type.FullName}");
return false;
ids = strEnum.ToArray();
return true;
}
instance = constructor.Invoke(Array.Empty<object>());
value = field.GetValue(instance);
if (value is EntProtoId protoId)
{
ids = [protoId];
return true;
}
return true;
if (value is IEnumerable<EntProtoId> protoIdEnum)
{
ids = protoIdEnum.Select(x=> x.Id).ToArray();
return true;
}
if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(ProtoId<>))
{
ids = [value.ToString()!];
return true;
}
foreach (var iface in field.FieldType.GetInterfaces())
{
if (!iface.IsGenericType)
continue;
if (iface.GetGenericTypeDefinition() != typeof(IEnumerable<>))
continue;
var enumType = iface.GetGenericArguments().Single();
if (!enumType.IsGenericType)
continue;
if (enumType.GetGenericTypeDefinition() != typeof(ProtoId<>))
continue;
ids = GetIdsMethod.MakeGenericMethod(proto).Invoke(null, [value]) as string[];
return ids != null;
}
return false;
}
private bool TryGetFieldPrototype(
FieldInfo field,
[NotNullWhen(true)] out Type? proto,
out bool canBeNull,
out bool canBeEmpty)
private static MethodInfo GetIdsMethod = typeof(PrototypeManager).GetMethod(nameof(GetIds), BindingFlags.NonPublic | BindingFlags.Static)!;
private static string[] GetIds<T>(IEnumerable<ProtoId<T>> enumerable) where T : class, IPrototype
{
proto = null;
canBeNull = false;
canBeEmpty = false;
return enumerable.Select(x => x.Id).ToArray();
}
private bool TryGetFieldPrototype(FieldInfo field, [NotNullWhen(true)] out Type? proto)
{
// Validate anything with the attribute
var attrib = field.GetCustomAttribute(typeof(ValidatePrototypeIdAttribute<>), false);
if (attrib != null)
{
@@ -142,46 +165,40 @@ public partial class PrototypeManager
return true;
}
if (!field.TryGetCustomAttribute(out DataFieldAttribute? dataField))
return false;
if (TryGetPrototypeFromType(field.FieldType, out proto))
return true;
var fieldType = field.FieldType;
canBeEmpty = dataField.Required;
DebugTools.Assert(!field.IsStatic);
// Resolve nullable structs
if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>))
// Allow validating arrays or lists.
foreach (var iface in field.FieldType.GetInterfaces().Where(x => x.IsGenericType))
{
fieldType = fieldType.GetGenericArguments().Single();
canBeNull = true;
if (iface.GetGenericTypeDefinition() != typeof(IEnumerable<>))
continue;
var enumType = iface.GetGenericArguments().Single();
if (TryGetPrototypeFromType(enumType, out proto))
return true;
}
if (fieldType == typeof(EntProtoId))
proto = null;
return false;
}
private bool TryGetPrototypeFromType(Type type, [NotNullWhen(true)] out Type? proto)
{
if (type == typeof(EntProtoId))
{
proto = typeof(EntityPrototype);
return true;
}
if (fieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(ProtoId<>))
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ProtoId<>))
{
proto = field.FieldType.GetGenericArguments().Single();
proto = type.GetGenericArguments().Single();
DebugTools.Assert(proto != typeof(EntityPrototype), "Use EntProtoId instead of ProtoId<EntityPrototype>");
return true;
}
// As far as I know there is no way to check for the nullability of a string field, so we will assume that null
// values imply that the field itself is properly marked as nullable.
canBeNull = true;
if (dataField.CustomTypeSerializer == null)
return false;
if (!dataField.CustomTypeSerializer.IsGenericType)
return false;
if (dataField.CustomTypeSerializer.GetGenericTypeDefinition() != typeof(PrototypeIdSerializer<>))
return false;
proto = dataField.CustomTypeSerializer.GetGenericArguments().First();
return true;
proto = null;
return false;
}
}

View File

@@ -53,7 +53,7 @@ public partial class PrototypeManager
var mapping = node.ToDataNodeCast<MappingDataNode>();
var id = mapping.Get<ValueDataNode>("id").Value;
var data = new PrototypeValidationData(mapping, resourcePath.ToString());
var data = new PrototypeValidationData(id, mapping, resourcePath.ToString());
mapping.Remove("type");
if (prototypes.GetOrNew(type).TryAdd(id, data))
@@ -65,10 +65,14 @@ public partial class PrototypeManager
}
}
var ctx = new YamlValidationContext();
var errors = new List<ErrorNode>();
foreach (var (type, instances) in prototypes)
{
foreach (var data in instances.Values)
var defaultErrorOccurred = false;
foreach (var (id, data) in instances)
{
errors.Clear();
EnsurePushed(data, instances, type);
if (data.Mapping.TryGet("abstract", out ValueDataNode? abstractNode)
&& bool.Parse(abstractNode.Value))
@@ -76,9 +80,25 @@ public partial class PrototypeManager
continue;
}
var result = _serializationManager.ValidateNode(type, data.Mapping).GetErrors().ToHashSet();
if (result.Count > 0)
dict.GetOrNew(data.File).UnionWith(result);
// Validate yaml directly
errors.AddRange(_serializationManager.ValidateNode(type, data.Mapping).GetErrors());
if (errors.Count > 0)
dict.GetOrNew(data.File).UnionWith(errors);
// Create instance & re-serialize it, to validate the default values of data-fields. We still validate
// the yaml directly just in case reading & writing the fields somehow modifies their values.
try
{
var instance = _serializationManager.Read(type, data.Mapping, ctx);
var mapping = _serializationManager.WriteValue(type, instance, alwaysWrite: true, ctx);
errors.AddRange(_serializationManager.ValidateNode(type, mapping, ctx).GetErrors());
if (errors.Count > 0)
dict.GetOrNew(data.File).UnionWith(errors);
}
catch (Exception ex)
{
errors.Add(new ErrorNode(new ValueDataNode(), $"Caught Exception while validating {type} prototype {id}. Exception: {ex}"));
}
}
}
@@ -152,12 +172,17 @@ public partial class PrototypeManager
private sealed class PrototypeValidationData
{
public readonly string Id;
public MappingDataNode Mapping;
public readonly string File;
public bool Pushed;
public PrototypeValidationData(MappingDataNode mapping, string file)
public string[]? Parents;
public MappingDataNode[]? ParentMappings;
public PrototypeValidationData(string id, MappingDataNode mapping, string file)
{
Id = id;
File = file;
Mapping = mapping;
}
@@ -176,23 +201,22 @@ public partial class PrototypeManager
if (!data.Mapping.TryGet(ParentDataFieldAttribute.Name, out var parentNode))
return;
var parents = _serializationManager.Read<string[]>(parentNode, notNullableOverride: true);
var parentNodes = new MappingDataNode[parents.Length];
DebugTools.AssertNull(data.Parents);
DebugTools.AssertNull(data.ParentMappings);
data.Parents = _serializationManager.Read<string[]>(parentNode, notNullableOverride: true);
data.ParentMappings = new MappingDataNode[data.Parents.Length];
foreach (var parentId in parents)
var i = 0;
foreach (var parentId in data.Parents)
{
var parent = prototypes[parentId];
EnsurePushed(parent, prototypes, type);
for (var i = 0; i < parents.Length; i++)
{
parentNodes[i] = parent.Mapping;
}
data.ParentMappings[i++] = parent.Mapping;
}
data.Mapping = _serializationManager.PushCompositionWithGenericNode(
type,
parentNodes,
data.ParentMappings,
data.Mapping);
}
}

View File

@@ -0,0 +1,62 @@
using System.Globalization;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
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;
namespace Robust.Shared.Prototypes;
internal sealed class YamlValidationContext : ISerializationContext, ITypeSerializer<EntityUid, ValueDataNode>
{
public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
public bool WritingReadingPrototypes => true;
public YamlValidationContext()
{
SerializerProvider.RegisterSerializer(this);
}
ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(ISerializationManager serializationManager,
ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context)
{
if (node.Value == "null" || node.Value == "invalid")
return new ValidatedValueNode(node);
return new ErrorNode(node, "Prototypes should not contain EntityUids", true);
}
public DataNode Write(ISerializationManager serializationManager, EntityUid value,
IDependencyCollection dependencies, bool alwaysWrite = false,
ISerializationContext? context = null)
{
if (!value.Valid)
return new ValueDataNode("invalid");
return new ValueDataNode(value.Id.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")
return EntityUid.Invalid;
return EntityUid.Parse(node.Value);
}
[MustUseReturnValue]
public EntityUid Copy(ISerializationManager serializationManager, EntityUid source, EntityUid target,
bool skipHook,
ISerializationContext? context = null)
{
return new((int)source);
}
}

View File

@@ -4,8 +4,9 @@ using Robust.Shared.Prototypes;
namespace Robust.Shared.Serialization.Manager.Attributes;
/// <summary>
/// This attribute should be used on string fields to validate that they correspond to a valid YAML prototype id.
/// If the field needs to be have a default value.
/// This attribute should be used on static string or string collection fields to validate that they correspond to
/// valid YAML prototype ids. This attribute is not required for static <see cref="ProtoId{T}"/> and
/// <see cref="EntProtoId"/> fields, as they automatically get validated.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public sealed class ValidatePrototypeIdAttribute<T> : Attribute where T : IPrototype

View File

@@ -143,7 +143,7 @@ namespace Robust.Shared.Serialization.Manager.Definition
nodeVariable),
call,
dfa.Required
? ExpressionUtils.ThrowExpression<RequiredFieldNotMappedException>(fieldDefinition.FieldType, tagConst)
? ExpressionUtils.ThrowExpression<RequiredFieldNotMappedException>(fieldDefinition.FieldType, tagConst, typeof(T))
: AssignIfNotDefaultExpression(i, targetParam, Expression.Constant(DefaultValues[i], fieldDefinition.FieldType))
)));
}

View File

@@ -4,7 +4,7 @@ namespace Robust.Shared.Serialization.Manager.Exceptions;
public sealed class RequiredFieldNotMappedException : Exception
{
public RequiredFieldNotMappedException(Type type, string field) : base($"Required field {field} of type {type} wasn't mapped.")
public RequiredFieldNotMappedException(Type type, string field, Type dataDef) : base($"Required field {field} of type {type} in {dataDef} wasn't mapped.")
{
}
}

View File

@@ -21,7 +21,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Pro
/// <summary>
/// Checks that a string corresponds to a valid prototype id. Note that any data fields using this serializer will
/// also be validated by <see cref="IPrototypeManager.ValidateFields"/>
/// also be validated by <see cref="IPrototypeManager.ValidateStaticFields"/>
/// </summary>
[Virtual]
public class PrototypeIdSerializer<TPrototype> : ITypeValidator<string, ValueDataNode> where TPrototype : class, IPrototype