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

namespace SpaceEngineersBlueprintEditor.SpaceEngineersCore.BlueprintEditing;

/// <summary>
/// Resolves the positions stored by a Space Engineers block group back to the
/// blocks occupying those cells. Group positions are not guaranteed to equal a
/// multi-cell block's Min coordinate.
/// </summary>
public static class BlockGroupResolver
{
    public static IReadOnlyList<MyObjectBuilder_CubeBlock> ResolveBlocks(
        MyObjectBuilder_CubeGrid grid,
        MyObjectBuilder_BlockGroup group)
    {
        if (grid is null)
        {
            throw new ArgumentNullException(nameof(grid));
        }
        if (group is null)
        {
            throw new ArgumentNullException(nameof(group));
        }
        if (grid.CubeBlocks is null || group.Blocks is null)
        {
            return Array.Empty<MyObjectBuilder_CubeBlock>();
        }

        var occupiedCells = BuildOccupiedCellIndex(grid.CubeBlocks);
        return ResolveBlocks(group.Blocks, occupiedCells);
    }

    /// <summary>
    /// Resolves every group in a grid while constructing the occupied-cell index
    /// only once.
    /// </summary>
    public static IReadOnlyDictionary<MyObjectBuilder_BlockGroup, IReadOnlyList<MyObjectBuilder_CubeBlock>>
        ResolveGroups(MyObjectBuilder_CubeGrid grid)
    {
        if (grid is null)
        {
            throw new ArgumentNullException(nameof(grid));
        }

        var result = new Dictionary<MyObjectBuilder_BlockGroup, IReadOnlyList<MyObjectBuilder_CubeBlock>>();
        if (grid.CubeBlocks is null || grid.BlockGroups is null)
        {
            return result;
        }

        var occupiedCells = BuildOccupiedCellIndex(grid.CubeBlocks);
        foreach (var group in grid.BlockGroups.Where(group => group is not null))
        {
            result[group] = group.Blocks is null
                ? Array.Empty<MyObjectBuilder_CubeBlock>()
                : ResolveBlocks(group.Blocks, occupiedCells);
        }

        return result;
    }

    private static IReadOnlyList<MyObjectBuilder_CubeBlock> ResolveBlocks(
        IEnumerable<Vector3I> positions,
        IReadOnlyDictionary<CellKey, MyObjectBuilder_CubeBlock> occupiedCells)
    {
        var result = new List<MyObjectBuilder_CubeBlock>();
        var addedBlocks = new HashSet<MyObjectBuilder_CubeBlock>();
        foreach (var position in positions)
        {
            if (occupiedCells.TryGetValue(ToKey(position), out var block) && addedBlocks.Add(block))
            {
                result.Add(block);
            }
        }

        return result;
    }

    /// <summary>
    /// Gets the axis-aligned cell dimensions occupied after applying the block's
    /// Forward/Up orientation to its definition size.
    /// </summary>
    public static Vector3I GetOrientedSize(MyObjectBuilder_CubeBlock block)
    {
        if (block is null)
        {
            throw new ArgumentNullException(nameof(block));
        }

        var definitionSize = TryGetDefinitionSize(block);
        var forward = ToVector(block.BlockOrientation.Forward);
        var up = ToVector(block.BlockOrientation.Up);
        var right = Cross(forward, up);
        var backward = -forward;
        var orientedSize = new Vector3I(
            Math.Abs(right.X) * definitionSize.X +
            Math.Abs(up.X) * definitionSize.Y +
            Math.Abs(backward.X) * definitionSize.Z,
            Math.Abs(right.Y) * definitionSize.X +
            Math.Abs(up.Y) * definitionSize.Y +
            Math.Abs(backward.Y) * definitionSize.Z,
            Math.Abs(right.Z) * definitionSize.X +
            Math.Abs(up.Z) * definitionSize.Y +
            Math.Abs(backward.Z) * definitionSize.Z);

        return orientedSize.X > 0 && orientedSize.Y > 0 && orientedSize.Z > 0
            ? orientedSize
            : definitionSize;
    }

    public static bool Occupies(MyObjectBuilder_CubeBlock block, Vector3I position)
    {
        if (block is null)
        {
            throw new ArgumentNullException(nameof(block));
        }

        var size = GetOrientedSize(block);
        return position.X >= block.Min.X && position.X < block.Min.X + size.X &&
               position.Y >= block.Min.Y && position.Y < block.Min.Y + size.Y &&
               position.Z >= block.Min.Z && position.Z < block.Min.Z + size.Z;
    }

    private static Dictionary<CellKey, MyObjectBuilder_CubeBlock> BuildOccupiedCellIndex(
        IEnumerable<MyObjectBuilder_CubeBlock> blocks)
    {
        var result = new Dictionary<CellKey, MyObjectBuilder_CubeBlock>();
        var blockList = blocks.Where(block => block is not null).ToArray();

        // Exact Min coordinates take precedence if a malformed blueprint contains
        // overlapping blocks.
        foreach (var block in blockList)
        {
            result[ToKey(block.Min)] = block;
        }

        foreach (var block in blockList)
        {
            var size = GetOrientedSize(block);
            for (var x = 0; x < size.X; x++)
            for (var y = 0; y < size.Y; y++)
            for (var z = 0; z < size.Z; z++)
            {
                var key = new CellKey(block.Min.X + x, block.Min.Y + y, block.Min.Z + z);
                if (!result.ContainsKey(key))
                {
                    result.Add(key, block);
                }
            }
        }

        return result;
    }

    private static Vector3I TryGetDefinitionSize(MyObjectBuilder_CubeBlock block)
    {
        try
        {
            var size = MyDefinitionManager.Static.GetCubeBlockDefinition(block)?.Size ?? Vector3I.One;
            return new Vector3I(
                Math.Max(1, size.X),
                Math.Max(1, size.Y),
                Math.Max(1, size.Z));
        }
        catch
        {
            // Modded or unavailable definitions can still be matched by Min.
            return Vector3I.One;
        }
    }

    private static Vector3I Cross(Vector3I left, Vector3I right) => new(
        left.Y * right.Z - left.Z * right.Y,
        left.Z * right.X - left.X * right.Z,
        left.X * right.Y - left.Y * right.X);

    private static Vector3I ToVector(Base6Directions.Direction direction)
    {
        return direction switch
        {
            Base6Directions.Direction.Forward => new Vector3I(0, 0, -1),
            Base6Directions.Direction.Backward => new Vector3I(0, 0, 1),
            Base6Directions.Direction.Left => new Vector3I(-1, 0, 0),
            Base6Directions.Direction.Right => new Vector3I(1, 0, 0),
            Base6Directions.Direction.Up => new Vector3I(0, 1, 0),
            Base6Directions.Direction.Down => new Vector3I(0, -1, 0),
            _ => new Vector3I(0, 0, -1)
        };
    }

    private static CellKey ToKey(SerializableVector3I position) =>
        new(position.X, position.Y, position.Z);

    private static CellKey ToKey(Vector3I position) =>
        new(position.X, position.Y, position.Z);

    private readonly struct CellKey : IEquatable<CellKey>
    {
        private readonly int x;
        private readonly int y;
        private readonly int z;

        public CellKey(int x, int y, int z)
        {
            this.x = x;
            this.y = y;
            this.z = z;
        }

        public bool Equals(CellKey other) => x == other.x && y == other.y && z == other.z;

        public override bool Equals(object? obj) => obj is CellKey other && Equals(other);

        public override int GetHashCode()
        {
            unchecked
            {
                var hash = x;
                hash = hash * 397 ^ y;
                return hash * 397 ^ z;
            }
        }
    }
}