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;
}
}
}
}