summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authororwell96 <orwell@bleipb.de>2020-10-18 15:27:55 +0200
committerorwell96 <orwell@bleipb.de>2020-10-18 15:41:08 +0200
commit7000c5220d9ce8864e028d3b035d86040e7de7aa (patch)
treefae8c7d0d6e83dfa10e41d2c7fd88269469f4085
downloadcellworld-master.tar.gz
cellworld-master.tar.bz2
cellworld-master.zip
Initial working stateHEADmaster
-rw-r--r--allocation.lua474
-rw-r--r--gridgen.lua248
-rw-r--r--init.lua100
-rw-r--r--logutil.lua48
-rw-r--r--mapgen.lua167
-rw-r--r--struct_register.lua67
-rw-r--r--structures.lua241
-rwxr-xr-xtextures/cellworld_placeholder.pngbin0 -> 325 bytes
-rw-r--r--utils.lua125
9 files changed, 1470 insertions, 0 deletions
diff --git a/allocation.lua b/allocation.lua
new file mode 100644
index 0000000..6f34059
--- /dev/null
+++ b/allocation.lua
@@ -0,0 +1,474 @@
+-- allocation.lua - Maintains the grid node allocation table
+--[[
+In contrast to classical mapgens, cellworld does not rely on perlin noise and "reproducable randomness".
+Because structures can have varying size, and it is not specified which direction is explored first,
+we cannot safely reconstruct the existing structures easily only from noise.
+
+Gridmine instead stores an "allocation map" along with the map, which stores which structures have been placed where.
+Generated structures are selected during generation on a "what fits" basis.
+
+Binary file format:
+
+1byte version
+2byte number of structureID entries
+[
+2byte structure id
+string structure name
+\n
+]
+[
+6byte cellPosition
+4byte indirection entry or structure entry
+]
+EOF
+
+Position:
+<gnx 2byte><gny 2byte><gnz 2byte>
+
+Indirection entry:
+I<xoff 1byte><yoff 1byte><zoff 1byte>
+This node is part of a structure whose origin is at x-xoff .. z-zoff
+
+Structure entry:
+S<rotation char><structureId 2bytes>
+
+All entries have a length of 4 bytes.
+
+]]--
+
+local function int_to_bytes(i, clip)
+ local x = i
+ if clip then
+ x=i+32768--clip to positive integers
+ end
+ local cH = math.floor(x / 256) % 256;
+ local cL = math.floor(x ) % 256;
+ return(string.char(cH, cL));
+end
+local function bytes_to_int(bytes, clip)
+ local t={string.byte(bytes,1,-1)}
+ local n =
+ t[1] * 256 +
+ t[2]
+ if clip then
+ return n-32768
+ else
+ return n
+ end
+end
+
+-- quick unit test
+assert(bytes_to_int(int_to_bytes(42))==42)
+assert(bytes_to_int(int_to_bytes(-42))~=-42)-- this must not work!
+assert(bytes_to_int(int_to_bytes(42,true),true)==42)
+assert(bytes_to_int(int_to_bytes(-42,true),true)==-42)
+
+--local variables for performance
+local gma_structids={}
+local gma_entries={}
+
+local function gmaget(x,y,z)
+ local ny=gma_entries[y]
+ if ny then
+ local nx=ny[x]
+ if nx then
+ return nx[z]
+ end
+ end
+ return nil
+end
+cellworld.alloc_get_cid = gmaget
+
+local function gmaset(x,y,z,v)
+ if not gma_entries[y] then
+ gma_entries[y]={}
+ end
+ if not gma_entries[y][x] then
+ gma_entries[y][x]={}
+ end
+ gma_entries[y][x][z]=v
+end
+
+local function format_cid(x,y,z,cid)
+ if not cid then
+ return "<nil>"
+ end
+
+ local indirection = string.sub(cid, 1, 1)
+ if indirection == "I" then
+ local ox,oy,oz = string.byte(cid, 2, 4)
+
+ local icid = gmaget(x-ox, y-oy, z-oz)
+ return "I "..ox.." "..oy.." "..oz.." -> "..x-ox.." "..y-oy.." "..z-oz.." "..format_cid(x-ox, y-oy, z-oz, icid)
+ elseif indirection == "S" then
+ local rotation = string.sub(cid, 2, 2)
+ local stid_char = string.sub(cid, 3, 4)
+ local stid = bytes_to_int(stid_char)
+ local stna = gma_structids[stid]
+ return "S str="..stid.." ("..(stna or "?")..")"
+ else
+ return "???"
+ end
+end
+
+local path=minetest.get_worldpath()..DIR_DELIM.."cellworld_allocation_map"
+--load
+function cellworld.load_allocation()
+ cellworld.log("loading structure allocation map")
+ local file, err = io.open(path, "rb")
+ if not file then
+ cellworld.log("Couldn't load the allocation database: ", err or "Unknown Error")
+ else
+ -- read version
+ local vers_byte = file:read(1)
+ local version = string.byte(vers_byte)
+ if version~=1 then
+ error("Doesn't support allocation file of version "..version)
+ end
+
+ -- read structure ids
+ local nstr_byte = file:read(2)
+ local nstr = bytes_to_int(nstr_byte)
+ for i = 1,nstr do
+ local stid_byte = file:read(2)
+ local stid = bytes_to_int(stid_byte)
+ local stna = file:read("*l")
+ cellworld.log("structure id:", stid, "->", stna)
+ gma_structids[stid] = stna
+ end
+ cellworld.log("read", nstr, "structure ids.")
+
+ -- read nodes. code suspiciously resembles advtrains ndb, not that I copied it :) ...
+ local cnt=0
+ local hst_x=file:read(2)
+ local hst_y=file:read(2)
+ local hst_z=file:read(2)
+ local cid=file:read(4)
+ while hst_z and hst_y and hst_x and cid and #hst_z==2 and #hst_y==2 and #hst_x==2 and #cid==4 do
+ gmaset(bytes_to_int(hst_x,true), bytes_to_int(hst_y,true), bytes_to_int(hst_z,true), cid)
+ cnt=cnt+1
+ hst_x=file:read(2)
+ hst_y=file:read(2)
+ hst_z=file:read(2)
+ cid=file:read(4)
+ end
+ cellworld.log("read", cnt, "structure allocations.")
+ file:close()
+ end
+end
+
+--save
+function cellworld.save_allocation()
+ local tmppath = path
+ local file, err = io.open(tmppath, "wb")
+ if not file then
+ cellworld.log("Couldn't save the allocation database: ", err or "Unknown Error")
+ else
+ cellworld.log("Saving allocation database...")
+ -- write version
+ file:write(string.char(1))
+
+ -- how many structid entries
+ local cnt = 0
+ for _,_ in pairs(gma_structids) do
+ cnt = cnt + 1
+ end
+ -- write structids
+ local nstr = 0
+ file:write(int_to_bytes(cnt))
+ for stid,stna in pairs(gma_structids) do
+ file:write(int_to_bytes(stid))
+ file:write(stna)
+ file:write("\n")
+ nstr = nstr+1
+ end
+ cellworld.log("wrote", nstr, "structure ids.")
+
+ -- write entries
+ local cnt = 0
+ for y, ny in pairs(gma_entries) do
+ for x, nx in pairs(ny) do
+ for z, cid in pairs(nx) do
+ file:write(int_to_bytes(x,true))
+ file:write(int_to_bytes(y,true))
+ file:write(int_to_bytes(z,true))
+ file:write(cid)
+ cnt=cnt+1
+ end
+ end
+ end
+ cellworld.log("wrote", cnt, "structure allocations.")
+ file:close()
+ end
+end
+
+function cellworld.dump_allocation()
+ for y, ny in pairs(gma_entries) do
+ for x, nx in pairs(ny) do
+ for z, cid in pairs(nx) do
+ cellworld.log(x,y,z,format_cid(x,y,z,cid))
+ end
+ end
+ end
+end
+
+function cellworld.alloc_dump_area(lx,ly,lz,ux,uy,uz)
+ cellworld.log("-- allocation dumping area",lx,ly,lz,"<>",ux,uy,uz)
+ for dx,dy,dz,dcid in cellworld.alloc_get_area_iterator_raw(lx,ly,lz,ux,uy,uz) do
+ cellworld.log(dx,dy,dz,format_cid(dx,dy,dz,dcid))
+ end
+ cellworld.log("-- end of dump")
+end
+
+local function resolve_structure(cid, ox, oy, oz)
+ local rotation = string.sub(cid, 2, 2)
+ local stid_char = string.sub(cid, 3, 4)
+ local stid = bytes_to_int(stid_char)
+ local stna = gma_structids[stid]
+ --cellworld.debug(" structure rot=",rotation,"stid=",stid,"stna=",stna)
+ if stna then
+ return stna, vector.new(ox,oy,oz), string.byte(rotation)
+ end
+ cellworld.log("[allocation] Warn: unknown structure name for id",stid,". Ignored")
+ return nil
+end
+
+--cellworld.alloc_get_cid(x,y,z)
+cellworld.alloc_get_cid = gmaget
+
+-- returns:
+-- structure_name, relative_pos, rotation
+-- Note: structure rotation not calculated here yet
+function cellworld.alloc_get_structure_at(cell_pos)
+ local cid = gmaget(cell_pos.x, cell_pos.y, cell_pos.z)
+ if not cid then return nil end
+ return cellworld.alloc_resolve_cid(cell_pos.x, cell_pos.y, cell_pos.z, cid)
+end
+
+-- returns:
+-- structure_name, relative_pos, rotation
+function cellworld.alloc_resolve_cid(x, y, z, cid)
+ --cellworld.debug("alloc_resolve_cid(", x, y, z,")")
+ local indirection = string.sub(cid, 1, 1)
+ if indirection == "I" then
+ local ox,oy,oz = string.byte(cid, 2, 4)
+ --cellworld.debug("alloc_resolve_cid(", x, y, z,")")
+ local icid = gmaget(x-ox, y-oy, z-oz)
+ --cellworld.debug(" indirection +",ox,oy,oz,format_cid(x-ox, y-oy, z-oz,icid))
+ if not icid then
+ cellworld.log("[allocation] Warn: Indirection at",vector.new(x,y,z),"has nil target. Ignored")
+ cellworld.alloc_dump_area(x-3,y-3,z-3,x+3,y+3,z+3)
+ return nil
+ end
+ indirection = string.sub(icid, 1, 1)
+ if indirection ~= "S" then
+ cellworld.log("[allocation] Warn: Indirection at",vector.new(x,y,z),"points to something other than S. Ignored")
+ cellworld.alloc_dump_area(x-3,y-3,z-3,x+3,y+3,z+3)
+ return nil
+ end
+ return resolve_structure(icid, ox, oy, oz)
+ elseif indirection == "S" then
+ return resolve_structure(cid, 0,0,0)
+ else
+ cellworld.log("[allocation] Warn: Unknown node type",indirection,"at gridpos",vector.new(x,y,z),". Ignored")
+ return nil
+ end
+end
+
+function cellworld.alloc_set_structure(cell_pos, structure_name, rotation, structure_extent, no_clobber)
+ --cellworld.debug("alloc_set_structure(",cell_pos, structure_name, rotation, structure_extent, no_clobber,")")
+ for x=0,structure_extent.x-1 do
+ for y=0,structure_extent.y-1 do
+ for z=0,structure_extent.z-1 do
+ -- write indirection
+ if x==0 and y==0 and z==0 then
+ --continue
+ elseif no_clobber and gmaget(cell_pos.x + x, cell_pos.y + y, cell_pos.z + z) then
+ local ocid = gmaget(cell_pos.x + x, cell_pos.y + y, cell_pos.z + z)
+ cellworld.log("[allocation] While trying to place",structure_name,"at grid position",cell_pos,"extent",structure_extent)
+ cellworld.log("[allocation] Not clobbering ",vector.new(cell_pos.x + x, cell_pos.y + y, cell_pos.z + z),"which has entry",format_cid(cell_pos.x + x, cell_pos.y + y, cell_pos.z + z, ocid))
+ error("Failed to place structure; see log...")
+ --continue
+ else
+ local indir = "I"..string.char(x)..string.char(y)..string.char(z)
+ --cellworld.debug(" indirection ",cell_pos,"+",x,y,z)
+ gmaset(cell_pos.x + x, cell_pos.y + y, cell_pos.z + z, indir)
+ end
+ end
+ end
+ end
+ if no_clobber and gmaget(cell_pos.x, cell_pos.y, cell_pos.z) then
+ return
+ end
+ -- write structure
+ local stid
+ for istid, istna in ipairs(gma_structids) do
+ if istna==structure_name then
+ stid = istid
+ break
+ end
+ end
+ if not stid then
+ stid = #gma_structids + 1
+ cellworld.debug(" allocating new structure id ",stid,"for",structure_name)
+ gma_structids[stid] = structure_name
+ end
+ local struct = "S"..string.char(rotation)..int_to_bytes(stid)
+ --cellworld.debug(" structure ",cell_pos,"rot=",rotation,"stid=",stid)
+ gmaset(cell_pos.x, cell_pos.y, cell_pos.z, struct)
+end
+
+function cellworld.alloc_has_space(cell_pos, structure_extent)
+ for _ in cellworld.alloc_get_area_iterator(cell_pos, vector.add(cell_pos, structure_extent)) do
+ --if any result, no space
+ return false
+ end
+ return true
+end
+
+-- Returns an iterator function that iterates over all allocation entries in the given area, in raw format.
+-- Iterator returns: x, y, z, cid
+-- where cid can be decrypted by calling cellworld.alloc_resolve_cid(x,y,z,cid)
+function cellworld.alloc_get_area_iterator(gp_min, gp_max)
+ return cellworld.alloc_get_area_iterator_raw(gp_min.x, gp_min.y, gp_min.z, gp_max.x, gp_max.y, gp_max.z)
+end
+function cellworld.alloc_get_area_iterator_raw(lx, ly, lz, ox, oy, oz)
+ local x, y, z = lx - 1, ly - 1, lz - 1
+ local it_y, it_x
+ return function()
+ while y<=oy do
+ if it_y then
+ while x<=ox do
+ if it_x then
+ while z<oz do
+ -- locating next z
+ z=z+1
+ if it_x[z] then
+ --[[local realcid = gmaget(x,y,z)
+ if realcid ~= it_x[z] then
+ cellworld.log(x,y,z,"CIDs dont match real=",
+ format_cid(x,y,z,realcid),"found=",
+ format_cid(x,y,z,it_x[z]))
+ cellworld.dump_allocation()
+ error("CIDs dont match in iterator")
+ end]]--
+ return x, y, z, it_x[z]
+ end
+ end
+ -- through with iterating it_x
+ z=lz-1
+ end
+ -- locating next x
+ x = x + 1
+ it_x = it_y[x]
+ end
+ -- through with iterating it_y
+ x=lx-1
+ it_x = nil
+ end
+ -- locating next y
+ y = y + 1
+ it_y = gma_entries[y]
+ end
+ -- through with iterating everything
+ return nil
+ end
+end
+
+
+--[[
+local TEST_table = {
+ [1] = {
+ [1] = {
+ [1]="A",
+ [4]="B",
+ [5]="C",
+ },
+ [3] = {
+ [2]="D",
+ },
+ [5] = {
+ },
+ },
+ [2] = {
+ [3] = {
+ [2]="E",
+ },
+ },
+ [4] = {
+ [1] = {
+ [1]="F",
+ [4]="G",
+ },
+ }
+}
+function cellworld.alloc_get_area_iterator_TEST(lx, ly, lz, ox, oy, oz)
+ local x, y, z = lx - 1, ly - 1, lz - 1
+ local it_y, it_x
+ return function()
+ while y<=oy do
+ if it_y then
+ while x<=ox do
+ if it_x then
+ while z<oz do
+ -- locating next z
+ z=z+1
+ cellworld.log("next z",z)
+ if it_x[z] then
+ return x, y, z, it_x[z]
+ end
+ end
+ -- through with iterating it_x
+ z=lz-1
+ end
+ -- locating next x
+ x = x + 1
+ it_x = it_y[x]
+ cellworld.log("next x",x,it_x~=nil)
+ end
+ -- through with iterating it_y
+ x=lx-1
+ end
+ -- locating next y
+ y = y + 1
+ it_y = TEST_table[y]
+
+ cellworld.log("next y",y,it_y~=nil)
+ end
+ -- through with iterating everything
+ return nil
+ end
+end
+
+for x,y,z,letter in cellworld.alloc_get_area_iterator_TEST(1,1,1,5,5,5) do
+ cellworld.log(x,y,z,letter)
+end
+]]--
+
+-- Checks efficiently whether no, some or all cells in the given area are allocated
+-- returns:
+-- 0 - no cells allocated
+-- 1 - some cells, but not all, allocated
+-- 2 - all cells allocated
+function cellworld.alloc_check_some_cells_allocated(gp_min, gp_max)
+ local have_skipped = false
+ local have_any_cell = false
+ for y=gp_min.y, gp_max.y do
+ local ly = gma_entries[y]
+ if ly then
+ for x=gp_min.x, gp_max.x do
+ local lx = ly[x]
+ if lx then
+ for z=gp_min.z, gp_max.z do
+ if lx[z] then
+ have_any_cell = true
+ else have_skipped=true end
+ end
+ else have_skipped=true end
+ end
+ else have_skipped=true end
+ end
+ if not have_any_cell then return 0 end
+ if have_skipped then return 1 end
+ return 2
+end
diff --git a/gridgen.lua b/gridgen.lua
new file mode 100644
index 0000000..770f1f6
--- /dev/null
+++ b/gridgen.lua
@@ -0,0 +1,248 @@
+-- gridgen.lua
+-- Actual cell generation process. These methods are called by the mapgen callback to allocate enough structure cells.
+-- All randomness happens here.
+
+--[[
+The mapgen callback initiates the gridgen as follows:
+1. mapgen builds a list of unallocated cells it requires to generate the current chunk
+2. as long as there are some left, pick one cell that has at least one neighbor generated, randomly
+The neighbor cell should have an exit towards the picked unallocated cell, only if there are no such cells it may fall back to any cell.
+3. instruct gridgen to generate from the cell that is present, into the direction of the missing cell.
+
+Given a cell and a direction, the gridgen then performs the following heuristic:
+1. initialize an area with the unallocated cell that was passed.
+2. assuming the passed direction as "forward", expand the area left, right, up and down as long as there's free space or the maximum structure size is hit.
+3. expand the area forward as long as there's space or the maximum structure size is hit
+ -> maybe extend randomly into each of the 5 directions. Could lead to problems otherwise
+The area now represents the maximum size of structures that would fit.
+4. filter the structures that do not fit into the frame.
+5. randomly select one of the remaining structures, taking their chance into account.
+6. using the degrees of freedom the structure has inside the frame, choose a random position of the new structure.
+if the origin cell had an exit: try to find a position so that the exit matches one of the new structure
+]]--
+
+local gg_verbose = function() end
+--local gg_verbose = cellworld.log
+
+-- Make gridgen generate at least the area specified by gp_min, gp_max (in grid units)
+-- Important: area may span 255x255x255 cells at maximum!
+function cellworld.gridgen_generate_cells(gp_min, gp_max)
+ cellworld.log("Gridgen: generating area", gp_min, gp_max)
+
+ -- let the allocation efficiently check whether one, all or some cells are allocated
+ local alloc = cellworld.alloc_check_some_cells_allocated(gp_min, gp_max)
+ if alloc == 2 then
+ -- nothing to do
+ cellworld.log("All cells allocated, nothing to do")
+ return
+ elseif alloc == 0 then
+ -- no cells allocated. seed a 1x1 structure somewhere
+ local posx, posy, posz = cellworld.random(gp_min.x, gp_max.x), cellworld.random(gp_min.y, gp_max.y), cellworld.random(gp_min.z, gp_max.z)
+ local posv = vector.new(posx, posy, posz)
+ local ones = vector.new(1,1,1)
+ local seedst = cellworld.structure_set.seed_structure
+ assert(seedst, "No seed structure defined")
+ cellworld.log("No cells allocated, seeding", seedst,"at",posv)
+ assert(vector.equals(cellworld.structures[seedst].size, ones))
+ cellworld.alloc_set_structure(posv, seedst, 0, ones, true)
+ end
+
+ gg_verbose("generate_cells searching sprout positions")
+ local count = 0
+ while true do
+ -- find cells as starting points
+ -- {cell = <pos>, dir = <dir>}
+ local cells_with_exit = {}
+ local cells_without_exit = {}
+ local ox, oy, oz = gp_min.x, gp_min.y, gp_min.z
+ for x,y,z,cid in cellworld.alloc_get_area_iterator(gp_min, gp_max) do
+ local stname, relpos, rotation = cellworld.alloc_resolve_cid(x,y,z,cid)
+ assert(stname, "alloc_resolve_cid failed to resolve")
+ assert(rotation==0, "Rotation is not supported yet")
+ local struct = cellworld.structures[stname]
+ if struct then
+ -- check_borders
+ local borders = cellworld.get_border_sides(relpos, struct.size)
+ for _,dir in ipairs(borders) do
+ local off = cellworld.direction_to_vector[dir]
+ if not cellworld.alloc_get_cid(x+off.x, y+off.y, z+off.z) then
+ -- possible candidate
+ local cell = cellworld.get_structure_cell(struct, relpos)
+ -- has it an exit here?
+ if cell.exits and cell.exits[dir] then
+ --gg_verbose(" found with exit",x,y,z,"dir",cellworld.dirrev[dir])
+ table.insert(cells_with_exit, {cell=vector.new(x,y,z), dir=dir})
+ else
+ --gg_verbose(" found without exit",x,y,z,"dir",cellworld.dirrev[dir])
+ table.insert(cells_without_exit, {cell=vector.new(x,y,z), dir=dir})
+ end
+ end
+ end
+ end
+ end
+ gg_verbose("generate_cells sprouting, found",#cells_with_exit,"cells with exit,",#cells_without_exit,"without.")
+ -- select one of the sprouts randomly
+ local selected_sprout, exit_present
+ if #cells_with_exit > 0 then
+ selected_sprout = cellworld.select_random_element(cells_with_exit)
+ exit_present = true
+ elseif #cells_without_exit > 0 then
+ selected_sprout = cellworld.select_random_element(cells_without_exit)
+ else
+ -- both lists are empty. We're either done, or there was another problem.
+ cellworld.log("generate_cells sprouted",count,"times")
+ return
+ end
+
+ cellworld.gridgen_fit_structure(selected_sprout.cell, selected_sprout.dir, exit_present)
+ count = count + 1
+ end
+end
+
+local function can_i_has_space(lx, ly, lz, ux, uy, uz)
+ -- this calls the iterator once to see if it produces a result
+ local res = cellworld.alloc_get_area_iterator_raw(lx, ly, lz, ux, uy, uz)() ~= nil
+ gg_verbose("Space check for area",lx,ly,lz,"<->",ux,uy,uz,"resulted in",(res and "BLOCKED" or "FREE"))
+ return not res
+end
+
+-- Allocate and place a random structure in the space ahead of sprout_pos towards sprout_dir, try to match exits if requested.
+function cellworld.gridgen_fit_structure(sprout_pos, sprout_dir, exit_present)
+ gg_verbose("fit_structure started with sprout",sprout_pos,"direction",cellworld.dirrev[sprout_dir],"exit present",exit_present)
+ local start_pos = vector.add(sprout_pos, cellworld.direction_to_vector[sprout_dir])
+ local stx, sty, stz = cellworld.v_decompose(start_pos) -- Start position
+ local slx, sly, slz = stx, sty, stz -- lower bound of free space
+ local sux, suy, suz = stx, sty, stz -- upper bound of free space
+
+ local max_struct_size = cellworld.maximum_structure_size
+ local msx, msy, msz = cellworld.v_decompose(max_struct_size) -- maximum structure size of all registered structures
+ local can_into_dir = {0, 1, 2, 3, 4, 5}
+ table.remove(can_into_dir, cellworld.opposite_directions[sprout_dir]+1)-- cannot into the sprout cell
+ -- extend the space randomly into directions
+ while #can_into_dir > 0 do
+ gg_verbose("Directions left: ",table.concat(can_into_dir,","))
+ local diridx = cellworld.random(1, #can_into_dir)
+ local dir = can_into_dir[diridx]
+ gg_verbose("area is now",slx,sly,slz,"<>",sux, suy, suz,", extending",cellworld.dirrev[dir],"(index",diridx,")")
+ if dir == cellworld.NORTH then
+ if suz-stz < msz and can_i_has_space(slx, sly, suz+1, sux, suy, suz+1) then
+ suz = suz + 1
+ else
+ gg_verbose("extending NORTH failed, hit limit",suz-stz < msz)
+ table.remove(can_into_dir, diridx)
+ end
+ end
+ if dir == cellworld.EAST then
+ if sux-stx < msx and can_i_has_space(sux+1, sly, slz, sux+1, suy, suz) then
+ sux = sux + 1
+ else
+ gg_verbose("extending EAST failed, hit limit",sux-stx < msx)
+ table.remove(can_into_dir, diridx)
+ end
+ end
+ if dir == cellworld.SOUTH then
+ if stz-slz < msz and can_i_has_space(slx, sly, slz-1, sux, suy, slz-1) then
+ slz = slz - 1
+ else
+ gg_verbose("extending SOUTH failed, hit limit",stz-slz < msz)
+ table.remove(can_into_dir, diridx)
+ end
+ end
+ if dir == cellworld.WEST then
+ if stx-slx < msx and can_i_has_space(slx-1, sly, slz, slx-1, suy, suz) then
+ slx = slx - 1
+ else
+ gg_verbose("extending WEST failed, hit limit",sux-stx < msx)
+ table.remove(can_into_dir, diridx)
+ end
+ end
+ if dir == cellworld.UP then
+ if suy-sty < msy and can_i_has_space(slx, suy+1, slz, sux, suy+1, suz) then
+ suy = suy + 1
+ else
+ gg_verbose("extending UP failed, hit limit",suy-sty < msy)
+ table.remove(can_into_dir, diridx)
+ end
+ end
+ if dir == cellworld.DOWN then
+ if sty-sly < msy and can_i_has_space(slx, sly-1, slz, sux, sly-1, suz) then
+ sly = sly - 1
+ else
+ gg_verbose("extending DOWN failed, hit limit",sty-sly < msy)
+ table.remove(can_into_dir, diridx)
+ end
+ end
+ end
+
+ -- determine how much space we have produced
+ local spx, spy, spz = sux-slx+1, suy-sly+1, suz-slz+1
+ gg_verbose("extending done, area is now",slx,sly,slz,"<>",sux, suy, suz,", space",spx, spy, spz)
+
+ -- filter available structures by their size and insert according to probability
+ local selstructs = {}
+ for stname, struct in pairs(cellworld.structures) do
+ local extx, exty, extz = cellworld.v_decompose(struct.size)
+ if extx <= spx and exty <= spy and extz <= spz then
+ gg_verbose("structure",stname,struct.size,"fits")
+ -- structure fits. insert chance times
+ for i=1,struct.chance do
+ selstructs[#selstructs+1] = stname
+ end
+ else
+ gg_verbose("structure",stname,struct.size,"doesnt fit")
+ end
+ end
+ if #selstructs==0 then
+ error("Failed to fit a structure into a space of",spx,spy,spz,", make sure that the structure set includes 1x1x1 structures as fallback!")
+ end
+
+ local stname = cellworld.select_random_element(selstructs)
+ local struct = cellworld.structures[stname]
+ gg_verbose("Selected ",stname, struct.size)
+
+ -- determine degrees of freedom
+ local extx, exty, extz = cellworld.v_decompose(struct.size)
+ -- we still want the structure to overlap with our sprout cell
+ local frlx, frly, frlz = math.max(slx, stx-extx+1), math.max(sly, sty-exty+1), math.max(slz, stz-extz+1)
+ local frux, fruy, fruz = math.min(sux-extx+1, stx), math.min(suy-exty+1, sty), math.min(suz-extz+1, stz)
+
+ gg_verbose("degrees of freedom",frlx, frly, frlz,"<>",frux, fruy, fruz)
+
+ local posx, posy, posz
+ if exit_present then
+ local opp_dir = cellworld.opposite_directions[sprout_dir]
+ -- find alignments with matching exits
+ for x in iterate_random_order(frlx, frux) do
+ for y in iterate_random_order(frly, fruy) do
+ for z in iterate_random_order(frlz, fruz) do
+ -- which structure cell borders our sprout cell?
+ local pos_in_struct = vector.new(stx - x, sty - y, stz - z)
+ local cell = cellworld.get_structure_cell(struct, pos_in_struct)
+ if cell.exits and cell.exits[opp_dir] then
+ gg_verbose("Alignment ",x,y,z,", in struct",pos_in_struct,", has matching exit")
+ posx, posy, posz = x,y,z
+ break --break out of the inner loop
+ end
+ end
+ if posx then break end --break out of the outer loop if inner loop broken
+ end
+ if posx then break end --break out of the outer loop if inner loop broken
+ end
+ end
+ -- if no matching exit found or none present on sprout cell, select one alignment randomly
+ if not posx then
+ posx, posy, posz = cellworld.random(frlx, frux), cellworld.random(frly, fruy), cellworld.random(frlz, fruz),
+ gg_verbose("Alignment ",posx, posy, posz,"selected randomly")
+ end
+
+ -- place the structure
+ gg_verbose("-!- placing",stname,"at",posx, posy, posz)
+ cellworld.alloc_set_structure(vector.new(posx, posy, posz), stname, 0, struct.size, true)
+end
+
+
+
+
+
+
+
diff --git a/init.lua b/init.lua
new file mode 100644
index 0000000..eac6791
--- /dev/null
+++ b/init.lua
@@ -0,0 +1,100 @@
+--[[
+CellWorld - Random grid-like underground structures
+(c) 2020 orwell96
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+]]--
+
+local DEBUG = true
+
+cellworld = {}
+
+-- global constants
+-- N [+z]
+-- W [-x] [+x] E
+-- S [-z]
+
+cellworld.NORTH = 0
+cellworld.EAST = 1
+cellworld.SOUTH = 2
+cellworld.WEST = 3
+cellworld.UP = 4
+cellworld.DOWN = 5
+
+cellworld.dirrev = {[0]="NORTH", "EAST", "SOUTH", "WEST", "UP", "DOWN"}
+
+
+local loadt1 = os.clock()
+
+local modpath = minetest.get_modpath(minetest.get_current_modname())
+-- load log utils
+local print_concat_table, dump = dofile(modpath..DIR_DELIM.."logutil.lua")
+
+cellworld.log = function(...)
+ minetest.log("action", "[cellworld] "..print_concat_table({...}))
+end
+cellworld.chat = function(...)
+ minetest.chat_send_all("[cellworld] "..print_concat_table({...}))
+end
+cellworld.warn = function(...)
+ local t = print_concat_table({...})
+ minetest.log("warning", "[cellworld] -!- "..t)
+ minetest.chat_send_all("[cellworld] -!- "..t)
+end
+
+if DEBUG then
+ cellworld.debug = function(...)
+ local t = print_concat_table({...})
+ minetest.log("action", "[cellworld] "..t)
+ minetest.chat_send_all("[cellworld] "..t)
+ end
+else
+ cellworld.debug = function() end
+end
+
+dofile(modpath..DIR_DELIM.."utils.lua")
+dofile(modpath..DIR_DELIM.."allocation.lua")
+dofile(modpath..DIR_DELIM.."gridgen.lua")
+dofile(modpath..DIR_DELIM.."mapgen.lua")
+dofile(modpath..DIR_DELIM.."struct_register.lua")
+dofile(modpath..DIR_DELIM.."structures.lua")
+
+local stcnt = 0
+for stname,_ in pairs(cellworld.structures) do
+ --cellworld.debug("Streg:",stname)
+ stcnt = stcnt + 1
+end
+
+--cellworld.load_allocation()
+
+-- TODO make this a on_shutdown
+minetest.register_chatcommand("sa",
+ {
+ params = "",
+ description = "Cellworld Save Allocation Map To File",
+ privs = {server=true},
+ func = function(name, param)
+ cellworld.save_allocation()
+ return true, "OK"
+ end,
+})
+
+cellworld.load_allocation()
+
+minetest.register_on_shutdown(function()
+ cellworld.save_allocation()
+end)
+
+cellworld.log("Loaded, active structure set is",cellworld.structure_set.name,", registered",stcnt,"structures.")
diff --git a/logutil.lua b/logutil.lua
new file mode 100644
index 0000000..5b01ea2
--- /dev/null
+++ b/logutil.lua
@@ -0,0 +1,48 @@
+-- Logutils - originally part of advtrains
+
+local function dump(t, intend)
+ local str
+ if not t then
+ str = "nil"
+ elseif type(t)=="table" then
+ if t.x and t.y and t.z then
+ str=minetest.pos_to_string(t)
+ else
+ str="{"
+ local intd = (intend or "") .. " "
+ for k,v in pairs(t) do
+ str = str .. "\n" .. intd .. dump(k, intd) .. " = " ..dump(v, intd)
+ end
+ str = str .. "\n" .. (intend or "") .. "}"
+ end
+ elseif type(t)=="boolean" then
+ if t then
+ str="true"
+ else
+ str="false"
+ end
+ elseif type(t)=="function" then
+ str="<function>"
+ elseif type(t)=="userdata" then
+ str="<userdata>"
+ else
+ str=""..t
+ end
+ return str
+end
+
+local function print_concat_table(tab)
+ -- go through table and find max entry
+ local maxe = 0
+ for k, _ in pairs(tab) do
+ maxe = math.max(maxe, k)
+ end
+
+ local t = {}
+ for i=1,maxe do
+ t[i] = dump(tab[i])
+ end
+ return table.concat(t, " ")
+end
+
+return print_concat_table, dump
diff --git a/mapgen.lua b/mapgen.lua
new file mode 100644
index 0000000..ad63bd7
--- /dev/null
+++ b/mapgen.lua
@@ -0,0 +1,167 @@
+-- mapgen.lua - code that actually writes the game world based on the structures in the allocation map.
+
+
+local mg_verbose = function() end
+--local mg_verbose = cellworld.debug
+
+-- on mapgen init, set the mapgen to singlenode
+minetest.register_on_mapgen_init(function(mgparams)
+ minetest.set_mapgen_setting("mg_name", "singlenode", true)
+ -- TODO maybe fixate the structure set in the cellworld meta.
+end)
+
+-- placeholder for when something goes wrong with the mapgen
+minetest.register_node("cellworld:placeholder", {
+ description = "Cellworld Unknown Node Placeholder",
+ tiles = {"cellworld_placeholder.png"},
+ is_ground_content = false,
+ groups = {cracky = 3,},
+})
+
+local placeholder_content_id = minetest.get_content_id("cellworld:placeholder")
+
+-- file-scoped buffer for vm node data
+local vm_data = {}
+
+local function place_cell_actions(basepos, actions, area, clipminw, clipmaxw)
+ for _, action in ipairs(actions) do
+ if action.action == "fill" then
+ local nodeid = minetest.get_content_id(action.node)
+ if not nodeid then
+ cellworld.warn("Unknown node",action.node,", filling with placeholder");
+ nodeid = placeholder_content_id
+ end
+ local rxl, ryl, rzl, rxu, ryu, rzu = unpack(action.coords) -- these are relative
+ if rxu<rxl then rxl,rxu = rxu,rxl end
+ if ryu<ryl then ryl,ryu = ryu,ryl end
+ if rzu<rzl then rzl,rzu = rzu,rzl end
+
+ local cxl, cyl, czl = rxl+basepos.x, ryl+basepos.y, rzl+basepos.z
+ local cxu, cyu, czu = rxu+basepos.x, ryu+basepos.y, rzu+basepos.z -- these are absolute
+
+ mg_verbose("Placing action fill with",action.node,"coords",cxl, cyl, czl,"-",cxu, cyu, czu)
+ -- limit to generation area
+ local axl, ayl, azl = math.max(cxl, clipminw.x), math.max(cyl, clipminw.y), math.max(czl, clipminw.z)
+ local axu, ayu, azu = math.min(cxu, clipmaxw.x), math.min(cyu, clipmaxw.y), math.min(czu, clipmaxw.z)
+ --place on vmanip
+ mg_verbose("clipped coords",axl, ayl, azl,"-",axu, ayu, azu)
+ if axu>=axl and ayu>=ayl and azu>=azl then
+ --local i = 0
+ for index in area:iter(axl, ayl, azl, axu, ayu, azu) do
+ --i = i + 1
+ --mg_verbose("va iter",i,"index",index,"pos",area:position(index))
+ vm_data[index] = nodeid
+ end
+ else
+ mg_verbose("Outside of voxel area!")
+ end
+ elseif action.action == "log" then
+ cellworld.log("Cell Generation Log:",action.message)
+ end
+ end
+end
+
+
+local function place_cell_on_vmanip(cellpos, cell, borders, area, clipminw, clipmaxw)
+ local basepos = cellworld.cell_to_world_pos(cellpos)
+ -- place "structure"
+ mg_verbose("plave_cell_on_vmanip structure at basepos",basepos)
+ place_cell_actions(basepos, cell.structure, area, clipminw, clipmaxw)
+ -- check exits
+ if cell.exits then
+ for _,border in ipairs(borders) do
+ if cell.exits[border] then
+ -- does this exit match one in the neighbor?
+ local n_cellpos = vector.add(cellpos, cellworld.direction_to_vector[border])
+ local n_stname, n_relpos = cellworld.alloc_get_structure_at(n_cellpos)
+ if n_stname then
+ local n_struct = cellworld.structures[n_stname]
+ if n_struct then
+ local n_border = cellworld.opposite_directions[border]
+ local n_cell = cellworld.get_structure_cell(n_struct, n_relpos)
+ if n_cell.exits and n_cell.exits[n_border] then
+ -- place exit
+ mg_verbose("border",border,"exit matches to",n_stname,n_relpos)
+ place_cell_actions(basepos, cell.exits[border], area, clipminw, clipmaxw)
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+
+local function allocate_chunk(minp, maxp)
+ local minwpos = cellworld.world_to_cell_pos(minp)
+ local maxwpos = cellworld.world_to_cell_pos(maxp)
+ local margin = vector.new(2,2,2)
+
+ local allocmin = vector.subtract(minwpos, margin)
+ local allocmax = vector.subtract(maxwpos, margin)
+
+ cellworld.gridgen_generate_cells(allocmin, allocmax)
+end
+
+
+local function run_chunk_generation(minposgen, maxposgen, emin, emax, vm, area)
+ vm:get_data(vm_data)
+
+ local mincell = cellworld.world_to_cell_pos(minposgen)
+ local maxcell = cellworld.world_to_cell_pos(maxposgen)
+ local i = 0
+ -- iterate over the current area
+ for x,y,z,cid in cellworld.alloc_get_area_iterator(mincell, maxcell) do
+ -- back to world position
+ local cellpos = vector.new(x,y,z)
+ --local basepos = cellworld.cell_to_world_pos(vector.new(x,y,z))
+ local stname, relcell = cellworld.alloc_resolve_cid(x,y,z,cid)
+ if not stname then
+ cellworld.warn("Cell at",basepos," is not allocated during mapgen callback. Ignoring!")
+ else
+ local struct = cellworld.structures[stname]
+ if not struct then
+ cellworld.warn("Cell at",basepos," has unknown structure",stname,". Ignoring!")
+ else
+ mg_verbose("At cellpos",cellpos,"placing",stname,"cell",relcell)
+ -- resolve structure cell
+ local cell = cellworld.get_structure_cell(struct, relcell)
+ local borders = cellworld.get_border_sides(relcell, struct.size, true)
+ place_cell_on_vmanip(cellpos, cell, borders, area, minposgen, maxposgen)
+ end
+ end
+ i = i + 1
+ end
+ mg_verbose("run_chunk_generation generated",i,"cells")
+
+ vm:set_data(vm_data)
+end
+
+
+
+-- this callback skeleton is taken from https://github.com/srifqi/superflat/blob/master/init.lua
+minetest.register_on_generated(function(minp, maxp, seed)
+ cellworld.log("==================================================")
+ cellworld.log("Generating chunk: ",minp, maxp)
+ local t1 = os.clock()
+
+ allocate_chunk(minp, maxp)
+
+ local chugent = os.clock()-t1
+ cellworld.log("Structure allocation took",string.format("%.2f", chugent*1000),"ms")
+ local t2 = os.clock()
+
+ local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
+ local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}
+
+ run_chunk_generation(minp, maxp, emin, emax, vm, area)
+
+ vm:write_to_map()
+ vm:set_lighting({day = 15, night = 15})
+ vm:update_liquids()
+ vm:calc_lighting()
+
+ local chugent2 = os.clock()-t2
+ local gentotal = os.clock()-t1
+ cellworld.log("Generation took",string.format("%.2f", chugent2*1000),"ms, total", string.format("%.2f", gentotal*1000),"ms")
+end)
diff --git a/struct_register.lua b/struct_register.lua
new file mode 100644
index 0000000..1143753
--- /dev/null
+++ b/struct_register.lua
@@ -0,0 +1,67 @@
+-- struct_register.lua - registration callbacks for structures.
+
+--[[
+The cellworld table contains the following structure-related entries:
+cellworld = {
+ structures = {
+ [st_name] = {...} -- all registered structures.
+ }
+ structure_set = {
+ -- the active structure set config, which determines grid size, mapgen base node a.s.o as passed to config_structure_set()
+ name = "foo" -- name of the active structure set (which is actually the first parameter), not specified in table.
+ grid_size = 7 -- how many nodes form a cell.
+ mapgen_base_node = "air" -- which node to place by default in map, passed to singlenode mg. TODO not implemented yet.
+ }
+ -- the most important settings from structure set are copied for easier access
+ grid_size = structure_set.grid_size,
+
+ -- of all registered structures, the maximum extents in x, y and z direction.
+ maximum_structure_size = vector.new(1,1,1)
+}
+]]--
+
+cellworld.structures = {}
+
+function cellworld.config_structure_set(name, definition)
+ if cellworld.structure_set then
+ error("Cellworld: Trying to register structure set '"..name.."', but '"..cellworld.structure_set.name..
+ "' has already been registered. There can only be one active structure set at a time.")
+ end
+ cellworld.structure_set = {
+ name = name,
+ grid_size = 9,
+ seed_structure = definition.seed_structure,
+ }
+ if definition.grid_size then
+ cellworld.structure_set.grid_size = definition.grid_size
+ end
+
+ cellworld.grid_size = cellworld.structure_set.grid_size
+
+ cellworld.maximum_structure_size = vector.new(1,1,1)
+end
+
+function cellworld.register_structure(name, def)
+ -- make definition checks
+ if not cellworld.structure_set then
+ error("Cellworld: can not register structures when config_structure_set() hasn't been called!")
+ elseif def.structure_set and def.structure_set ~= cellworld.structure_set.name then
+ error("Cellworld: Structure '"..name.."' can not be registered because the structure set it was defined for ('"..name..
+ "') doesn't match the active structure set ('"..cellworld.structure_set.name.."')!")
+ end
+
+ if not def.chance then
+ def.chance = 50
+ end
+
+ -- TODO validate structure of cells
+
+ cellworld.structures[name] = def
+
+ -- update maxstructsize
+ local mss = cellworld.maximum_structure_size
+ mss.x = math.max(def.size.x, mss.x)
+ mss.y = math.max(def.size.y, mss.y)
+ mss.z = math.max(def.size.z, mss.z)
+
+end
diff --git a/structures.lua b/structures.lua
new file mode 100644
index 0000000..bdf37ef
--- /dev/null
+++ b/structures.lua
@@ -0,0 +1,241 @@
+-- cellworld test structures. Later, put this into separate mods
+
+--[[ cellworld structure table
+struct = {
+ structure_set = "foo" -- the expected structure set.
+ size = {
+ x = 1 -- how many grid units this spans horizontally in x direction
+ y = 1 -- how many grid units this spans vertically in y direction
+ z = 1 -- how many grid units this spans horizontally in z direction
+ }
+
+ no_rotate = false -- whether structure may not be rotated
+
+ chance = 20 -- Chance of spawning this structure. = number of times inserted into the selection table
+
+ -- to specify the structure, two approaches can be used: either table-based, or function-based
+ -- Table-based:
+ cells = {
+ -- due to the way cellworld works, nodes are generated on a per-cell level. so, each grid_size^3 cube needs to be specified independently.
+ ["x|y|z" ] = {
+ structure = {
+ action = "fill", -- action to perform. Can be registered, to be specified later. For now, only 'fill' supported.
+ node="default:air", -- remaining parameters are action-dependent. Here: node to fill with.
+ param2 = 0, -- optional: param2 to set, defaults to 0, or
+ param2 = {0,1,2,3}, -- table with 4 entries, per structure rotation
+ coords = {-x,-y,-z, +x,+y,+z} -- specify bounds of filled area. Relative to node origin and rotated with structure rotation
+ }
+ exits = {
+ [cellworld.EAST] = { -- the exit on the specified side of the structure exists and should be carved by this description if the neighboring structure also has an exit at this place.
+ ... actions ...
+ }
+ }
+ -- exit definitions do nothing if the specified wall is not on the outside of the structure
+ -- vertical exits do not exist currently.
+ -- a missing exits table is synonym to an empty exits table.
+ }
+ }
+ -- If an entry in cells does not exist (or the entire cells table), can also use
+ cell = {
+ ..actions.. -- fallback if not in nodes table
+ }
+
+ -- function-based:
+ make_node(gridnode_pos, vmiface) -- to be specified
+ exit_exists(gridnode_pos, exit_dir)
+ make_exit(gridnode_pos, exit_dir, vmiface)
+}
+]]--
+
+-- Configure the current structure set. There can only be one active structure set at a time, so calling this twice results in an error.
+cellworld.config_structure_set("space",{
+ mapgen_base_node = "air",
+ grid_size = 9,
+ seed_structure = "space:platform",
+})
+
+cellworld.register_structure("space:nothing", {
+ structure_set = "space", -- this could maybe also be a list... is there an use case for this?
+ size = {
+ x=1,
+ y=1,
+ z=1,
+ },
+
+ chance = 5,
+
+ cell = {
+ -- do nothing
+ structure={},
+ exits={}
+ }
+})
+
+cellworld.register_structure("space:platform", {
+ structure_set = "space",
+ size = {
+ x=1,
+ y=1,
+ z=1,
+ },
+
+ chance = 10,
+
+ cell = {
+ structure = {
+ {action="fill", node="default:stone", coords = {2, 1, 2, 6, 1, 6}},
+ },
+ exits = {
+ [cellworld.EAST] = {
+ {action="fill", node="default:stone", coords = {7, 1, 3, 8, 1, 5}},
+ },
+ [cellworld.WEST] = {
+ {action="fill", node="default:stone", coords = {0, 1, 3, 1, 1, 5}},
+ },
+ [cellworld.NORTH] = {
+ {action="fill", node="default:stone", coords = {3, 1, 7, 5, 1, 8}},
+ },
+ [cellworld.SOUTH] = {
+ {action="fill", node="default:stone", coords = {3, 1, 0, 5, 1, 1}},
+ },
+ }
+ }
+})
+
+cellworld.register_structure("space:fountain", {
+ structure_set = "space",
+ size = {
+ x=3,
+ y=1,
+ z=3,
+ },
+
+ chance = 1,
+
+ cells = {
+ ["1|0|0"]={ -- south exit
+ structure = {
+ {action="fill", node="default:stonebrick", coords = {2, 1, 2, 6, 1, 6}},
+ {action="fill", node="default:stonebrick", coords = {0, 1, 7, 8, 1, 8}},
+ },
+ exits = {
+ [cellworld.SOUTH] = {
+ {action="fill", node="default:stone", coords = {3, 1, 0, 5, 1, 1}},
+ },
+ }
+ },
+ ["1|0|2"]={ -- north exit
+ structure = {
+ {action="fill", node="default:stonebrick", coords = {2, 1, 2, 6, 1, 6}},
+ {action="fill", node="default:stonebrick", coords = {0, 1, 0, 8, 1, 1}},
+ },
+ exits = {
+ [cellworld.NORTH] = {
+ {action="fill", node="default:stone", coords = {3, 1, 7, 5, 1, 8}},
+ },
+ }
+ },
+ ["0|0|1"]={ -- west exit
+ structure = {
+ {action="fill", node="default:stonebrick", coords = {2, 1, 2, 6, 1, 6}},
+ {action="fill", node="default:stonebrick", coords = {7, 1, 0, 8, 1, 8}},
+ },
+ exits = {
+ [cellworld.WEST] = {
+ {action="fill", node="default:stone", coords = {0, 1, 3, 1, 1, 5}},
+ },
+ }
+ },
+ ["2|0|1"]={ -- east exit
+ structure = {
+ {action="fill", node="default:stonebrick", coords = {2, 1, 2, 6, 1, 6}},
+ {action="fill", node="default:stonebrick", coords = {0, 1, 0, 1, 1, 8}},
+ },
+ exits = {
+ [cellworld.EAST] = {
+ {action="fill", node="default:stone", coords = {7, 1, 3, 8, 1, 5}},
+ },
+ }
+ },
+ ["1|0|1"]={ -- center
+ structure = {
+ {action="fill", node="default:stonebrick", coords = {0, 1, 0, 8, 1, 8}},
+ {action="fill", node="default:stonebrick", coords = {2, 2, 2, 6, 2, 6}},
+ {action="fill", node="air", coords = {3, 2, 3, 5, 2, 5}},
+ {action="fill", node="default:water_source", coords = {4, 4, 4, 4, 4, 4}},
+ },
+ },
+ },
+
+ cell = {
+ structure = {
+ },
+ }
+})
+
+cellworld.register_structure("space:stair", {
+ structure_set = "space",
+ size = {
+ x=3,
+ y=2,
+ z=1,
+ },
+
+ chance = 5,
+
+ cells = {
+ ["0|0|0"]={
+ structure = {
+ {action="fill", node="default:stone", coords = {2, 1, 2, 8, 1, 6}},
+ },
+ exits = {
+ [cellworld.WEST] = {
+ {action="fill", node="default:stone", coords = {0, 1, 3, 1, 1, 5}},
+ },
+ [cellworld.NORTH] = {
+ {action="fill", node="default:stone", coords = {3, 1, 7, 5, 1, 8}},
+ },
+ [cellworld.SOUTH] = {
+ {action="fill", node="default:stone", coords = {3, 1, 0, 5, 1, 1}},
+ },
+ }
+ },
+ ["1|0|0"]={ -- middle lower part
+ structure = {
+ {action="fill", node="default:stone", coords = {8, 8, 2, 8, 8, 6}},
+ {action="fill", node="default:stone", coords = {7, 7, 2, 7, 7, 6}},
+ {action="fill", node="default:stone", coords = {6, 6, 2, 6, 6, 6}},
+ {action="fill", node="default:stone", coords = {5, 5, 2, 5, 5, 6}},
+ {action="fill", node="default:stone", coords = {4, 4, 2, 4, 4, 6}},
+ {action="fill", node="default:stone", coords = {3, 3, 2, 3, 3, 6}},
+ {action="fill", node="default:stone", coords = {2, 2, 2, 2, 2, 6}},
+ {action="fill", node="default:stone", coords = {0, 1, 2, 1, 1, 6}},
+ },
+ },
+ ["2|1|0"]={
+ structure = {
+ {action="fill", node="default:stone", coords = {1, 1, 2, 6, 1, 6}},
+ {action="fill", node="default:stone", coords = {0, 0, 2, 0, 0, 6}},
+ },
+ exits = {
+ [cellworld.EAST] = {
+ {action="fill", node="default:stone", coords = {7, 1, 3, 8, 1, 5}},
+ },
+ [cellworld.NORTH] = {
+ {action="fill", node="default:stone", coords = {3, 1, 7, 5, 1, 8}},
+ },
+ [cellworld.SOUTH] = {
+ {action="fill", node="default:stone", coords = {3, 1, 0, 5, 1, 1}},
+ },
+ }
+ },
+ },
+
+ cell = {
+ structure = {
+ --{action="fill", node="default:stone", coords = {4, 4, 4, 4, 4, 4}},
+ },
+ }
+})
+
+
diff --git a/textures/cellworld_placeholder.png b/textures/cellworld_placeholder.png
new file mode 100755
index 0000000..f0a40fd
--- /dev/null
+++ b/textures/cellworld_placeholder.png
Binary files differ
diff --git a/utils.lua b/utils.lua
new file mode 100644
index 0000000..b6a7ef9
--- /dev/null
+++ b/utils.lua
@@ -0,0 +1,125 @@
+-- misc. utilities
+
+-- returns a list of directions to where this cell in a structure is at the structure border.
+-- returns an empty list for inner nodes
+-- horizontal_only restricts search to only the horizontal plane (no y)
+function cellworld.get_border_sides(rel_pos, str_size, horizontal_only)
+ local borders = {}
+
+ if rel_pos.z == str_size.z-1 then
+ borders[#borders+1] = cellworld.NORTH
+ end
+ if rel_pos.x == str_size.x-1 then
+ borders[#borders+1] = cellworld.EAST
+ end
+
+ if rel_pos.z == 0 then
+ borders[#borders+1] = cellworld.SOUTH
+ end
+ if rel_pos.x == 0 then
+ borders[#borders+1] = cellworld.WEST
+ end
+
+ if not horizontal_only then
+ if rel_pos.y == str_size.y-1 then
+ borders[#borders+1] = cellworld.UP
+ end
+ if rel_pos.y == 0 then
+ borders[#borders+1] = cellworld.DOWN
+ end
+ end
+
+ return borders
+
+end
+
+cellworld.opposite_directions = {
+ [cellworld.EAST] = cellworld.WEST,
+ [cellworld.WEST] = cellworld.EAST,
+ [cellworld.NORTH]= cellworld.SOUTH,
+ [cellworld.SOUTH]= cellworld.NORTH,
+ [cellworld.UP] = cellworld.DOWN,
+ [cellworld.DOWN] = cellworld.UP,
+}
+
+cellworld.direction_to_vector = {
+ [cellworld.EAST] = vector.new(1,0,0),
+ [cellworld.WEST] = vector.new(-1,0,0),
+ [cellworld.NORTH]= vector.new(0,0,1),
+ [cellworld.SOUTH]= vector.new(0,0,-1),
+ [cellworld.UP] = vector.new(0,1,0),
+ [cellworld.DOWN] = vector.new(0,-1,0),
+}
+
+-- quickly hash a position. Only suitable for relative positions, limited to a range of 0..255 each coordinate
+function cellworld.small_pos_hash(x,y,z)
+ return y*65536 + x*256 + z
+end
+
+-- returns the relative structure cell definition
+function cellworld.get_structure_cell(struct, relpos)
+ if struct.cells then
+ local pstr = relpos.x.."|"..relpos.y.."|"..relpos.z
+ if struct.cells[pstr] then
+ return struct.cells[pstr]
+ end
+ end
+ return struct.cell
+end
+
+cellworld.pcgrandom = PcgRandom(os.time())
+
+function cellworld.random(min, max)
+ return cellworld.pcgrandom:next(min,max)
+end
+
+-- returns random element from the list
+function cellworld.select_random_element(list)
+ local r = cellworld.random(1, #list)
+ return list[r]
+end
+
+-- returns a for iterator that iterates from 'from' to 'to' integers but in random order
+function iterate_random_order(from, to)
+ local list = {}
+ for i=from,to do
+ list[#list+1] = i
+ end
+ return function()
+ if #list==0 then return nil end
+ local r = cellworld.random(1, #list)
+ return table.remove(list, r)
+ end
+end
+
+function cellworld.v_decompose(vec)
+ return vec.x, vec.y, vec.z
+end
+
+-- convert back and forth
+function cellworld.world_to_cell_pos(wpos)
+ if not cellworld.grid_size then
+ error("Grid size hasn't been configured yet, is a structure set loaded?")
+ end
+ return vector.new(
+ math.floor(wpos.x / cellworld.grid_size),
+ math.floor(wpos.y / cellworld.grid_size),
+ math.floor(wpos.z / cellworld.grid_size)
+ )
+end
+
+-- upper: output the upper corner of the cell
+function cellworld.cell_to_world_pos(cpos, upper)
+ if not cellworld.grid_size then
+ error("Grid size hasn't been configured yet, is a structure set loaded?")
+ end
+ local wi=0
+ if upper then
+ wi = cellworld.grid_size-1
+ end
+ return vector.new(
+ cpos.x * cellworld.grid_size + wi,
+ cpos.y * cellworld.grid_size + wi,
+ cpos.z * cellworld.grid_size + wi
+ )
+end