I'm experiencing issues with the terrain generation causing the level of new chunks to mismatch existing data. I suspect this was caused by a Minecraft update or bug. Checking this thread it seems others here have had the same issue. I tried the Smooth filter but was disapointed in the result.
I've developed a "decliff" filter that seems to do a much better job with this specific issue See before and after screen shots below:
Things to note:
1. Changes are averaged across width of selection so select more of either side of cliff for more subtle change
2. Cliff must be parallel to the long edge of the selection
3. Ensure selection goes below ground level and above any trees/ground on cliff top
Raise/Lower option allows limit of only raising the cliff base or lowering the cliff top.
from numpy import zeros, array
import itertools
from pymclevel import alphaMaterials
am = alphaMaterials
# Consider below materials when determining terrain height
blocks = [
am.Stone,
am.Grass,
am.Dirt,
am.Bedrock,
am.Sand,
am.Sandstone,
am.Clay,
am.Gravel,
am.GoldOre,
am.IronOre,
am.CoalOre,
am.LapisLazuliOre,
am.DiamondOre,
am.RedstoneOre,
am.RedstoneOreGlowing,
am.Netherrack,
am.SoulSand,
am.Glowstone
]
terrainBlocktypes = [b.ID for b in blocks]
terrainBlockmask = zeros((256,), dtype='bool')
# Truth table used to calculate terrain height
# trees, leaves, etc. sit on top of terrain
terrainBlockmask[terrainBlocktypes] = True;
inputs = (
# Option to limit change to raise_cliff_floor / lower_cliff_top
# Default is to adjust both and meet somewhere in the middle
("Raise/Lower", ("Both", "Lower Only", "Raise Only")),
)
#
# Calculate the maximum adjustment that can be made from
# cliff_pos in direction dir (-1/1) keeping terain at most
# maxstep blocks away from previous column
def maxadj(heightmap, slice_no, cliff_pos, dir, pushup, maxstep, slice_width):
ret = 0
if dir < 0:
if cliff_pos < 2: return 0
end=0
else:
if cliff_pos > slice_width - 2: return 0
end=slice_width-1
for cur_pos in range(cliff_pos, end, dir):
if pushup:
ret = ret + \
max([0, maxstep - dir * heightmap[slice_no, cur_pos] + \
dir * heightmap[slice_no, cur_pos + dir]])
else:
ret = ret + \
min([0,-maxstep + dir * heightmap[slice_no, cur_pos] - \
dir * heightmap[slice_no, cur_pos + dir]])
return ret
#
# Raise/lower column at cliff face by adj and decrement change as we move away
# from the face. Each level will be at most maxstep blocks from those beside it.
#
# This function dosn't actually change anything, but just sets array 'new'
# with the desired height.
def adjheight(orig, new, slice_no, cliff_pos, dir, adj, can_adj, maxstep, slice_width):
cur_adj = adj
prev = 0
done_adj = 0
if dir < 0:
end=1
else:
end=slice_width-1
if adj == 0 or can_adj == 0:
for cur_pos in range(cliff_pos, end, dir):
new[slice_no, cur_pos] = orig[slice_no, cur_pos]
else:
for cur_pos in range(cliff_pos, end, dir):
if adj > 0:
done_adj = done_adj + \
max([0,maxstep - orig[slice_no, cur_pos] + \
orig[slice_no, cur_pos + dir]])
if orig[slice_no, cur_pos] - \
orig[slice_no, cur_pos + dir] > 0:
cur_adj=max([0, cur_adj-orig[slice_no, cur_pos] + \
orig[slice_no, cur_pos + dir]])
prev=adj-cur_adj
else:
done_adj = done_adj + \
min([0,-maxstep + \
orig[slice_no, cur_pos] - \
orig[slice_no, cur_pos + dir]])
if orig[slice_no, cur_pos] - \
orig[slice_no, cur_pos + dir] > 0:
cur_adj=min([0, cur_adj + orig[slice_no, cur_pos] - orig[slice_no, cur_pos + dir]])
prev=adj-cur_adj
new[slice_no, cur_pos] = max([0, orig[slice_no, cur_pos] + cur_adj])
if cur_adj != 0 and \
abs(prev) < abs(int(adj*done_adj/can_adj)):
cur_adj = cur_adj + (prev - int(adj*done_adj/can_adj))
prev = int(adj*done_adj/can_adj)
new[slice_no, end] = orig[slice_no, end]
def perform(level, box, options):
if box.volume > 16000000:
raise ValueError, "Volume too big for this filter method!"
RLOption = options["Raise/Lower"]
schema = level.extractSchematic(box);
schema.removeEntitiesInBox(schema.bounds)
schema.removeTileEntitiesInBox(schema.bounds)
terrainBlocks = terrainBlockmask[schema.Blocks]
coords = terrainBlocks.nonzero();
# Swap values around so long edge of selected rectangle is first
# - the long edge is assumed to run parallel to the cliff face
# and we want to process slices perpendicular to the face
# heightmap will have x,z (or z,x) index with highest ground level
if schema.Width > schema.Length:
heightmap = zeros((schema.Width, schema.Length), dtype='float32')
heightmap[coords[0], coords[1]] = coords[2]
newHeightmap = zeros((schema.Width, schema.Length), dtype='uint16')
slice_count=schema.Width
slice_width=schema.Length
else:
heightmap = zeros((schema.Length, schema.Width), dtype='float32')
heightmap[coords[1], coords[0]] = coords[2]
newHeightmap = zeros((schema.Length, schema.Width), dtype='uint16')
slice_count=schema.Length
slice_width=schema.Width
nonTerrainBlocks = ~terrainBlocks
nonTerrainBlocks &= schema.Blocks != 0
for slice_no in range(0, slice_count):
cliff_height=0
# determine pos and height of cliff in this slice
for cur_pos in range(0, slice_width-1):
if abs(heightmap[slice_no,cur_pos] - \
heightmap[slice_no,cur_pos+1]) > abs(cliff_height):
cliff_height= \
heightmap[slice_no,cur_pos] - \
heightmap[slice_no,cur_pos+1]
cliff_pos=cur_pos
if abs(cliff_height) < 2:
# nothing to adjust - just copy heightmap to newHightmap
adjheight(heightmap, newHeightmap, slice_no, 0, 1, 0, 1, 1, slice_width)
continue
# Try to keep adjusted columns within 1 column of their neighbours
# but ramp up to 4 blocks up/down on each column when needed
for max_step in range(1,4):
can_left=maxadj(heightmap,slice_no,cliff_pos,-1,cliff_height<0,max_step,slice_width)
can_right=maxadj(heightmap,slice_no,cliff_pos+1,1,cliff_height>0,max_step,slice_width)
if can_right < 0 and RLOption == "Raise Only": can_right=0
if can_right > 0 and RLOption == "Lower Only": can_right=0
if can_left < 0 and RLOption == "Raise Only": can_left=0
if can_left > 0 and RLOption == "Lower Only": can_left=0
if cliff_height < 0 and can_right - can_left < cliff_height:
if abs(can_left) > abs(can_right):
adj_left=-1*(cliff_height - max([int(cliff_height/2), can_right]))
adj_right=cliff_height + adj_left
else:
adj_right=cliff_height - max([int(cliff_height/2), -can_left])
adj_left=-1*(cliff_height - adj_right +1)
else:
if cliff_height > 0 and can_right - can_left > cliff_height:
if abs(can_left) > abs(can_right):
adj_left=-1*(cliff_height - min([int(cliff_height/2), can_right]))
adj_right=cliff_height + adj_left
else:
adj_right=cliff_height - min([int(cliff_height/2), -can_left]) -1
adj_left=-1*(cliff_height - adj_right)
else:
adj_right=0
adj_left=0
continue
break
adjheight(heightmap, newHeightmap, slice_no, cliff_pos, -1, adj_left, can_left, max_step, slice_width)
adjheight(heightmap, newHeightmap, slice_no, cliff_pos+1, 1, adj_right, can_right, max_step, slice_width)
# OK, newHeightMap has new height for each column
# so it's just a matter of moving everything up/down
for x, z in itertools.product(xrange(1, schema.Width - 1), xrange(1, schema.Length - 1)):
if schema.Width > schema.Length:
oh = heightmap[x, z];
nh = newHeightmap[x, z]
else:
oh = heightmap[z, x];
nh = newHeightmap[z, x]
delta = nh - oh
column = array(schema.Blocks[x, z])
# Keep bottom 5 blocks, so we don't loose bedrock
keep=min([5,nh])
Waterdepth=0
# Detect Water on top
if column[oh+1:oh+2] == am.WaterStill.ID or \
column[oh+1:oh+2] == am.Ice.ID:
for cur_pos in range(oh+1,schema.Height):
if column[cur_pos:cur_pos+1] != am.WaterStill.ID and \
column[cur_pos:cur_pos+1] != am.Ice.ID: break
Waterdepth=Waterdepth + 1
if delta == 0:
column[oh:] = schema.Blocks[x, z, oh:]
if delta < 0:
# Moving column down
column[keep:delta] = schema.Blocks[x, z, keep-delta:]
column[delta:] = am.Air.ID
if Waterdepth > 0:
# Avoid steping small lakes, etc on cliff top
# replace with dirt 'n grass
column[nh:nh+1] = am.Grass.ID
column[nh+1:nh+1+delta] = am.Air.ID
if delta > 0:
# Moving column up
column[keep+delta:] = schema.Blocks[x, z, keep:-delta]
# Put stone in gap at the bottom
column[keep:keep+delta] = am.Stone.ID
if Waterdepth > 0:
if Waterdepth > delta:
# Retain Ice
if column[nh+Waterdepth:nh+Waterdepth+1] == am.Ice.ID:
column[nh+Waterdepth-delta:nh+1+Waterdepth-delta] = \
am.Ice.ID
column[nh+1+Waterdepth-delta:nh+1+Waterdepth] = am.Air.ID
else:
if Waterdepth < delta - 2:
column[nh:nh+1] = am.Grass.ID
column[nh+1:nh+1+Waterdepth] = am.Air.ID
else:
# Beach at the edge
column[nh-4:nh-2] = am.Sandstone.ID
column[nh-2:nh+1] = am.Sand.ID
column[nh+1:nh+1+Waterdepth] = am.Air.ID
schema.Blocks[x, z] = column
level.copyBlocksFrom(schema, schema.bounds, box.origin);