You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
325 lines
11 KiB
325 lines
11 KiB
using System.IO; |
|
|
|
public partial class Terrain |
|
{ |
|
// These mirror the modes / shapes in 'terrain_editing_controls.gd'. |
|
enum ToolMode { Height, Flatten, Paint, Erase } |
|
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; } |
|
|
|
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. |
|
// TODO: Support texture blending somehow. |
|
// TODO: Clear empty chunks. |
|
|
|
// Data structure for holding tiles to be modified. |
|
var tilesToChange = new Dictionary<TilePos, TileModification>(); |
|
|
|
// Utility function to set 'Affected' for the specified position and corner. |
|
void SetCornerAffected((TilePos Position, Corner Corner) pair) |
|
=> tilesToChange.GetOrAddNew(pair.Position).Affected[pair.Corner] = true; |
|
|
|
// Utility function to get the height for the specified position and corner. |
|
short GetCornerHeight((TilePos Position, Corner Corner) pair) |
|
=> Data.GetTileOrDefault(pair.Position).Height[pair.Corner]; |
|
|
|
if (toolMode is ToolMode.Paint or ToolMode.Erase) { |
|
// In PAINT or ERASE 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(){ Affected = new(true) }); |
|
|
|
} else if (toolShape == ToolShape.Corner) { |
|
// With the CORNER shape, only a single corner is affected. |
|
|
|
// Modify selected corner itself. |
|
SetCornerAffected(hover); |
|
|
|
// 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 = GetCornerHeight(hover); |
|
foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) |
|
if (GetCornerHeight(neighbor) == height) |
|
SetCornerAffected(neighbor); |
|
} |
|
|
|
} 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(){ Affected = 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)) |
|
if (GetCornerHeight(neighbor) == height) |
|
SetCornerAffected(neighbor); |
|
} |
|
} |
|
|
|
} |
|
|
|
// Modify the terrain when left mouse button is pressed. |
|
if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { |
|
prevent_default = true; |
|
|
|
// Fill with current tile data. |
|
foreach (var (pos, modified) in tilesToChange) |
|
modified.Original = modified.New = Data.GetTileOrDefault(pos); |
|
|
|
string action; |
|
if (toolMode == ToolMode.Paint) { |
|
action = "Paint terrain"; |
|
foreach (var (_, modified) in tilesToChange) |
|
modified.New.TexturePrimary = texture; |
|
} else if (toolMode == ToolMode.Erase) { |
|
action = "Erase terrain"; |
|
foreach (var (_, modified) in tilesToChange) |
|
modified.New = default; |
|
} else if (isFlatten) { |
|
action = "Flatten terrain"; |
|
var amount = GetCornerHeight(hover); |
|
foreach (var (_, modified) in tilesToChange) { |
|
ref var height = ref modified.New.Height; |
|
if (modified.Affected.TopLeft ) height.TopLeft = amount; |
|
if (modified.Affected.TopRight ) height.TopRight = amount; |
|
if (modified.Affected.BottomRight) height.BottomRight = amount; |
|
if (modified.Affected.BottomLeft ) height.BottomLeft = amount; |
|
} |
|
} else { |
|
action = isRaise ? "Raise terrain" : "Lower terrain"; |
|
var amount = isRaise ? (short)+1 : (short)-1; |
|
foreach (var (_, modified) in tilesToChange) { |
|
ref var height = ref modified.New.Height; |
|
if (modified.Affected.TopLeft ) height.TopLeft += amount; |
|
if (modified.Affected.TopRight ) height.TopRight += amount; |
|
if (modified.Affected.BottomRight) height.BottomRight += amount; |
|
if (modified.Affected.BottomLeft ) height.BottomLeft += amount; |
|
} |
|
} |
|
|
|
if (EditorUndoRedo is EditorUndoRedoManager undo) { |
|
undo.CreateAction(action, backwardUndoOps: false); |
|
|
|
var doData = Pack(tilesToChange.Select(e => (e.Key, e.Value.New))); |
|
var undoData = Pack(tilesToChange.Select(e => (e.Key, e.Value.Original))); |
|
|
|
undo.AddDoMethod(this, nameof(DoModifyTerrain), doData); |
|
undo.AddDoMethod(this, nameof(UpdateMeshAndShape)); |
|
undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); |
|
|
|
undo.AddUndoMethod(this, nameof(DoModifyTerrain), undoData); |
|
undo.AddUndoMethod(this, nameof(UpdateMeshAndShape)); |
|
undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); |
|
|
|
undo.CommitAction(true); |
|
} |
|
} |
|
|
|
UpdateEditToolMesh(tilesToChange.Select(e => (e.Key, e.Value.Affected))); |
|
return prevent_default; |
|
} |
|
|
|
class TileModification |
|
{ |
|
public Corners<bool> Affected; |
|
public Tile Original; |
|
public Tile New; |
|
} |
|
|
|
public void EditorUnfocus() |
|
=> ClearEditToolMesh(); |
|
|
|
public void DoModifyTerrain(byte[] data) |
|
{ |
|
foreach (var (pos, tile) in Unpack(data)) |
|
Data[pos] = tile; |
|
} |
|
|
|
|
|
void UpdateEditToolMesh(IEnumerable<(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[] Pack(IEnumerable<(TilePos Position, Tile tile)> data) |
|
{ |
|
using var stream = new MemoryStream(); |
|
using var writer = new BinaryWriter(stream); |
|
foreach (var (pos, tile) in data) { |
|
writer.Write(pos.X); |
|
writer.Write(pos.Z); |
|
writer.Write(tile.Height.TopLeft); |
|
writer.Write(tile.Height.TopRight); |
|
writer.Write(tile.Height.BottomRight); |
|
writer.Write(tile.Height.BottomLeft); |
|
writer.Write(tile.TexturePrimary); |
|
writer.Write(tile.TextureSecondary); |
|
writer.Write(tile.TextureBlend); |
|
} |
|
return stream.ToArray(); |
|
} |
|
|
|
static IEnumerable<(TilePos Position, Tile tile)> Unpack(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 height = new Corners<short>( |
|
reader.ReadInt16(), reader.ReadInt16(), |
|
reader.ReadInt16(), reader.ReadInt16()); |
|
|
|
var texPrimary = reader.ReadByte(); |
|
var texSecondary = reader.ReadByte(); |
|
var texBlend = reader.ReadByte(); |
|
|
|
yield return (new(x, y), new(){ |
|
Height = height, |
|
TexturePrimary = texPrimary, |
|
TextureSecondary = texSecondary, |
|
TextureBlend = texBlend, |
|
}); |
|
} |
|
} |
|
}
|
|
|