using System.IO; using System.Runtime.InteropServices; public partial class Terrain { enum ToolMode { Height, Flatten, Paint } enum ToolShape { Corner, Circle, Square } // Set by the terrain editing plugin. 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 = (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. if (isEven) hover.Position = hover.Corner switch { Corner.TopLeft => hover.Position.Offset(0, 0), Corner.TopRight => hover.Position.Offset(1, 0), Corner.BottomRight => hover.Position.Offset(1, 1), Corner.BottomLeft => hover.Position.Offset(0, 1), _ => throw new InvalidOperationException(), }; IEnumerable GetTilesInSquare() { var min = hover.Position.Offset(-radius, -radius); var max = hover.Position.Offset(+radius, +radius); if (isEven) max = max.Offset(-1, -1); for (var x = min.X; x <= max.X; x++) for (var y = min.Y; y <= max.Y; y++) yield return new(x, y); } IEnumerable GetTilesInRadius() { var center = isEven ? hover.Position.ToVector2I() : hover.Position.ToCenter(); var distanceSqr = Pow(radius + 0.25f * (isEven ? -1 : 1), 2); return GetTilesInSquare().Where(tile => center.DistanceSquaredTo(tile.ToCenter()) < distanceSqr); } // TODO: Handle different tool modes, such as painting. // TODO: Allow click-dragging which doesn't affect already changed tiles / corners. // TODO: Use ArrayMesh instead of ImmediateMesh. // TODO: Dynamically expand terrain instead of having it be a set size. // Holds onto all the tiles and which of their corners corners will be affected by this edit operation. var tilesToChange = new Dictionary>(); ref Corners GetTileToChange(TilePos position) // Don't look at this black magic. The Dictionary type should have this by default I swear! => ref CollectionsMarshal.GetValueRefOrAddDefault(tilesToChange, position, out _dummy); if (toolShape == ToolShape.Corner) { // Modify selected corner itself. GetTileToChange(hover.Position)[hover.Corner] = true; if (isConnected) { var height = GetTile(hover.Position).Height[hover.Corner]; foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) { var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner]; if (neighborHeight != height) continue; GetTileToChange(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) GetTileToChange(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 = GetTile(pos); foreach (var corner in Enum.GetValues()) { var height = tile.Height[corner]; foreach (var neighbor in GetNeighbors(pos, corner)) { if (tiles.Contains(neighbor.Position)) continue; var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner]; if (neighborHeight != height) continue; GetTileToChange(neighbor.Position)[neighbor.Corner] = true; } } } } // Raise / lower the terrain when left mouse button is pressed. if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { prevent_default = true; const float AdjustHeight = 0.5f; var amount = isFlatten ? GetTile(hover.Position).Height[hover.Corner] : isRaise ? AdjustHeight : -AdjustHeight; var tilesPrevious = new List<(TilePos, Corners)>(); var tilesChanged = new List<(TilePos, Corners)>(); foreach (var (pos, corners) in tilesToChange) { var tile = GetTile(pos); tilesPrevious.Add((pos, tile.Height)); var newHeight = tile.Height; if (corners.TopLeft ) newHeight.TopLeft = isFlatten ? amount : newHeight.TopLeft + amount; if (corners.TopRight ) newHeight.TopRight = isFlatten ? amount : newHeight.TopRight + amount; if (corners.BottomRight) newHeight.BottomRight = isFlatten ? amount : newHeight.BottomRight + amount; if (corners.BottomLeft ) newHeight.BottomLeft = isFlatten ? amount : newHeight.BottomLeft + amount; tilesChanged.Add((pos, newHeight)); } if (EditorUndoRedo is EditorUndoRedoManager undo) { var name = "Modify terrain height"; // TODO: Change name depending on tool mode. undo.CreateAction(name, backwardUndoOps: false); undo.AddDoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesChanged)); undo.AddDoMethod(this, nameof(UpdateMeshAndShape)); undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); undo.AddUndoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesPrevious)); 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 Unpack(data)) { var tile = GetTile(pos); tile.Height = corners; SetTile(pos, tile); } } void UpdateEditToolMesh(Dictionary> 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 = GetTileCornerPositions(tile); foreach (var side in Enum.GetValues()) { 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 + (Size + Vector2.One) / 2; var pos = TilePos.From(coord); 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 (pos, corner); } static readonly Dictionary _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.Y + e.Y), e.Opposite)); static byte[] Pack(IEnumerable<(TilePos Position, Corners 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.Y); writer.Write(corners.TopLeft); writer.Write(corners.TopRight); writer.Write(corners.BottomRight); writer.Write(corners.BottomLeft); } return stream.ToArray(); } static IEnumerable<(TilePos Position, Corners Corners)> 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 corners = new Corners( reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); yield return (new(x, y), corners); } } }