XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
UTF-8
using VRage;
using VRage.Game;

namespace SpaceEngineersBlueprintEditor.SpaceEngineersCore.BlueprintEditing;

/// <summary>
/// Identifies the two kinds of surface controls exposed by the sheet-metal editor.
/// </summary>
public enum CubeDeformationControlKind
{
    Corner,
    FaceCenter
}

/// <summary>
/// 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.
/// </summary>
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));
}

/// <summary>
/// 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.
/// </summary>
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;
    }
}

/// <summary>
/// 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.
/// </summary>
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<CubeDeformationControlPoint> 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<BoneInfo> 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;
        }
    }
}