|
|
|
using System.IO;
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
|
|
|
public partial class Terrain
|
|
|
|
{
|
|
|
|
// These mirror the modes / shapes in 'terrain_editing_controls.gd'.
|
|
|
|
enum ToolMode { Height, Flatten, Paint }
|
|
|
|
enum ToolShape { Corner, Circle, Square }
|
|
|
|
|
|
|
|
// Set by the terrain editing plugin.
|
|
|
|
// Enables access to the in-editor undo/redo system.
|
|
|
|
public EditorUndoRedoManager EditorUndoRedo { get; set; }
|
|
|
|
|
|
|
|
// Dummy value to satisfy the overly careful compiler.
|
|
|
|
static bool _dummy = false;
|
|
|
|
|
|
|
|
Material _editToolMaterial;
|
|
|
|
public override void _EnterTree()
|
|
|
|
{
|
|
|
|
_editToolMaterial = new StandardMaterial3D {
|
|
|
|
VertexColorUseAsAlbedo = true,
|
|
|
|
BlendMode = BaseMaterial3D.BlendModeEnum.Mix,
|
|
|
|
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
|
|
|
|
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled,
|
|
|
|
NoDepthTest = true,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Vector3 _lastPosition;
|
|
|
|
public bool EditorInput(InputEvent ev, Control controls, Vector3 maybePos)
|
|
|
|
{
|
|
|
|
var prevent_default = false;
|
|
|
|
|
|
|
|
// If 'maybePosition' is Vector3.Zero, use previous know position, otherwise update.
|
|
|
|
var position = (maybePos == Vector3.Zero) ? _lastPosition : (_lastPosition = maybePos);
|
|
|
|
|
|
|
|
var toolMode = (ToolMode)(int)controls.Get("tool_mode");
|
|
|
|
var toolShape = (ToolShape)(int)controls.Get("tool_shape");
|
|
|
|
var texture = (byte)(int)controls.Get("texture");
|
|
|
|
var drawSize = (int)controls.Get("draw_size");
|
|
|
|
|
|
|
|
var isRaise = (bool)controls.Get("is_raise");
|
|
|
|
var isConnected = (bool)controls.Get("is_connected");
|
|
|
|
var isFlatten = toolMode == ToolMode.Flatten;
|
|
|
|
var isCorner = toolShape == ToolShape.Corner;
|
|
|
|
|
|
|
|
var hover = ToTilePos(position);
|
|
|
|
if (isCorner) drawSize = 1;
|
|
|
|
var isEven = (drawSize % 2) == 0;
|
|
|
|
var radius = FloorToInt(drawSize / 2.0f);
|
|
|
|
|
|
|
|
// Offset hover tile position by corner.
|
|
|
|
// FIXME: This causes FLATTEN to calculate the wrong height in some cases.
|
|
|
|
if (isEven) hover.Position += hover.Corner switch {
|
|
|
|
Corner.TopLeft => (0, 0),
|
|
|
|
Corner.TopRight => (1, 0),
|
|
|
|
Corner.BottomRight => (1, 1),
|
|
|
|
Corner.BottomLeft => (0, 1),
|
|
|
|
_ => throw new InvalidOperationException(),
|
|
|
|
};
|
|
|
|
|
|
|
|
IEnumerable<TilePos> GetTilesInSquare() {
|
|
|
|
var min = hover.Position + (-radius, -radius);
|
|
|
|
var max = hover.Position + (+radius, +radius);
|
|
|
|
if (isEven) max += (-1, -1);
|
|
|
|
for (var x = min.X; x <= max.X; x++)
|
|
|
|
for (var z = min.Z; z <= max.Z; z++)
|
|
|
|
yield return new(x, z);
|
|
|
|
}
|
|
|
|
|
|
|
|
IEnumerable<TilePos> GetTilesInRadius() {
|
|
|
|
var center = isEven ? (Vector2I)hover.Position
|
|
|
|
: hover.Position.Center;
|
|
|
|
var distanceSqr = Pow(radius + (isEven ? -1 : 1) * 0.25f, 2);
|
|
|
|
return GetTilesInSquare().Where(tile =>
|
|
|
|
center.DistanceSquaredTo(tile.Center) < distanceSqr);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners.
|
|
|
|
// TODO: Use ArrayMesh instead of ImmediateMesh.
|
|
|
|
|
|
|
|
// Holds onto all the tiles and which of their corners corners will be affected by this edit operation.
|
|
|
|
var tilesToChange = new Dictionary<TilePos, Corners<bool>>();
|
|
|
|
|
|
|
|
// Don't look at this black magic. The Dictionary type should have this by default I swear!
|
|
|
|
// Basically, this returns a reference to an entry in the dictionary that can be modified directly.
|
|
|
|
ref Corners<bool> Tile(TilePos position) => ref CollectionsMarshal.GetValueRefOrAddDefault(tilesToChange, position, out _dummy);
|
|
|
|
|
|
|
|
if (toolMode == ToolMode.Paint) {
|
|
|
|
// In PAINT mode, only full tiles are ever affected.
|
|
|
|
// So this makes populating 'tilesToChange' very straight-forward.
|
|
|
|
|
|
|
|
var tiles = toolShape switch {
|
|
|
|
// While in PAINT mode, the CORNER shape instead affects
|
|
|
|
// a single tile, regardless of the current 'draw_size'.
|
|
|
|
ToolShape.Corner => [ hover.Position ],
|
|
|
|
ToolShape.Circle => GetTilesInRadius(),
|
|
|
|
ToolShape.Square => GetTilesInSquare(),
|
|
|
|
_ => throw new InvalidOperationException(),
|
|
|
|
};
|
|
|
|
|
|
|
|
foreach (var pos in tiles)
|
|
|
|
tilesToChange.Add(pos, new(true));
|
|
|
|
|
|
|
|
} else if (toolShape == ToolShape.Corner) {
|
|
|
|
// With the CORNER shape, only a single corner is affected.
|
|
|
|
|
|
|
|
// Modify selected corner itself.
|
|
|
|
Tile(hover.Position)[hover.Corner] = true;
|
|
|
|
|
|
|
|
// If the 'connected_toggle' button is active, move "connected" corners.
|
|
|
|
// This is a simplified version of the code below that only affects the 3 neighboring corners.
|
|
|
|
if (isConnected) {
|
|
|
|
var height = Data.GetTileOrDefault(hover.Position).Height[hover.Corner];
|
|
|
|
foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) {
|
|
|
|
var neighborHeight = Data.GetTileOrDefault(neighbor.Position).Height[neighbor.Corner];
|
|
|
|
if (neighborHeight != height) continue;
|
|
|
|
Tile(neighbor.Position)[neighbor.Corner] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
var tiles = (toolShape switch {
|
|
|
|
ToolShape.Circle => GetTilesInRadius(),
|
|
|
|
ToolShape.Square => GetTilesInSquare(),
|
|
|
|
_ => throw new InvalidOperationException(),
|
|
|
|
}).ToHashSet();
|
|
|
|
|
|
|
|
// Modify selected tiles themselves.
|
|
|
|
foreach (var pos in tiles)
|
|
|
|
tilesToChange.Add(pos, new(true));
|
|
|
|
|
|
|
|
// If the 'connected_toggle' button is active, move "connected" corners.
|
|
|
|
// Connected corners are the ones that are at the same height as ones already being moved.
|
|
|
|
if (isConnected) foreach (var pos in tiles) {
|
|
|
|
var tile = Data.GetTileOrDefault(pos);
|
|
|
|
foreach (var corner in Enum.GetValues<Corner>()) {
|
|
|
|
var height = tile.Height[corner];
|
|
|
|
foreach (var neighbor in GetNeighbors(pos, corner)) {
|
|
|
|
if (tiles.Contains(neighbor.Position)) continue;
|
|
|
|
var neighborHeight = Data.GetTileOrDefault(neighbor.Position).Height[neighbor.Corner];
|
|
|
|
if (neighborHeight != height) continue;
|
|
|
|
Tile(neighbor.Position)[neighbor.Corner] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Modify the terrain when left mouse button is pressed.
|
|
|
|
if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) {
|
|
|
|
prevent_default = true;
|
|
|
|
|
|
|
|
string action;
|
|
|
|
StringName method;
|
|
|
|
Variant[] doArgs;
|
|
|
|
Variant[] undoArgs;
|
|
|
|
|
|
|
|
if (toolMode == ToolMode.Paint) {
|
|
|
|
// TODO: Support blending somehow.
|
|
|
|
var tilesPrevious = new List<(TilePos, byte)>();
|
|
|
|
var tilesChanged = new List<(TilePos, byte)>();
|
|
|
|
|
|
|
|
foreach (var (pos, corners) in tilesToChange) {
|
|
|
|
var tile = Data.GetTileOrDefault(pos);
|
|
|
|
tilesPrevious.Add((pos, tile.TexturePrimary));
|
|
|
|
tilesChanged.Add((pos, texture));
|
|
|
|
}
|
|
|
|
|
|
|
|
action = "Paint terrain";
|
|
|
|
method = nameof(DoModifyTerrainTexture);
|
|
|
|
doArgs = [ PackTextureData(tilesChanged) ];
|
|
|
|
undoArgs = [ PackTextureData(tilesPrevious) ];
|
|
|
|
} else {
|
|
|
|
var tilesPrevious = new List<(TilePos, Corners<short>)>();
|
|
|
|
var tilesChanged = new List<(TilePos, Corners<short>)>();
|
|
|
|
|
|
|
|
var amount = isFlatten ? Data.GetTileOrDefault(hover.Position).Height[hover.Corner]
|
|
|
|
: isRaise ? (short)+1 : (short)-1;
|
|
|
|
|
|
|
|
foreach (var (pos, corners) in tilesToChange) {
|
|
|
|
var tile = Data.GetTileOrDefault(pos);
|
|
|
|
tilesPrevious.Add((pos, tile.Height));
|
|
|
|
|
|
|
|
var newHeight = tile.Height;
|
|
|
|
if (isFlatten) {
|
|
|
|
if (corners.TopLeft ) newHeight.TopLeft = amount;
|
|
|
|
if (corners.TopRight ) newHeight.TopRight = amount;
|
|
|
|
if (corners.BottomRight) newHeight.BottomRight = amount;
|
|
|
|
if (corners.BottomLeft ) newHeight.BottomLeft = amount;
|
|
|
|
} else {
|
|
|
|
if (corners.TopLeft ) newHeight.TopLeft += amount;
|
|
|
|
if (corners.TopRight ) newHeight.TopRight += amount;
|
|
|
|
if (corners.BottomRight) newHeight.BottomRight += amount;
|
|
|
|
if (corners.BottomLeft ) newHeight.BottomLeft += amount;
|
|
|
|
}
|
|
|
|
tilesChanged.Add((pos, newHeight));
|
|
|
|
}
|
|
|
|
|
|
|
|
action = isFlatten ? "Flatten terrain"
|
|
|
|
: isRaise ? "Raise terrain"
|
|
|
|
: "Lower Terrain";
|
|
|
|
method = nameof(DoModifyTerrainHeight);
|
|
|
|
doArgs = [ PackHeightData(tilesChanged) ];
|
|
|
|
undoArgs = [ PackHeightData(tilesPrevious) ];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (EditorUndoRedo is EditorUndoRedoManager undo) {
|
|
|
|
undo.CreateAction(action, backwardUndoOps: false);
|
|
|
|
|
|
|
|
undo.AddDoMethod(this, method, doArgs);
|
|
|
|
undo.AddDoMethod(this, nameof(UpdateMeshAndShape));
|
|
|
|
undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged);
|
|
|
|
|
|
|
|
undo.AddUndoMethod(this, method, undoArgs);
|
|
|
|
undo.AddUndoMethod(this, nameof(UpdateMeshAndShape));
|
|
|
|
undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged);
|
|
|
|
|
|
|
|
undo.CommitAction(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
UpdateEditToolMesh(tilesToChange);
|
|
|
|
return prevent_default;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void EditorUnfocus()
|
|
|
|
=> ClearEditToolMesh();
|
|
|
|
|
|
|
|
public void DoModifyTerrainHeight(byte[] data)
|
|
|
|
{
|
|
|
|
foreach (var (pos, corners) in UnpackHeightData(data))
|
|
|
|
Data[pos].Height = corners;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void DoModifyTerrainTexture(byte[] data)
|
|
|
|
{
|
|
|
|
foreach (var (pos, texture) in UnpackTextureData(data))
|
|
|
|
Data[pos].TexturePrimary = texture;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UpdateEditToolMesh(Dictionary<TilePos, Corners<bool>> tiles)
|
|
|
|
{
|
|
|
|
var mesh = GetOrCreateMesh("EditToolMesh");
|
|
|
|
mesh.ClearSurfaces();
|
|
|
|
mesh.SurfaceBegin(Mesh.PrimitiveType.Lines);
|
|
|
|
|
|
|
|
void AddLine((Vector3 Position, bool Visible) start,
|
|
|
|
(Vector3 Position, bool Visible) end) {
|
|
|
|
mesh.SurfaceSetColor(start.Visible ? Colors.Blue : Colors.Transparent);
|
|
|
|
mesh.SurfaceAddVertex(start.Position);
|
|
|
|
mesh.SurfaceSetColor(end.Visible ? Colors.Blue : Colors.Transparent);
|
|
|
|
mesh.SurfaceAddVertex(end.Position);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (var (tile, visible) in tiles) {
|
|
|
|
var positions = ToPositions(tile);
|
|
|
|
foreach (var side in Enum.GetValues<Side>()) {
|
|
|
|
var (corner1, corner2) = side.GetCorners();
|
|
|
|
if (!visible[corner1] && !visible[corner2]) continue;
|
|
|
|
AddLine((positions[corner1], visible[corner1]),
|
|
|
|
(positions[corner2], visible[corner2]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mesh.SurfaceEnd();
|
|
|
|
mesh.SurfaceSetMaterial(0, _editToolMaterial);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ClearEditToolMesh()
|
|
|
|
=> GetNodeOrNull("EditToolMesh")?.QueueFree();
|
|
|
|
|
|
|
|
|
|
|
|
(TilePos Position, Corner Corner) ToTilePos(Vector3 position)
|
|
|
|
{
|
|
|
|
var local = ToLocal(position);
|
|
|
|
var coord = new Vector2(local.X, local.Z) / TileSize;
|
|
|
|
var corner = coord.PosMod(1).RoundToVector2I() switch {
|
|
|
|
(0, 0) => Corner.TopLeft,
|
|
|
|
(1, 0) => Corner.TopRight,
|
|
|
|
(1, 1) => Corner.BottomRight,
|
|
|
|
(0, 1) => Corner.BottomLeft,
|
|
|
|
_ => throw new InvalidOperationException(),
|
|
|
|
};
|
|
|
|
return ((TilePos)coord, corner);
|
|
|
|
}
|
|
|
|
|
|
|
|
static readonly Dictionary<Corner, (int X, int Z, Corner Opposite)[]> _offsetLookup = new(){
|
|
|
|
[Corner.TopLeft ] = [(-1, -1, Corner.BottomRight), (-1, 0, Corner.TopRight ), (0, -1, Corner.BottomLeft )],
|
|
|
|
[Corner.TopRight ] = [(+1, -1, Corner.BottomLeft ), (+1, 0, Corner.TopLeft ), (0, -1, Corner.BottomRight)],
|
|
|
|
[Corner.BottomRight] = [(+1, +1, Corner.TopLeft ), (+1, 0, Corner.BottomLeft ), (0, +1, Corner.TopRight )],
|
|
|
|
[Corner.BottomLeft ] = [(-1, +1, Corner.TopRight ), (-1, 0, Corner.BottomRight), (0, +1, Corner.TopLeft )],
|
|
|
|
};
|
|
|
|
static IEnumerable<(TilePos Position, Corner Corner)> GetNeighbors(TilePos pos, Corner corner)
|
|
|
|
=> _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Z + e.Z), e.Opposite));
|
|
|
|
|
|
|
|
|
|
|
|
static byte[] PackHeightData(IEnumerable<(TilePos Position, Corners<short> Corners)> data)
|
|
|
|
{
|
|
|
|
using var stream = new MemoryStream();
|
|
|
|
using var writer = new BinaryWriter(stream);
|
|
|
|
foreach (var (pos, corners) in data) {
|
|
|
|
writer.Write(pos.X);
|
|
|
|
writer.Write(pos.Z);
|
|
|
|
writer.Write(corners.TopLeft);
|
|
|
|
writer.Write(corners.TopRight);
|
|
|
|
writer.Write(corners.BottomRight);
|
|
|
|
writer.Write(corners.BottomLeft);
|
|
|
|
}
|
|
|
|
return stream.ToArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
static IEnumerable<(TilePos Position, Corners<short> Corners)> UnpackHeightData(byte[] data)
|
|
|
|
{
|
|
|
|
using var stream = new MemoryStream(data);
|
|
|
|
using var reader = new BinaryReader(stream);
|
|
|
|
while (stream.Position < stream.Length) {
|
|
|
|
var x = reader.ReadInt32();
|
|
|
|
var y = reader.ReadInt32();
|
|
|
|
var corners = new Corners<short>(
|
|
|
|
reader.ReadInt16(), reader.ReadInt16(),
|
|
|
|
reader.ReadInt16(), reader.ReadInt16());
|
|
|
|
yield return (new(x, y), corners);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static byte[] PackTextureData(IEnumerable<(TilePos Position, byte Texture)> data)
|
|
|
|
{
|
|
|
|
using var stream = new MemoryStream();
|
|
|
|
using var writer = new BinaryWriter(stream);
|
|
|
|
foreach (var (pos, texture) in data) {
|
|
|
|
writer.Write(pos.X);
|
|
|
|
writer.Write(pos.Z);
|
|
|
|
writer.Write(texture);
|
|
|
|
}
|
|
|
|
return stream.ToArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
static IEnumerable<(TilePos Position, byte Texture)> UnpackTextureData(byte[] data)
|
|
|
|
{
|
|
|
|
using var stream = new MemoryStream(data);
|
|
|
|
using var reader = new BinaryReader(stream);
|
|
|
|
while (stream.Position < stream.Length) {
|
|
|
|
var x = reader.ReadInt32();
|
|
|
|
var y = reader.ReadInt32();
|
|
|
|
var texture = reader.ReadByte();
|
|
|
|
yield return (new(x, y), texture);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|