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 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 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(); // 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()) { 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 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)> 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()) { 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 _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( 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, }); } } }