From 7000c5220d9ce8864e028d3b035d86040e7de7aa Mon Sep 17 00:00:00 2001 From: orwell96 Date: Sun, 18 Oct 2020 15:27:55 +0200 Subject: Initial working state --- allocation.lua | 474 +++++++++++++++++++++++++++++++++++++ gridgen.lua | 248 +++++++++++++++++++ init.lua | 100 ++++++++ logutil.lua | 48 ++++ mapgen.lua | 167 +++++++++++++ struct_register.lua | 67 ++++++ structures.lua | 241 +++++++++++++++++++ textures/cellworld_placeholder.png | Bin 0 -> 325 bytes utils.lua | 125 ++++++++++ 9 files changed, 1470 insertions(+) create mode 100644 allocation.lua create mode 100644 gridgen.lua create mode 100644 init.lua create mode 100644 logutil.lua create mode 100644 mapgen.lua create mode 100644 struct_register.lua create mode 100644 structures.lua create mode 100755 textures/cellworld_placeholder.png create mode 100644 utils.lua 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: + + +Indirection entry: +I +This node is part of a structure whose origin is at x-xoff .. z-zoff + +Structure entry: +S + +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 "" + 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 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 = , 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 . + +]]-- + +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="" + elseif type(t)=="userdata" then + str="" + 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=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 Binary files /dev/null and b/textures/cellworld_placeholder.png 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 -- cgit v1.2.3