using Sandbox.Definitions; using VRage; using VRage.Game; using VRageMath; namespace SpaceEngineersBlueprintEditor.SpaceEngineersCore.BlueprintEditing; /// /// 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. /// public static class BlockGroupResolver { public static IReadOnlyList 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(); } var occupiedCells = BuildOccupiedCellIndex(grid.CubeBlocks); return ResolveBlocks(group.Blocks, occupiedCells); } /// /// Resolves every group in a grid while constructing the occupied-cell index /// only once. /// public static IReadOnlyDictionary> ResolveGroups(MyObjectBuilder_CubeGrid grid) { if (grid is null) { throw new ArgumentNullException(nameof(grid)); } var result = new Dictionary>(); 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() : ResolveBlocks(group.Blocks, occupiedCells); } return result; } private static IReadOnlyList ResolveBlocks( IEnumerable positions, IReadOnlyDictionary occupiedCells) { var result = new List(); var addedBlocks = new HashSet(); foreach (var position in positions) { if (occupiedCells.TryGetValue(ToKey(position), out var block) && addedBlocks.Add(block)) { result.Add(block); } } return result; } /// /// Gets the axis-aligned cell dimensions occupied after applying the block's /// Forward/Up orientation to its definition size. /// 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 BuildOccupiedCellIndex( IEnumerable blocks) { var result = new Dictionary(); 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 { 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; } } } }