@ -4,16 +4,13 @@ using System.Runtime.InteropServices;
public partial class Terrain
public partial class Terrain
{
{
// These mirror the modes / shapes in 'terrain_editing_controls.gd'.
// These mirror the modes / shapes in 'terrain_editing_controls.gd'.
enum ToolMode { Height , Flatten , Paint }
enum ToolMode { Height , Flatten , Paint , Erase }
enum ToolShape { Corner , Circle , Square }
enum ToolShape { Corner , Circle , Square }
// Set by the terrain editing plugin.
// Set by the terrain editing plugin.
// Enables access to the in-editor undo/redo system.
// Enables access to the in-editor undo/redo system.
public EditorUndoRedoManager EditorUndoRedo { get ; set ; }
public EditorUndoRedoManager EditorUndoRedo { get ; set ; }
// Dummy value to satisfy the overly careful compiler.
static bool _d ummy = false ;
Material _ editToolMaterial ;
Material _ editToolMaterial ;
public override void _ EnterTree ( )
public override void _ EnterTree ( )
{
{
@ -78,16 +75,22 @@ public partial class Terrain
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners.
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners.
// TODO: Use ArrayMesh instead of ImmediateMesh.
// TODO: Use ArrayMesh instead of ImmediateMesh.
// TODO: Support texture blending somehow.
// TODO: Clear empty chunks.
// Holds onto all the tiles and which of their corners corners will be affected by this edit operation.
// Data structure for holding tiles to be modified .
var tilesToChange = new Dictionary < TilePos , Corners < bool > > ( ) ;
var tilesToChange = new Dictionary < TilePos , TileModification > ( ) ;
// Don't look at this black magic. The Dictionary type should have this by default I swear!
// Utility function to set 'Affected' for the specified position and corner.
// Basically, this returns a reference to an entry in the dictionary that can be modified directly.
void SetCornerAffected ( ( TilePos Position , Corner Corner ) pair )
ref Corners < bool > Tile ( TilePos position ) = > ref CollectionsMarshal . GetValueRefOrAddDefault ( tilesToChange , position , out _d ummy ) ;
= > tilesToChange . GetOrAddNew ( pair . Position ) . Affected [ pair . Corner ] = true ;
if ( toolMode = = ToolMode . Paint ) {
// Utility function to get the height for the specified position and corner.
// In PAINT mode, only full tiles are ever affected.
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.
// So this makes populating 'tilesToChange' very straight-forward.
var tiles = toolShape switch {
var tiles = toolShape switch {
@ -100,23 +103,21 @@ public partial class Terrain
} ;
} ;
foreach ( var pos in tiles )
foreach ( var pos in tiles )
tilesToChange . Add ( pos , new ( true ) ) ;
tilesToChange . Add ( pos , new ( ) { Affected = new ( true ) } ) ;
} else if ( toolShape = = ToolShape . Corner ) {
} else if ( toolShape = = ToolShape . Corner ) {
// With the CORNER shape, only a single corner is affected.
// With the CORNER shape, only a single corner is affected.
// Modify selected corner itself.
// Modify selected corner itself.
Tile ( hover . Position ) [ hover . Corner ] = true ;
SetCornerAffected ( hover ) ;
// If the 'connected_toggle' button is active, move "connected" corners.
// 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.
// This is a simplified version of the code below that only affects the 3 neighboring corners.
if ( isConnected ) {
if ( isConnected ) {
var height = Data . GetTileOrDefault ( hover . Position ) . Height [ hover . Corner ] ;
var height = GetCornerHeight ( hover ) ;
foreach ( var neighbor in GetNeighbors ( hover . Position , hover . Corner ) ) {
foreach ( var neighbor in GetNeighbors ( hover . Position , hover . Corner ) )
var neighborHeight = Data . GetTileOrDefault ( neighbor . Position ) . Height [ neighbor . Corner ] ;
if ( GetCornerHeight ( neighbor ) = = height )
if ( neighborHeight ! = height ) continue ;
SetCornerAffected ( neighbor ) ;
Tile ( neighbor . Position ) [ neighbor . Corner ] = true ;
}
}
}
} else {
} else {
@ -129,7 +130,7 @@ public partial class Terrain
// Modify selected tiles themselves.
// Modify selected tiles themselves.
foreach ( var pos in tiles )
foreach ( var pos in tiles )
tilesToChange . Add ( pos , new ( true ) ) ;
tilesToChange . Add ( pos , new ( ) { Affected = new ( true ) } ) ;
// If the 'connected_toggle' button is active, move "connected" corners.
// 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.
// Connected corners are the ones that are at the same height as ones already being moved.
@ -137,12 +138,10 @@ public partial class Terrain
var tile = Data . GetTileOrDefault ( pos ) ;
var tile = Data . GetTileOrDefault ( pos ) ;
foreach ( var corner in Enum . GetValues < Corner > ( ) ) {
foreach ( var corner in Enum . GetValues < Corner > ( ) ) {
var height = tile . Height [ corner ] ;
var height = tile . Height [ corner ] ;
foreach ( var neighbor in GetNeighbors ( pos , corner ) ) {
foreach ( var neighbor in GetNeighbors ( pos , corner ) )
if ( tiles . Contains ( neighbor . Position ) ) continue ;
if ( ! tiles . Contains ( neighbor . Position ) )
var neighborHeight = Data . GetTileOrDefault ( neighbor . Position ) . Height [ neighbor . Corner ] ;
if ( GetCornerHeight ( neighbor ) = = height )
if ( neighborHeight ! = height ) continue ;
SetCornerAffected ( neighbor ) ;
Tile ( neighbor . Position ) [ neighbor . Corner ] = true ;
}
}
}
}
}
@ -152,68 +151,52 @@ public partial class Terrain
if ( ev is InputEventMouseButton { ButtonIndex : MouseButton . Left , Pressed : true } ) {
if ( ev is InputEventMouseButton { ButtonIndex : MouseButton . Left , Pressed : true } ) {
prevent_default = true ;
prevent_default = true ;
string action ;
// Fill with current tile data.
StringName method ;
foreach ( var ( pos , modified ) in tilesToChange )
Variant [ ] doArgs ;
modified . Original = modified . New = Data . GetTileOrDefault ( pos ) ;
Variant [ ] undoArgs ;
string action ;
if ( toolMode = = ToolMode . Paint ) {
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" ;
action = "Paint terrain" ;
method = nameof ( DoModifyTerrainTexture ) ;
foreach ( var ( _ , modified ) in tilesToChange )
doArgs = [ PackTextureData ( tilesChanged ) ] ;
modified . New . TexturePrimary = texture ;
undoArgs = [ PackTextureData ( tilesPrevious ) ] ;
} else if ( toolMode = = ToolMode . Erase ) {
} else {
action = "Erase terrain" ;
var tilesPrevious = new List < ( TilePos , Corners < short > ) > ( ) ;
foreach ( var ( _ , modified ) in tilesToChange )
var tilesChanged = new List < ( TilePos , Corners < short > ) > ( ) ;
modified . New = default ;
} else if ( isFlatten ) {
var amount = isFlatten ? Data . GetTileOrDefault ( hover . Position ) . Height [ hover . Corner ]
action = "Flatten terrain" ;
: isRaise ? ( short ) + 1 : ( short ) - 1 ;
var amount = GetCornerHeight ( hover ) ;
foreach ( var ( _ , modified ) in tilesToChange ) {
foreach ( var ( pos , corners ) in tilesToChange ) {
ref var height = ref modified . New . Height ;
var tile = Data . GetTileOrDefault ( pos ) ;
if ( modified . Affected . TopLeft ) height . TopLeft = amount ;
tilesPrevious . Add ( ( pos , tile . Height ) ) ;
if ( modified . Affected . TopRight ) height . TopRight = amount ;
if ( modified . Affected . BottomRight ) height . BottomRight = amount ;
var newHeight = tile . Height ;
if ( modified . Affected . BottomLeft ) height . BottomLeft = amount ;
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 ) ) ;
} 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 ;
}
}
action = isFlatten ? "Flatten terrain"
: isRaise ? "Raise terrain"
: "Lower Terrain" ;
method = nameof ( DoModifyTerrainHeight ) ;
doArgs = [ PackHeightData ( tilesChanged ) ] ;
undoArgs = [ PackHeightData ( tilesPrevious ) ] ;
}
}
if ( EditorUndoRedo is EditorUndoRedoManager undo ) {
if ( EditorUndoRedo is EditorUndoRedoManager undo ) {
undo . CreateAction ( action , backwardUndoOps : false ) ;
undo . CreateAction ( action , backwardUndoOps : false ) ;
undo . AddDoMethod ( this , method , doArgs ) ;
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 , nameof ( UpdateMeshAndShape ) ) ;
undo . AddDoMethod ( this , GodotObject . MethodName . NotifyPropertyListChanged ) ;
undo . AddDoMethod ( this , GodotObject . MethodName . NotifyPropertyListChanged ) ;
undo . AddUndoMethod ( this , method , undoArgs ) ;
undo . AddUndoMethod ( this , nameof ( DoModifyTerrain ) , undoData ) ;
undo . AddUndoMethod ( this , nameof ( UpdateMeshAndShape ) ) ;
undo . AddUndoMethod ( this , nameof ( UpdateMeshAndShape ) ) ;
undo . AddUndoMethod ( this , GodotObject . MethodName . NotifyPropertyListChanged ) ;
undo . AddUndoMethod ( this , GodotObject . MethodName . NotifyPropertyListChanged ) ;
@ -221,27 +204,28 @@ public partial class Terrain
}
}
}
}
UpdateEditToolMesh ( tilesToChange ) ;
UpdateEditToolMesh ( tilesToChange . Select ( e = > ( e . Key , e . Value . Affected ) ) ) ;
return prevent_default ;
return prevent_default ;
}
}
public void EditorUnfocus ( )
class TileModification
= > ClearEditToolMesh ( ) ;
public void DoModifyTerrainHeight ( byte [ ] data )
{
{
foreach ( var ( pos , corners ) in UnpackHeightData ( data ) )
public Corners < bool > Affected ;
Data [ pos ] . Height = corners ;
public Tile Original ;
public Tile New ;
}
}
public void DoModifyTerrainTexture ( byte [ ] data )
public void EditorUnfocus ( )
= > ClearEditToolMesh ( ) ;
public void DoModifyTerrain ( byte [ ] data )
{
{
foreach ( var ( pos , texture ) in UnpackTextureData ( data ) )
foreach ( var ( pos , til e ) in Unpack ( data ) )
Data [ pos ] . TexturePrimary = texture ;
Data [ pos ] = til e ;
}
}
void UpdateEditToolMesh ( Dictionary < TilePos , Corners < bool > > tiles )
void UpdateEditToolMesh ( IEnumerable < ( TilePos , Corners < bool > ) > tiles )
{
{
var mesh = GetOrCreateMesh ( "EditToolMesh" ) ;
var mesh = GetOrCreateMesh ( "EditToolMesh" ) ;
mesh . ClearSurfaces ( ) ;
mesh . ClearSurfaces ( ) ;
@ -297,56 +281,46 @@ public partial class Terrain
= > _ offsetLookup [ corner ] . Select ( e = > ( new TilePos ( pos . X + e . X , pos . Z + e . Z ) , e . Opposite ) ) ;
= > _ 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 )
static byte [ ] Pack ( IEnumerable < ( TilePos Position , Tile tile ) > data )
{
{
using var stream = new MemoryStream ( ) ;
using var stream = new MemoryStream ( ) ;
using var writer = new BinaryWriter ( stream ) ;
using var writer = new BinaryWriter ( stream ) ;
foreach ( var ( pos , corners ) in data ) {
foreach ( var ( pos , tile ) in data ) {
writer . Write ( pos . X ) ;
writer . Write ( pos . X ) ;
writer . Write ( pos . Z ) ;
writer . Write ( pos . Z ) ;
writer . Write ( corners . TopLeft ) ;
writer . Write ( tile . Height . TopLeft ) ;
writer . Write ( corners . TopRight ) ;
writer . Write ( tile . Height . TopRight ) ;
writer . Write ( corners . BottomRight ) ;
writer . Write ( tile . Height . BottomRight ) ;
writer . Write ( corners . BottomLeft ) ;
writer . Write ( tile . Height . BottomLeft ) ;
writer . Write ( tile . TexturePrimary ) ;
writer . Write ( tile . TextureSecondary ) ;
writer . Write ( tile . TextureBlend ) ;
}
}
return stream . ToArray ( ) ;
return stream . ToArray ( ) ;
}
}
static IEnumerable < ( TilePos Position , Corners < short > Corners ) > UnpackHeightData ( byte [ ] data )
static IEnumerable < ( TilePos Position , Tile tile ) > Unpack ( byte [ ] data )
{
{
using var stream = new MemoryStream ( data ) ;
using var stream = new MemoryStream ( data ) ;
using var reader = new BinaryReader ( stream ) ;
using var reader = new BinaryReader ( stream ) ;
while ( stream . Position < stream . Length ) {
while ( stream . Position < stream . Length ) {
var x = reader . ReadInt32 ( ) ;
var x = reader . ReadInt32 ( ) ;
var y = reader . ReadInt32 ( ) ;
var y = reader . ReadInt32 ( ) ;
var corners = new Corners < short > (
var height = new Corners < short > (
reader . ReadInt16 ( ) , reader . ReadInt16 ( ) ,
reader . ReadInt16 ( ) , reader . ReadInt16 ( ) ,
reader . ReadInt16 ( ) , reader . ReadInt16 ( ) ) ;
reader . ReadInt16 ( ) , reader . ReadInt16 ( ) ) ;
yield return ( new ( x , y ) , corners ) ;
}
}
static byte [ ] PackTextureData ( IEnumerable < ( TilePos Position , byte Texture ) > data )
var texPrimary = reader . ReadByte ( ) ;
{
var texSecondary = reader . ReadByte ( ) ;
using var stream = new MemoryStream ( ) ;
var texBlend = reader . ReadByte ( ) ;
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 )
yield return ( new ( x , y ) , new ( ) {
{
Height = height ,
using var stream = new MemoryStream ( data ) ;
TexturePrimary = texPrimary ,
using var reader = new BinaryReader ( stream ) ;
TextureSecondary = texSecondary ,
while ( stream . Position < stream . Length ) {
TextureBlend = texBlend ,
var x = reader . ReadInt32 ( ) ;
} ) ;
var y = reader . ReadInt32 ( ) ;
var texture = reader . ReadByte ( ) ;
yield return ( new ( x , y ) , texture ) ;
}
}
}
}
}
}