using VRage; using VRage.Game; namespace SpaceEngineersBlueprintEditor.SpaceEngineersCore.BlueprintEditing; /// /// Identifies the two kinds of surface controls exposed by the sheet-metal editor. /// public enum CubeDeformationControlKind { Corner, FaceCenter } /// /// A grid-normalized bone offset. A value of 1 moves the bone by one complete /// large- or small-grid cell, depending on the grid being edited. /// public readonly struct CubeDeformationOffset { public float X { get; } public float Y { get; } public float Z { get; } public CubeDeformationOffset(float x, float y, float z) { X = x; Y = y; Z = z; } public static CubeDeformationOffset Zero { get; } = new(0, 0, 0); public CubeDeformationOffset Clamp() => new( ClampComponent(X), ClampComponent(Y), ClampComponent(Z)); private static float ClampComponent(float value) => Math.Max(-1f, Math.Min(1f, value)); } /// /// Describes one editable bone on a cube surface. Local bone coordinates use /// Space Engineers' 3x3x3 skeleton lattice and are therefore in the 0..2 range. /// public readonly struct CubeDeformationControlPoint { public string Id { get; } public CubeDeformationControlKind Kind { get; } public int LocalX { get; } public int LocalY { get; } public int LocalZ { get; } public CubeDeformationOffset Offset { get; } public CubeDeformationControlPoint( string id, CubeDeformationControlKind kind, int localX, int localY, int localZ, CubeDeformationOffset offset) { Id = id; Kind = kind; LocalX = localX; LocalY = localY; LocalZ = localZ; Offset = offset; } } /// /// Reads and writes the native Space Engineers grid-skeleton representation used /// for armour deformation. Changes made here are serialized into the blueprint's /// standard CubeGrid/Skeleton section. /// public static class CubeDeformationService { public const int BoneDensity = 2; private const byte NeutralBoneComponent = 127; private static readonly ControlPointDefinition[] ControlPointDefinitions = [ new("Corner000", CubeDeformationControlKind.Corner, 0, 0, 0), new("Corner002", CubeDeformationControlKind.Corner, 0, 0, 2), new("Corner020", CubeDeformationControlKind.Corner, 0, 2, 0), new("Corner022", CubeDeformationControlKind.Corner, 0, 2, 2), new("Corner200", CubeDeformationControlKind.Corner, 2, 0, 0), new("Corner202", CubeDeformationControlKind.Corner, 2, 0, 2), new("Corner220", CubeDeformationControlKind.Corner, 2, 2, 0), new("Corner222", CubeDeformationControlKind.Corner, 2, 2, 2), new("FaceXNegative", CubeDeformationControlKind.FaceCenter, 0, 1, 1), new("FaceXPositive", CubeDeformationControlKind.FaceCenter, 2, 1, 1), new("FaceYNegative", CubeDeformationControlKind.FaceCenter, 1, 0, 1), new("FaceYPositive", CubeDeformationControlKind.FaceCenter, 1, 2, 1), new("FaceZNegative", CubeDeformationControlKind.FaceCenter, 1, 1, 0), new("FaceZPositive", CubeDeformationControlKind.FaceCenter, 1, 1, 2) ]; public static IReadOnlyList GetControlPoints( MyObjectBuilder_CubeGrid grid, MyObjectBuilder_CubeBlock block) { EnsureArguments(grid, block); var result = new CubeDeformationControlPoint[ControlPointDefinitions.Length]; for (var index = 0; index < ControlPointDefinitions.Length; index++) { var definition = ControlPointDefinitions[index]; result[index] = new( definition.Id, definition.Kind, definition.LocalX, definition.LocalY, definition.LocalZ, GetOffset(grid, block, definition.LocalX, definition.LocalY, definition.LocalZ)); } return result; } public static CubeDeformationOffset GetOffset( MyObjectBuilder_CubeGrid grid, MyObjectBuilder_CubeBlock block, int localX, int localY, int localZ) { EnsureArguments(grid, block); ValidateSurfaceControl(localX, localY, localZ); var globalPosition = GetGlobalBonePosition(block, localX, localY, localZ); if (grid.Skeleton is null) { return CubeDeformationOffset.Zero; } for (var index = grid.Skeleton.Count - 1; index >= 0; index--) { var bone = grid.Skeleton[index]; if (IsSamePosition(bone.BonePosition, globalPosition)) { return new( DecodeComponent(bone.BoneOffset.X), DecodeComponent(bone.BoneOffset.Y), DecodeComponent(bone.BoneOffset.Z)); } } return CubeDeformationOffset.Zero; } public static void SetOffset( MyObjectBuilder_CubeGrid grid, MyObjectBuilder_CubeBlock block, int localX, int localY, int localZ, CubeDeformationOffset offset) { EnsureArguments(grid, block); ValidateSurfaceControl(localX, localY, localZ); var globalPosition = GetGlobalBonePosition(block, localX, localY, localZ); grid.Skeleton ??= []; RemoveBoneAt(grid.Skeleton, globalPosition); var clampedOffset = offset.Clamp(); if (IsNeutral(clampedOffset)) { return; } grid.Skeleton.Add(new BoneInfo { BonePosition = globalPosition, BoneOffset = new SerializableVector3UByte( EncodeComponent(clampedOffset.X), EncodeComponent(clampedOffset.Y), EncodeComponent(clampedOffset.Z)) }); } public static void ResetControlPoint( MyObjectBuilder_CubeGrid grid, MyObjectBuilder_CubeBlock block, int localX, int localY, int localZ) => SetOffset(grid, block, localX, localY, localZ, CubeDeformationOffset.Zero); public static void ResetBlock(MyObjectBuilder_CubeGrid grid, MyObjectBuilder_CubeBlock block) { EnsureArguments(grid, block); if (grid.Skeleton is null) { return; } foreach (var definition in ControlPointDefinitions) { RemoveBoneAt( grid.Skeleton, GetGlobalBonePosition(block, definition.LocalX, definition.LocalY, definition.LocalZ)); } } private static SerializableVector3I GetGlobalBonePosition( MyObjectBuilder_CubeBlock block, int localX, int localY, int localZ) => new( block.Min.X * BoneDensity + localX, block.Min.Y * BoneDensity + localY, block.Min.Z * BoneDensity + localZ); private static void RemoveBoneAt(List skeleton, SerializableVector3I position) { for (var index = skeleton.Count - 1; index >= 0; index--) { if (IsSamePosition(skeleton[index].BonePosition, position)) { skeleton.RemoveAt(index); } } } private static bool IsSamePosition(SerializableVector3I left, SerializableVector3I right) => left.X == right.X && left.Y == right.Y && left.Z == right.Z; private static bool IsNeutral(CubeDeformationOffset offset) => Math.Abs(offset.X) < 0.0001f && Math.Abs(offset.Y) < 0.0001f && Math.Abs(offset.Z) < 0.0001f; private static byte EncodeComponent(float value) { var normalized = (Math.Max(-1f, Math.Min(1f, value)) / 2f + 0.5f) * byte.MaxValue; return (byte)normalized; } private static float DecodeComponent(byte value) => Math.Max(-1f, Math.Min(1f, (value - NeutralBoneComponent) * 2f / byte.MaxValue)); private static void ValidateSurfaceControl(int localX, int localY, int localZ) { var isKnownControl = ControlPointDefinitions.Any(definition => definition.LocalX == localX && definition.LocalY == localY && definition.LocalZ == localZ); if (!isKnownControl) { throw new ArgumentOutOfRangeException( nameof(localX), "Only cube corners and face centers can be edited by this service."); } } private static void EnsureArguments(MyObjectBuilder_CubeGrid grid, MyObjectBuilder_CubeBlock block) { if (grid is null) { throw new ArgumentNullException(nameof(grid)); } if (block is null) { throw new ArgumentNullException(nameof(block)); } } private readonly struct ControlPointDefinition { public string Id { get; } public CubeDeformationControlKind Kind { get; } public int LocalX { get; } public int LocalY { get; } public int LocalZ { get; } public ControlPointDefinition( string id, CubeDeformationControlKind kind, int localX, int localY, int localZ) { Id = id; Kind = kind; LocalX = localX; LocalY = localY; LocalZ = localZ; } } }