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