diff --git a/.gitignore b/.gitignore index 8039c093..1847e03c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ Debug/voxel_engine /build /cmake-build-** /screenshots +/export +/config /out /misc diff --git a/res/config/defaults.toml b/res/config/defaults.toml new file mode 100644 index 00000000..53de523f --- /dev/null +++ b/res/config/defaults.toml @@ -0,0 +1 @@ +generator = "core:default" diff --git a/res/content/base/blocks/coal_ore.json b/res/content/base/blocks/coal_ore.json new file mode 100644 index 00000000..23c050c6 --- /dev/null +++ b/res/content/base/blocks/coal_ore.json @@ -0,0 +1,3 @@ +{ + "texture": "coal_ore" +} diff --git a/res/content/base/blocks/dirt.json b/res/content/base/blocks/dirt.json index 1495377a..81453c4b 100644 --- a/res/content/base/blocks/dirt.json +++ b/res/content/base/blocks/dirt.json @@ -1,4 +1,5 @@ { "texture": "dirt", - "material": "base:ground" -} \ No newline at end of file + "material": "base:ground", + "surface-replacement": "base:grass_block" +} diff --git a/res/content/base/config/defaults.toml b/res/content/base/config/defaults.toml new file mode 100644 index 00000000..d292d2ce --- /dev/null +++ b/res/content/base/config/defaults.toml @@ -0,0 +1 @@ +generator = "base:demo" diff --git a/res/content/base/content.json b/res/content/base/content.json index 9b696dfd..cec825a8 100644 --- a/res/content/base/content.json +++ b/res/content/base/content.json @@ -1,8 +1,6 @@ { - "entities": [ - "drop", - "player", - "falling_block" + "items": [ + "bazalt_breaker" ], "blocks": [ "dirt", @@ -28,9 +26,12 @@ "pipe", "lightbulb", "torch", - "wooden_door" + "wooden_door", + "coal_ore" ], - "items": [ - "bazalt_breaker" + "entities": [ + "drop", + "player", + "falling_block" ] } \ No newline at end of file diff --git a/res/content/base/generators/demo.files/biomes.toml b/res/content/base/generators/demo.files/biomes.toml new file mode 100644 index 00000000..bd2e11e8 --- /dev/null +++ b/res/content/base/generators/demo.files/biomes.toml @@ -0,0 +1,67 @@ +[forest] +parameters = [ + {weight=1, value=1}, + {weight=0.5, value=0.2} +] +layers = [ + {below-sea-level=false, height=1, block="base:grass_block"}, + {below-sea-level=false, height=7, block="base:dirt"}, + {height=-1, block="base:stone"}, + {height=1, block="base:bazalt"} +] +sea-layers = [ + {height=-1, block="base:water"} +] +plant-chance = 0.4 +plants = [ + {weight=1, block="base:grass"}, + {weight=0.03, block="base:flower"} +] +structure-chance = 0.032 +structures = [ + {name="tree0", weight=1}, + {name="tree1", weight=1}, + {name="tree2", weight=1}, + {name="tower", weight=0.002} +] + + +[desert] +parameters = [ + {weight=0.3, value=0}, + {weight=0.1, value=0} +] +layers = [ + {height=6, block="base:sand"}, + {height=-1, block="base:stone"}, + {height=1, block="base:bazalt"} +] +sea-layers = [ + {height=-1, block="base:water"} +] + + +[plains] +parameters = [ + {weight=0.6, value=0.5}, + {weight=0.6, value=0.5} +] +layers = [ + {below-sea-level=false, height=1, block="base:grass_block"}, + {below-sea-level=false, height=5, block="base:dirt"}, + {height=-1, block="base:stone"}, + {height=1, block="base:bazalt"} +] +sea-layers = [ + {height=-1, block="base:water"} +] +plant-chance = 0.3 +plants = [ + {weight=1, block="base:grass"}, + {weight=0.03, block="base:flower"} +] +structure-chance=0.0001 +structures = [ + {name="tree0", weight=1}, + {name="tree1", weight=1} +] diff --git a/res/content/base/generators/demo.files/fragments/coal_ore0.vox b/res/content/base/generators/demo.files/fragments/coal_ore0.vox new file mode 100644 index 00000000..882afcb7 Binary files /dev/null and b/res/content/base/generators/demo.files/fragments/coal_ore0.vox differ diff --git a/res/content/base/generators/demo.files/fragments/tower.vox b/res/content/base/generators/demo.files/fragments/tower.vox new file mode 100644 index 00000000..a6b58857 Binary files /dev/null and b/res/content/base/generators/demo.files/fragments/tower.vox differ diff --git a/res/content/base/generators/demo.files/fragments/tree0.vox b/res/content/base/generators/demo.files/fragments/tree0.vox new file mode 100644 index 00000000..78e87cbf Binary files /dev/null and b/res/content/base/generators/demo.files/fragments/tree0.vox differ diff --git a/res/content/base/generators/demo.files/fragments/tree1.vox b/res/content/base/generators/demo.files/fragments/tree1.vox new file mode 100644 index 00000000..6c2aec44 Binary files /dev/null and b/res/content/base/generators/demo.files/fragments/tree1.vox differ diff --git a/res/content/base/generators/demo.files/fragments/tree2.vox b/res/content/base/generators/demo.files/fragments/tree2.vox new file mode 100644 index 00000000..0aac534c Binary files /dev/null and b/res/content/base/generators/demo.files/fragments/tree2.vox differ diff --git a/res/content/base/generators/demo.files/ores.json b/res/content/base/generators/demo.files/ores.json new file mode 100644 index 00000000..9ccf696c --- /dev/null +++ b/res/content/base/generators/demo.files/ores.json @@ -0,0 +1,3 @@ +[ + {"struct": "coal_ore0", "rarity": 4400} +] diff --git a/res/content/base/generators/demo.files/script.lua b/res/content/base/generators/demo.files/script.lua new file mode 100644 index 00000000..7596d226 --- /dev/null +++ b/res/content/base/generators/demo.files/script.lua @@ -0,0 +1,81 @@ +local _, dir = parse_path(__DIR__) +local ores = require "base:generation/ores" +ores.load(dir) + +function place_structures(x, z, w, d, seed, hmap, chunk_height) + local placements = {} + ores.place(placements, x, z, w, d, seed, hmap, chunk_height) + return placements +end + +function place_structures_wide(x, z, w, d, seed, chunk_height) + local placements = {} + if math.random() < 0.05 then -- generate caves + local sx = x + math.random() * 10 - 5 + local sy = math.random() * (chunk_height / 4) + 10 + local sz = z + math.random() * 10 - 5 + + local dir = math.random() * math.pi * 2 + local dir_inertia = (math.random() - 0.5) * 2 + local elevation = -3 + local width = math.random() * 3 + 2 + + for i=1,18 do + local dx = math.sin(dir) * 10 + local dz = -math.cos(dir) * 10 + + local ex = sx + dx + local ey = sy + elevation + local ez = sz + dz + + table.insert(placements, + {":line", 0, {sx, sy, sz}, {ex, ey, ez}, width}) + + sx = ex + sy = ey + sz = ez + + dir_inertia = dir_inertia * 0.8 + (math.random() - 0.5) * math.pow(math.random(), 2) * 8 + elevation = elevation * 0.9 + (math.random() - 0.4) * (1.0-math.pow(math.random(), 4)) * 8 + dir = dir + dir_inertia + end + end + return placements +end + +function generate_heightmap(x, y, w, h, seed, s) + local umap = Heightmap(w, h) + local vmap = Heightmap(w, h) + umap.noiseSeed = seed + vmap.noiseSeed = seed + vmap:noise({x+521, y+70}, 0.1*s, 3, 25.8) + vmap:noise({x+95, y+246}, 0.15*s, 3, 25.8) + + local map = Heightmap(w, h) + map.noiseSeed = seed + map:noise({x, y}, 0.8*s, 4, 0.02) + map:cellnoise({x, y}, 0.1*s, 3, 0.3, umap, vmap) + map:add(0.7) + + local rivermap = Heightmap(w, h) + rivermap.noiseSeed = seed + rivermap:noise({x+21, y+12}, 0.1*s, 4) + rivermap:abs() + rivermap:mul(2.0) + rivermap:pow(0.15) + rivermap:max(0.5) + map:mul(rivermap) + return map +end + +function generate_biome_parameters(x, y, w, h, seed, s) + local tempmap = Heightmap(w, h) + tempmap.noiseSeed = seed + 5324 + tempmap:noise({x, y}, 0.04*s, 6) + local hummap = Heightmap(w, h) + hummap.noiseSeed = seed + 953 + hummap:noise({x, y}, 0.04*s, 6) + tempmap:pow(3) + hummap:pow(3) + return tempmap, hummap +end diff --git a/res/content/base/generators/demo.files/structures.toml b/res/content/base/generators/demo.files/structures.toml new file mode 100644 index 00000000..3dcf3cda --- /dev/null +++ b/res/content/base/generators/demo.files/structures.toml @@ -0,0 +1,5 @@ +tree0 = {} +tree1 = {} +tree2 = {} +tower = {} +coal_ore0 = {} diff --git a/res/content/base/generators/demo.toml b/res/content/base/generators/demo.toml new file mode 100644 index 00000000..f9952f61 --- /dev/null +++ b/res/content/base/generators/demo.toml @@ -0,0 +1,4 @@ +# 1 - temperature +# 2 - humidity +biome-parameters = 2 +sea-level = 64 diff --git a/res/content/base/modules/generation/ores.lua b/res/content/base/modules/generation/ores.lua new file mode 100644 index 00000000..3b2c2f91 --- /dev/null +++ b/res/content/base/modules/generation/ores.lua @@ -0,0 +1,29 @@ +local ores = {} + +function ores.load(directory) + ores.ores = file.read_combined_list(directory.."/ores.json") +end + +function ores.place(placements, x, z, w, d, seed, hmap, chunk_height) + local BLOCKS_PER_CHUNK = w * d * chunk_height + for _, ore in ipairs(ores.ores) do + local count = BLOCKS_PER_CHUNK / ore.rarity + + -- average count is less than 1 + local addchance = math.fmod(count, 1.0) + if math.random() < addchance then + count = count + 1 + end + + for i=1,count do + local sx = math.random() * w + local sz = math.random() * d + local sy = math.random() * (chunk_height * 0.5) + if sy < hmap:at(sx, sz) * chunk_height - 6 then + table.insert(placements, {ore.struct, {sx, sy, sz}, math.random()*4, -1}) + end + end + end +end + +return ores diff --git a/res/content/base/preload.json b/res/content/base/preload.json index 1e519ca2..33f38f36 100644 --- a/res/content/base/preload.json +++ b/res/content/base/preload.json @@ -7,13 +7,6 @@ "models": [ "drop-item" ], - "shaders": [ - "ui3d", - "entity", - "screen", - "background", - "skybox_gen" - ], "textures": [ "misc/moon", "misc/sun", diff --git a/res/content/base/resource-aliases.json b/res/content/base/resource-aliases.json new file mode 100644 index 00000000..1d51f5c3 --- /dev/null +++ b/res/content/base/resource-aliases.json @@ -0,0 +1,8 @@ +{ + "camera": { + "base:first-person": "core:first-person", + "base:third-person-front": "core:third-person-front", + "base:third-person-back": "core:third-person-back", + "base:cinematic": "core:cinematic" + } +} diff --git a/res/content/base/textures/blocks/coal_ore.png b/res/content/base/textures/blocks/coal_ore.png new file mode 100644 index 00000000..98f36abe Binary files /dev/null and b/res/content/base/textures/blocks/coal_ore.png differ diff --git a/res/generators/default.files/biomes.toml b/res/generators/default.files/biomes.toml new file mode 100644 index 00000000..90a98744 --- /dev/null +++ b/res/generators/default.files/biomes.toml @@ -0,0 +1,8 @@ +[flat] +parameters = [] +layers = [ + {height=-1, block="core:obstacle"} +] +sea-layers = [ + {height=-1, block="core:obstacle"} +] diff --git a/res/generators/default.toml b/res/generators/default.toml new file mode 100644 index 00000000..2360052d --- /dev/null +++ b/res/generators/default.toml @@ -0,0 +1 @@ +biome-parameters = 0 diff --git a/res/layouts/pages/generators.xml.lua b/res/layouts/pages/generators.xml.lua index dd9b6743..64649cd5 100644 --- a/res/layouts/pages/generators.xml.lua +++ b/res/layouts/pages/generators.xml.lua @@ -1,15 +1,20 @@ settings = session.get_entry('new_world') function on_open() - local names = core.get_generators() - table.sort(names) + local names = generation.get_generators() + local keys = {} + for key in pairs(names) do + table.insert(keys, key) + end + table.sort(keys) local panel = document.root - for _,k in ipairs(names) do + for _, key in ipairs(keys) do + local caption = names[key] panel:add(gui.template("generator", { - callback=string.format("settings.generator=%q menu:back()", k), - id=k, - name=settings.generator_name(k) + callback=string.format("settings.generator=%q menu:back()", key), + id=key, + name=settings.generator_name(caption) })) end panel:add("") diff --git a/res/layouts/pages/new_world.xml.lua b/res/layouts/pages/new_world.xml.lua index e92d5ae0..25e6f424 100644 --- a/res/layouts/pages/new_world.xml.lua +++ b/res/layouts/pages/new_world.xml.lua @@ -10,12 +10,7 @@ function save_state() end function settings.generator_name(id) - local prefix, name = parse_path(id) - if prefix == "core" then - return gui.str(name, "world.generators") - else - return id - end + return gui.str(id, "world.generators"):gsub("^%l", string.upper) end function create_world() @@ -34,12 +29,12 @@ function on_open() "%s [%s]", gui.str("Content", "menu"), #pack.get_installed() ) if settings.generator == nil then - settings.generator = core.get_default_generator() + settings.generator = generation.get_default_generator() end document.generator_btn.text = string.format( "%s: %s", gui.str("World generator", "world"), - settings.generator_name(settings.generator) + settings.generator_name(generation.get_generators()[settings.generator]) ) document.name_box.text = settings.name or '' document.seed_box.text = settings.seed or '' diff --git a/res/preload.json b/res/preload.json index 92d371cf..72f96c6b 100644 --- a/res/preload.json +++ b/res/preload.json @@ -1,8 +1,13 @@ { "shaders": [ "ui", + "ui3d", "main", - "lines" + "lines", + "entity", + "screen", + "background", + "skybox_gen" ], "textures": [ "gui/menubg", diff --git a/res/content/base/resources.json b/res/resources.json similarity index 100% rename from res/content/base/resources.json rename to res/resources.json diff --git a/res/scripts/stdcmd.lua b/res/scripts/stdcmd.lua index 61dccba8..a2edd9b8 100644 --- a/res/scripts/stdcmd.lua +++ b/res/scripts/stdcmd.lua @@ -149,3 +149,41 @@ console.add_command( end end ) + +console.add_command( + "fragment.save x:int y:int z:int w:int h:int d:int name:str='untitled' crop:bool=false", + "Save fragment", + function(args, kwargs) + local x = args[1] + local y = args[2] + local z = args[3] + + local w = args[4] + local h = args[5] + local d = args[6] + + local name = args[7] + local crop = args[8] + + local fragment = generation.create_fragment( + {x, y, z}, {x + w, y + h, z + d}, crop, false + ) + local filename = 'export:'..name..'.vox' + generation.save_fragment(fragment, filename, crop) + console.log("fragment with size "..vec3.tostring(fragment.size).. + " has been saved as "..file.resolve(filename)) + end +) + +console.add_command( + "fragment.crop filename:str", + "Crop fragment", + function(args, kwargs) + local filename = args[1] + local fragment = generation.load_fragment(filename) + fragment:crop() + generation.save_fragment(fragment, filename, crop) + console.log("fragment with size "..vec3.tostring(fragment.size).. + " has been saved as "..file.resolve(filename)) + end +) diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 32dcc653..8fbc779f 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -1,89 +1,6 @@ --- kit of standard functions - --- Check if given table is an array -function is_array(x) - if #t > 0 then - return true - end - for k, v in pairs(x) do - return false - end - return true -end - --- Get entry-point and filename from `entry-point:filename` path -function parse_path(path) - local index = string.find(path, ':') - if index == nil then - error("invalid path syntax (':' missing)") - end - return string.sub(path, 1, index-1), string.sub(path, index+1, -1) -end - -package = { - loaded={} -} -local __cached_scripts = {} -local __warnings_hidden = {} - -function on_deprecated_call(name, alternatives) - if __warnings_hidden[name] then - return - end - __warnings_hidden[name] = true - if alternatives then - debug.warning("deprecated function called ("..name.."), use ".. - alternatives.." instead\n"..debug.traceback()) - else - debug.warning("deprecated function called ("..name..")\n"..debug.traceback()) - end -end - --- Load script with caching --- --- path - script path `contentpack:filename`. --- Example `base:scripts/tests.lua` --- --- nocache - ignore cached script, load anyway -local function __load_script(path, nocache) - local packname, filename = parse_path(path) - - -- __cached_scripts used in condition because cached result may be nil - if not nocache and __cached_scripts[path] ~= nil then - return package.loaded[path] - end - if not file.isfile(path) then - error("script '"..filename.."' not found in '"..packname.."'") - end - - local script, err = load(file.read(path), path) - if script == nil then - error(err) - end - local result = script() - if not nocache then - __cached_scripts[path] = script - package.loaded[path] = result - end - return result -end - -function __scripts_cleanup() - print("cleaning scripts cache") - for k, v in pairs(__cached_scripts) do - local packname, _ = parse_path(k) - if packname ~= "core" then - print("unloaded "..k) - __cached_scripts[k] = nil - package.loaded[k] = nil - end - end -end - -function require(path) - local prefix, file = parse_path(path) - return __load_script(prefix..":modules/"..file..".lua") -end +------------------------------------------------ +------ Extended kit of standard functions ------ +------------------------------------------------ function sleep(timesec) local start = time.uptime() @@ -92,20 +9,6 @@ function sleep(timesec) end end -function pack.is_installed(packid) - return file.isfile(packid..":package.json") -end - -function pack.data_file(packid, name) - file.mkdirs("world:data/"..packid) - return "world:data/"..packid.."/"..name -end - -function pack.shared_file(packid, name) - file.mkdirs("config:"..packid) - return "config:"..packid.."/"..name -end - -- events events = { handlers = {} @@ -233,59 +136,6 @@ function session.reset_entry(name) session.entries[name] = nil end -function timeit(iters, func, ...) - local tm = time.uptime() - for i=1,iters do - func(...) - end - print("[time mcs]", (time.uptime()-tm) * 1000000) -end - -function table.has(t, x) - for i,v in ipairs(t) do - if v == x then - return true - end - end - return false -end - -function table.index(t, x) - for i,v in ipairs(t) do - if v == x then - return i - end - end - return -1 -end - -function table.remove_value(t, x) - local index = table.index(t, x) - if index ~= -1 then - table.remove(t, index) - end -end - -function table.tostring(t) - local s = '[' - for i,v in ipairs(t) do - s = s..tostring(v) - if i < #t then - s = s..', ' - end - end - return s..']' -end - -function file.readlines(path) - local str = file.read(path) - local lines = {} - for s in str:gmatch("[^\r\n]+") do - table.insert(lines, s) - end - return lines -end - stdcomp = require "core:internal/stdcomp" entities.get = stdcomp.get_Entity entities.get_all = function(uids) @@ -302,145 +152,6 @@ end math.randomseed(time.uptime() * 1536227939) ----------------------------------------------- - -function math.clamp(_in, low, high) - return math.min(math.max(_in, low), high) -end - -function math.rand(low, high) - return low + (high - low) * math.random() -end - ----------------------------------------------- - -function table.copy(t) - local copied = {} - - for k, v in pairs(t) do - copied[k] = v - end - - return copied -end - -function table.count_pairs(t) - local count = 0 - - for k, v in pairs(t) do - count = count + 1 - end - - return count -end - -function table.random(t) - return t[math.random(1, #t)] -end - ----------------------------------------------- - -local pattern_escape_replacements = { - ["("] = "%(", - [")"] = "%)", - ["."] = "%.", - ["%"] = "%%", - ["+"] = "%+", - ["-"] = "%-", - ["*"] = "%*", - ["?"] = "%?", - ["["] = "%[", - ["]"] = "%]", - ["^"] = "%^", - ["$"] = "%$", - ["\0"] = "%z" -} - -function string.pattern_safe(str) - return string.gsub(str, ".", pattern_escape_replacements) -end - ---local totable = string.ToTable -local string_sub = string.sub -local string_find = string.find -local string_len = string.len -function string.explode(separator, str, withpattern) - --if (separator == "") then return totable(str) end - if (withpattern == nil) then withpattern = false end - - local ret = {} - local current_pos = 1 - - for i = 1, string_len(str) do - local start_pos, end_pos = string_find(str, separator, current_pos, not withpattern) - if (not start_pos) then break end - ret[i] = string_sub(str, current_pos, start_pos - 1) - current_pos = end_pos + 1 - end - - ret[#ret + 1] = string_sub(str, current_pos) - - return ret -end - -function string.split(str, delimiter) - return string.explode(delimiter, str) -end - -function string.formatted_time(seconds, format) - if (not seconds) then seconds = 0 end - local hours = math.floor(seconds / 3600) - local minutes = math.floor((seconds / 60) % 60) - local millisecs = (seconds - math.floor(seconds)) * 1000 - seconds = math.floor(seconds % 60) - - if (format) then - return string.format(format, minutes, seconds, millisecs) - else - return { h = hours, m = minutes, s = seconds, ms = millisecs } - end -end - -function string.replace(str, tofind, toreplace) - local tbl = string.Explode(tofind, str) - if (tbl[1]) then return table.concat(tbl, toreplace) end - return str -end - -function string.trim(s, char) - if char then char = string.pattern_safe(char) else char = "%s" end - return string.match(s, "^" .. char .. "*(.-)" .. char .. "*$") or s -end - -function string.trim_right(s, char) - if char then char = string.pattern_safe(char) else char = "%s" end - return string.match(s, "^(.-)" .. char .. "*$") or s -end - -function string.trim_left(s, char) - if char then char = string.pattern_safe(char) else char = "%s" end - return string.match(s, "^" .. char .. "*(.+)$") or s -end - -local meta = getmetatable("") - -function meta:__index(key) - local val = string[key] - if (val ~= nil) then - return val - elseif (tonumber(key)) then - return string.sub(self, key, key) - end -end - -function string.starts_with(str, start) - return string.sub(str, 1, string.len(start)) == start -end - -function string.ends_with(str, endStr) - return endStr == "" or string.sub(str, -string.len(endStr)) == endStr -end - -- --------- Deprecated functions ------ -- local function wrap_deprecated(func, name, alternatives) return function (...) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua new file mode 100644 index 00000000..6cd175ba --- /dev/null +++ b/res/scripts/stdmin.lua @@ -0,0 +1,289 @@ +-- Check if given table is an array +function is_array(x) + if #t > 0 then + return true + end + for k, v in pairs(x) do + return false + end + return true +end + +-- Get entry-point and filename from `entry-point:filename` path +function parse_path(path) + local index = string.find(path, ':') + if index == nil then + error("invalid path syntax (':' missing)") + end + return string.sub(path, 1, index-1), string.sub(path, index+1, -1) +end + +function pack.is_installed(packid) + return file.isfile(packid..":package.json") +end + +function pack.data_file(packid, name) + file.mkdirs("world:data/"..packid) + return "world:data/"..packid.."/"..name +end + +function pack.shared_file(packid, name) + file.mkdirs("config:"..packid) + return "config:"..packid.."/"..name +end + + +function timeit(iters, func, ...) + local tm = time.uptime() + for i=1,iters do + func(...) + end + print("[time mcs]", (time.uptime()-tm) * 1000000) +end + +---------------------------------------------- + +function math.clamp(_in, low, high) + return math.min(math.max(_in, low), high) +end + +function math.rand(low, high) + return low + (high - low) * math.random() +end + +---------------------------------------------- + +function table.copy(t) + local copied = {} + + for k, v in pairs(t) do + copied[k] = v + end + + return copied +end + +function table.count_pairs(t) + local count = 0 + + for k, v in pairs(t) do + count = count + 1 + end + + return count +end + +function table.random(t) + return t[math.random(1, #t)] +end + +---------------------------------------------- + +local pattern_escape_replacements = { + ["("] = "%(", + [")"] = "%)", + ["."] = "%.", + ["%"] = "%%", + ["+"] = "%+", + ["-"] = "%-", + ["*"] = "%*", + ["?"] = "%?", + ["["] = "%[", + ["]"] = "%]", + ["^"] = "%^", + ["$"] = "%$", + ["\0"] = "%z" +} + +function string.pattern_safe(str) + return string.gsub(str, ".", pattern_escape_replacements) +end + +local string_sub = string.sub +local string_find = string.find +local string_len = string.len +function string.explode(separator, str, withpattern) + if (withpattern == nil) then withpattern = false end + + local ret = {} + local current_pos = 1 + + for i = 1, string_len(str) do + local start_pos, end_pos = string_find(str, separator, current_pos, not withpattern) + if (not start_pos) then break end + ret[i] = string_sub(str, current_pos, start_pos - 1) + current_pos = end_pos + 1 + end + + ret[#ret + 1] = string_sub(str, current_pos) + + return ret +end + +function string.split(str, delimiter) + return string.explode(delimiter, str) +end + +function string.formatted_time(seconds, format) + if (not seconds) then seconds = 0 end + local hours = math.floor(seconds / 3600) + local minutes = math.floor((seconds / 60) % 60) + local millisecs = (seconds - math.floor(seconds)) * 1000 + seconds = math.floor(seconds % 60) + + if (format) then + return string.format(format, minutes, seconds, millisecs) + else + return { h = hours, m = minutes, s = seconds, ms = millisecs } + end +end + +function string.replace(str, tofind, toreplace) + local tbl = string.Explode(tofind, str) + if (tbl[1]) then return table.concat(tbl, toreplace) end + return str +end + +function string.trim(s, char) + if char then char = string.pattern_safe(char) else char = "%s" end + return string.match(s, "^" .. char .. "*(.-)" .. char .. "*$") or s +end + +function string.trim_right(s, char) + if char then char = string.pattern_safe(char) else char = "%s" end + return string.match(s, "^(.-)" .. char .. "*$") or s +end + +function string.trim_left(s, char) + if char then char = string.pattern_safe(char) else char = "%s" end + return string.match(s, "^" .. char .. "*(.+)$") or s +end + +local meta = getmetatable("") + +function meta:__index(key) + local val = string[key] + if (val ~= nil) then + return val + elseif (tonumber(key)) then + return string.sub(self, key, key) + end +end + +function string.starts_with(str, start) + return string.sub(str, 1, string.len(start)) == start +end + +function string.ends_with(str, endStr) + return endStr == "" or string.sub(str, -string.len(endStr)) == endStr +end + +function table.has(t, x) + for i,v in ipairs(t) do + if v == x then + return true + end + end + return false +end + +function table.index(t, x) + for i,v in ipairs(t) do + if v == x then + return i + end + end + return -1 +end + +function table.remove_value(t, x) + local index = table.index(t, x) + if index ~= -1 then + table.remove(t, index) + end +end + +function table.tostring(t) + local s = '[' + for i,v in ipairs(t) do + s = s..tostring(v) + if i < #t then + s = s..', ' + end + end + return s..']' +end + +function file.readlines(path) + local str = file.read(path) + local lines = {} + for s in str:gmatch("[^\r\n]+") do + table.insert(lines, s) + end + return lines +end + +package = { + loaded={} +} +local __cached_scripts = {} +local __warnings_hidden = {} + +function on_deprecated_call(name, alternatives) + if __warnings_hidden[name] then + return + end + __warnings_hidden[name] = true + if alternatives then + debug.warning("deprecated function called ("..name.."), use ".. + alternatives.." instead\n"..debug.traceback()) + else + debug.warning("deprecated function called ("..name..")\n"..debug.traceback()) + end +end + +-- Load script with caching +-- +-- path - script path `contentpack:filename`. +-- Example `base:scripts/tests.lua` +-- +-- nocache - ignore cached script, load anyway +local function __load_script(path, nocache) + local packname, filename = parse_path(path) + + -- __cached_scripts used in condition because cached result may be nil + if not nocache and __cached_scripts[path] ~= nil then + return package.loaded[path] + end + if not file.isfile(path) then + error("script '"..filename.."' not found in '"..packname.."'") + end + + local script, err = load(file.read(path), path) + if script == nil then + error(err) + end + local result = script() + if not nocache then + __cached_scripts[path] = script + package.loaded[path] = result + end + return result +end + +function require(path) + local prefix, file = parse_path(path) + return __load_script(prefix..":modules/"..file..".lua") +end + +function __scripts_cleanup() + debug.log("cleaning scripts cache") + for k, v in pairs(__cached_scripts) do + local packname, _ = parse_path(k) + if packname ~= "core" then + debug.log("unloaded "..k) + __cached_scripts[k] = nil + package.loaded[k] = nil + end + end +end diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index fbae5756..cd2168b2 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -42,7 +42,7 @@ menu.Contents Menu=Меню контентпаков world.Seed=Зерно world.Name=Название world.World generator=Генератор мира -world.generators.default=Обычный +world.generators.default=По-умолчанию world.generators.flat=Плоский world.Create World=Создать Мир world.convert-request=Есть изменения в индексах! Конвертировать мир? diff --git a/res/textures/blocks/obstacle.png b/res/textures/blocks/obstacle.png new file mode 100644 index 00000000..a15d3bde Binary files /dev/null and b/res/textures/blocks/obstacle.png differ diff --git a/res/textures/blocks/struct_air.png b/res/textures/blocks/struct_air.png new file mode 100644 index 00000000..d5a7de09 Binary files /dev/null and b/res/textures/blocks/struct_air.png differ diff --git a/src/coders/json.cpp b/src/coders/json.cpp index 9c189998..05db4e13 100644 --- a/src/coders/json.cpp +++ b/src/coders/json.cpp @@ -242,7 +242,7 @@ dv::value Parser::parseValue() { } else if (literal == "nan") { return NAN; } - throw error("invalid literal "); + throw error("invalid keyword " + literal); } if (next == '{') { return parseObject(); diff --git a/src/constants.hpp b/src/constants.hpp index da4acfc9..e99c1c7f 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -23,6 +23,8 @@ inline constexpr uint REGION_FORMAT_VERSION = 3; inline constexpr uint MAX_OPEN_REGION_FILES = 32; inline constexpr blockid_t BLOCK_AIR = 0; +inline constexpr blockid_t BLOCK_OBSTACLE = 1; +inline constexpr blockid_t BLOCK_STRUCT_AIR = 2; inline constexpr itemid_t ITEM_EMPTY = 0; inline constexpr entityid_t ENTITY_NONE = 0; diff --git a/src/content/Content.cpp b/src/content/Content.cpp index 677258ad..68e9cec3 100644 --- a/src/content/Content.cpp +++ b/src/content/Content.cpp @@ -10,6 +10,8 @@ #include "objects/EntityDef.hpp" #include "objects/rigging.hpp" #include "voxels/Block.hpp" +#include "world/generator/VoxelFragment.hpp" +#include "world/generator/GeneratorDef.hpp" #include "ContentPack.hpp" ContentIndices::ContentIndices( @@ -28,6 +30,7 @@ Content::Content( ContentUnitDefs blocks, ContentUnitDefs items, ContentUnitDefs entities, + ContentUnitDefs generators, UptrsMap packs, UptrsMap blockMaterials, UptrsMap skeletons, @@ -40,6 +43,7 @@ Content::Content( blocks(std::move(blocks)), items(std::move(items)), entities(std::move(entities)), + generators(std::move(generators)), drawGroups(std::move(drawGroups)) { for (size_t i = 0; i < RESOURCE_TYPES_COUNT; i++) { this->resourceIndices[i] = std::move(resourceIndices[i]); diff --git a/src/content/Content.hpp b/src/content/Content.hpp index 375d0e25..3a836c4e 100644 --- a/src/content/Content.hpp +++ b/src/content/Content.hpp @@ -19,12 +19,13 @@ class Block; struct BlockMaterial; struct ItemDef; struct EntityDef; +struct GeneratorDef; namespace rigging { class SkeletonConfig; } -constexpr const char* contenttype_name(ContentType type) { +constexpr const char* ContentType_name(ContentType type) { switch (type) { case ContentType::NONE: return "none"; @@ -34,6 +35,8 @@ constexpr const char* contenttype_name(ContentType type) { return "item"; case ContentType::ENTITY: return "entity"; + case ContentType::GENERATOR: + return "generator"; default: return "unknown"; } @@ -117,6 +120,10 @@ public: } return *found->second; } + + const auto& getDefs() const { + return defs; + } }; class ResourceIndices { @@ -130,12 +137,21 @@ public: static constexpr size_t MISSING = SIZE_MAX; - void add(std::string name, dv::value map) { + void add(const std::string& name, dv::value map) { indices[name] = names.size(); names.push_back(name); savedData->push_back(std::move(map)); } + void addAlias(const std::string& name, const std::string& alias) { + size_t index = indexOf(name); + if (index == MISSING) { + throw std::runtime_error( + "resource does not exists: "+name); + } + indices[alias] = index; + } + const std::string& getName(size_t index) const { return names.at(index); } @@ -189,6 +205,7 @@ public: ContentUnitDefs blocks; ContentUnitDefs items; ContentUnitDefs entities; + ContentUnitDefs generators; std::unique_ptr const drawGroups; ResourceIndicesSet resourceIndices {}; @@ -198,6 +215,7 @@ public: ContentUnitDefs blocks, ContentUnitDefs items, ContentUnitDefs entities, + ContentUnitDefs generators, UptrsMap packs, UptrsMap blockMaterials, UptrsMap skeletons, diff --git a/src/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index 4bf194bc..30e65911 100644 --- a/src/content/ContentBuilder.cpp +++ b/src/content/ContentBuilder.cpp @@ -72,6 +72,7 @@ std::unique_ptr ContentBuilder::build() { blocks.build(), items.build(), entities.build(), + generators.build(), std::move(packs), std::move(blockMaterials), std::move(skeletons), @@ -81,11 +82,16 @@ std::unique_ptr ContentBuilder::build() { // Now, it's time to resolve foreign keys for (Block* def : blockDefsIndices) { def->rt.pickingItem = content->items.require(def->pickingItem).rt.id; + def->rt.surfaceReplacement = content->blocks.require(def->surfaceReplacement).rt.id; } for (ItemDef* def : itemDefsIndices) { def->rt.placingBlock = content->blocks.require(def->placingBlock).rt.id; } + for (auto& [name, def] : content->generators.getDefs()) { + def->prepare(content.get()); + } + return content; } diff --git a/src/content/ContentBuilder.hpp b/src/content/ContentBuilder.hpp index 7961d1bf..44099304 100644 --- a/src/content/ContentBuilder.hpp +++ b/src/content/ContentBuilder.hpp @@ -8,6 +8,8 @@ #include "ContentPack.hpp" #include "items/ItemDef.hpp" #include "objects/EntityDef.hpp" +#include "world/generator/VoxelFragment.hpp" +#include "world/generator/GeneratorDef.hpp" #include "voxels/Block.hpp" template @@ -67,6 +69,7 @@ public: ContentUnitBuilder blocks {allNames, ContentType::BLOCK}; ContentUnitBuilder items {allNames, ContentType::ITEM}; ContentUnitBuilder entities {allNames, ContentType::ENTITY}; + ContentUnitBuilder generators {allNames, ContentType::GENERATOR}; ResourceIndicesSet resourceIndices {}; ~ContentBuilder(); diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index 8daf1dcb..7a772d84 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -28,8 +28,10 @@ using namespace data; static debug::Logger logger("content-loader"); -ContentLoader::ContentLoader(ContentPack* pack, ContentBuilder& builder) - : pack(pack), builder(builder) { +ContentLoader::ContentLoader( + ContentPack* pack, ContentBuilder& builder, const ResPaths& paths +) + : pack(pack), builder(builder), paths(paths) { auto runtime = std::make_unique( *pack, scripting::create_pack_environment(*pack) ); @@ -51,15 +53,53 @@ static void detect_defs( if (name[0] == '_') { continue; } - if (fs::is_regular_file(file) && file.extension() == ".json") { - detected.push_back(prefix.empty() ? name : prefix + ":" + name); - } else if (fs::is_directory(file)) { + if (fs::is_regular_file(file) && files::is_data_file(file)) { + auto map = files::read_object(file); + std::string id = prefix.empty() ? name : prefix + ":" + name; + detected.emplace_back(id); + } else if (fs::is_directory(file) && + file.extension() != fs::u8path(".files")) { detect_defs(file, name, detected); } } } } +static void detect_defs_pairs( + const fs::path& folder, + const std::string& prefix, + std::vector>& detected +) { + if (fs::is_directory(folder)) { + for (const auto& entry : fs::directory_iterator(folder)) { + const fs::path& file = entry.path(); + std::string name = file.stem().string(); + if (name[0] == '_') { + continue; + } + if (fs::is_regular_file(file) && files::is_data_file(file)) { + auto map = files::read_object(file); + std::string id = prefix.empty() ? name : prefix + ":" + name; + std::string caption = util::id_to_caption(id); + map.at("caption").get(caption); + detected.emplace_back(id, name); + } else if (fs::is_directory(file) && + file.extension() != fs::u8path(".files")) { + detect_defs_pairs(file, name, detected); + } + } + } +} + +std::vector> ContentLoader::scanContent( + const ContentPack& pack, ContentType type +) { + std::vector> detected; + detect_defs_pairs( + pack.folder / ContentPack::getFolderFor(type), pack.id, detected); + return detected; +} + bool ContentLoader::fixPackIndices( const fs::path& folder, dv::value& indicesRoot, @@ -95,14 +135,14 @@ bool ContentLoader::fixPackIndices( void ContentLoader::fixPackIndices() { auto folder = pack->folder; - auto indexFile = pack->getContentFile(); + auto contentFile = pack->getContentFile(); auto blocksFolder = folder / ContentPack::BLOCKS_FOLDER; auto itemsFolder = folder / ContentPack::ITEMS_FOLDER; auto entitiesFolder = folder / ContentPack::ENTITIES_FOLDER; dv::value root; - if (fs::is_regular_file(indexFile)) { - root = files::read_json(indexFile); + if (fs::is_regular_file(contentFile)) { + root = files::read_json(contentFile); } else { root = dv::object(); } @@ -114,7 +154,7 @@ void ContentLoader::fixPackIndices() { if (modified) { // rewrite modified json - files::write_json(indexFile, root); + files::write_json(contentFile, root); } } @@ -277,6 +317,7 @@ void ContentLoader::loadBlock( root.at("hidden").get(def.hidden); root.at("draw-group").get(def.drawGroup); root.at("picking-item").get(def.pickingItem); + root.at("surface-replacement").get(def.surfaceReplacement); root.at("script-name").get(def.scriptName); root.at("ui-layout").get(def.uiLayout); root.at("inventory-size").get(def.inventorySize); @@ -511,6 +552,18 @@ void ContentLoader::loadItem( } } +static std::tuple create_unit_id( + const std::string& packid, const std::string& name +) { + size_t colon = name.find(':'); + if (colon == std::string::npos) { + return {packid, packid + ":" + name, name}; + } + auto otherPackid = name.substr(0, colon); + auto full = otherPackid + ":" + name; + return {otherPackid, full, otherPackid + "/" + name}; +} + void ContentLoader::loadBlockMaterial( BlockMaterial& def, const fs::path& file ) { @@ -520,23 +573,7 @@ void ContentLoader::loadBlockMaterial( root.at("break-sound").get(def.breakSound); } -void ContentLoader::load() { - logger.info() << "loading pack [" << pack->id << "]"; - - fixPackIndices(); - - auto folder = pack->folder; - - fs::path scriptFile = folder / fs::path("scripts/world.lua"); - if (fs::is_regular_file(scriptFile)) { - scripting::load_world_script( - env, pack->id, scriptFile, runtime->worldfuncsset - ); - } - - if (!fs::is_regular_file(pack->getContentFile())) return; - - auto root = files::read_json(pack->getContentFile()); +void ContentLoader::loadContent(const dv::value& root) { std::vector> pendingDefs; auto getJsonParent = [this](const std::string& prefix, const std::string& name) { auto configFile = pack->folder / fs::path(prefix + "/" + name + ".json"); @@ -693,39 +730,53 @@ void ContentLoader::load() { ); } } +} - fs::path materialsDir = folder / fs::u8path("block_materials"); - if (fs::is_directory(materialsDir)) { - for (const auto& entry : fs::directory_iterator(materialsDir)) { - const fs::path& file = entry.path(); - std::string name = pack->id + ":" + file.stem().u8string(); - loadBlockMaterial(builder.createBlockMaterial(name), file); - } - } - - fs::path skeletonsDir = folder / fs::u8path("skeletons"); - if (fs::is_directory(skeletonsDir)) { - for (const auto& entry : fs::directory_iterator(skeletonsDir)) { - const fs::path& file = entry.path(); - std::string name = pack->id + ":" + file.stem().u8string(); - std::string text = files::read_string(file); - builder.add( - rigging::SkeletonConfig::parse(text, file.u8string(), name) - ); - } - } - - fs::path componentsDir = folder / fs::u8path("scripts/components"); - if (fs::is_directory(componentsDir)) { - for (const auto& entry : fs::directory_iterator(componentsDir)) { - fs::path scriptfile = entry.path(); - if (fs::is_regular_file(scriptfile)) { - auto name = pack->id + ":" + scriptfile.stem().u8string(); - scripting::load_entity_component(name, scriptfile); +static inline void foreach_file( + const fs::path& dir, std::function handler +) { + if (fs::is_directory(dir)) { + for (const auto& entry : fs::directory_iterator(dir)) { + const auto& path = entry.path(); + if (fs::is_directory(path)) { + continue; } + handler(path); } } +} +void ContentLoader::load() { + logger.info() << "loading pack [" << pack->id << "]"; + + fixPackIndices(); + + auto folder = pack->folder; + + // Load main world script + fs::path scriptFile = folder / fs::path("scripts/world.lua"); + if (fs::is_regular_file(scriptFile)) { + scripting::load_world_script( + env, pack->id, scriptFile, runtime->worldfuncsset + ); + } + + // Load world generators + fs::path generatorsDir = folder / fs::u8path("generators"); + foreach_file(generatorsDir, [this](const fs::path& file) { + std::string name = file.stem().u8string(); + auto [packid, full, filename] = + create_unit_id(pack->id, file.stem().u8string()); + + auto& def = builder.generators.create(full); + try { + loadGenerator(def, full, name); + } catch (const std::runtime_error& err) { + throw std::runtime_error("generator '"+full+"': "+err.what()); + } + }); + + // Load pack resources.json fs::path resourcesFile = folder / fs::u8path("resources.json"); if (fs::exists(resourcesFile)) { auto resRoot = files::read_json(resourcesFile); @@ -733,10 +784,62 @@ void ContentLoader::load() { if (auto resType = ResourceType_from(key)) { loadResources(*resType, arr); } else { + // Ignore unknown resources logger.warning() << "unknown resource type: " << key; } } } + + // Load pack resources aliases + fs::path aliasesFile = folder / fs::u8path("resource-aliases.json"); + if (fs::exists(aliasesFile)) { + auto resRoot = files::read_json(aliasesFile); + for (const auto& [key, arr] : resRoot.asObject()) { + if (auto resType = ResourceType_from(key)) { + loadResourceAliases(*resType, arr); + } else { + // Ignore unknown resources + logger.warning() << "unknown resource type: " << key; + } + } + } + + // Load block materials + fs::path materialsDir = folder / fs::u8path("block_materials"); + if (fs::is_directory(materialsDir)) { + for (const auto& entry : fs::directory_iterator(materialsDir)) { + const auto& file = entry.path(); + auto [packid, full, filename] = + create_unit_id(pack->id, file.stem().u8string()); + loadBlockMaterial( + builder.createBlockMaterial(full), + materialsDir / fs::u8path(filename + ".json") + ); + } + } + + // Load skeletons + fs::path skeletonsDir = folder / fs::u8path("skeletons"); + foreach_file(skeletonsDir, [this](const fs::path& file) { + std::string name = pack->id + ":" + file.stem().u8string(); + std::string text = files::read_string(file); + builder.add( + rigging::SkeletonConfig::parse(text, file.u8string(), name) + ); + }); + + // Load entity components + fs::path componentsDir = folder / fs::u8path("scripts/components"); + foreach_file(componentsDir, [this](const fs::path& file) { + auto name = pack->id + ":" + file.stem().u8string(); + scripting::load_entity_component(name, file); + }); + + // Process content.json and load defined content units + auto contentFile = pack->getContentFile(); + if (fs::exists(contentFile)) { + loadContent(files::read_json(contentFile)); + } } void ContentLoader::loadResources(ResourceType type, const dv::value& list) { @@ -746,3 +849,11 @@ void ContentLoader::loadResources(ResourceType type, const dv::value& list) { ); } } + +void ContentLoader::loadResourceAliases(ResourceType type, const dv::value& aliases) { + for (const auto& [alias, name] : aliases.asObject()) { + builder.resourceIndices[static_cast(type)].addAlias( + name.asString(), alias + ); + } +} diff --git a/src/content/ContentLoader.hpp b/src/content/ContentLoader.hpp index da5d6fbc..1b011cbb 100644 --- a/src/content/ContentLoader.hpp +++ b/src/content/ContentLoader.hpp @@ -14,7 +14,9 @@ struct BlockMaterial; struct ItemDef; struct EntityDef; struct ContentPack; +struct GeneratorDef; +class ResPaths; class ContentBuilder; class ContentPackRuntime; struct ContentPackStats; @@ -25,6 +27,7 @@ class ContentLoader { scriptenv env; ContentBuilder& builder; ContentPackStats* stats; + const ResPaths& paths; void loadBlock( Block& def, const std::string& full, const std::string& name @@ -35,6 +38,9 @@ class ContentLoader { void loadEntity( EntityDef& def, const std::string& full, const std::string& name ); + void loadGenerator( + GeneratorDef& def, const std::string& full, const std::string& name + ); static void loadCustomBlockModel(Block& def, const dv::value& primitives); static void loadBlockMaterial(BlockMaterial& def, const fs::path& file); @@ -48,14 +54,27 @@ class ContentLoader { EntityDef& def, const std::string& name, const fs::path& file ); void loadResources(ResourceType type, const dv::value& list); -public: - ContentLoader(ContentPack* pack, ContentBuilder& builder); + void loadResourceAliases(ResourceType type, const dv::value& aliases); - bool fixPackIndices( + void loadContent(const dv::value& map); +public: + ContentLoader( + ContentPack* pack, + ContentBuilder& builder, + const ResPaths& paths + ); + + // Refresh pack content.json + static bool fixPackIndices( const fs::path& folder, dv::value& indicesRoot, const std::string& contentSection ); + + static std::vector> scanContent( + const ContentPack& pack, ContentType type + ); + void fixPackIndices(); void load(); }; diff --git a/src/content/ContentPack.hpp b/src/content/ContentPack.hpp index 7815d009..7b7b19e8 100644 --- a/src/content/ContentPack.hpp +++ b/src/content/ContentPack.hpp @@ -6,6 +6,7 @@ #include #include "typedefs.hpp" +#include "content_fwd.hpp" class EnginePaths; @@ -51,6 +52,7 @@ struct ContentPack { static inline const fs::path BLOCKS_FOLDER = "blocks"; static inline const fs::path ITEMS_FOLDER = "items"; static inline const fs::path ENTITIES_FOLDER = "entities"; + static inline const fs::path GENERATORS_FOLDER = "generators"; static const std::vector RESERVED_NAMES; static bool is_pack(const fs::path& folder); @@ -69,6 +71,16 @@ struct ContentPack { ); static ContentPack createCore(const EnginePaths*); + + static inline fs::path getFolderFor(ContentType type) { + switch (type) { + case ContentType::BLOCK: return ContentPack::BLOCKS_FOLDER; + case ContentType::ITEM: return ContentPack::ITEMS_FOLDER; + case ContentType::ENTITY: return ContentPack::ENTITIES_FOLDER; + case ContentType::GENERATOR: return ContentPack::GENERATORS_FOLDER; + case ContentType::NONE: return fs::u8path(""); + } + } }; struct ContentPackStats { diff --git a/src/content/content_fwd.hpp b/src/content/content_fwd.hpp index b1618593..076a433f 100644 --- a/src/content/content_fwd.hpp +++ b/src/content/content_fwd.hpp @@ -5,7 +5,7 @@ class Content; class ContentPackRuntime; -enum class ContentType { NONE, BLOCK, ITEM, ENTITY }; +enum class ContentType { NONE, BLOCK, ITEM, ENTITY, GENERATOR }; enum class ResourceType : size_t { CAMERA, LAST = CAMERA }; diff --git a/src/content/loading/GeneratorLoader.cpp b/src/content/loading/GeneratorLoader.cpp new file mode 100644 index 00000000..fcbcebc5 --- /dev/null +++ b/src/content/loading/GeneratorLoader.cpp @@ -0,0 +1,225 @@ +#include "../ContentLoader.hpp" + +#include "../ContentPack.hpp" + +#include "files/files.hpp" +#include "files/engine_paths.hpp" +#include "logic/scripting/scripting.hpp" +#include "world/generator/GeneratorDef.hpp" +#include "world/generator/VoxelFragment.hpp" +#include "debug/Logger.hpp" +#include "util/stringutil.hpp" + +static BlocksLayer load_layer( + const dv::value& map, uint& lastLayersHeight, bool& hasResizeableLayer +) { + const auto& name = map["block"].asString(); + int height = map["height"].asInteger(); + bool belowSeaLevel = true; + map.at("below-sea-level").get(belowSeaLevel); + + if (hasResizeableLayer) { + lastLayersHeight += height; + } + if (height == -1) { + if (hasResizeableLayer) { + throw std::runtime_error("only one resizeable layer allowed"); + } + hasResizeableLayer = true; + } + return BlocksLayer {name, height, belowSeaLevel, {}}; +} + +static inline BlocksLayers load_layers( + const dv::value& layersArr, const std::string& fieldname +) { + uint lastLayersHeight = 0; + bool hasResizeableLayer = false; + std::vector layers; + + for (int i = 0; i < layersArr.size(); i++) { + const auto& layerMap = layersArr[i]; + try { + layers.push_back( + load_layer(layerMap, lastLayersHeight, hasResizeableLayer)); + } catch (const std::runtime_error& err) { + throw std::runtime_error( + fieldname+" #"+std::to_string(i)+": "+err.what()); + } + } + return BlocksLayers {std::move(layers), lastLayersHeight}; +} + +static inline BiomeElementList load_biome_element_list( + const dv::value map, + const std::string& chanceName, + const std::string& arrName, + const std::string& nameName +) { + float chance = 0.0f; + map.at(chanceName).get(chance); + std::vector entries; + if (map.has(arrName)) { + const auto& arr = map[arrName]; + for (const auto& entry : arr) { + const auto& name = entry[nameName].asString(); + float weight = entry["weight"].asNumber(); + if (weight <= 0.0f) { + throw std::runtime_error("weight must be positive"); + } + entries.push_back(WeightedEntry {name, weight, {}}); + } + } + std::sort(entries.begin(), entries.end(), std::greater()); + return BiomeElementList(std::move(entries), chance); +} + +static inline BiomeElementList load_plants(const dv::value& biomeMap) { + return load_biome_element_list(biomeMap, "plant-chance", "plants", "block"); +} + +static inline BiomeElementList load_structures(const dv::value map) { + return load_biome_element_list(map, "structure-chance", "structures", "name"); +} + +static debug::Logger logger("generator-loader"); + +static inline Biome load_biome( + const dv::value& biomeMap, + const std::string& name, + uint parametersCount +) { + std::vector parameters; + + const auto& paramsArr = biomeMap["parameters"]; + if (paramsArr.size() < parametersCount) { + throw std::runtime_error( + std::to_string(parametersCount)+" parameters expected"); + } + for (size_t i = 0; i < parametersCount; i++) { + const auto& paramMap = paramsArr[i]; + float value = paramMap["value"].asNumber(); + float weight = paramMap["weight"].asNumber(); + parameters.push_back(BiomeParameter {value, weight}); + } + + auto plants = load_plants(biomeMap); + auto groundLayers = load_layers(biomeMap["layers"], "layers"); + auto seaLayers = load_layers(biomeMap["sea-layers"], "sea-layers"); + + BiomeElementList structures; + if (biomeMap.has("structures")) { + structures = load_structures(biomeMap); + } + return Biome { + name, + std::move(parameters), + std::move(plants), + std::move(structures), + std::move(groundLayers), + std::move(seaLayers)}; +} + +static VoxelStructureMeta load_structure_meta( + const std::string& name, const dv::value& config +) { + VoxelStructureMeta meta; + meta.name = name; + + return meta; +} + +static std::vector> load_structures( + const fs::path& structuresFile +) { + auto structuresDir = structuresFile.parent_path() / fs::path("fragments"); + auto map = files::read_object(structuresFile); + + std::vector> structures; + for (auto& [name, config] : map.asObject()) { + auto structFile = structuresDir / fs::u8path(name + ".vox"); + logger.debug() << "loading voxel fragment " << structFile.u8string(); + if (!fs::exists(structFile)) { + throw std::runtime_error("structure file does not exist (" + + structFile.u8string()); + } + auto fragment = std::make_unique(); + fragment->deserialize(files::read_binary_json(structFile)); + logger.info() << "fragment " << name << " has size [" << + fragment->getSize().x << ", " << fragment->getSize().y << ", " << + fragment->getSize().z << "]"; + + structures.push_back(std::make_unique( + load_structure_meta(name, config), + std::move(fragment) + )); + } + return structures; +} + +static void load_structures(GeneratorDef& def, const fs::path& structuresFile) { + auto rawStructures = load_structures(structuresFile); + def.structures.resize(rawStructures.size()); + + for (int i = 0; i < rawStructures.size(); i++) { + def.structures[i] = std::move(rawStructures[i]); + } + // build indices map + for (size_t i = 0; i < def.structures.size(); i++) { + auto& structure = def.structures[i]; + def.structuresIndices[structure->meta.name] = i; + } +} + +static inline const auto STRUCTURES_FILE = fs::u8path("structures.toml"); +static inline const auto BIOMES_FILE = fs::u8path("biomes.toml"); +static inline const auto GENERATORS_DIR = fs::u8path("generators"); + +static void load_biomes(GeneratorDef& def, const dv::value& root) { + for (const auto& [biomeName, biomeMap] : root.asObject()) { + try { + def.biomes.push_back( + load_biome(biomeMap, biomeName, def.biomeParameters)); + } catch (const std::runtime_error& err) { + throw std::runtime_error("biome "+biomeName+": "+err.what()); + } + } +} + +void ContentLoader::loadGenerator( + GeneratorDef& def, const std::string& full, const std::string& name +) { + auto packDir = pack->folder; + auto generatorsDir = packDir / GENERATORS_DIR; + auto generatorFile = generatorsDir / fs::u8path(name + ".toml"); + if (!fs::exists(generatorFile)) { + return; + } + auto map = files::read_toml(generatorsDir / fs::u8path(name + ".toml")); + map.at("caption").get(def.caption); + map.at("biome-parameters").get(def.biomeParameters); + map.at("biome-bpd").get(def.biomesBPD); + map.at("heights-bpd").get(def.heightsBPD); + map.at("sea-level").get(def.seaLevel); + map.at("wide-structs-chunks-radius").get(def.wideStructsChunksRadius); + + auto folder = generatorsDir / fs::u8path(name + ".files"); + auto scriptFile = folder / fs::u8path("script.lua"); + + auto structuresFile = folder / STRUCTURES_FILE; + if (fs::exists(structuresFile)) { + load_structures(def, structuresFile); + } + + auto biomesFile = GENERATORS_DIR / fs::u8path(name + ".files") / BIOMES_FILE; + auto biomesMap = paths.readCombinedObject(biomesFile.u8string()); + if (biomesMap.empty()) { + throw std::runtime_error( + "generator " + util::quote(def.name) + + ": at least one biome required" + ); + } + load_biomes(def, biomesMap); + def.script = scripting::load_generator( + def, scriptFile, pack->id+":generators/"+name+".files"); +} diff --git a/src/core_defs.cpp b/src/core_defs.cpp index cf8dc1f4..7d19acd1 100644 --- a/src/core_defs.cpp +++ b/src/core_defs.cpp @@ -12,18 +12,21 @@ // All in-game definitions (blocks, items, etc..) void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) { - Block& block = builder->blocks.create("core:air"); - block.replaceable = true; - block.drawGroup = 1; - block.lightPassing = true; - block.skyLightPassing = true; - block.obstacle = false; - block.selectable = false; - block.model = BlockModel::none; - block.pickingItem = "core:empty"; - - ItemDef& item = builder->items.create("core:empty"); - item.iconType = item_icon_type::none; + { + Block& block = builder->blocks.create(CORE_AIR); + block.replaceable = true; + block.drawGroup = 1; + block.lightPassing = true; + block.skyLightPassing = true; + block.obstacle = false; + block.selectable = false; + block.model = BlockModel::none; + block.pickingItem = CORE_EMPTY; + } + { + ItemDef& item = builder->items.create(CORE_EMPTY); + item.iconType = item_icon_type::none; + } auto bindsFile = paths->getResourcesFolder()/fs::path("bindings.toml"); if (fs::is_regular_file(bindsFile)) { @@ -31,4 +34,34 @@ void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) { bindsFile.u8string(), files::read_string(bindsFile) ); } + + { + Block& block = builder->blocks.create(CORE_OBSTACLE); + for (uint i = 0; i < 6; i++) { + block.textureFaces[i] = "obstacle"; + } + block.hitboxes = {AABB()}; + block.breakable = false; + ItemDef& item = builder->items.create(CORE_OBSTACLE+".item"); + item.iconType = item_icon_type::block; + item.icon = CORE_OBSTACLE; + item.placingBlock = CORE_OBSTACLE; + item.caption = block.caption; + } + { + Block& block = builder->blocks.create(CORE_STRUCT_AIR); + for (uint i = 0; i < 6; i++) { + block.textureFaces[i] = "struct_air"; + } + block.drawGroup = -1; + block.skyLightPassing = true; + block.lightPassing = true; + block.hitboxes = {AABB()}; + block.obstacle = false; + ItemDef& item = builder->items.create(CORE_STRUCT_AIR+".item"); + item.iconType = item_icon_type::block; + item.icon = CORE_STRUCT_AIR; + item.placingBlock = CORE_STRUCT_AIR; + item.caption = block.caption; + } } diff --git a/src/core_defs.hpp b/src/core_defs.hpp index 32b2424c..0f25de3a 100644 --- a/src/core_defs.hpp +++ b/src/core_defs.hpp @@ -4,6 +4,8 @@ inline const std::string CORE_EMPTY = "core:empty"; inline const std::string CORE_AIR = "core:air"; +inline const std::string CORE_OBSTACLE = "core:obstacle"; +inline const std::string CORE_STRUCT_AIR = "core:struct_air"; inline const std::string TEXTURE_NOTFOUND = "notfound"; diff --git a/src/data/dv.hpp b/src/data/dv.hpp index 94d96453..8070849f 100644 --- a/src/data/dv.hpp +++ b/src/data/dv.hpp @@ -505,6 +505,9 @@ namespace dv { inline bool isNumber() const noexcept { return type == value_type::number; } + inline bool isBoolean() const noexcept { + return type == value_type::boolean; + } }; inline bool is_numeric(const value& val) { diff --git a/src/data/dv_util.hpp b/src/data/dv_util.hpp index 4a6bf033..e016edb0 100644 --- a/src/data/dv_util.hpp +++ b/src/data/dv_util.hpp @@ -5,8 +5,8 @@ #include namespace dv { - template - inline dv::value to_value(glm::vec vec) { + template + inline dv::value to_value(glm::vec vec) { auto list = dv::list(); for (size_t i = 0; i < n; i++) { list.add(vec[i]); @@ -14,8 +14,8 @@ namespace dv { return list; } - template - inline dv::value to_value(glm::mat mat) { + template + inline dv::value to_value(glm::mat mat) { auto list = dv::list(); for (size_t i = 0; i < n; i++) { for (size_t j = 0; j < m; j++) { @@ -32,14 +32,18 @@ namespace dv { } } - template - void get_vec(const dv::value& map, const std::string& key, glm::vec& vec) { + template + void get_vec(const dv::value& map, const std::string& key, glm::vec& vec) { if (!map.has(key)) { return; } auto& list = map[key]; for (size_t i = 0; i < n; i++) { - vec[i] = list[i].asNumber(); + if constexpr (std::is_floating_point()) { + vec[i] = list[i].asNumber(); + } else { + vec[i] = list[i].asInteger(); + } } } diff --git a/src/engine.cpp b/src/engine.cpp index 8a3b3147..6ba4fcee 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -31,13 +31,10 @@ #include "logic/scripting/scripting.hpp" #include "util/listutil.hpp" #include "util/platform.hpp" -#include "voxels/DefaultWorldGenerator.hpp" -#include "voxels/FlatWorldGenerator.hpp" #include "window/Camera.hpp" #include "window/Events.hpp" #include "window/input.hpp" #include "window/Window.hpp" -#include "world/WorldGenerators.hpp" #include "settings.hpp" #include @@ -51,11 +48,6 @@ static debug::Logger logger("engine"); namespace fs = std::filesystem; -static void add_world_generators() { - WorldGenerators::addGenerator("core:default"); - WorldGenerators::addGenerator("core:flat"); -} - static void create_channel(Engine* engine, std::string name, NumberSetting& setting) { if (name != "master") { audio::create_channel(name); @@ -115,7 +107,6 @@ Engine::Engine(EngineSettings& settings, SettingsHandler& settingsHandler, Engin keepAlive(settings.ui.language.observe([=](auto lang) { setLanguage(lang); }, true)); - add_world_generators(); scripting::initialize(this); basePacks = files::read_list(resdir/fs::path("config/builtins.list")); @@ -318,21 +309,28 @@ void Engine::loadContent() { names = manager.assembly(names); contentPacks = manager.getAll(names); - std::vector resRoots; - { - auto pack = ContentPack::createCore(paths); - resRoots.push_back({"core", pack.folder}); - ContentLoader(&pack, contentBuilder).load(); - load_configs(pack.folder); - } + auto corePack = ContentPack::createCore(paths); + + // Setup filesystem entry points + std::vector resRoots { + {"core", corePack.folder} + }; for (auto& pack : contentPacks) { resRoots.push_back({pack.id, pack.folder}); - ContentLoader(&pack, contentBuilder).load(); + } + resPaths = std::make_unique(resdir, resRoots); + + // Load content + { + ContentLoader(&corePack, contentBuilder, *resPaths).load(); + load_configs(corePack.folder); + } + for (auto& pack : contentPacks) { + ContentLoader(&pack, contentBuilder, *resPaths).load(); load_configs(pack.folder); } content = contentBuilder.build(); - resPaths = std::make_unique(resdir, resRoots); langs::setup(resdir, langs::current->getId(), contentPacks); loadAssets(); @@ -347,6 +345,11 @@ void Engine::resetContent() { resRoots.push_back({"core", pack.folder}); load_configs(pack.folder); } + auto manager = createPacksManager(fs::path()); + manager.scan(); + for (const auto& pack : manager.getAll(basePacks)) { + resRoots.push_back({pack.id, pack.folder}); + } resPaths = std::make_unique(resdir, resRoots); contentPacks.clear(); content.reset(); @@ -355,8 +358,6 @@ void Engine::resetContent() { loadAssets(); onAssetsLoaded(); - auto manager = createPacksManager(fs::path()); - manager.scan(); contentPacks = manager.getAll(basePacks); } @@ -413,6 +414,12 @@ const Content* Engine::getContent() const { return content.get(); } +std::vector Engine::getAllContentPacks() { + auto packs = getContentPacks(); + packs.insert(packs.begin(), ContentPack::createCore(paths)); + return packs; +} + std::vector& Engine::getContentPacks() { return contentPacks; } diff --git a/src/engine.hpp b/src/engine.hpp index de25c220..75b9a1b7 100644 --- a/src/engine.hpp +++ b/src/engine.hpp @@ -130,6 +130,8 @@ public: /// @brief Get selected content packs std::vector& getContentPacks(); + std::vector getAllContentPacks(); + std::vector& getBasePacks(); /// @brief Get current screen diff --git a/src/files/WorldRegions.cpp b/src/files/WorldRegions.cpp index a96fc61b..0b26f011 100644 --- a/src/files/WorldRegions.cpp +++ b/src/files/WorldRegions.cpp @@ -5,6 +5,7 @@ #include #include "debug/Logger.hpp" +#include "coders/json.hpp" #include "coders/byte_utils.hpp" #include "coders/rle.hpp" #include "coders/binary_json.hpp" diff --git a/src/files/engine_paths.cpp b/src/files/engine_paths.cpp index f24e8515..11d88bca 100644 --- a/src/files/engine_paths.cpp +++ b/src/files/engine_paths.cpp @@ -10,11 +10,15 @@ #include #include "WorldFiles.hpp" +#include "debug/Logger.hpp" + +static debug::Logger logger("engine-paths"); static inline auto SCREENSHOTS_FOLDER = std::filesystem::u8path("screenshots"); static inline auto CONTENT_FOLDER = std::filesystem::u8path("content"); static inline auto WORLDS_FOLDER = std::filesystem::u8path("worlds"); static inline auto CONFIG_FOLDER = std::filesystem::u8path("config"); +static inline auto EXPORT_FOLDER = std::filesystem::u8path("export"); static inline auto CONTROLS_FILE = std::filesystem::u8path("controls.toml"); static inline auto SETTINGS_FILE = std::filesystem::u8path("settings.toml"); @@ -48,6 +52,10 @@ void EnginePaths::prepare() { if (!fs::is_directory(contentFolder)) { fs::create_directories(contentFolder); } + auto exportFolder = userFilesFolder / EXPORT_FOLDER; + if (!fs::is_directory(exportFolder)) { + fs::create_directories(exportFolder); + } } std::filesystem::path EnginePaths::getUserFilesFolder() const { @@ -153,15 +161,23 @@ void EnginePaths::setContentPacks(std::vector* contentPacks) { this->contentPacks = contentPacks; } +std::tuple EnginePaths::parsePath(std::string_view path) { + size_t separator = path.find(':'); + if (separator == std::string::npos) { + return {"", std::string(path)}; + } + auto prefix = std::string(path.substr(0, separator)); + auto filename = std::string(path.substr(separator + 1)); + return {prefix, filename}; +} + std::filesystem::path EnginePaths::resolve( const std::string& path, bool throwErr ) { - size_t separator = path.find(':'); - if (separator == std::string::npos) { + auto [prefix, filename] = EnginePaths::parsePath(path); + if (prefix.empty()) { throw files_access_error("no entry point specified"); } - std::string prefix = path.substr(0, separator); - std::string filename = path.substr(separator + 1); filename = toCanonic(fs::u8path(filename)).u8string(); if (prefix == "res" || prefix == "core") { @@ -176,6 +192,9 @@ std::filesystem::path EnginePaths::resolve( if (prefix == "world") { return currentWorldFolder / fs::u8path(filename); } + if (prefix == "export") { + return userFilesFolder / EXPORT_FOLDER / fs::u8path(filename); + } if (contentPacks) { for (auto& pack : *contentPacks) { @@ -244,6 +263,56 @@ std::vector ResPaths::listdir( return entries; } +dv::value ResPaths::readCombinedList(const std::string& filename) const { + dv::value list = dv::list(); + for (const auto& root : roots) { + auto path = root.path / fs::u8path(filename); + if (!fs::exists(path)) { + continue; + } + try { + auto value = files::read_object(path); + if (!value.isList()) { + logger.warning() << "reading combined list " << root.name << ":" + << filename << " is not a list (skipped)"; + continue; + } + for (const auto& elem : value) { + list.add(elem); + } + } catch (const std::runtime_error& err) { + logger.warning() << "reading combined list " << root.name << ":" + << filename << ": " << err.what(); + } + } + return list; +} + +dv::value ResPaths::readCombinedObject(const std::string& filename) const { + dv::value object = dv::object(); + for (const auto& root : roots) { + auto path = root.path / fs::u8path(filename); + if (!fs::exists(path)) { + continue; + } + try { + auto value = files::read_object(path); + if (!value.isObject()) { + logger.warning() + << "reading combined object " << root.name << ": " + << filename << " is not an object (skipped)"; + } + for (const auto& [key, element] : value.asObject()) { + object[key] = element; + } + } catch (const std::runtime_error& err) { + logger.warning() << "reading combined object " << root.name << ":" + << filename << ": " << err.what(); + } + } + return object; +} + const std::filesystem::path& ResPaths::getMainRoot() const { return mainRoot; } diff --git a/src/files/engine_paths.hpp b/src/files/engine_paths.hpp index b70954c3..c5289243 100644 --- a/src/files/engine_paths.hpp +++ b/src/files/engine_paths.hpp @@ -4,7 +4,9 @@ #include #include #include +#include +#include "data/dv.hpp" #include "content/ContentPack.hpp" @@ -41,6 +43,10 @@ public: std::filesystem::path resolve(const std::string& path, bool throwErr = true); + static std::tuple parsePath(std::string_view view); + + static inline auto CONFIG_DEFAULTS = + std::filesystem::u8path("config/defaults.toml"); private: std::filesystem::path userFilesFolder {"."}; std::filesystem::path resourcesFolder {"res"}; @@ -62,6 +68,13 @@ public: std::vector listdir(const std::string& folder) const; std::vector listdirRaw(const std::string& folder) const; + /// @brief Read all found list versions from all packs and combine into a + /// single list. Invalid versions will be skipped with logging a warning + /// @param file *.json file path relative to entry point + dv::value readCombinedList(const std::string& file) const; + + dv::value readCombinedObject(const std::string& file) const; + const std::filesystem::path& getMainRoot() const; private: diff --git a/src/files/files.cpp b/src/files/files.cpp index 5a84b27f..8be834b5 100644 --- a/src/files/files.cpp +++ b/src/files/files.cpp @@ -165,3 +165,36 @@ std::vector files::read_list(const fs::path& filename) { } return lines; } + +#include + +#include "coders/json.hpp" +#include "coders/toml.hpp" + +using DecodeFunc = dv::value(*)(std::string_view, std::string_view); + +static std::map data_decoders { + {fs::u8path(".json"), json::parse}, + {fs::u8path(".toml"), toml::parse}, +}; + +bool files::is_data_file(const fs::path& file) { + return is_data_interchange_format(file.extension()); +} + +bool files::is_data_interchange_format(const fs::path& ext) { + return data_decoders.find(ext) != data_decoders.end(); +} + +dv::value files::read_object(const fs::path& file) { + const auto& found = data_decoders.find(file.extension()); + if (found == data_decoders.end()) { + throw std::runtime_error("unknown file format"); + } + auto text = read_string(file); + try { + return found->second(file.u8string(), text); + } catch (const parsing_error& err) { + throw std::runtime_error(err.errorLog()); + } +} diff --git a/src/files/files.hpp b/src/files/files.hpp index 5055e5a9..99ccc936 100644 --- a/src/files/files.hpp +++ b/src/files/files.hpp @@ -65,7 +65,16 @@ namespace files { /// @brief Read JSON or BJSON file /// @param file *.json or *.bjson file dv::value read_json(const fs::path& file); + dv::value read_binary_json(const fs::path& file); + + /// @brief Read TOML file + /// @param file *.toml file dv::value read_toml(const fs::path& file); + std::vector read_list(const fs::path& file); + + bool is_data_file(const fs::path& file); + bool is_data_interchange_format(const fs::path& ext); + dv::value read_object(const fs::path& file); } diff --git a/src/files/util.hpp b/src/files/util.hpp new file mode 100644 index 00000000..5967299e --- /dev/null +++ b/src/files/util.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +namespace files { + inline bool is_valid_name(std::string_view name) { + static std::string illegalChars = "\\/%?!<>:; "; + for (char c : illegalChars) { + if (name.find(c) != std::string::npos) { + return false; + } + } + return !name.empty(); + } +} diff --git a/src/frontend/debug_panel.cpp b/src/frontend/debug_panel.cpp index fa827724..c16dd5dd 100644 --- a/src/frontend/debug_panel.cpp +++ b/src/frontend/debug_panel.cpp @@ -84,7 +84,7 @@ std::shared_ptr create_debug_panel( return L"frustum-culling: "+std::wstring(culling ? L"on" : L"off"); })); panel->add(create_label([=]() { - return L"chunks: "+std::to_wstring(level->chunks->chunksCount)+ + return L"chunks: "+std::to_wstring(level->chunks->getChunksCount())+ L" visible: "+std::to_wstring(level->chunks->visible); })); panel->add(create_label([=]() { diff --git a/src/frontend/hud.cpp b/src/frontend/hud.cpp index 925627e7..54194dc5 100644 --- a/src/frontend/hud.cpp +++ b/src/frontend/hud.cpp @@ -17,6 +17,7 @@ #include "graphics/core/Mesh.hpp" #include "graphics/core/Shader.hpp" #include "graphics/core/Texture.hpp" +#include "graphics/core/ImageData.hpp" #include "graphics/render/WorldRenderer.hpp" #include "graphics/ui/elements/InventoryView.hpp" #include "graphics/ui/elements/Menu.hpp" @@ -29,6 +30,8 @@ #include "items/Inventory.hpp" #include "items/ItemDef.hpp" #include "logic/scripting/scripting.hpp" +#include "logic/LevelController.hpp" +#include "world/generator/WorldGenerator.hpp" #include "maths/voxmaths.hpp" #include "objects/Player.hpp" #include "physics/Hitbox.hpp" @@ -37,6 +40,7 @@ #include "voxels/Block.hpp" #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" +#include "voxels/ChunksStorage.hpp" #include "window/Camera.hpp" #include "window/Events.hpp" #include "window/input.hpp" @@ -140,12 +144,16 @@ std::shared_ptr Hud::createHotbar() { return view; } -Hud::Hud(Engine* engine, LevelFrontend* frontend, Player* player) - : assets(engine->getAssets()), - gui(engine->getGUI()), - frontend(frontend), - player(player) -{ +static constexpr uint WORLDGEN_IMG_SIZE = 128U; + +Hud::Hud(Engine* engine, LevelFrontend* frontend, Player* player) + : assets(engine->getAssets()), + gui(engine->getGUI()), + frontend(frontend), + player(player), + debugImgWorldGen(std::make_unique( + ImageFormat::rgba8888, WORLDGEN_IMG_SIZE, WORLDGEN_IMG_SIZE + )) { contentAccess = createContentAccess(); contentAccess->setId("hud.content-access"); contentAccessPanel = std::make_shared( @@ -177,6 +185,14 @@ Hud::Hud(Engine* engine, LevelFrontend* frontend, Player* player) dplotter->setGravity(Gravity::bottom_right); dplotter->setInteractive(false); add(HudElement(hud_element_mode::permanent, nullptr, dplotter, true)); + + assets->store(Texture::from(debugImgWorldGen.get()), DEBUG_WORLDGEN_IMAGE); + + add(HudElement(hud_element_mode::permanent, nullptr, + guiutil::create( + "" + ), true)); } Hud::~Hud() { @@ -250,6 +266,53 @@ void Hud::updateHotbarControl() { } } +void Hud::updateWorldGenDebugVisualization() { + auto level = frontend->getLevel(); + auto generator = + frontend->getController()->getChunksController()->getGenerator(); + auto debugInfo = generator->createDebugInfo(); + + int width = debugImgWorldGen->getWidth(); + int height = debugImgWorldGen->getHeight(); + ubyte* data = debugImgWorldGen->getData(); + + int ox = debugInfo.areaOffsetX; + int oz = debugInfo.areaOffsetY; + + int areaWidth = debugInfo.areaWidth; + int areaHeight = debugInfo.areaHeight; + + for (int z = 0; z < height; z++) { + for (int x = 0; x < width; x++) { + int cx = x + ox; + int cz = z + oz; + + int ax = x - (width - areaWidth) / 2; + int az = z - (height - areaHeight) / 2; + + data[(z * width + x) * 4 + 1] = + level->chunks->getChunk(ax + ox, az + oz) ? 255 : 0; + data[(z * width + x) * 4 + 0] = + level->chunksStorage->get(ax + ox, az + oz) ? 255 : 0; + + if (ax < 0 || az < 0 || + ax >= areaWidth || az >= areaHeight) { + data[(z * width + x) * 4 + 2] = 0; + data[(z * width + x) * 4 + 3] = 0; + data[(z * width + x) * 4 + 3] = 100; + continue; + } + auto value = debugInfo.areaLevels[az * areaWidth + ax] * 25; + + // Chunk is already generated + data[(z * width + x) * 4 + 2] = value; + data[(z * width + x) * 4 + 3] = 150; + } + } + auto texture = assets->get(DEBUG_WORLDGEN_IMAGE); + texture->reload(*debugImgWorldGen); +} + void Hud::update(bool visible) { auto level = frontend->getLevel(); auto menu = gui->getMenu(); @@ -296,6 +359,10 @@ void Hud::update(bool visible) { } } cleanup(); + + if (player->debug) { + updateWorldGenDebugVisualization(); + } } /// @brief Show inventory on the screen and turn on inventory mode blocking movement diff --git a/src/frontend/hud.hpp b/src/frontend/hud.hpp index 26d0d9d3..05e93e32 100644 --- a/src/frontend/hud.hpp +++ b/src/frontend/hud.hpp @@ -18,6 +18,7 @@ class LevelFrontend; class UiDocument; class DrawContext; class Viewport; +class ImageData; namespace gui { class GUI; @@ -107,6 +108,8 @@ class Hud : public util::ObjectsKeeper { /// @brief UI element will be dynamicly positioned near to inventory or in screen center std::shared_ptr secondUI = nullptr; + + std::unique_ptr debugImgWorldGen; std::shared_ptr createContentAccess(); std::shared_ptr createHotbar(); @@ -117,6 +120,7 @@ class Hud : public util::ObjectsKeeper { void cleanup(); void showExchangeSlot(); + void updateWorldGenDebugVisualization(); public: Hud(Engine* engine, LevelFrontend* frontend, Player* player); ~Hud(); @@ -167,4 +171,7 @@ public: Player* getPlayer() const; std::shared_ptr getBlockInventory(); + + /// @brief Runtime updating debug visualization texture + inline static std::string DEBUG_WORLDGEN_IMAGE = "#debug.img.worldgen"; }; diff --git a/src/graphics/core/GLTexture.cpp b/src/graphics/core/GLTexture.cpp index 9a47ef22..9572f415 100644 --- a/src/graphics/core/GLTexture.cpp +++ b/src/graphics/core/GLTexture.cpp @@ -41,7 +41,13 @@ void GLTexture::unbind() { glBindTexture(GL_TEXTURE_2D, 0); } -void GLTexture::reload(const ubyte* data){ +void GLTexture::reload(const ImageData& image) { + width = image.getWidth(); + height = image.getHeight(); + reload(image.getData()); +} + +void GLTexture::reload(const ubyte* data) { glBindTexture(GL_TEXTURE_2D, id); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, static_cast(data)); diff --git a/src/graphics/core/GLTexture.hpp b/src/graphics/core/GLTexture.hpp index 10e002e9..7f9e8f22 100644 --- a/src/graphics/core/GLTexture.hpp +++ b/src/graphics/core/GLTexture.hpp @@ -16,6 +16,8 @@ public: void setNearestFilter(); + virtual void reload(const ImageData& image) override; + virtual std::unique_ptr readData() override; virtual uint getId() const override; diff --git a/src/graphics/core/Texture.hpp b/src/graphics/core/Texture.hpp index 9e16f670..4b3f1651 100644 --- a/src/graphics/core/Texture.hpp +++ b/src/graphics/core/Texture.hpp @@ -20,6 +20,8 @@ public: virtual void bind() = 0; virtual void unbind() = 0; + virtual void reload(const ImageData& image) = 0; + virtual std::unique_ptr readData() = 0; virtual uint getWidth() const { diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 5c47d8ee..78cfba29 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -80,7 +80,7 @@ WorldRenderer::~WorldRenderer() = default; bool WorldRenderer::drawChunk( size_t index, Camera* camera, Shader* shader, bool culling ) { - auto chunk = level->chunks->chunks[index]; + auto chunk = level->chunks->getChunks()[index]; if (!chunk->flags.lighted) { return false; } @@ -123,15 +123,16 @@ void WorldRenderer::drawChunks(Chunks* chunks, Camera* camera, Shader* shader) { // [warning] this whole method is not thread-safe for chunks std::vector indices; - for (size_t i = 0; i < chunks->volume; i++) { - if (chunks->chunks[i] == nullptr) continue; + for (size_t i = 0; i < chunks->getVolume(); i++) { + if (chunks->getChunks()[i] == nullptr) continue; indices.emplace_back(i); } float px = camera->position.x / static_cast(CHUNK_W) - 0.5f; float pz = camera->position.z / static_cast(CHUNK_D) - 0.5f; std::sort(indices.begin(), indices.end(), [chunks, px, pz](auto i, auto j) { - const auto a = chunks->chunks[i].get(); - const auto b = chunks->chunks[j].get(); + const auto& chunksBuffer = chunks->getChunks(); + const auto a = chunksBuffer[i].get(); + const auto b = chunksBuffer[j].get(); auto adx = (a->x - px); auto adz = (a->z - pz); auto bdx = (b->x - px); diff --git a/src/items/Inventories.hpp b/src/items/Inventories.hpp index aed3e090..1af4ec8a 100644 --- a/src/items/Inventories.hpp +++ b/src/items/Inventories.hpp @@ -15,7 +15,7 @@ using inventories_map = std::unordered_map>; class Inventories { Level& level; inventories_map map; - PseudoRandom random; + util::PseudoRandom random; public: Inventories(Level& level); ~Inventories(); diff --git a/src/items/Inventory.cpp b/src/items/Inventory.cpp index dd869a93..92f925ef 100644 --- a/src/items/Inventory.cpp +++ b/src/items/Inventory.cpp @@ -91,17 +91,11 @@ void Inventory::convert(const ContentReport* report) { } } -// TODO: remove void Inventory::convert(dv::value& data, const ContentReport* report) { - auto& slotsarr = data["slots"]; - for (auto& item : data["slots"]) { - itemid_t id = item["id"].asInteger(ITEM_EMPTY); - itemid_t replacement = report->items.getId(id); - item["id"] = replacement; - if (replacement == 0 && item.has("count")) { - item.erase("count"); - } - } + Inventory inventory; + inventory.deserialize(data); + inventory.convert(report); + data = inventory.serialize(); } const size_t Inventory::npos = -1; diff --git a/src/items/Inventory.hpp b/src/items/Inventory.hpp index 95d0936a..e12029f8 100644 --- a/src/items/Inventory.hpp +++ b/src/items/Inventory.hpp @@ -14,6 +14,8 @@ class Inventory : public Serializable { int64_t id; std::vector slots; public: + Inventory() = default; + Inventory(int64_t id, size_t size); Inventory(const Inventory& orig); diff --git a/src/lighting/Lighting.cpp b/src/lighting/Lighting.cpp index df1e358d..6aad3049 100644 --- a/src/lighting/Lighting.cpp +++ b/src/lighting/Lighting.cpp @@ -23,8 +23,9 @@ Lighting::Lighting(const Content* content, Chunks* chunks) Lighting::~Lighting() = default; void Lighting::clear(){ - for (size_t index = 0; index < chunks->volume; index++){ - auto chunk = chunks->chunks[index]; + const auto& chunks = this->chunks->getChunks(); + for (size_t index = 0; index < chunks.size(); index++){ + auto chunk = chunks[index]; if (chunk == nullptr) continue; Lightmap& lightmap = chunk->lightmap; diff --git a/src/logic/BlocksController.cpp b/src/logic/BlocksController.cpp index 570c1f46..32802780 100644 --- a/src/logic/BlocksController.cpp +++ b/src/logic/BlocksController.cpp @@ -124,17 +124,17 @@ void BlocksController::randomTick( void BlocksController::randomTick(int tickid, int parts) { auto indices = level->content->getIndices(); - const int w = chunks->w; - const int d = chunks->d; + int width = chunks->getWidth(); + int height = chunks->getHeight(); int segments = 4; - for (uint z = padding; z < d - padding; z++) { - for (uint x = padding; x < w - padding; x++) { - int index = z * w + x; + for (uint z = padding; z < height - padding; z++) { + for (uint x = padding; x < width - padding; x++) { + int index = z * width + x; if ((index + tickid) % parts != 0) { continue; } - auto& chunk = chunks->chunks[index]; + auto& chunk = chunks->getChunks()[index]; if (chunk == nullptr || !chunk->flags.lighted) { continue; } diff --git a/src/logic/ChunksController.cpp b/src/logic/ChunksController.cpp index 66d9f539..07492f29 100644 --- a/src/logic/ChunksController.cpp +++ b/src/logic/ChunksController.cpp @@ -15,10 +15,9 @@ #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" #include "voxels/ChunksStorage.hpp" -#include "voxels/WorldGenerator.hpp" #include "world/Level.hpp" #include "world/World.hpp" -#include "world/WorldGenerators.hpp" +#include "world/generator/WorldGenerator.hpp" const uint MAX_WORK_PER_FRAME = 128; const uint MIN_SURROUNDING = 9; @@ -28,14 +27,19 @@ ChunksController::ChunksController(Level* level, uint padding) chunks(level->chunks.get()), lighting(level->lighting.get()), padding(padding), - generator(WorldGenerators::createGenerator( - level->getWorld()->getGenerator(), level->content - )) { -} + generator(std::make_unique( + level->content->generators.require(level->getWorld()->getGenerator()), + level->content, + level->getWorld()->getSeed() + )) {} ChunksController::~ChunksController() = default; -void ChunksController::update(int64_t maxDuration) { +void ChunksController::update( + int64_t maxDuration, int loadDistance, int centerX, int centerY +) { + generator->update(centerX, centerY, loadDistance); + int64_t mcstotal = 0; for (uint i = 0; i < MAX_WORK_PER_FRAME; i++) { @@ -52,16 +56,17 @@ void ChunksController::update(int64_t maxDuration) { } bool ChunksController::loadVisible() { - const int w = chunks->w; - const int d = chunks->d; + int sizeX = chunks->getWidth(); + int sizeY = chunks->getHeight(); int nearX = 0; int nearZ = 0; - int minDistance = ((w - padding * 2) / 2) * ((w - padding * 2) / 2); - for (uint z = padding; z < d - padding; z++) { - for (uint x = padding; x < w - padding; x++) { - int index = z * w + x; - auto& chunk = chunks->chunks[index]; + bool assigned = false; + int minDistance = ((sizeX - padding * 2) / 2) * ((sizeY - padding * 2) / 2); + for (uint z = padding; z < sizeY - padding; z++) { + for (uint x = padding; x < sizeX - padding; x++) { + int index = z * sizeX + x; + auto& chunk = chunks->getChunks()[index]; if (chunk != nullptr) { if (chunk->flags.loaded && !chunk->flags.lighted) { if (buildLights(chunk)) { @@ -70,25 +75,25 @@ bool ChunksController::loadVisible() { } continue; } - int lx = x - w / 2; - int lz = z - d / 2; + int lx = x - sizeX / 2; + int lz = z - sizeY / 2; int distance = (lx * lx + lz * lz); if (distance < minDistance) { minDistance = distance; nearX = x; nearZ = z; + assigned = true; } } } - const auto& chunk = chunks->chunks[nearZ * w + nearX]; - if (chunk != nullptr) { + const auto& chunk = chunks->getChunks()[nearZ * sizeX + nearX]; + if (chunk != nullptr || !assigned) { return false; } - - const int ox = chunks->ox; - const int oz = chunks->oz; - createChunk(nearX + ox, nearZ + oz); + int offsetX = chunks->getOffsetX(); + int offsetY = chunks->getOffsetY(); + createChunk(nearX + offsetX, nearZ + offsetY); return true; } @@ -117,7 +122,7 @@ void ChunksController::createChunk(int x, int z) { auto& chunkFlags = chunk->flags; if (!chunkFlags.loaded) { - generator->generate(chunk->voxels, x, z, level->getWorld()->getSeed()); + generator->generate(chunk->voxels, x, z); chunkFlags.unsaved = true; } chunk->updateHeights(); diff --git a/src/logic/ChunksController.hpp b/src/logic/ChunksController.hpp index 5e607f51..8566ee1b 100644 --- a/src/logic/ChunksController.hpp +++ b/src/logic/ChunksController.hpp @@ -28,5 +28,13 @@ public: ~ChunksController(); /// @param maxDuration milliseconds reserved for chunks loading - void update(int64_t maxDuration); + void update( + int64_t maxDuration, + int loadDistance, + int centerX, + int centerY); + + const WorldGenerator* getGenerator() const { + return generator.get(); + } }; diff --git a/src/logic/CommandsInterpreter.cpp b/src/logic/CommandsInterpreter.cpp index d2e5bb03..cc8ff900 100644 --- a/src/logic/CommandsInterpreter.cpp +++ b/src/logic/CommandsInterpreter.cpp @@ -39,6 +39,7 @@ class CommandParser : BasicParser { {"int", ArgType::integer}, {"str", ArgType::string}, {"sel", ArgType::selector}, + {"bool", ArgType::boolean}, {"enum", ArgType::enumvalue}, }; public: @@ -250,6 +251,11 @@ public: return selectorCheck(arg, value); case ArgType::integer: return typeCheck(arg, dv::value_type::integer, value, "integer"); + case ArgType::boolean: + if (!arg->optional) { + throw typeError(arg->name, "boolean", value); + } + return value.isBoolean(); case ArgType::string: if (!value.isString()) { return !arg->optional; diff --git a/src/logic/CommandsInterpreter.hpp b/src/logic/CommandsInterpreter.hpp index af3e54ac..ce9ccaba 100644 --- a/src/logic/CommandsInterpreter.hpp +++ b/src/logic/CommandsInterpreter.hpp @@ -8,7 +8,7 @@ #include "data/dv.hpp" namespace cmd { - enum class ArgType { number, integer, enumvalue, selector, string }; + enum class ArgType { number, integer, enumvalue, selector, boolean, string }; inline std::string argtype_name(ArgType type) { switch (type) { @@ -20,6 +20,8 @@ namespace cmd { return "enumeration"; case ArgType::selector: return "selector"; + case ArgType::boolean: + return "boolean"; case ArgType::string: return "string"; default: diff --git a/src/logic/EngineController.cpp b/src/logic/EngineController.cpp index c8666d9d..f6b58a0d 100644 --- a/src/logic/EngineController.cpp +++ b/src/logic/EngineController.cpp @@ -124,7 +124,7 @@ static void show_content_missing( auto root = dv::object(); auto& contentEntries = root.list("content"); for (auto& entry : report->getMissingContent()) { - std::string contentName = contenttype_name(entry.type); + std::string contentName = ContentType_name(entry.type); auto& contentEntry = contentEntries.object(); contentEntry["type"] = contentName; contentEntry["name"] = entry.name; diff --git a/src/logic/LevelController.cpp b/src/logic/LevelController.cpp index 1badb0dc..f0bee4c5 100644 --- a/src/logic/LevelController.cpp +++ b/src/logic/LevelController.cpp @@ -10,6 +10,7 @@ #include "settings.hpp" #include "world/Level.hpp" #include "world/World.hpp" +#include "maths/voxmaths.hpp" #include "scripting/scripting.hpp" static debug::Logger logger("level-control"); @@ -38,7 +39,10 @@ void LevelController::update(float delta, bool input, bool pause) { position.z, settings.chunks.loadDistance.get() + settings.chunks.padding.get() * 2 ); - chunks->update(settings.chunks.loadSpeed.get()); + chunks->update( + settings.chunks.loadSpeed.get(), settings.chunks.loadDistance.get(), + floordiv(position.x, CHUNK_W), floordiv(position.z, CHUNK_D) + ); if (!pause) { // update all objects that needed @@ -91,6 +95,10 @@ BlocksController* LevelController::getBlocksController() { return blocks.get(); } +ChunksController* LevelController::getChunksController() { + return chunks.get(); +} + PlayerController* LevelController::getPlayerController() { return player.get(); } diff --git a/src/logic/LevelController.hpp b/src/logic/LevelController.hpp index fe16ed19..a2c5af3e 100644 --- a/src/logic/LevelController.hpp +++ b/src/logic/LevelController.hpp @@ -34,5 +34,6 @@ public: Player* getPlayer(); BlocksController* getBlocksController(); + ChunksController* getChunksController(); PlayerController* getPlayerController(); }; diff --git a/src/logic/scripting/lua/api_lua.hpp b/src/logic/scripting/lua/libs/api_lua.hpp similarity index 97% rename from src/logic/scripting/lua/api_lua.hpp rename to src/logic/scripting/lua/libs/api_lua.hpp index 68a95cb8..f856f5f4 100644 --- a/src/logic/scripting/lua/api_lua.hpp +++ b/src/logic/scripting/lua/libs/api_lua.hpp @@ -3,7 +3,7 @@ #include #include -#include "lua_util.hpp" +#include "../lua_util.hpp" /// Definitions can be found in local .cpp files /// having same names as declarations @@ -30,6 +30,7 @@ extern const luaL_Reg jsonlib[]; extern const luaL_Reg mat4lib[]; extern const luaL_Reg packlib[]; extern const luaL_Reg playerlib[]; +extern const luaL_Reg generationlib[]; extern const luaL_Reg quatlib[]; // quat.cpp extern const luaL_Reg timelib[]; extern const luaL_Reg tomllib[]; diff --git a/src/logic/scripting/lua/lib__rigidbody.cpp b/src/logic/scripting/lua/libs/lib__rigidbody.cpp similarity index 100% rename from src/logic/scripting/lua/lib__rigidbody.cpp rename to src/logic/scripting/lua/libs/lib__rigidbody.cpp diff --git a/src/logic/scripting/lua/lib__skeleton.cpp b/src/logic/scripting/lua/libs/lib__skeleton.cpp similarity index 100% rename from src/logic/scripting/lua/lib__skeleton.cpp rename to src/logic/scripting/lua/libs/lib__skeleton.cpp diff --git a/src/logic/scripting/lua/lib__transform.cpp b/src/logic/scripting/lua/libs/lib__transform.cpp similarity index 100% rename from src/logic/scripting/lua/lib__transform.cpp rename to src/logic/scripting/lua/libs/lib__transform.cpp diff --git a/src/logic/scripting/lua/libaudio.cpp b/src/logic/scripting/lua/libs/libaudio.cpp similarity index 100% rename from src/logic/scripting/lua/libaudio.cpp rename to src/logic/scripting/lua/libs/libaudio.cpp diff --git a/src/logic/scripting/lua/libblock.cpp b/src/logic/scripting/lua/libs/libblock.cpp similarity index 95% rename from src/logic/scripting/lua/libblock.cpp rename to src/logic/scripting/lua/libs/libblock.cpp index 193c8520..df388fea 100644 --- a/src/logic/scripting/lua/libblock.cpp +++ b/src/logic/scripting/lua/libs/libblock.cpp @@ -281,7 +281,13 @@ static int l_get_model(lua::State* L) { static int l_get_hitbox(lua::State* L) { if (auto def = require_block(L)) { - auto& hitbox = def->rt.hitboxes[lua::tointeger(L, 2)].at(0); + size_t rotation = lua::tointeger(L, 2); + if (def->rotatable) { + rotation %= def->rotations.MAX_COUNT; + } else { + rotation = 0; + } + auto& hitbox = def->rt.hitboxes[rotation].at(0); lua::createtable(L, 2, 0); lua::pushvec3(L, hitbox.min()); diff --git a/src/logic/scripting/lua/libcamera.cpp b/src/logic/scripting/lua/libs/libcamera.cpp similarity index 100% rename from src/logic/scripting/lua/libcamera.cpp rename to src/logic/scripting/lua/libs/libcamera.cpp diff --git a/src/logic/scripting/lua/libconsole.cpp b/src/logic/scripting/lua/libs/libconsole.cpp similarity index 100% rename from src/logic/scripting/lua/libconsole.cpp rename to src/logic/scripting/lua/libs/libconsole.cpp diff --git a/src/logic/scripting/lua/libcore.cpp b/src/logic/scripting/lua/libs/libcore.cpp similarity index 87% rename from src/logic/scripting/lua/libcore.cpp rename to src/logic/scripting/lua/libs/libcore.cpp index 2e1034a0..6f0dd8b8 100644 --- a/src/logic/scripting/lua/libcore.cpp +++ b/src/logic/scripting/lua/libs/libcore.cpp @@ -3,6 +3,7 @@ #include "constants.hpp" #include "engine.hpp" +#include "content/Content.hpp" #include "files/engine_paths.hpp" #include "files/settings_io.hpp" #include "frontend/menu.hpp" @@ -11,8 +12,10 @@ #include "logic/LevelController.hpp" #include "window/Events.hpp" #include "window/Window.hpp" +#include "world/generator/WorldGenerator.hpp" #include "world/Level.hpp" -#include "world/WorldGenerators.hpp" +#include "util/listutil.hpp" + #include "api_lua.hpp" using namespace scripting; @@ -170,27 +173,6 @@ static int l_quit(lua::State*) { return 0; } -/// @brief Get the default world generator -/// @return The ID of the default world generator -static int l_get_default_generator(lua::State* L) { - return lua::pushstring(L, WorldGenerators::getDefaultGeneratorID()); -} - -/// @brief Get a list of all world generators -/// @return A table with the IDs of all world generators -static int l_get_generators(lua::State* L) { - const auto& generators = WorldGenerators::getGeneratorsIDs(); - lua::createtable(L, generators.size(), 0); - - int i = 0; - for (auto& id : generators) { - lua::pushstring(L, id); - lua::rawseti(L, i + 1); - i++; - } - return 1; -} - const luaL_Reg corelib[] = { {"new_world", lua::wrap}, {"open_world", lua::wrap}, @@ -203,6 +185,4 @@ const luaL_Reg corelib[] = { {"str_setting", lua::wrap}, {"get_setting_info", lua::wrap}, {"quit", lua::wrap}, - {"get_default_generator", lua::wrap}, - {"get_generators", lua::wrap}, {NULL, NULL}}; diff --git a/src/logic/scripting/lua/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp similarity index 100% rename from src/logic/scripting/lua/libentity.cpp rename to src/logic/scripting/lua/libs/libentity.cpp diff --git a/src/logic/scripting/lua/libentity.hpp b/src/logic/scripting/lua/libs/libentity.hpp similarity index 100% rename from src/logic/scripting/lua/libentity.hpp rename to src/logic/scripting/lua/libs/libentity.hpp diff --git a/src/logic/scripting/lua/libfile.cpp b/src/logic/scripting/lua/libs/libfile.cpp similarity index 91% rename from src/logic/scripting/lua/libfile.cpp rename to src/logic/scripting/lua/libs/libfile.cpp index 5da4358d..52f7e4db 100644 --- a/src/logic/scripting/lua/libfile.cpp +++ b/src/logic/scripting/lua/libs/libfile.cpp @@ -158,7 +158,7 @@ static int l_file_write_bytes(lua::State* L) { fs::path path = resolve_path(lua::require_string(L, pathIndex)); - if (auto bytearray = lua::touserdata(L, -1)) { + if (auto bytearray = lua::touserdata(L, -1)) { auto& bytes = bytearray->data(); return lua::pushboolean( L, files::write_bytes(path, bytes.data(), bytes.size()) @@ -247,6 +247,14 @@ static int l_file_gzip_decompress(lua::State* L) { } } +static int l_file_read_combined_list(lua::State* L) { + std::string path = lua::require_string(L, 1); + if (path.find(':') != std::string::npos) { + throw std::runtime_error("entry point must not be specified"); + } + return lua::pushvalue(L, engine->getResPaths()->readCombinedList(path)); +} + const luaL_Reg filelib[] = { {"exists", lua::wrap}, {"find", lua::wrap}, @@ -265,4 +273,5 @@ const luaL_Reg filelib[] = { {"write", lua::wrap}, {"gzip_compress", lua::wrap}, {"gzip_decompress", lua::wrap}, + {"read_combined_list", lua::wrap}, {NULL, NULL}}; diff --git a/src/logic/scripting/lua/libs/libgeneration.cpp b/src/logic/scripting/lua/libs/libgeneration.cpp new file mode 100644 index 00000000..ca97815c --- /dev/null +++ b/src/logic/scripting/lua/libs/libgeneration.cpp @@ -0,0 +1,85 @@ +#include "api_lua.hpp" + +#include "files/files.hpp" +#include "files/util.hpp" +#include "coders/binary_json.hpp" +#include "world/Level.hpp" +#include "world/generator/VoxelFragment.hpp" +#include "content/ContentLoader.hpp" +#include "engine.hpp" +#include "../lua_custom_types.hpp" + +using namespace scripting; + +static int l_save_fragment(lua::State* L) { + auto paths = engine->getPaths(); + auto fragment = lua::touserdata(L, 1); + auto file = paths->resolve(lua::require_string(L, 2), true); + auto map = fragment->getFragment()->serialize(); + auto bytes = json::to_binary(map, true); + files::write_bytes(file, bytes.data(), bytes.size()); + return 0; +} + +static int l_create_fragment(lua::State* L) { + auto pointA = lua::tovec<3>(L, 1); + auto pointB = lua::tovec<3>(L, 2); + bool crop = lua::toboolean(L, 3); + bool saveEntities = lua::toboolean(L, 4); + + auto fragment = + VoxelFragment::create(level, pointA, pointB, crop, saveEntities); + return lua::newuserdata( + L, std::shared_ptr(std::move(fragment)) + ); +} + +static int l_load_fragment(lua::State* L) { + auto paths = engine->getPaths(); + auto filename = lua::require_string(L, 1); + auto path = paths->resolve(filename); + if (!std::filesystem::exists(path)) { + throw std::runtime_error("file "+path.u8string()+" does not exist"); + } + auto map = files::read_binary_json(path); + + auto fragment = std::make_shared(); + fragment->deserialize(map); + return lua::newuserdata(L, std::move(fragment)); +} + +/// @brief Get a list of all world generators +/// @return A table with the IDs of all world generators +static int l_get_generators(lua::State* L) { + auto packs = engine->getAllContentPacks(); + + lua::createtable(L, 0, 0); + + int i = 1; + for (const auto& pack : packs) { + auto pairs = ContentLoader::scanContent(pack, ContentType::GENERATOR); + for (const auto& [name, caption] : pairs) { + lua::pushstring(L, caption); + lua::setfield(L, name); + i++; + } + } + return 1; +} + +/// @brief Get the default world generator +/// @return The ID of the default world generator +static int l_get_default_generator(lua::State* L) { + auto combined = engine->getResPaths()->readCombinedObject( + EnginePaths::CONFIG_DEFAULTS.u8string() + ); + return lua::pushstring(L, combined["generator"].asString()); +} + +const luaL_Reg generationlib[] = { + {"create_fragment", lua::wrap}, + {"save_fragment", lua::wrap}, + {"load_fragment", lua::wrap}, + {"get_generators", lua::wrap}, + {"get_default_generator", lua::wrap}, + {NULL, NULL}}; diff --git a/src/logic/scripting/lua/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp similarity index 100% rename from src/logic/scripting/lua/libgui.cpp rename to src/logic/scripting/lua/libs/libgui.cpp diff --git a/src/logic/scripting/lua/libhud.cpp b/src/logic/scripting/lua/libs/libhud.cpp similarity index 100% rename from src/logic/scripting/lua/libhud.cpp rename to src/logic/scripting/lua/libs/libhud.cpp diff --git a/src/logic/scripting/lua/libinput.cpp b/src/logic/scripting/lua/libs/libinput.cpp similarity index 100% rename from src/logic/scripting/lua/libinput.cpp rename to src/logic/scripting/lua/libs/libinput.cpp diff --git a/src/logic/scripting/lua/libinventory.cpp b/src/logic/scripting/lua/libs/libinventory.cpp similarity index 100% rename from src/logic/scripting/lua/libinventory.cpp rename to src/logic/scripting/lua/libs/libinventory.cpp diff --git a/src/logic/scripting/lua/libitem.cpp b/src/logic/scripting/lua/libs/libitem.cpp similarity index 100% rename from src/logic/scripting/lua/libitem.cpp rename to src/logic/scripting/lua/libs/libitem.cpp diff --git a/src/logic/scripting/lua/libjson.cpp b/src/logic/scripting/lua/libs/libjson.cpp similarity index 100% rename from src/logic/scripting/lua/libjson.cpp rename to src/logic/scripting/lua/libs/libjson.cpp diff --git a/src/logic/scripting/lua/libmat4.cpp b/src/logic/scripting/lua/libs/libmat4.cpp similarity index 95% rename from src/logic/scripting/lua/libmat4.cpp rename to src/logic/scripting/lua/libs/libmat4.cpp index 078c229a..fb3d9c25 100644 --- a/src/logic/scripting/lua/libmat4.cpp +++ b/src/logic/scripting/lua/libs/libmat4.cpp @@ -77,7 +77,7 @@ static int l_mul(lua::State* L) { /// transformed copy of matrix mat4.(matrix: float[16], vec: float[3], /// dst: float[16]) -> sets dst to transformed version of matrix template -inline int l_transform_func(lua::State* L) { +inline int l_binop_func(lua::State* L) { uint argc = lua::gettop(L); switch (argc) { case 1: { @@ -272,9 +272,9 @@ static int l_tostring(lua::State* L) { const luaL_Reg mat4lib[] = { {"idt", lua::wrap}, {"mul", lua::wrap}, - {"scale", lua::wrap>}, + {"scale", lua::wrap>}, {"rotate", lua::wrap}, - {"translate", lua::wrap>}, + {"translate", lua::wrap>}, {"inverse", lua::wrap}, {"transpose", lua::wrap}, {"determinant", lua::wrap}, diff --git a/src/logic/scripting/lua/libpack.cpp b/src/logic/scripting/lua/libs/libpack.cpp similarity index 92% rename from src/logic/scripting/lua/libpack.cpp rename to src/logic/scripting/lua/libs/libpack.cpp index bc9bb204..fdd9ae8a 100644 --- a/src/logic/scripting/lua/libpack.cpp +++ b/src/logic/scripting/lua/libs/libpack.cpp @@ -16,11 +16,9 @@ using namespace scripting; static int l_pack_get_folder(lua::State* L) { std::string packName = lua::tostring(L, 1); - if (packName == "core") { - auto folder = engine->getPaths()->getResourcesFolder().u8string() + "/"; - return lua::pushstring(L, folder); - } - for (auto& pack : engine->getContentPacks()) { + auto packs = engine->getAllContentPacks(); + + for (auto& pack : packs) { if (pack.id == packName) { return lua::pushstring(L, pack.folder.u8string() + "/"); } diff --git a/src/logic/scripting/lua/libplayer.cpp b/src/logic/scripting/lua/libs/libplayer.cpp similarity index 100% rename from src/logic/scripting/lua/libplayer.cpp rename to src/logic/scripting/lua/libs/libplayer.cpp diff --git a/src/logic/scripting/lua/libquat.cpp b/src/logic/scripting/lua/libs/libquat.cpp similarity index 100% rename from src/logic/scripting/lua/libquat.cpp rename to src/logic/scripting/lua/libs/libquat.cpp diff --git a/src/logic/scripting/lua/libtime.cpp b/src/logic/scripting/lua/libs/libtime.cpp similarity index 100% rename from src/logic/scripting/lua/libtime.cpp rename to src/logic/scripting/lua/libs/libtime.cpp diff --git a/src/logic/scripting/lua/libtoml.cpp b/src/logic/scripting/lua/libs/libtoml.cpp similarity index 100% rename from src/logic/scripting/lua/libtoml.cpp rename to src/logic/scripting/lua/libs/libtoml.cpp diff --git a/src/logic/scripting/lua/libvecn.cpp b/src/logic/scripting/lua/libs/libvecn.cpp similarity index 100% rename from src/logic/scripting/lua/libvecn.cpp rename to src/logic/scripting/lua/libs/libvecn.cpp diff --git a/src/logic/scripting/lua/libworld.cpp b/src/logic/scripting/lua/libs/libworld.cpp similarity index 100% rename from src/logic/scripting/lua/libworld.cpp rename to src/logic/scripting/lua/libs/libworld.cpp diff --git a/src/logic/scripting/lua/lua_commons.hpp b/src/logic/scripting/lua/lua_commons.hpp index d0b99f67..ff94ade7 100644 --- a/src/logic/scripting/lua/lua_commons.hpp +++ b/src/logic/scripting/lua/lua_commons.hpp @@ -24,9 +24,29 @@ namespace lua { luaerror(const std::string& message); }; - void log_error(const std::string& text); - using State = lua_State; using Number = lua_Number; using Integer = lua_Integer; + + /// @brief Automatically resets stack top element index to the initial state + /// (when stackguard was created). Prevents Lua stack leak on exception + /// occurred out of Lua execution time, when engine controls scripting. + /// + /// + /// stackguard allows to not place lua::pop(...) into 'catch' blocks. + class stackguard { + int top; + State* state; + public: + stackguard(State* state) : state(state) { + top = lua_gettop(state); + } + + ~stackguard() { + lua_settop(state, top); + } + }; + + void log_error(const std::string& text); + } diff --git a/src/logic/scripting/lua/lua_custom_types.hpp b/src/logic/scripting/lua/lua_custom_types.hpp index 4b603aca..bb8037ee 100644 --- a/src/logic/scripting/lua/lua_custom_types.hpp +++ b/src/logic/scripting/lua/lua_custom_types.hpp @@ -5,6 +5,10 @@ #include "lua_commons.hpp" +struct fnl_state; +class Heightmap; +class VoxelFragment; + namespace lua { class Userdata { public: @@ -12,12 +16,12 @@ namespace lua { virtual const std::string& getTypeName() const = 0; }; - class Bytearray : public Userdata { + class LuaBytearray : public Userdata { std::vector buffer; public: - Bytearray(size_t capacity); - Bytearray(std::vector buffer); - virtual ~Bytearray(); + LuaBytearray(size_t capacity); + LuaBytearray(std::vector buffer); + virtual ~LuaBytearray(); const std::string& getTypeName() const override { return TYPENAME; @@ -27,6 +31,63 @@ namespace lua { } static int createMetatable(lua::State*); - inline static std::string TYPENAME = "bytearray"; + inline static std::string TYPENAME = "Bytearray"; }; + static_assert(!std::is_abstract()); + + class LuaHeightmap : public Userdata { + std::shared_ptr map; + std::unique_ptr noise; + public: + LuaHeightmap(const std::shared_ptr& map); + LuaHeightmap(uint width, uint height); + + virtual ~LuaHeightmap(); + + uint getWidth() const; + + uint getHeight() const; + + float* getValues(); + + const float* getValues() const; + + const std::string& getTypeName() const override { + return TYPENAME; + } + + const std::shared_ptr& getHeightmap() const { + return map; + } + + fnl_state* getNoise() { + return noise.get(); + } + + void setSeed(int64_t seed); + + static int createMetatable(lua::State*); + inline static std::string TYPENAME = "Heightmap"; + }; + static_assert(!std::is_abstract()); + + class LuaVoxelFragment : public Userdata { + std::shared_ptr fragment; + public: + LuaVoxelFragment(std::shared_ptr fragment); + + virtual ~LuaVoxelFragment(); + + std::shared_ptr getFragment() const { + return fragment; + } + + const std::string& getTypeName() const override { + return TYPENAME; + } + + static int createMetatable(lua::State*); + inline static std::string TYPENAME = "VoxelFragment"; + }; + static_assert(!std::is_abstract()); } diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index 01df4ea9..42ea18ec 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -3,9 +3,11 @@ #include #include +#include "files/files.hpp" +#include "files/engine_paths.hpp" #include "debug/Logger.hpp" #include "util/stringutil.hpp" -#include "api_lua.hpp" +#include "libs/api_lua.hpp" #include "lua_custom_types.hpp" static debug::Logger logger("lua-state"); @@ -17,7 +19,7 @@ luaerror::luaerror(const std::string& message) : std::runtime_error(message) { } static void remove_lib_funcs( - lua::State* L, const char* libname, const char* funcs[] + State* L, const char* libname, const char* funcs[] ) { if (getglobal(L, libname)) { for (uint i = 0; funcs[i]; i++) { @@ -28,48 +30,52 @@ static void remove_lib_funcs( } } -static void create_libs(lua::State* L) { - openlib(L, "audio", audiolib); +[[nodiscard]] scriptenv lua::create_environment(State* L) { + int id = lua::create_environment(L, 0); + return std::shared_ptr(new int(id), [=](int* id) { //-V508 + lua::remove_environment(L, *id); + delete id; + }); +} + +static void create_libs(State* L, StateType stateType) { openlib(L, "block", blocklib); - openlib(L, "console", consolelib); openlib(L, "core", corelib); openlib(L, "file", filelib); - openlib(L, "gui", guilib); - openlib(L, "input", inputlib); - openlib(L, "inventory", inventorylib); + openlib(L, "generation", generationlib); openlib(L, "item", itemlib); openlib(L, "json", jsonlib); openlib(L, "mat4", mat4lib); openlib(L, "pack", packlib); - openlib(L, "player", playerlib); openlib(L, "quat", quatlib); openlib(L, "time", timelib); openlib(L, "toml", tomllib); openlib(L, "vec2", vec2lib); openlib(L, "vec3", vec3lib); openlib(L, "vec4", vec4lib); - openlib(L, "world", worldlib); - openlib(L, "entities", entitylib); - openlib(L, "cameras", cameralib); + if (stateType == StateType::BASE) { + openlib(L, "gui", guilib); + openlib(L, "input", inputlib); + openlib(L, "inventory", inventorylib); + openlib(L, "world", worldlib); + openlib(L, "audio", audiolib); + openlib(L, "console", consolelib); + openlib(L, "player", playerlib); - // components - openlib(L, "__skeleton", skeletonlib); - openlib(L, "__rigidbody", rigidbodylib); - openlib(L, "__transform", transformlib); + openlib(L, "entities", entitylib); + openlib(L, "cameras", cameralib); + + // components + openlib(L, "__skeleton", skeletonlib); + openlib(L, "__rigidbody", rigidbodylib); + openlib(L, "__transform", transformlib); + } addfunc(L, "print", lua::wrap); } -void lua::initialize() { - logger.info() << LUA_VERSION; - logger.info() << LUAJIT_VERSION; - - auto L = luaL_newstate(); - if (L == nullptr) { - throw luaerror("could not to initialize Lua"); - } - main_thread = L; +void lua::init_state(State* L, StateType stateType) { // Allowed standard libraries pop(L, luaopen_base(L)); pop(L, luaopen_math(L)); @@ -82,7 +88,7 @@ void lua::initialize() { const char* removed_os[] { "execute", "exit", "remove", "rename", "setlocale", "tmpname", nullptr}; remove_lib_funcs(L, "os", removed_os); - create_libs(L); + create_libs(L, stateType); pushglobals(L); setglobal(L, env_name(0)); @@ -95,15 +101,24 @@ void lua::initialize() { initialize_libs_extends(L); - newusertype(L, "bytearray"); + newusertype(L); + newusertype(L); + newusertype(L); +} + +void lua::initialize(const EnginePaths& paths) { + logger.info() << LUA_VERSION; + logger.info() << LUAJIT_VERSION; + + main_thread = create_state(paths, StateType::BASE); } void lua::finalize() { - lua_close(main_thread); + lua::close(main_thread); } bool lua::emit_event( - lua::State* L, const std::string& name, std::function args + State* L, const std::string& name, std::function args ) { getglobal(L, "events"); getfield(L, "emit"); @@ -114,6 +129,19 @@ bool lua::emit_event( return result; } -lua::State* lua::get_main_thread() { +State* lua::get_main_state() { return main_thread; } + +State* lua::create_state(const EnginePaths& paths, StateType stateType) { + auto L = luaL_newstate(); + if (L == nullptr) { + throw luaerror("could not initialize Lua state"); + } + init_state(L, stateType); + + auto resDir = paths.getResourcesFolder(); + auto src = files::read_string(resDir / fs::u8path("scripts/stdmin.lua")); + lua::pop(L, lua::execute(L, 0, src, "")); + return L; +} diff --git a/src/logic/scripting/lua/lua_engine.hpp b/src/logic/scripting/lua/lua_engine.hpp index 1690fbbd..dd1e794d 100644 --- a/src/logic/scripting/lua/lua_engine.hpp +++ b/src/logic/scripting/lua/lua_engine.hpp @@ -7,14 +7,25 @@ #include "logic/scripting/scripting_functional.hpp" #include "lua_util.hpp" +class EnginePaths; + namespace lua { - void initialize(); + enum class StateType { + BASE, + GENERATOR, + }; + + void initialize(const EnginePaths& paths); void finalize(); bool emit_event( - lua::State*, + State*, const std::string& name, - std::function args = [](auto*) { return 0; } + std::function args = [](auto*) { return 0; } ); - lua::State* get_main_thread(); + State* get_main_state(); + State* create_state(const EnginePaths& paths, StateType stateType); + [[nodiscard]] scriptenv create_environment(State* L); + + void init_state(State* L, StateType stateType); } diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index 51185898..e1d7a230 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -1,6 +1,6 @@ #include -#include "api_lua.hpp" +#include "libs/api_lua.hpp" #include "debug/Logger.hpp" static debug::Logger logger("lua-debug"); diff --git a/src/logic/scripting/lua/lua_overrides.cpp b/src/logic/scripting/lua/lua_overrides.cpp index 8233d95a..12b0bbff 100644 --- a/src/logic/scripting/lua/lua_overrides.cpp +++ b/src/logic/scripting/lua/lua_overrides.cpp @@ -1,6 +1,6 @@ #include -#include "api_lua.hpp" +#include "libs/api_lua.hpp" /// @brief Modified version of luaB_print from lbaselib.c int l_print(lua::State* L) { diff --git a/src/logic/scripting/lua/lua_util.cpp b/src/logic/scripting/lua/lua_util.cpp index 6152a254..44583e4c 100644 --- a/src/logic/scripting/lua/lua_util.cpp +++ b/src/logic/scripting/lua/lua_util.cpp @@ -277,7 +277,7 @@ int lua::create_environment(State* L, int parent) { return id; } -void lua::removeEnvironment(State* L, int id) { +void lua::remove_environment(State* L, int id) { if (id == 0) { return; } diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index 219f9df6..05b37c1d 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -206,6 +206,13 @@ namespace lua { inline lua::Integer tointeger(lua::State* L, int idx) { return lua_tointeger(L, idx); } + inline uint64_t touinteger(lua::State* L, int idx) { + auto val = lua_tointeger(L, idx); + if (val < 0) { + throw std::runtime_error("negative value"); + } + return static_cast(val); + } inline lua::Number tonumber(lua::State* L, int idx) { return lua_tonumber(L, idx); } @@ -241,10 +248,12 @@ namespace lua { return 1; } - template - inline void newusertype(lua::State* L, const std::string& name) { + template + inline std::enable_if_t> + newusertype(lua::State* L) { + const std::string& name = T::TYPENAME; usertypeNames[typeid(T)] = name; - func(L); + T::createMetatable(L); pushcfunction(L, userdata_destructor); setfield(L, "__gc"); @@ -396,17 +405,26 @@ namespace lua { } int pushvalue(lua::State*, const dv::value& value); + + [[nodiscard]] dv::value tovalue(lua::State*, int idx); inline bool getfield(lua::State* L, const std::string& name, int idx = -1) { lua_getfield(L, idx, name.c_str()); - if (isnil(L, -1)) { + if (isnil(L, idx)) { pop(L); return false; } return true; } + inline int requirefield(lua::State* L, const std::string& name, int idx = -1) { + if (getfield(L, name, idx)) { + return 1; + } + throw std::runtime_error("object has no member '"+name+"'"); + } + inline bool hasfield(lua::State* L, const std::string& name, int idx = -1) { lua_getfield(L, idx, name.c_str()); if (isnil(L, -1)) { @@ -528,9 +546,13 @@ namespace lua { return 0; } int create_environment(lua::State*, int parent); - void removeEnvironment(lua::State*, int id); + void remove_environment(lua::State*, int id); void dump_stack(lua::State*); + inline void close(lua::State* L) { + lua_close(L); + } + inline void addfunc( lua::State* L, const std::string& name, lua_CFunction func ) { @@ -546,19 +568,12 @@ namespace lua { setglobal(L, name); } - inline int requirefield(lua::State* L, const std::string& name, int idx = -1) { - if (getfield(L, name, idx)) { - return 1; - } - throw std::runtime_error("object has no member '"+name+"'"); - } - inline const char* require_string_field( lua::State* L, const std::string& name, int idx=-1 ) { requirefield(L, name, idx); auto value = require_string(L, -1); - lua::pop(L); + pop(L); return value; } @@ -567,7 +582,16 @@ namespace lua { ) { requirefield(L, name, idx); auto value = tointeger(L, -1); - lua::pop(L); + pop(L); + return value; + } + + inline Number require_number_field( + lua::State* L, const std::string& name, int idx=-1 + ) { + requirefield(L, name, idx); + auto value = tonumber(L, -1); + pop(L); return value; } @@ -581,4 +605,43 @@ namespace lua { } return def; } + + inline Integer get_integer_field( + lua::State* L, const std::string& name, Integer def, int idx=-1 + ) { + if (getfield(L, name, idx)) { + auto value = tointeger(L, -1); + pop(L); + return value; + } + return def; + } + + inline Number get_number_field( + lua::State* L, const std::string& name, Number def, int idx=-1 + ) { + if (getfield(L, name, idx)) { + auto value = tonumber(L, -1); + pop(L); + return value; + } + return def; + } + + inline Integer get_integer_field( + lua::State* L, const std::string& name, + Integer def, Integer min, Integer max, int idx=-1 + ) { + if (getfield(L, name, idx)) { + auto value = tointeger(L, -1); + if (value < min || value > max) { + throw std::runtime_error( + "value is out of range [" + +std::to_string(min)+", "+std::to_string(max)+"]"); + } + pop(L); + return value; + } + return def; + } } diff --git a/src/logic/scripting/lua/lua_custom_types.cpp b/src/logic/scripting/lua/usertypes/lua_type_bytearray.cpp similarity index 61% rename from src/logic/scripting/lua/lua_custom_types.cpp rename to src/logic/scripting/lua/usertypes/lua_type_bytearray.cpp index 8ee90d92..7be3fc10 100644 --- a/src/logic/scripting/lua/lua_custom_types.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_bytearray.cpp @@ -1,31 +1,31 @@ -#include "lua_custom_types.hpp" +#include "../lua_custom_types.hpp" #include -#include "lua_util.hpp" +#include "../lua_util.hpp" using namespace lua; -Bytearray::Bytearray(size_t capacity) : buffer(capacity) { +LuaBytearray::LuaBytearray(size_t capacity) : buffer(capacity) { buffer.resize(capacity); } -Bytearray::Bytearray(std::vector buffer) : buffer(std::move(buffer)) { +LuaBytearray::LuaBytearray(std::vector buffer) : buffer(std::move(buffer)) { } -Bytearray::~Bytearray() { +LuaBytearray::~LuaBytearray() { } -static int l_bytearray_append(lua::State* L) { - if (auto buffer = touserdata(L, 1)) { +static int l_append(lua::State* L) { + if (auto buffer = touserdata(L, 1)) { auto value = tointeger(L, 2); buffer->data().push_back(static_cast(value)); } return 0; } -static int l_bytearray_insert(lua::State* L) { - auto buffer = touserdata(L, 1); +static int l_insert(lua::State* L) { + auto buffer = touserdata(L, 1); if (buffer == nullptr) { return 0; } @@ -39,8 +39,8 @@ static int l_bytearray_insert(lua::State* L) { return 0; } -static int l_bytearray_remove(lua::State* L) { - auto buffer = touserdata(L, 1); +static int l_remove(lua::State* L) { + auto buffer = touserdata(L, 1); if (buffer == nullptr) { return 0; } @@ -53,13 +53,13 @@ static int l_bytearray_remove(lua::State* L) { return 0; } -static std::unordered_map bytearray_methods { - {"append", lua::wrap}, - {"insert", lua::wrap}, - {"remove", lua::wrap}, +static std::unordered_map methods { + {"append", lua::wrap}, + {"insert", lua::wrap}, + {"remove", lua::wrap}, }; -static int l_bytearray_meta_meta_call(lua::State* L) { +static int l_meta_meta_call(lua::State* L) { if (lua_istable(L, 2)) { size_t len = objlen(L, 2); std::vector buffer(len); @@ -69,24 +69,24 @@ static int l_bytearray_meta_meta_call(lua::State* L) { buffer[i] = static_cast(tointeger(L, -1)); pop(L); } - return newuserdata(L, std::move(buffer)); + return newuserdata(L, std::move(buffer)); } auto size = tointeger(L, 2); if (size < 0) { throw std::runtime_error("size can not be less than 0"); } - return newuserdata(L, static_cast(size)); + return newuserdata(L, static_cast(size)); } -static int l_bytearray_meta_index(lua::State* L) { - auto buffer = touserdata(L, 1); +static int l_meta_index(lua::State* L) { + auto buffer = touserdata(L, 1); if (buffer == nullptr) { return 0; } auto& data = buffer->data(); if (isstring(L, 2)) { - auto found = bytearray_methods.find(tostring(L, 2)); - if (found != bytearray_methods.end()) { + auto found = methods.find(tostring(L, 2)); + if (found != methods.end()) { return pushcfunction(L, found->second); } } @@ -97,8 +97,8 @@ static int l_bytearray_meta_index(lua::State* L) { return pushinteger(L, data[index]); } -static int l_bytearray_meta_newindex(lua::State* L) { - auto buffer = touserdata(L, 1); +static int l_meta_newindex(lua::State* L) { + auto buffer = touserdata(L, 1); if (buffer == nullptr) { return 0; } @@ -115,15 +115,15 @@ static int l_bytearray_meta_newindex(lua::State* L) { return 0; } -static int l_bytearray_meta_len(lua::State* L) { - if (auto buffer = touserdata(L, 1)) { +static int l_meta_len(lua::State* L) { + if (auto buffer = touserdata(L, 1)) { return pushinteger(L, buffer->data().size()); } return 0; } -static int l_bytearray_meta_tostring(lua::State* L) { - auto buffer = touserdata(L, 1); +static int l_meta_tostring(lua::State* L) { + auto buffer = touserdata(L, 1); if (buffer == nullptr) { return 0; } @@ -146,9 +146,9 @@ static int l_bytearray_meta_tostring(lua::State* L) { } } -static int l_bytearray_meta_add(lua::State* L) { - auto bufferA = touserdata(L, 1); - auto bufferB = touserdata(L, 2); +static int l_meta_add(lua::State* L) { + auto bufferA = touserdata(L, 1); + auto bufferB = touserdata(L, 2); if (bufferA == nullptr || bufferB == nullptr) { return 0; } @@ -159,24 +159,24 @@ static int l_bytearray_meta_add(lua::State* L) { ab.reserve(dataA.size() + dataB.size()); ab.insert(ab.end(), dataA.begin(), dataA.end()); ab.insert(ab.end(), dataB.begin(), dataB.end()); - return newuserdata(L, std::move(ab)); + return newuserdata(L, std::move(ab)); } -int Bytearray::createMetatable(lua::State* L) { +int LuaBytearray::createMetatable(lua::State* L) { createtable(L, 0, 6); - pushcfunction(L, lua::wrap); + pushcfunction(L, lua::wrap); setfield(L, "__index"); - pushcfunction(L, lua::wrap); + pushcfunction(L, lua::wrap); setfield(L, "__newindex"); - pushcfunction(L, lua::wrap); + pushcfunction(L, lua::wrap); setfield(L, "__len"); - pushcfunction(L, lua::wrap); + pushcfunction(L, lua::wrap); setfield(L, "__tostring"); - pushcfunction(L, lua::wrap); + pushcfunction(L, lua::wrap); setfield(L, "__add"); createtable(L, 0, 1); - pushcfunction(L, lua::wrap); + pushcfunction(L, lua::wrap); setfield(L, "__call"); setmetatable(L); return 1; diff --git a/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp b/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp new file mode 100644 index 00000000..06a031da --- /dev/null +++ b/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp @@ -0,0 +1,302 @@ +#include "../lua_custom_types.hpp" + +#include +#include +#include +#include + +#include "util/functional_util.hpp" +#define FNL_IMPL +#include "maths/FastNoiseLite.h" +#include "coders/imageio.hpp" +#include "files/util.hpp" +#include "graphics/core/ImageData.hpp" +#include "maths/Heightmap.hpp" +#include "engine.hpp" +#include "../lua_util.hpp" + +using namespace lua; + +LuaHeightmap::LuaHeightmap(const std::shared_ptr& map) + : map(map), noise(std::make_unique(fnlCreateState())) { +} + +LuaHeightmap::LuaHeightmap(uint width, uint height) + : map(std::make_shared(width, height)), + noise(std::make_unique(fnlCreateState())) +{} + +LuaHeightmap::~LuaHeightmap() { +} + +void LuaHeightmap::setSeed(int64_t seed) { + noise->seed = seed; +} + +uint LuaHeightmap::getWidth() const { + return map->getWidth(); +} + +uint LuaHeightmap::getHeight() const { + return map->getHeight(); +} + +float* LuaHeightmap::getValues() { + return map->getValues(); +} + +const float* LuaHeightmap::getValues() const { + return map->getValues(); +} + +static int l_dump(lua::State* L) { + auto paths = scripting::engine->getPaths(); + if (auto heightmap = touserdata(L, 1)) { + auto file = paths->resolve(require_string(L, 2)); + uint w = heightmap->getWidth(); + uint h = heightmap->getHeight(); + ImageData image(ImageFormat::rgb888, w, h); + auto heights = heightmap->getValues(); + auto raster = image.getData(); + for (uint y = 0; y < h; y++) { + for (uint x = 0; x < w; x++) { + uint i = y * w + x; + int val = heights[i] * 127 + 128; + val = std::max(std::min(val, 255), 0); + raster[i*3] = val; + raster[i*3 + 1] = val; + raster[i*3 + 2] = val; + } + } + imageio::write(file.u8string(), &image); + } + return 0; +} + +static int l_at(lua::State* L) { + if (auto heightmap = touserdata(L, 1)) { + int x = lua::tointeger(L, 2); + int y = lua::tointeger(L, 3); + return lua::pushnumber(L, heightmap->getHeightmap()->get(x, y)); + } + return 0; +} + +template +static int l_noise(lua::State* L) { + if (auto heightmap = touserdata(L, 1)) { + uint w = heightmap->getWidth(); + uint h = heightmap->getHeight(); + auto heights = heightmap->getValues(); + auto noise = heightmap->getNoise(); + + auto offset = tovec<2>(L, 2); + + float s = tonumber(L, 3); + int octaves = 1; + float multiplier = 1.0f; + if (gettop(L) > 3) { + octaves = tointeger(L, 4); + } + if (gettop(L) > 4) { + multiplier = tonumber(L, 5); + } + const LuaHeightmap* shiftMapX = nullptr; + const LuaHeightmap* shiftMapY = nullptr; + if (gettop(L) > 5) { + shiftMapX = touserdata(L, 6); + } + if (gettop(L) > 6) { + shiftMapY = touserdata(L, 7); + } + noise->noise_type = noise_type; + for (uint y = 0; y < h; y++) { + for (uint x = 0; x < w; x++) { + uint i = y * w + x; + for (uint c = 0; c < octaves; c++) { + float m = s * (1 << c); + float value = heights[i]; + float u = (x + offset.x) * m; + float v = (y + offset.y) * m; + if (shiftMapX) { + u += shiftMapX->getValues()[i]; + } + if (shiftMapY) { + v += shiftMapY->getValues()[i]; + } + + value += fnlGetNoise2D(noise, u, v) / + static_cast(1 << c) * multiplier; + heights[i] = value; + } + } + } + } + return 0; +} + +template class Op> +static int l_binop_func(lua::State* L) { + Op op; + if (auto heightmap = touserdata(L, 1)) { + uint w = heightmap->getWidth(); + uint h = heightmap->getHeight(); + auto heights = heightmap->getValues(); + + if (isnumber(L, 2)) { + float scalar = tonumber(L, 2); + for (uint y = 0; y < h; y++) { + for (uint x = 0; x < w; x++) { + uint i = y * w + x; + heights[i] = op(heights[i], scalar); + } + } + } else { + auto map = touserdata(L, 2); + for (uint y = 0; y < h; y++) { + for (uint x = 0; x < w; x++) { + uint i = y * w + x; + heights[i] = op(heights[i], map->getValues()[i]); + } + } + } + } + return 0; +} + +template class Op> +static int l_unaryop_func(lua::State* L) { + Op op; + if (auto heightmap = touserdata(L, 1)) { + uint w = heightmap->getWidth(); + uint h = heightmap->getHeight(); + auto heights = heightmap->getValues(); + for (uint y = 0; y < h; y++) { + for (uint x = 0; x < w; x++) { + uint i = y * w + x; + heights[i] = op(heights[i]); + } + } + } + return 0; +} + +static int l_resize(lua::State* L) { + if (auto heightmap = touserdata(L, 1)) { + uint width = touinteger(L, 2); + uint height = touinteger(L, 3); + auto interpName = tostring(L, 4); + auto interpolation = InterpolationType::NEAREST; + if (!std::strcmp(interpName, "linear")) { + interpolation = InterpolationType::LINEAR; + } + heightmap->getHeightmap()->resize(width, height, interpolation); + } + return 0; +} + +static int l_crop(lua::State* L) { + if (auto heightmap = touserdata(L, 1)) { + uint srcX = touinteger(L, 2); + uint srcY = touinteger(L, 3); + + uint dstWidth = touinteger(L, 4); + uint dstHeight = touinteger(L, 5); + + heightmap->getHeightmap()->crop(srcX, srcY, dstWidth, dstHeight); + } + return 0; +} + +static std::unordered_map methods { + {"dump", lua::wrap}, + {"noise", lua::wrap>}, + {"cellnoise", lua::wrap>}, + {"pow", lua::wrap>}, + {"add", lua::wrap>}, + {"mul", lua::wrap>}, + {"min", lua::wrap>}, + {"max", lua::wrap>}, + {"abs", lua::wrap>}, + {"resize", lua::wrap}, + {"crop", lua::wrap}, + {"at", lua::wrap}, +}; + +static int l_meta_meta_call(lua::State* L) { + auto width = tointeger(L, 2); + auto height = tointeger(L, 3); + if (width <= 0 || height <= 0) { + throw std::runtime_error("width and height must be greather than 0"); + } + return newuserdata( + L, static_cast(width), static_cast(height) + ); +} + +static int l_meta_index(lua::State* L) { + auto map = touserdata(L, 1); + if (map == nullptr) { + return 0; + } + if (isstring(L, 2)) { + auto fieldname = tostring(L, 2); + if (!std::strcmp(fieldname, "width")) { + return pushinteger(L, map->getWidth()); + } else if (!std::strcmp(fieldname, "height")) { + return pushinteger(L, map->getHeight()); + } else { + auto found = methods.find(tostring(L, 2)); + if (found != methods.end()) { + return pushcfunction(L, found->second); + } + } + } + return 0; +} + +static int l_meta_newindex(lua::State* L) { + auto map = touserdata(L, 1); + if (map == nullptr) { + return 0; + } + if (isstring(L, 2)) { + auto fieldname = tostring(L, 2); + if (!std::strcmp(fieldname, "noiseSeed")) { + map->setSeed(tointeger(L, 3)); + } + } + return 0; +} + +static int l_meta_tostring(lua::State* L) { + auto map = touserdata(L, 1); + if (map == nullptr) { + return 0; + } + + std::stringstream stream; + stream << std::hex << reinterpret_cast(map); + auto ptrstr = stream.str(); + + return pushstring( + L, "Heightmap(" + std::to_string(map->getWidth()) + + "*" + std::to_string(map->getHeight()) + " at 0x" + ptrstr + ")" + ); +} + +int LuaHeightmap::createMetatable(lua::State* L) { + createtable(L, 0, 3); + pushcfunction(L, lua::wrap); + setfield(L, "__tostring"); + pushcfunction(L, lua::wrap); + setfield(L, "__index"); + pushcfunction(L, lua::wrap); + setfield(L, "__newindex"); + + createtable(L, 0, 1); + pushcfunction(L, lua::wrap); + setfield(L, "__call"); + setmetatable(L); + return 1; +} diff --git a/src/logic/scripting/lua/usertypes/lua_type_voxelstructure.cpp b/src/logic/scripting/lua/usertypes/lua_type_voxelstructure.cpp new file mode 100644 index 00000000..eb6a24ab --- /dev/null +++ b/src/logic/scripting/lua/usertypes/lua_type_voxelstructure.cpp @@ -0,0 +1,58 @@ +#include "../lua_custom_types.hpp" + +#include "../lua_util.hpp" + +#include "world/generator/VoxelFragment.hpp" +#include "util/stringutil.hpp" + +using namespace lua; + +LuaVoxelFragment::LuaVoxelFragment(std::shared_ptr fragment) + : fragment(std::move(fragment)) {} + +LuaVoxelFragment::~LuaVoxelFragment() { +} + +static int l_crop(lua::State* L) { + if (auto fragment = touserdata(L, 1)) { + fragment->getFragment()->crop(); + } + return 0; +} + +static std::unordered_map methods { + {"crop", lua::wrap}, +}; + +static int l_meta_tostring(lua::State* L) { + return pushstring(L, "VoxelFragment(0x" + util::tohex( + reinterpret_cast(topointer(L, 1)))+")"); +} + +static int l_meta_index(lua::State* L) { + auto fragment = touserdata(L, 1); + if (fragment == nullptr) { + return 0; + } + if (isstring(L, 2)) { + auto fieldname = tostring(L, 2); + if (!std::strcmp(fieldname, "size")) { + return pushivec(L, fragment->getFragment()->getSize()); + } else { + auto found = methods.find(tostring(L, 2)); + if (found != methods.end()) { + return pushcfunction(L, found->second); + } + } + } + return 0; +} + +int LuaVoxelFragment::createMetatable(lua::State* L) { + createtable(L, 0, 2); + pushcfunction(L, lua::wrap); + setfield(L, "__tostring"); + pushcfunction(L, lua::wrap); + setfield(L, "__index"); + return 1; +} diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 1bb7eaec..42120534 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -3,6 +3,7 @@ #include #include +#include "scripting_commons.hpp" #include "content/Content.hpp" #include "content/ContentPack.hpp" #include "debug/Logger.hpp" @@ -14,6 +15,9 @@ #include "items/ItemDef.hpp" #include "logic/BlocksController.hpp" #include "logic/LevelController.hpp" +#include "lua/lua_engine.hpp" +#include "lua/lua_custom_types.hpp" +#include "maths/Heightmap.hpp" #include "objects/Entities.hpp" #include "objects/EntityDef.hpp" #include "objects/Player.hpp" @@ -21,7 +25,6 @@ #include "util/timeutil.hpp" #include "voxels/Block.hpp" #include "world/Level.hpp" -#include "lua/lua_engine.hpp" using namespace scripting; @@ -36,11 +39,11 @@ const ContentIndices* scripting::indices = nullptr; BlocksController* scripting::blocks = nullptr; LevelController* scripting::controller = nullptr; -static void load_script(const fs::path& name, bool throwable) { +void scripting::load_script(const fs::path& name, bool throwable) { auto paths = scripting::engine->getPaths(); fs::path file = paths->getResourcesFolder() / fs::path("scripts") / name; std::string src = files::read_string(file); - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); lua::loadbuffer(L, 0, src, file.u8string()); if (throwable) { lua::call(L, 0, 0); @@ -49,9 +52,17 @@ static void load_script(const fs::path& name, bool throwable) { } } +int scripting::load_script( + int env, const std::string& type, const fs::path& file +) { + std::string src = files::read_string(file); + logger.info() << "script (" << type << ") " << file.u8string(); + return lua::execute(lua::get_main_state(), env, src, file.u8string()); +} + void scripting::initialize(Engine* engine) { scripting::engine = engine; - lua::initialize(); + lua::initialize(*engine->getPaths()); load_script(fs::path("stdlib.lua"), true); load_script(fs::path("stdcmd.lua"), true); @@ -65,7 +76,7 @@ void scripting::initialize(Engine* engine) { [[nodiscard]] scriptenv scripting::create_pack_environment( const ContentPack& pack ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); int id = lua::create_environment(L, 0); lua::pushenv(L, id); lua::pushvalue(L, -1); @@ -74,7 +85,7 @@ void scripting::initialize(Engine* engine) { lua::setfield(L, "PACK_ID"); lua::pop(L); return std::shared_ptr(new int(id), [=](int* id) { //-V508 - lua::removeEnvironment(L, *id); + lua::remove_environment(L, *id); delete id; }); } @@ -82,7 +93,7 @@ void scripting::initialize(Engine* engine) { [[nodiscard]] scriptenv scripting::create_doc_environment( const scriptenv& parent, const std::string& name ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); int id = lua::create_environment(L, *parent); lua::pushenv(L, id); lua::pushvalue(L, -1); @@ -101,7 +112,7 @@ void scripting::initialize(Engine* engine) { } lua::pop(L); return std::shared_ptr(new int(id), [=](int* id) { //-V508 - lua::removeEnvironment(L, *id); + lua::remove_environment(L, *id); delete id; }); } @@ -109,7 +120,7 @@ void scripting::initialize(Engine* engine) { [[nodiscard]] static scriptenv create_component_environment( const scriptenv& parent, int entityIdx, const std::string& name ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); int id = lua::create_environment(L, *parent); lua::pushvalue(L, entityIdx); @@ -131,13 +142,13 @@ void scripting::initialize(Engine* engine) { lua::pop(L); return std::shared_ptr(new int(id), [=](int* id) { //-V508 - lua::removeEnvironment(L, *id); + lua::remove_environment(L, *id); delete id; }); } void scripting::process_post_runnables() { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); if (lua::getglobal(L, "__process_post_runnables")) { lua::call_nothrow(L, 0); } @@ -151,28 +162,28 @@ void scripting::on_world_load(LevelController* controller) { scripting::controller = controller; load_script("world.lua", false); - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); for (auto& pack : scripting::engine->getContentPacks()) { lua::emit_event(L, pack.id + ".worldopen"); } } void scripting::on_world_tick() { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); for (auto& pack : scripting::engine->getContentPacks()) { lua::emit_event(L, pack.id + ".worldtick"); } } void scripting::on_world_save() { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); for (auto& pack : scripting::engine->getContentPacks()) { lua::emit_event(L, pack.id + ".worldsave"); } } void scripting::on_world_quit() { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); for (auto& pack : scripting::engine->getContentPacks()) { lua::emit_event(L, pack.id + ".worldquit"); } @@ -197,21 +208,21 @@ void scripting::on_world_quit() { void scripting::on_blocks_tick(const Block& block, int tps) { std::string name = block.name + ".blockstick"; - lua::emit_event(lua::get_main_thread(), name, [tps](auto L) { + lua::emit_event(lua::get_main_state(), name, [tps](auto L) { return lua::pushinteger(L, tps); }); } void scripting::update_block(const Block& block, int x, int y, int z) { std::string name = block.name + ".update"; - lua::emit_event(lua::get_main_thread(), name, [x, y, z](auto L) { + lua::emit_event(lua::get_main_state(), name, [x, y, z](auto L) { return lua::pushivec_stack(L, glm::ivec3(x, y, z)); }); } void scripting::random_update_block(const Block& block, int x, int y, int z) { std::string name = block.name + ".randupdate"; - lua::emit_event(lua::get_main_thread(), name, [x, y, z](auto L) { + lua::emit_event(lua::get_main_state(), name, [x, y, z](auto L) { return lua::pushivec_stack(L, glm::ivec3(x, y, z)); }); } @@ -220,7 +231,7 @@ void scripting::on_block_placed( Player* player, const Block& block, int x, int y, int z ) { std::string name = block.name + ".placed"; - lua::emit_event(lua::get_main_thread(), name, [x, y, z, player](auto L) { + lua::emit_event(lua::get_main_state(), name, [x, y, z, player](auto L) { lua::pushivec_stack(L, glm::ivec3(x, y, z)); lua::pushinteger(L, player ? player->getId() : -1); return 4; @@ -234,7 +245,7 @@ void scripting::on_block_placed( for (auto& [packid, pack] : content->getPacks()) { if (pack->worldfuncsset.onblockplaced) { lua::emit_event( - lua::get_main_thread(), + lua::get_main_state(), packid + ".blockplaced", world_event_args ); @@ -248,7 +259,7 @@ void scripting::on_block_broken( if (block.rt.funcsset.onbroken) { std::string name = block.name + ".broken"; lua::emit_event( - lua::get_main_thread(), + lua::get_main_state(), name, [x, y, z, player](auto L) { lua::pushivec_stack(L, glm::ivec3(x, y, z)); @@ -266,7 +277,7 @@ void scripting::on_block_broken( for (auto& [packid, pack] : content->getPacks()) { if (pack->worldfuncsset.onblockbroken) { lua::emit_event( - lua::get_main_thread(), + lua::get_main_state(), packid + ".blockbroken", world_event_args ); @@ -278,7 +289,7 @@ bool scripting::on_block_interact( Player* player, const Block& block, glm::ivec3 pos ) { std::string name = block.name + ".interact"; - return lua::emit_event(lua::get_main_thread(), name, [pos, player](auto L) { + return lua::emit_event(lua::get_main_state(), name, [pos, player](auto L) { lua::pushivec_stack(L, pos); lua::pushinteger(L, player->getId()); return 4; @@ -288,7 +299,7 @@ bool scripting::on_block_interact( bool scripting::on_item_use(Player* player, const ItemDef& item) { std::string name = item.name + ".use"; return lua::emit_event( - lua::get_main_thread(), + lua::get_main_state(), name, [player](lua::State* L) { return lua::pushinteger(L, player->getId()); } ); @@ -299,7 +310,7 @@ bool scripting::on_item_use_on_block( ) { std::string name = item.name + ".useon"; return lua::emit_event( - lua::get_main_thread(), + lua::get_main_state(), name, [ipos, normal, player](auto L) { lua::pushivec_stack(L, ipos); @@ -315,7 +326,7 @@ bool scripting::on_item_break_block( ) { std::string name = item.name + ".blockbreakby"; return lua::emit_event( - lua::get_main_thread(), + lua::get_main_state(), name, [x, y, z, player](auto L) { lua::pushivec_stack(L, glm::ivec3(x, y, z)); @@ -328,7 +339,7 @@ bool scripting::on_item_break_block( dv::value scripting::get_component_value( const scriptenv& env, const std::string& name ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); lua::pushenv(L, *env); if (lua::getfield(L, name)) { return lua::tovalue(L, -1); @@ -343,7 +354,7 @@ void scripting::on_entity_spawn( const dv::value& args, const dv::value& saved ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); lua::requireglobal(L, STDCOMP); if (lua::getfield(L, "new_Entity")) { lua::pushinteger(L, eid); @@ -411,7 +422,7 @@ static void process_entity_callback( const std::string& name, std::function args ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); lua::pushenv(L, *env); if (lua::getfield(L, name)) { if (args) { @@ -441,7 +452,7 @@ void scripting::on_entity_despawn(const Entity& entity) { process_entity_callback( entity, "on_despawn", &entity_funcs_set::on_despawn, nullptr ); - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); lua::get_from(L, "stdcomp", "remove_Entity", true); lua::pushinteger(L, entity.getUID()); lua::call(L, 1, 0); @@ -541,7 +552,7 @@ void scripting::on_entity_used(const Entity& entity, Player* player) { } void scripting::on_entities_update(int tps, int parts, int part) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); lua::get_from(L, STDCOMP, "update", true); lua::pushinteger(L, tps); lua::pushinteger(L, parts); @@ -551,7 +562,7 @@ void scripting::on_entities_update(int tps, int parts, int part) { } void scripting::on_entities_render(float delta) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); lua::get_from(L, STDCOMP, "render", true); lua::pushnumber(L, delta); lua::call_nothrow(L, 1, 0); @@ -564,7 +575,7 @@ void scripting::on_ui_open( auto argsptr = std::make_shared>(std::move(args)); std::string name = layout->getId() + ".open"; - lua::emit_event(lua::get_main_thread(), name, [=](auto L) { + lua::emit_event(lua::get_main_state(), name, [=](auto L) { for (const auto& value : *argsptr) { lua::pushvalue(L, value); } @@ -576,7 +587,7 @@ void scripting::on_ui_progress( UiDocument* layout, int workDone, int workTotal ) { std::string name = layout->getId() + ".progress"; - lua::emit_event(lua::get_main_thread(), name, [=](auto L) { + lua::emit_event(lua::get_main_state(), name, [=](auto L) { lua::pushinteger(L, workDone); lua::pushinteger(L, workTotal); return 2; @@ -585,7 +596,7 @@ void scripting::on_ui_progress( void scripting::on_ui_close(UiDocument* layout, Inventory* inventory) { std::string name = layout->getId() + ".close"; - lua::emit_event(lua::get_main_thread(), name, [inventory](auto L) { + lua::emit_event(lua::get_main_state(), name, [inventory](auto L) { return lua::pushinteger(L, inventory ? inventory->getId() : 0); }); } @@ -593,7 +604,7 @@ void scripting::on_ui_close(UiDocument* layout, Inventory* inventory) { bool scripting::register_event( int env, const std::string& name, const std::string& id ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); if (lua::pushenv(L, env) == 0) { lua::pushglobals(L); } @@ -615,15 +626,7 @@ bool scripting::register_event( } int scripting::get_values_on_stack() { - return lua::gettop(lua::get_main_thread()); -} - -static void load_script( - int env, const std::string& type, const fs::path& file -) { - std::string src = files::read_string(file); - logger.info() << "script (" << type << ") " << file.u8string(); - lua::execute(lua::get_main_thread(), env, src, file.u8string()); + return lua::gettop(lua::get_main_state()); } void scripting::load_block_script( @@ -633,7 +636,7 @@ void scripting::load_block_script( block_funcs_set& funcsset ) { int env = *senv; - load_script(env, "block", file); + lua::pop(lua::get_main_state(), load_script(env, "block", file)); funcsset.init = register_event(env, "init", prefix + ".init"); funcsset.update = register_event(env, "on_update", prefix + ".update"); funcsset.randupdate = @@ -653,7 +656,7 @@ void scripting::load_item_script( item_funcs_set& funcsset ) { int env = *senv; - load_script(env, "item", file); + lua::pop(lua::get_main_state(), load_script(env, "item", file)); funcsset.init = register_event(env, "init", prefix + ".init"); funcsset.on_use = register_event(env, "on_use", prefix + ".use"); funcsset.on_use_on_block = @@ -665,7 +668,7 @@ void scripting::load_item_script( void scripting::load_entity_component( const std::string& name, const fs::path& file ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); std::string src = files::read_string(file); logger.info() << "script (component) " << file.u8string(); lua::loadbuffer(L, 0, src, "C!" + name); @@ -679,7 +682,7 @@ void scripting::load_world_script( world_funcs_set& funcsset ) { int env = *senv; - load_script(env, "world", file); + lua::pop(lua::get_main_state(), load_script(env, "world", file)); register_event(env, "init", prefix + ".init"); register_event(env, "on_world_open", prefix + ".worldopen"); register_event(env, "on_world_tick", prefix + ".worldtick"); @@ -698,7 +701,7 @@ void scripting::load_layout_script( uidocscript& script ) { int env = *senv; - load_script(env, "layout", file); + lua::pop(lua::get_main_state(), load_script(env, "layout", file)); script.onopen = register_event(env, "on_open", prefix + ".open"); script.onprogress = register_event(env, "on_progress", prefix + ".progress"); diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index 223dd67b..a34c21cf 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -32,6 +32,8 @@ class BlocksController; class LevelController; class Entity; struct EntityDef; +class GeneratorScript; +struct GeneratorDef; namespace scripting { extern Engine* engine; @@ -144,6 +146,12 @@ namespace scripting { void load_entity_component(const std::string& name, const fs::path& file); + std::unique_ptr load_generator( + const GeneratorDef& def, + const fs::path& file, + const std::string& dirPath + ); + /// @brief Load package-specific world script /// @param env environment /// @param packid content-pack id diff --git a/src/logic/scripting/scripting_commons.hpp b/src/logic/scripting/scripting_commons.hpp new file mode 100644 index 00000000..93e05ca7 --- /dev/null +++ b/src/logic/scripting/scripting_commons.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace scripting { + void load_script(const std::filesystem::path& name, bool throwable); + + [[nodiscard]] + int load_script(int env, const std::string& type, const std::filesystem::path& file); +} diff --git a/src/logic/scripting/scripting_functional.cpp b/src/logic/scripting/scripting_functional.cpp index 02398b49..d09c9432 100644 --- a/src/logic/scripting/scripting_functional.cpp +++ b/src/logic/scripting/scripting_functional.cpp @@ -12,7 +12,7 @@ static debug::Logger logger("scripting_func"); runnable scripting::create_runnable( const scriptenv& env, const std::string& src, const std::string& file ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); try { lua::loadbuffer(L, *env, src, file); return lua::create_runnable(L); @@ -25,7 +25,7 @@ runnable scripting::create_runnable( static lua::State* process_callback( const scriptenv& env, const std::string& src, const std::string& file ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); try { if (lua::eval(L, *env, src, file) != 0) { return L; @@ -163,7 +163,7 @@ vec2supplier scripting::create_vec2_supplier( value_to_string_func scripting::create_tostring( const scriptenv& env, const std::string& src, const std::string& file ) { - auto L = lua::get_main_thread(); + auto L = lua::get_main_state(); try { lua::loadbuffer(L, *env, src, file); lua::call(L, 0, 1); diff --git a/src/logic/scripting/scripting_hud.cpp b/src/logic/scripting/scripting_hud.cpp index f48e42da..a711453c 100644 --- a/src/logic/scripting/scripting_hud.cpp +++ b/src/logic/scripting/scripting_hud.cpp @@ -5,7 +5,7 @@ #include "files/files.hpp" #include "frontend/hud.hpp" #include "objects/Player.hpp" -#include "lua/api_lua.hpp" +#include "lua/libs/api_lua.hpp" #include "lua/lua_engine.hpp" #include "scripting.hpp" @@ -17,11 +17,11 @@ Hud* scripting::hud = nullptr; void scripting::on_frontend_init(Hud* hud) { scripting::hud = hud; - lua::openlib(lua::get_main_thread(), "hud", hudlib); + lua::openlib(lua::get_main_state(), "hud", hudlib); for (auto& pack : engine->getContentPacks()) { lua::emit_event( - lua::get_main_thread(), + lua::get_main_state(), pack.id + ".hudopen", [&](lua::State* L) { return lua::pushinteger(L, hud->getPlayer()->getId()); @@ -33,7 +33,7 @@ void scripting::on_frontend_init(Hud* hud) { void scripting::on_frontend_render() { for (auto& pack : engine->getContentPacks()) { lua::emit_event( - lua::get_main_thread(), + lua::get_main_state(), pack.id + ".hudrender", [&](lua::State* L) { return 0; } ); @@ -43,7 +43,7 @@ void scripting::on_frontend_render() { void scripting::on_frontend_close() { for (auto& pack : engine->getContentPacks()) { lua::emit_event( - lua::get_main_thread(), + lua::get_main_state(), pack.id + ".hudclose", [&](lua::State* L) { return lua::pushinteger(L, hud->getPlayer()->getId()); @@ -60,7 +60,7 @@ void scripting::load_hud_script( std::string src = files::read_string(file); logger.info() << "loading script " << file.u8string(); - lua::execute(lua::get_main_thread(), env, src, file.u8string()); + lua::execute(lua::get_main_state(), env, src, file.u8string()); register_event(env, "init", packid + ".init"); register_event(env, "on_hud_open", packid + ".hudopen"); diff --git a/src/logic/scripting/scripting_world_generation.cpp b/src/logic/scripting/scripting_world_generation.cpp new file mode 100644 index 00000000..a1865e48 --- /dev/null +++ b/src/logic/scripting/scripting_world_generation.cpp @@ -0,0 +1,239 @@ +#include "scripting.hpp" + +#include +#include + +#include "scripting_commons.hpp" +#include "typedefs.hpp" +#include "lua/lua_engine.hpp" +#include "lua/lua_custom_types.hpp" +#include "content/Content.hpp" +#include "voxels/Block.hpp" +#include "voxels/Chunk.hpp" +#include "data/dv.hpp" +#include "world/generator/GeneratorDef.hpp" +#include "util/timeutil.hpp" +#include "files/files.hpp" +#include "engine.hpp" +#include "debug/Logger.hpp" + +using namespace lua; + +static debug::Logger logger("generator-scripting"); + +class LuaGeneratorScript : public GeneratorScript { + State* L; + const GeneratorDef& def; + scriptenv env; +public: + LuaGeneratorScript(State* L, const GeneratorDef& def, scriptenv env) + : L(L), def(def), env(std::move(env)) { + } + + virtual ~LuaGeneratorScript() { + env.reset(); + if (L != get_main_state()) { + close(L); + } + } + + std::shared_ptr generateHeightmap( + const glm::ivec2& offset, const glm::ivec2& size, uint64_t seed, uint bpd + ) override { + pushenv(L, *env); + if (getfield(L, "generate_heightmap")) { + pushivec_stack(L, offset); + pushivec_stack(L, size); + pushinteger(L, seed); + pushinteger(L, bpd); + if (call_nothrow(L, 6)) { + auto map = touserdata(L, -1)->getHeightmap(); + pop(L, 2); + return map; + } + } + pop(L); + return std::make_shared(size.x, size.y); + } + + std::vector> generateParameterMaps( + const glm::ivec2& offset, const glm::ivec2& size, uint64_t seed, uint bpd + ) override { + std::vector> maps; + + uint biomeParameters = def.biomeParameters; + pushenv(L, *env); + if (getfield(L, "generate_biome_parameters")) { + pushivec_stack(L, offset); + pushivec_stack(L, size); + pushinteger(L, seed); + pushinteger(L, bpd); + if (call_nothrow(L, 6, biomeParameters)) { + for (int i = biomeParameters-1; i >= 0; i--) { + maps.push_back( + touserdata(L, -1-i)->getHeightmap()); + } + pop(L, 1+biomeParameters); + return maps; + } + } + pop(L); + for (uint i = 0; i < biomeParameters; i++) { + maps.push_back(std::make_shared(size.x, size.y)); + } + return maps; + } + + void perform_line(lua::State* L, std::vector& placements) { + rawgeti(L, 2); + blockid_t block = touinteger(L, -1); + pop(L); + + rawgeti(L, 3); + glm::ivec3 a = tovec3(L, -1); + pop(L); + + rawgeti(L, 4); + glm::ivec3 b = tovec3(L, -1); + pop(L); + + rawgeti(L, 5); + int radius = touinteger(L, -1); + pop(L); + + int priority = 0; + if (objlen(L, -1) >= 6) { + rawgeti(L, 6); + priority = tointeger(L, -1); + pop(L); + } + + placements.emplace_back(priority, LinePlacement {block, a, b, radius}); + } + + void perform_placement(lua::State* L, std::vector& placements) { + rawgeti(L, 1); + int structIndex = 0; + if (isstring(L, -1)) { + const char* name = require_string(L, -1); + if (!std::strcmp(name, ":line")) { + pop(L); + + perform_line(L, placements); + return; + } + const auto& found = def.structuresIndices.find(name); + if (found != def.structuresIndices.end()) { + structIndex = found->second; + } + } else { + structIndex = tointeger(L, -1); + } + pop(L); + + rawgeti(L, 2); + glm::ivec3 pos = tovec3(L, -1); + pop(L); + + rawgeti(L, 3); + uint8_t rotation = tointeger(L, -1) & 0b11; + pop(L); + + int priority = 1; + if (objlen(L, -1) >= 4) { + rawgeti(L, 4); + priority = tointeger(L, -1); + pop(L); + } + + placements.emplace_back( + priority, StructurePlacement {structIndex, pos, rotation} + ); + } + + std::vector placeStructuresWide( + const glm::ivec2& offset, + const glm::ivec2& size, + uint64_t seed, + uint chunkHeight + ) override { + std::vector placements {}; + + stackguard _(L); + pushenv(L, *env); + if (getfield(L, "place_structures_wide")) { + pushivec_stack(L, offset); + pushivec_stack(L, size); + pushinteger(L, seed); + pushinteger(L, chunkHeight); + if (call_nothrow(L, 6, 1)) { + int len = objlen(L, -1); + for (int i = 1; i <= len; i++) { + rawgeti(L, i); + + perform_placement(L, placements); + + pop(L); + } + pop(L); + } + } + return placements; + } + + std::vector placeStructures( + const glm::ivec2& offset, const glm::ivec2& size, uint64_t seed, + const std::shared_ptr& heightmap, uint chunkHeight + ) override { + std::vector placements {}; + + stackguard _(L); + pushenv(L, *env); + if (getfield(L, "place_structures")) { + pushivec_stack(L, offset); + pushivec_stack(L, size); + pushinteger(L, seed); + newuserdata(L, heightmap); + pushinteger(L, chunkHeight); + if (call_nothrow(L, 7, 1)) { + int len = objlen(L, -1); + for (int i = 1; i <= len; i++) { + rawgeti(L, i); + + perform_placement(L, placements); + + pop(L); + } + pop(L); + } + } + return placements; + } +}; + +std::unique_ptr scripting::load_generator( + const GeneratorDef& def, const fs::path& file, const std::string& dirPath +) { + auto L = create_state(*engine->getPaths(), StateType::GENERATOR); + auto env = create_environment(L); + stackguard _(L); + + pushenv(L, *env); + pushstring(L, dirPath); + setfield(L, "__DIR__"); + pushstring(L, dirPath + "/script.lua"); + setfield(L, "__FILE__"); + + pop(L); + + if (fs::exists(file)) { + std::string src = files::read_string(file); + logger.info() << "script (generator) " << file.u8string(); + pop(L, execute(L, *env, src, file.u8string())); + } else { + // Use default (empty) script + pop(L, execute(L, *env, "", "")); + } + + return std::make_unique(L, def, std::move(env)); +} diff --git a/src/maths/Heightmap.cpp b/src/maths/Heightmap.cpp new file mode 100644 index 00000000..d3b048de --- /dev/null +++ b/src/maths/Heightmap.cpp @@ -0,0 +1,107 @@ +#include "Heightmap.hpp" + +#include +#include +#include +#include + +static inline float smootherstep(float x) { + return glm::smoothstep(std::floor(x), std::floor(x)+1, x); +} + +static inline float sample_at( + const float* buffer, + uint width, + uint x, uint y +) { + return buffer[y*width+x]; +} + +static inline float sample_at( + const float* buffer, + uint width, uint height, + float x, float y, + InterpolationType interp +) { + // std::floor is redundant here because x and y are positive values + uint ix = static_cast(x); + uint iy = static_cast(y); + float val = buffer[iy*width+ix]; + if (interp == InterpolationType::NEAREST) { + return val; + } + float tx = x - ix; + float ty = y - iy; + + switch (interp) { + case InterpolationType::LINEAR: { + float s00 = val; + float s10 = sample_at(buffer, width, + ix + 1 < width ? ix + 1 : ix, iy); + float s01 = sample_at(buffer, width, ix, + iy + 1 < height ? iy + 1 : iy); + float s11 = sample_at(buffer, width, + ix + 1 < width ? ix + 1 : ix, iy + 1 < height ? iy + 1 : iy); + + float a00 = s00; + float a10 = s10 - s00; + float a01 = s01 - s00; + float a11 = s11 - s10 - s01 + s00; + + return a00 + a10*tx + a01*ty + a11*tx*ty; + } + // TODO: implement CUBIC (Bicubic) interpolation + default: + throw std::runtime_error("interpolation type is not implemented"); + } + return val; +} + +void Heightmap::resize( + uint dstwidth, uint dstheight, InterpolationType interp +) { + if (width == dstwidth && height == dstheight) { + return; + } + std::vector dst; + dst.resize(dstwidth*dstheight); + + uint index = 0; + for (uint y = 0; y < dstheight; y++) { + for (uint x = 0; x < dstwidth; x++, index++) { + float sx = static_cast(x) / dstwidth * width; + float sy = static_cast(y) / dstheight * height; + dst[index] = sample_at(buffer.data(), width, height, sx, sy, interp); + } + } + + width = dstwidth; + height = dstheight; + buffer = std::move(dst); +} + +void Heightmap::crop( + uint srcx, uint srcy, uint dstwidth, uint dstheight +) { + if (srcx + dstwidth > width || srcy + dstheight > height) { + throw std::runtime_error( + "crop zone is not fully inside of the source image"); + } + if (dstwidth == width && dstheight == height) { + return; + } + + std::vector dst; + dst.resize(dstwidth*dstheight); + + for (uint y = 0; y < dstheight; y++) { + std::memcpy( + dst.data()+y*dstwidth, + buffer.data()+(y+srcy)*width+srcx, + dstwidth*sizeof(float)); + } + + width = dstwidth; + height = dstheight; + buffer = std::move(dst); +} diff --git a/src/maths/Heightmap.hpp b/src/maths/Heightmap.hpp new file mode 100644 index 00000000..8e7e5594 --- /dev/null +++ b/src/maths/Heightmap.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +#include "typedefs.hpp" +#include "maths/Heightmap.hpp" + +enum class InterpolationType { + NEAREST, + LINEAR, + CUBIC, +}; + +class Heightmap { + std::vector buffer; + uint width, height; +public: + Heightmap(uint width, uint height) + : width(width), height(height) { + buffer.resize(width*height); + } + + Heightmap(uint width, uint height, std::vector buffer) + : width(width), height(height), buffer(std::move(buffer)) {} + + ~Heightmap() = default; + + void resize(uint width, uint height, InterpolationType interpolation); + + void crop(uint srcX, uint srcY, uint dstWidth, uint dstHeight); + + uint getWidth() const { + return width; + } + + uint getHeight() const { + return height; + } + + float get(uint x, uint y) { + return buffer.at(y * width + x); + } + + float getUnchecked(uint x, uint y) { + return buffer[y * width + x]; + } + + float* getValues() { + return buffer.data(); + } + + const float* getValues() const { + return buffer.data(); + } +}; diff --git a/src/maths/util.hpp b/src/maths/util.hpp index 5e671082..7500bf2a 100644 --- a/src/maths/util.hpp +++ b/src/maths/util.hpp @@ -4,49 +4,130 @@ #include -class PseudoRandom { - unsigned short seed; -public: - PseudoRandom() { - seed = (unsigned short)time(0); +#define GLM_ENABLE_EXPERIMENTAL +#include +#include + +namespace util { + constexpr inline float EPSILON = 1e-6f; + + class PseudoRandom { + unsigned short seed; + public: + PseudoRandom() { + seed = static_cast(time(0)); + } + + int rand() { + seed = (seed + 0x7ed5 + (seed << 6)); + seed = (seed ^ 0xc23c ^ (seed >> 9)); + seed = (seed + 0x1656 + (seed << 3)); + seed = ((seed + 0xa264) ^ (seed << 4)); + seed = (seed + 0xfd70 - (seed << 3)); + seed = (seed ^ 0xba49 ^ (seed >> 8)); + + return static_cast(seed); + } + + int32_t rand32() { + return (rand() << 16) | rand(); + } + + uint32_t randU32() { + return (rand() << 16) | rand(); + } + + int64_t rand64() { + uint64_t x = randU32(); + uint64_t y = randU32(); + return (x << 32ULL) | y; + } + + uint64_t randU64() { + uint64_t x = randU32(); + uint64_t y = randU32(); + return (x << 32ULL) | y; + } + + float randFloat() { + return randU32() / static_cast(UINT32_MAX); + } + + double randDouble() { + return randU64() / static_cast(UINT64_MAX); + } + + void setSeed(int number) { + seed = (static_cast(number * 23729) ^ + static_cast(number + 16786)); + rand(); + } + void setSeed(int number1, int number2) { + seed = ((static_cast(number1 * 23729) | + static_cast(number2 % 16786)) ^ + static_cast(number2 * number1)); + rand(); + } + }; + + /// @return integer square of distance between two points + /// @note glm::distance2 does not support integer vectors + inline int distance2(const glm::ivec3& a, const glm::ivec3& b) { + return (b.x - a.x) * (b.x - a.x) + + (b.y - a.y) * (b.y - a.y) + + (b.z - a.z) * (b.z - a.z); } - int rand() { - seed = (seed + 0x7ed5 + (seed << 6)); - seed = (seed ^ 0xc23c ^ (seed >> 9)); - seed = (seed + 0x1656 + (seed << 3)); - seed = ((seed + 0xa264) ^ (seed << 4)); - seed = (seed + 0xfd70 - (seed << 3)); - seed = (seed ^ 0xba49 ^ (seed >> 8)); - - return (int)seed; + /// @return integer square of distance between two points + inline int distance2(int ax, int ay, int az, int bx, int by, int bz) { + return (bx - ax) * (bx - ax) + + (by - ay) * (by - ay) + + (bz - az) * (bz - az); } - int32_t rand32() { - return (rand() << 16) | rand(); + /// @return integer square of vector length + /// @note glm::length2 does not support integer vectors + inline int length2(const glm::ivec3& a) { + return a.x * a.x + a.y * a.y + a.z * a.z; } - uint32_t randU32() { - return (rand() << 16) | rand(); + /// @return integer square of vector length + inline int length2(int x, int y, int z) { + return x * x + y * y + z * z; } - int64_t rand64() { - uint64_t x = randU32(); - uint64_t y = randU32(); - return (x << 32ULL) | y; + /// @brief Find nearest point on segment to given + /// @param a segment point A + /// @param b segment point B + /// @param point given point (may be anywhere) + /// @return nearest point on the segment to given point + inline glm::vec3 closest_point_on_segment( + const glm::vec3& a, const glm::vec3& b, const glm::vec3& point + ) { + auto vec = b - a; + float da = glm::distance2(point, a); + float db = glm::distance2(point, b); + float len = glm::length2(vec); + float t = (((da - db) / len) * 0.5f + 0.5f); + t = std::min(1.0f, std::max(0.0f, t)); + return a + vec * t; } - void setSeed(int number) { - seed = - ((unsigned short)(number * 23729) ^ (unsigned short)(number + 16786) - ); - rand(); + /// @brief Find nearest point on segment to given + /// @param a segment point A + /// @param b segment point B + /// @param point given point (may be anywhere) + /// @note this overload is actually faster (comment out method to compare) + /// @return nearest point on the segment to given point + inline glm::ivec3 closest_point_on_segment( + const glm::ivec3& a, const glm::ivec3& b, const glm::ivec3& point + ) { + auto vec = b - a; + float da = distance2(point, a); + float db = distance2(point, b); + float len = length2(vec); + float t = (((da - db) / len) * 0.5f + 0.5f); + t = std::min(1.0f, std::max(0.0f, t)); + return a + glm::ivec3(glm::vec3(vec) * t); } - void setSeed(int number1, int number2) { - seed = - (((unsigned short)(number1 * 23729) | - (unsigned short)(number2 % 16786)) ^ - (unsigned short)(number2 * number1)); - rand(); - } -}; +} diff --git a/src/objects/Player.cpp b/src/objects/Player.cpp index 0f1df818..223c1828 100644 --- a/src/objects/Player.cpp +++ b/src/objects/Player.cpp @@ -16,13 +16,14 @@ #include "world/Level.hpp" #include "data/dv_util.hpp" -const float CROUCH_SPEED_MUL = 0.35f; -const float RUN_SPEED_MUL = 1.5f; -const float PLAYER_GROUND_DAMPING = 10.0f; -const float PLAYER_AIR_DAMPING = 7.0f; -const float FLIGHT_SPEED_MUL = 4.0f; -const float CHEAT_SPEED_MUL = 5.0f; -const float JUMP_FORCE = 8.0f; +constexpr float CROUCH_SPEED_MUL = 0.35f; +constexpr float RUN_SPEED_MUL = 1.5f; +constexpr float PLAYER_GROUND_DAMPING = 10.0f; +constexpr float PLAYER_AIR_DAMPING = 7.0f; +constexpr float FLIGHT_SPEED_MUL = 4.0f; +constexpr float CHEAT_SPEED_MUL = 5.0f; +constexpr float JUMP_FORCE = 8.0f; +constexpr int SPAWN_ATTEMPTS_PER_UPDATE = 64; Player::Player( Level* level, @@ -37,9 +38,9 @@ Player::Player( position(position), inventory(std::move(inv)), eid(eid), - camera(level->getCamera("base:first-person")), - spCamera(level->getCamera("base:third-person-front")), - tpCamera(level->getCamera("base:third-person-back")), + camera(level->getCamera("core:first-person")), + spCamera(level->getCamera("core:third-person-front")), + tpCamera(level->getCamera("core:third-person-back")), currentCamera(camera) { camera->setFov(glm::radians(90.0f)); spCamera->setFov(glm::radians(90.0f)); @@ -156,7 +157,9 @@ void Player::postUpdate() { flight = false; } if (spawnpoint.y <= 0.1) { - attemptToFindSpawnpoint(); + for (int i = 0; i < SPAWN_ATTEMPTS_PER_UPDATE; i++) { + attemptToFindSpawnpoint(); + } } auto& skeleton = entity->getSkeleton(); diff --git a/src/util/AreaMap2D.hpp b/src/util/AreaMap2D.hpp new file mode 100644 index 00000000..a14a0a5f --- /dev/null +++ b/src/util/AreaMap2D.hpp @@ -0,0 +1,198 @@ +#pragma once + +#include +#include +#include +#include + +namespace util { + + template + class AreaMap2D { + public: + using OutCallback = std::function; + private: + TCoord offsetX = 0, offsetY = 0; + TCoord sizeX, sizeY; + std::vector firstBuffer; + std::vector secondBuffer; + OutCallback outCallback; + + size_t valuesCount = 0; + + void translate(TCoord dx, TCoord dy) { + if (dx == 0 && dy == 0) { + return; + } + std::fill(secondBuffer.begin(), secondBuffer.end(), T{}); + for (TCoord y = 0; y < sizeY; y++) { + for (TCoord x = 0; x < sizeX; x++) { + auto& value = firstBuffer[y * sizeX + x]; + auto nx = x - dx; + auto ny = y - dy; + if (value == T{}) { + continue; + } + if (nx < 0 || ny < 0 || nx >= sizeX || ny >= sizeY) { + if (outCallback) { + outCallback(x + offsetX, y + offsetY, value); + } + valuesCount--; + continue; + } + secondBuffer[ny * sizeX + nx] = value; + } + } + std::swap(firstBuffer, secondBuffer); + offsetX += dx; + offsetY += dy; + } + public: + AreaMap2D(TCoord width, TCoord height) + : sizeX(width), sizeY(height), + firstBuffer(width * height), secondBuffer(width * height) { + } + + const T* getIf(TCoord x, TCoord y) const { + auto lx = x - offsetX; + auto ly = y - offsetY; + if (lx < 0 || ly < 0 || lx >= sizeX || ly >= sizeY) { + return nullptr; + } + return &firstBuffer[ly * sizeX + lx]; + } + + T get(TCoord x, TCoord y) const { + auto lx = x - offsetX; + auto ly = y - offsetY; + if (lx < 0 || ly < 0 || lx >= sizeX || ly >= sizeY) { + return T{}; + } + return firstBuffer[ly * sizeX + lx]; + } + + T get(TCoord x, TCoord y, const T& def) const { + if (auto ptr = getIf(x, y)) { + const auto& value = *ptr; + if (value == T{}) { + return def; + } + return value; + } + return def; + } + + bool isInside(TCoord x, TCoord y) const { + auto lx = x - offsetX; + auto ly = y - offsetY; + return !(lx < 0 || ly < 0 || lx >= sizeX || ly >= sizeY); + } + + const T& require(TCoord x, TCoord y) const { + auto lx = x - offsetX; + auto ly = y - offsetY; + if (lx < 0 || ly < 0 || lx >= sizeX || ly >= sizeY) { + throw std::invalid_argument("position is out of window"); + } + return firstBuffer[ly * sizeX + lx]; + } + + bool set(TCoord x, TCoord y, T value) { + auto lx = x - offsetX; + auto ly = y - offsetY; + if (lx < 0 || ly < 0 || lx >= sizeX || ly >= sizeY) { + return false; + } + auto& element = firstBuffer[ly * sizeX + lx]; + if (value && !element) { + valuesCount++; + } + if (element && !value) { + valuesCount--; + } + element = std::move(value); + return true; + } + + void setOutCallback(const OutCallback& callback) { + outCallback = callback; + } + + void resize(TCoord newSizeX, TCoord newSizeY) { + if (newSizeX < sizeX) { + TCoord delta = sizeX - newSizeX; + translate(delta / 2, 0); + translate(-delta, 0); + translate(delta, 0); + } + if (newSizeY < sizeY) { + TCoord delta = sizeY - newSizeY; + translate(0, delta / 2); + translate(0, -delta); + translate(0, delta); + } + const TCoord newVolume = newSizeX * newSizeY; + std::vector newFirstBuffer(newVolume); + std::vector newSecondBuffer(newVolume); + for (TCoord y = 0; y < sizeY && y < newSizeY; y++) { + for (TCoord x = 0; x < sizeX && x < newSizeX; x++) { + newFirstBuffer[y * newSizeX + x] = firstBuffer[y * sizeX + x]; + } + } + sizeX = newSizeX; + sizeY = newSizeY; + firstBuffer = std::move(newFirstBuffer); + secondBuffer = std::move(newSecondBuffer); + } + + void setCenter(TCoord centerX, TCoord centerY) { + auto deltaX = centerX - (offsetX + sizeX / 2); + auto deltaY = centerY - (offsetY + sizeY / 2); + if (deltaX | deltaY) { + translate(deltaX, deltaY); + } + } + + void clear() { + for (TCoord y = 0; y < sizeY; y++) { + for (TCoord x = 0; x < sizeX; x++) { + auto i = y * sizeX + x; + auto value = firstBuffer[i]; + firstBuffer[i] = {}; + if (outCallback) { + outCallback(x + offsetX, y + offsetY, value); + } + } + } + valuesCount = 0; + } + + TCoord getOffsetX() const { + return offsetX; + } + + TCoord getOffsetY() const { + return offsetY; + } + + TCoord getWidth() const { + return sizeX; + } + + TCoord getHeight() const { + return sizeX; + } + + const std::vector& getBuffer() const { + return firstBuffer; + } + + size_t count() const { + return valuesCount; + } + + TCoord area() const { + return sizeX * sizeY; + } + }; +} diff --git a/src/util/functional_util.hpp b/src/util/functional_util.hpp new file mode 100644 index 00000000..2672c4fc --- /dev/null +++ b/src/util/functional_util.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace util { + template + struct pow { + constexpr T operator()(T a, T b) const { + return glm::pow(a, b); + } + }; + + template + struct min { + constexpr T operator()(T a, T b) const { + return glm::min(a, b); + } + }; + + template + struct max { + constexpr T operator()(T a, T b) const { + return glm::max(a, b); + } + }; + + template + struct abs { + constexpr T operator()(T a) const { + return glm::abs(a); + } + }; +} diff --git a/src/util/listutil.hpp b/src/util/listutil.hpp index dec9499b..b3df6deb 100644 --- a/src/util/listutil.hpp +++ b/src/util/listutil.hpp @@ -1,15 +1,20 @@ #pragma once #include -#include #include #include namespace util { template - bool contains(const std::vector& vec, const T& value) { + inline bool contains(const std::vector& vec, const T& value) { return std::find(vec.begin(), vec.end(), value) != vec.end(); } + template + inline void concat(std::vector& a, const std::vector& b) { + a.reserve(a.size() + b.size()); + a.insert(a.end(), b.begin(), b.end()); + } + std::string to_string(const std::vector& vec); } diff --git a/src/util/stringutil.cpp b/src/util/stringutil.cpp index 4452301b..c104bd5e 100644 --- a/src/util/stringutil.cpp +++ b/src/util/stringutil.cpp @@ -312,13 +312,17 @@ std::string util::base64_encode(const ubyte* data, size_t size) { return ss.str(); } -std::string util::mangleid(uint64_t value) { - // todo: use base64 +std::string util::tohex(uint64_t value) { std::stringstream ss; ss << std::hex << value; return ss.str(); } +std::string util::mangleid(uint64_t value) { + // todo: use base64 + return tohex(value); +} + util::Buffer util::base64_decode(const char* str, size_t size) { util::Buffer bytes((size / 4) * 3); ubyte* dst = bytes.data(); @@ -401,6 +405,13 @@ std::wstring util::capitalized(const std::wstring& str) { str.substr(1); } +std::string util::capitalized(const std::string& str) { + if (str.empty()) return str; + static const std::locale loc(""); + return std::string({static_cast(std::toupper(str[0], loc))}) + + str.substr(1); +} + std::wstring util::pascal_case(const std::wstring& str) { if (str.empty()) return str; static const std::locale loc(""); diff --git a/src/util/stringutil.hpp b/src/util/stringutil.hpp index 21d58e5e..c7bc2fb5 100644 --- a/src/util/stringutil.hpp +++ b/src/util/stringutil.hpp @@ -60,6 +60,8 @@ namespace util { util::Buffer base64_decode(const char* str, size_t size); util::Buffer base64_decode(const std::string& str); + std::string tohex(uint64_t value); + std::string mangleid(uint64_t value); int replaceAll( @@ -69,6 +71,8 @@ namespace util { double parse_double(const std::string& str); double parse_double(const std::string& str, size_t offset, size_t len); + std::string capitalized(const std::string& str); + std::wstring lower_case(const std::wstring& str); std::wstring upper_case(const std::wstring& str); std::wstring capitalized(const std::wstring& str); diff --git a/src/voxels/Block.hpp b/src/voxels/Block.hpp index 3ba6785a..e9b91664 100644 --- a/src/voxels/Block.hpp +++ b/src/voxels/Block.hpp @@ -181,6 +181,9 @@ public: /// @brief Block script name in blocks/ without extension std::string scriptName = name.substr(name.find(':') + 1); + /// @brief Block will be used instead of this if generated on surface + std::string surfaceReplacement = name; + /// @brief Default block layout will be used by hud.open_block(...) std::string uiLayout = name; @@ -214,6 +217,8 @@ public: /// @brief picking item integer id itemid_t pickingItem = 0; + + blockid_t surfaceReplacement = 0; } rt {}; Block(const std::string& name); diff --git a/src/voxels/Chunks.cpp b/src/voxels/Chunks.cpp index 7196670f..df00d72f 100644 --- a/src/voxels/Chunks.cpp +++ b/src/voxels/Chunks.cpp @@ -21,12 +21,11 @@ #include "world/LevelEvents.hpp" #include "Block.hpp" #include "Chunk.hpp" -#include "WorldGenerator.hpp" #include "voxel.hpp" Chunks::Chunks( - uint32_t w, - uint32_t d, + int32_t w, + int32_t d, int32_t ox, int32_t oz, WorldFiles* wfile, @@ -34,34 +33,31 @@ Chunks::Chunks( ) : level(level), indices(level->content->getIndices()), - chunks(w * d), - chunksSecond(w * d), - w(w), - d(d), - ox(ox), - oz(oz), + areaMap(w, d), worldFiles(wfile) { - volume = static_cast(w) * static_cast(d); - chunksCount = 0; + areaMap.setCenter(ox-w/2, oz-d/2); + areaMap.setOutCallback([this](int, int, const auto& chunk) { + save(chunk.get()); + }); } voxel* Chunks::get(int32_t x, int32_t y, int32_t z) const { - x -= ox * CHUNK_W; - z -= oz * CHUNK_D; - int cx = floordiv(x, CHUNK_W); - int cy = floordiv(y, CHUNK_H); - int cz = floordiv(z, CHUNK_D); - if (cx < 0 || cy < 0 || cz < 0 || cx >= int(w) || cy >= 1 || cz >= int(d)) { + if (y < 0 || y >= CHUNK_H) { return nullptr; } - auto& chunk = chunks[cz * w + cx]; // not thread safe + int cx = floordiv(x, CHUNK_W); + int cz = floordiv(z, CHUNK_D); + auto ptr = areaMap.getIf(cx, cz); + if (ptr == nullptr) { + return nullptr; + } + Chunk* chunk = ptr->get(); // not thread safe if (chunk == nullptr) { return nullptr; } int lx = x - cx * CHUNK_W; - int ly = y - cy * CHUNK_H; int lz = z - cz * CHUNK_D; - return &chunk->voxels[(ly * CHUNK_D + lz) * CHUNK_W + lx]; + return &chunk->voxels[(y * CHUNK_D + lz) * CHUNK_W + lx]; } const AABB* Chunks::isObstacleAt(float x, float y, float z) { @@ -88,8 +84,8 @@ const AABB* Chunks::isObstacleAt(float x, float y, float z) { def.rotatable ? def.rt.hitboxes[v->state.rotation] : def.hitboxes; for (const auto& hitbox : boxes) { if (hitbox.contains( - {x - ix - offset.x, y - iy - offset.y, z - iz - offset.z} - )) { + {x - ix - offset.x, y - iy - offset.y, z - iz - offset.z} + )) { return &hitbox; } } @@ -116,61 +112,62 @@ bool Chunks::isObstacleBlock(int32_t x, int32_t y, int32_t z) { } ubyte Chunks::getLight(int32_t x, int32_t y, int32_t z, int channel) { - x -= ox * CHUNK_W; - z -= oz * CHUNK_D; - int cx = floordiv(x, CHUNK_W); - int cy = floordiv(y, CHUNK_H); - int cz = floordiv(z, CHUNK_D); - if (cx < 0 || cy < 0 || cz < 0 || cx >= int(w) || cy >= 1 || cz >= int(d)) { + if (y < 0 || y >= CHUNK_H) { return 0; } - const auto& chunk = chunks[(cy * d + cz) * w + cx]; + int cx = floordiv(x, CHUNK_W); + int cz = floordiv(z, CHUNK_D); + + auto ptr = areaMap.getIf(cx, cz); + if (ptr == nullptr) { + return 0; + } + Chunk* chunk = ptr->get(); if (chunk == nullptr) { return 0; } int lx = x - cx * CHUNK_W; - int ly = y - cy * CHUNK_H; int lz = z - cz * CHUNK_D; - return chunk->lightmap.get(lx, ly, lz, channel); + return chunk->lightmap.get(lx, y, lz, channel); } light_t Chunks::getLight(int32_t x, int32_t y, int32_t z) { - x -= ox * CHUNK_W; - z -= oz * CHUNK_D; - int cx = floordiv(x, CHUNK_W); - int cy = floordiv(y, CHUNK_H); - int cz = floordiv(z, CHUNK_D); - if (cx < 0 || cy < 0 || cz < 0 || cx >= int(w) || cy >= 1 || cz >= int(d)) { + if (y < 0 || y >= CHUNK_H) { return 0; } - const auto& chunk = chunks[(cy * d + cz) * w + cx]; + int cx = floordiv(x, CHUNK_W); + int cz = floordiv(z, CHUNK_D); + + auto ptr = areaMap.getIf(cx, cz); + if (ptr == nullptr) { + return 0; + } + Chunk* chunk = ptr->get(); if (chunk == nullptr) { return 0; } int lx = x - cx * CHUNK_W; - int ly = y - cy * CHUNK_H; int lz = z - cz * CHUNK_D; - return chunk->lightmap.get(lx, ly, lz); + return chunk->lightmap.get(lx, y, lz); } Chunk* Chunks::getChunkByVoxel(int32_t x, int32_t y, int32_t z) { - if (y < 0 || y >= CHUNK_H) return nullptr; - x -= ox * CHUNK_W; - z -= oz * CHUNK_D; + if (y < 0 || y >= CHUNK_H) { + return nullptr; + } int cx = floordiv(x, CHUNK_W); int cz = floordiv(z, CHUNK_D); - if (cx < 0 || cz < 0 || cx >= int(w) || cz >= int(d)) return nullptr; - return chunks[cz * w + cx].get(); + if (auto ptr = areaMap.getIf(cx, cz)) { + return ptr->get(); + } + return nullptr; } Chunk* Chunks::getChunk(int x, int z) { - x -= ox; - z -= oz; - if (x < 0 || z < 0 || x >= static_cast(w) || - z >= static_cast(d)) { - return nullptr; + if (auto ptr = areaMap.getIf(x, z)) { + return ptr->get(); } - return chunks[z * w + x].get(); + return nullptr; } glm::ivec3 Chunks::seekOrigin( @@ -355,17 +352,13 @@ void Chunks::set( if (y < 0 || y >= CHUNK_H) { return; } - int32_t gx = x; - int32_t gz = z; - x -= ox * CHUNK_W; - z -= oz * CHUNK_D; int cx = floordiv(x, CHUNK_W); int cz = floordiv(z, CHUNK_D); - if (cx < 0 || cz < 0 || cx >= static_cast(w) || - cz >= static_cast(d)) { + auto ptr = areaMap.getIf(cx, cz); + if (ptr == nullptr) { return; } - Chunk* chunk = chunks[cz * w + cx].get(); + Chunk* chunk = ptr->get(); if (chunk == nullptr) { return; } @@ -380,7 +373,7 @@ void Chunks::set( chunk->removeBlockInventory(lx, y, lz); } if (prevdef.rt.extended && !vox.state.segment) { - eraseSegments(prevdef, vox.state, gx, y, gz); + eraseSegments(prevdef, vox.state, x, y, z); } if (prevdef.dataStruct) { if (auto found = chunk->blocksMetadata.find(index)) { @@ -396,7 +389,7 @@ void Chunks::set( vox.state = state; chunk->setModifiedAndUnsaved(); if (!state.segment && newdef.rt.extended) { - repairSegments(newdef, state, gx, y, gz); + repairSegments(newdef, state, x, y, z); } if (y < chunk->bottom) @@ -406,15 +399,18 @@ void Chunks::set( else if (id == 0) chunk->updateHeights(); - if (lx == 0 && (chunk = getChunk(cx + ox - 1, cz + oz))) + if (lx == 0 && (chunk = getChunk(cx - 1, cz))) { chunk->flags.modified = true; - if (lz == 0 && (chunk = getChunk(cx + ox, cz + oz - 1))) + } + if (lz == 0 && (chunk = getChunk(cx, cz - 1))) { chunk->flags.modified = true; - - if (lx == CHUNK_W - 1 && (chunk = getChunk(cx + ox + 1, cz + oz))) + } + if (lx == CHUNK_W - 1 && (chunk = getChunk(cx, cz))) { chunk->flags.modified = true; - if (lz == CHUNK_D - 1 && (chunk = getChunk(cx + ox, cz + oz + 1))) + } + if (lz == CHUNK_D - 1 && (chunk = getChunk(cx, cz + 1))) { chunk->flags.modified = true; + } } voxel* Chunks::rayCast( @@ -435,9 +431,9 @@ voxel* Chunks::rayCast( float dz = dir.z; float t = 0.0f; - int ix = floor(px); - int iy = floor(py); - int iz = floor(pz); + int ix = std::floor(px); + int iy = std::floor(py); + int iz = std::floor(pz); int stepx = (dx > 0.0f) ? 1 : -1; int stepy = (dy > 0.0f) ? 1 : -1; @@ -445,9 +441,9 @@ voxel* Chunks::rayCast( constexpr float infinity = std::numeric_limits::infinity(); constexpr float epsilon = 1e-6f; // 0.000001 - float txDelta = (fabs(dx) < epsilon) ? infinity : abs(1.0f / dx); - float tyDelta = (fabs(dy) < epsilon) ? infinity : abs(1.0f / dy); - float tzDelta = (fabs(dz) < epsilon) ? infinity : abs(1.0f / dz); + float txDelta = (std::fabs(dx) < epsilon) ? infinity : std::fabs(1.0f / dx); + float tyDelta = (std::fabs(dy) < epsilon) ? infinity : std::fabs(1.0f / dy); + float tzDelta = (std::fabs(dz) < epsilon) ? infinity : std::fabs(1.0f / dz); float xdist = (stepx > 0) ? (ix + 1 - px) : (px - ix); float ydist = (stepy > 0) ? (iy + 1 - py) : (py - iy); @@ -578,9 +574,9 @@ glm::vec3 Chunks::rayCastToObstacle( constexpr float infinity = std::numeric_limits::infinity(); constexpr float epsilon = 1e-6f; // 0.000001 - float txDelta = (fabs(dx) < epsilon) ? infinity : abs(1.0f / dx); - float tyDelta = (fabs(dy) < epsilon) ? infinity : abs(1.0f / dy); - float tzDelta = (fabs(dz) < epsilon) ? infinity : abs(1.0f / dz); + float txDelta = (std::fabs(dx) < epsilon) ? infinity : std::fabs(1.0f / dx); + float tyDelta = (std::fabs(dy) < epsilon) ? infinity : std::fabs(1.0f / dy); + float tzDelta = (std::fabs(dz) < epsilon) ? infinity : std::fabs(1.0f / dz); float xdist = (stepx > 0) ? (ix + 1 - px) : (px - ix); float ydist = (stepy > 0) ? (iy + 1 - py) : (py - iy); @@ -653,97 +649,19 @@ glm::vec3 Chunks::rayCastToObstacle( } void Chunks::setCenter(int32_t x, int32_t z) { - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); - cx -= ox + w / 2; - cz -= oz + d / 2; - if (cx | cz) { - translate(cx, cz); - } -} - -void Chunks::translate(int32_t dx, int32_t dz) { - for (uint i = 0; i < volume; i++) { - chunksSecond[i] = nullptr; - } - for (uint32_t z = 0; z < d; z++) { - for (uint32_t x = 0; x < w; x++) { - auto& chunk = chunks[z * w + x]; - int nx = x - dx; - int nz = z - dz; - if (chunk == nullptr) continue; - if (nx < 0 || nz < 0 || nx >= static_cast(w) || - nz >= static_cast(d)) { - level->events->trigger(EVT_CHUNK_HIDDEN, chunk.get()); - save(chunk.get()); - chunksCount--; - continue; - } - chunksSecond[nz * w + nx] = chunk; - } - } - std::swap(chunks, chunksSecond); - - ox += dx; - oz += dz; + areaMap.setCenter(floordiv(x, CHUNK_W), floordiv(z, CHUNK_D)); } void Chunks::resize(uint32_t newW, uint32_t newD) { - if (newW < w) { - int delta = w - newW; - translate(delta / 2, 0); - translate(-delta, 0); - translate(delta, 0); - } - if (newD < d) { - int delta = d - newD; - translate(0, delta / 2); - translate(0, -delta); - translate(0, delta); - } - const int newVolume = newW * newD; - std::vector> newChunks(newVolume); - std::vector> newChunksSecond(newVolume); - for (int z = 0; z < static_cast(d) && z < static_cast(newD); - z++) { - for (int x = 0; x < static_cast(w) && x < static_cast(newW); - x++) { - newChunks[z * newW + x] = chunks[z * w + x]; - } - } - w = newW; - d = newD; - volume = newVolume; - chunks = std::move(newChunks); - chunksSecond = std::move(newChunksSecond); -} - -void Chunks::_setOffset(int32_t x, int32_t z) { - ox = x; - oz = z; + areaMap.resize(newW, newD); } bool Chunks::putChunk(const std::shared_ptr& chunk) { - int x = chunk->x; - int z = chunk->z; - x -= ox; - z -= oz; - if (x < 0 || z < 0 || x >= static_cast(w) || - z >= static_cast(d)) { - return false; - } - chunks[z * w + x] = chunk; - chunksCount++; - return true; + return areaMap.set(chunk->x, chunk->z, chunk); } void Chunks::saveAndClear() { - for (size_t i = 0; i < volume; i++) { - auto chunk = chunks[i].get(); - chunks[i] = nullptr; - save(chunk); - } - chunksCount = 0; + areaMap.clear(); } void Chunks::save(Chunk* chunk) { @@ -770,7 +688,8 @@ void Chunks::save(Chunk* chunk) { } void Chunks::saveAll() { - for (size_t i = 0; i < volume; i++) { + const auto& chunks = areaMap.getBuffer(); + for (size_t i = 0; i < areaMap.area(); i++) { if (auto& chunk = chunks[i]) { save(chunk.get()); } diff --git a/src/voxels/Chunks.hpp b/src/voxels/Chunks.hpp index 0d3e07e4..6e73193a 100644 --- a/src/voxels/Chunks.hpp +++ b/src/voxels/Chunks.hpp @@ -9,6 +9,7 @@ #include "typedefs.hpp" #include "voxel.hpp" +#include "util/AreaMap2D.hpp" class VoxelRenderer; @@ -33,19 +34,15 @@ class Chunks { void setRotationExtended( const Block& def, blockstate state, glm::ivec3 origin, uint8_t rotation ); + + util::AreaMap2D, int32_t> areaMap; public: - std::vector> chunks; - std::vector> chunksSecond; - size_t volume; - size_t chunksCount; size_t visible = 0; - uint32_t w, d; - int32_t ox, oz; WorldFiles* worldFiles; Chunks( - uint32_t w, - uint32_t d, + int32_t w, + int32_t d, int32_t ox, int32_t oz, WorldFiles* worldFiles, @@ -105,14 +102,38 @@ public: bool isReplaceableBlock(int32_t x, int32_t y, int32_t z); bool isObstacleBlock(int32_t x, int32_t y, int32_t z); - // does not move chunks inside - void _setOffset(int32_t x, int32_t z); - void setCenter(int32_t x, int32_t z); - void translate(int32_t x, int32_t z); void resize(uint32_t newW, uint32_t newD); void saveAndClear(); void save(Chunk* chunk); void saveAll(); + + const std::vector>& getChunks() const { + return areaMap.getBuffer(); + } + + int getWidth() const { + return areaMap.getWidth(); + } + + int getHeight() const { + return areaMap.getHeight(); + } + + int getOffsetX() const { + return areaMap.getOffsetX(); + } + + int getOffsetY() const { + return areaMap.getOffsetY(); + } + + size_t getChunksCount() const { + return areaMap.count(); + } + + size_t getVolume() const { + return areaMap.area(); + } }; diff --git a/src/voxels/ChunksStorage.cpp b/src/voxels/ChunksStorage.cpp index 75925a38..75b37b9e 100644 --- a/src/voxels/ChunksStorage.cpp +++ b/src/voxels/ChunksStorage.cpp @@ -108,10 +108,10 @@ void ChunksStorage::getVoxels(VoxelsVolume* volume, bool backlight) const { int ecz = floordiv(z + d, CHUNK_D); int cw = ecx - scx + 1; - int ch = ecz - scz + 1; + int cd = ecz - scz + 1; - // cw*ch chunks will be scanned - for (int cz = scz; cz < scz + ch; cz++) { + // cw*cd chunks will be scanned + for (int cz = scz; cz < scz + cd; cz++) { for (int cx = scx; cx < scx + cw; cx++) { const auto& found = chunksMap.find(glm::ivec2(cx, cz)); if (found == chunksMap.end()) { diff --git a/src/voxels/DefaultWorldGenerator.cpp b/src/voxels/DefaultWorldGenerator.cpp deleted file mode 100644 index dc5361e8..00000000 --- a/src/voxels/DefaultWorldGenerator.cpp +++ /dev/null @@ -1,241 +0,0 @@ -#include "DefaultWorldGenerator.hpp" - -#include "Block.hpp" -#include "Chunk.hpp" -#include "voxel.hpp" - -#define FNL_IMPL -#include -#include - -#include -#include -#include -#include -#include - -#include "content/Content.hpp" -#include "core_defs.hpp" -#include "maths/FastNoiseLite.h" -#include "maths/util.hpp" -#include "maths/voxmaths.hpp" - -// will be refactored in generators update - -const int SEA_LEVEL = 55; - -enum class MAPS { SAND, TREE, CLIFF, HEIGHT }; -#define MAPS_LEN 4 - -class Map2D { - int x, z; - int w, d; - float* heights[MAPS_LEN]; -public: - Map2D(int x, int z, int w, int d) : x(x), z(z), w(w), d(d) { - for (int i = 0; i < MAPS_LEN; i++) heights[i] = new float[w * d]; - } - ~Map2D() { - for (int i = 0; i < MAPS_LEN; i++) delete[] heights[i]; - } - - inline float get(MAPS map, int x, int z) { - x -= this->x; - z -= this->z; - if (x < 0 || z < 0 || x >= w || z >= d) { - throw std::runtime_error("out of heightmap"); - } - return heights[(int)map][z * w + x]; - } - - inline void set(MAPS map, int x, int z, float value) { - x -= this->x; - z -= this->z; - if (x < 0 || z < 0 || x >= w || z >= d) { - throw std::runtime_error("out of heightmap"); - } - heights[(int)map][z * w + x] = value; - } -}; - -float calc_height(fnl_state* noise, int cur_x, int cur_z) { - float height = 0; - - height += fnlGetNoise2D( - noise, cur_x * 0.0125f * 8 - 125567, cur_z * 0.0125f * 8 + 3546 - ); - height += fnlGetNoise2D( - noise, cur_x * 0.025f * 8 + 4647, cur_z * 0.025f * 8 - 3436 - ) * - 0.5f; - height += fnlGetNoise2D( - noise, cur_x * 0.05f * 8 - 834176, cur_z * 0.05f * 8 + 23678 - ) * - 0.25f; - height += - fnlGetNoise2D( - noise, - cur_x * 0.2f * 8 + - fnlGetNoise2D( - noise, cur_x * 0.1f * 8 - 23557, cur_z * 0.1f * 8 - 6568 - ) * 50, - cur_z * 0.2f * 8 + - fnlGetNoise2D( - noise, cur_x * 0.1f * 8 + 4363, cur_z * 0.1f * 8 + 4456 - ) * 50 - ) * - fnlGetNoise2D(noise, cur_x * 0.01f - 834176, cur_z * 0.01f + 23678) * - 0.25; - height += - fnlGetNoise2D(noise, cur_x * 0.1f * 8 - 3465, cur_z * 0.1f * 8 + 4534) * - 0.125f; - height *= - fnlGetNoise2D(noise, cur_x * 0.1f + 1000, cur_z * 0.1f + 1000) * 0.5f + - 0.5f; - height += 1.0f; - height *= 64.0f; - return height; -} - -int generate_tree( - fnl_state* noise, - PseudoRandom* random, - Map2D& heights, - // Map2D& humidity, - int cur_x, - int cur_y, - int cur_z, - int tileSize, - blockid_t idWood, - blockid_t idLeaves -) { - const int tileX = floordiv(cur_x, tileSize); - const int tileZ = floordiv(cur_z, tileSize); - - random->setSeed(tileX * 4325261 + tileZ * 12160951 + tileSize * 9431111); - - int randomX = (random->rand() % (tileSize / 2)) - tileSize / 4; - int randomZ = (random->rand() % (tileSize / 2)) - tileSize / 4; - - int centerX = tileX * tileSize + tileSize / 2 + randomX; - int centerZ = tileZ * tileSize + tileSize / 2 + randomZ; - - bool gentree = - (random->rand() % 10) < heights.get(MAPS::TREE, centerX, centerZ) * 13; - if (!gentree) return 0; - - int height = (int)(heights.get(MAPS::HEIGHT, centerX, centerZ)); - if (height < SEA_LEVEL + 1) return 0; - int lx = cur_x - centerX; - int radius = random->rand() % 4 + 2; - int ly = cur_y - height - 3 * radius; - int lz = cur_z - centerZ; - if (lx == 0 && lz == 0 && cur_y - height < (3 * radius + radius / 2)) - return idWood; - if (lx * lx + ly * ly / 2 + lz * lz < radius * radius) return idLeaves; - return 0; -} - -void DefaultWorldGenerator::generate(voxel* voxels, int cx, int cz, int seed) { - const int treesTile = 12; - fnl_state noise = fnlCreateState(); - noise.noise_type = FNL_NOISE_OPENSIMPLEX2; - noise.seed = seed * 60617077 % 25896307; - PseudoRandom randomtree; - PseudoRandom randomgrass; - - int padding = 8; - Map2D heights( - cx * CHUNK_W - padding, - cz * CHUNK_D - padding, - CHUNK_W + padding * 2, - CHUNK_D + padding * 2 - ); - - for (int z = -padding; z < CHUNK_D + padding; z++) { - for (int x = -padding; x < CHUNK_W + padding; x++) { - int cur_x = x + cx * CHUNK_W; - int cur_z = z + cz * CHUNK_D; - float height = calc_height(&noise, cur_x, cur_z); - float hum = fnlGetNoise2D(&noise, cur_x * 0.3 + 633, cur_z * 0.3); - float sand = - fnlGetNoise2D(&noise, cur_x * 0.1 - 633, cur_z * 0.1 + 1000); - float cliff = pow((sand + abs(sand)) / 2, 2); - float w = pow(fmax(-abs(height - SEA_LEVEL) + 4, 0) / 6, 2) * cliff; - float h1 = -abs(height - SEA_LEVEL - 0.03); - float h2 = abs(height - SEA_LEVEL + 0.04); - float h = (h1 + h2) * 100; - height += (h * w); - heights.set(MAPS::HEIGHT, cur_x, cur_z, height); - heights.set(MAPS::TREE, cur_x, cur_z, hum); - heights.set(MAPS::SAND, cur_x, cur_z, sand); - heights.set(MAPS::CLIFF, cur_x, cur_z, cliff); - } - } - - for (int z = 0; z < CHUNK_D; z++) { - int cur_z = z + cz * CHUNK_D; - for (int x = 0; x < CHUNK_W; x++) { - int cur_x = x + cx * CHUNK_W; - float height = heights.get(MAPS::HEIGHT, cur_x, cur_z); - - for (int cur_y = 0; cur_y < CHUNK_H; cur_y++) { - // int cur_y = y; - int id = cur_y < SEA_LEVEL ? idWater : BLOCK_AIR; - blockstate state {}; - if ((cur_y == (int)height) && (SEA_LEVEL - 2 < cur_y)) { - id = idGrassBlock; - } else if (cur_y < (height - 6)) { - id = idStone; - } else if (cur_y < height) { - id = idDirt; - } else { - int tree = generate_tree( - &noise, - &randomtree, - heights, - cur_x, - cur_y, - cur_z, - treesTile, - idWood, - idLeaves - ); - if (tree) { - id = tree; - state.rotation = BLOCK_DIR_UP; - } - } - float sand = fmax( - heights.get(MAPS::SAND, cur_x, cur_z), - heights.get(MAPS::CLIFF, cur_x, cur_z) - ); - if (((height - (1.1 - 0.2 * pow(height - 54, 4)) + (5 * sand)) < - cur_y + (height - 0.01 - (int)height)) && - (cur_y < height)) { - id = idSand; - } - if (cur_y <= 2) id = idBazalt; - - randomgrass.setSeed(cur_x, cur_z); - if ((id == 0) && ((height > SEA_LEVEL + 0.4) || (sand > 0.1)) && - ((int)(height + 1) == cur_y) && - ((unsigned short)randomgrass.rand() > 56000)) { - id = idGrass; - } - if ((id == 0) && (height > SEA_LEVEL + 0.4) && - ((int)(height + 1) == cur_y) && - ((unsigned short)randomgrass.rand() > 65000)) { - id = idFlower; - } - if ((height > SEA_LEVEL + 1) && ((int)(height + 1) == cur_y) && - ((unsigned short)randomgrass.rand() > 65533)) { - id = idWood; - state.rotation = BLOCK_DIR_UP; - } - voxels[(cur_y * CHUNK_D + z) * CHUNK_W + x].id = id; - voxels[(cur_y * CHUNK_D + z) * CHUNK_W + x].state = state; - } - } - } -} diff --git a/src/voxels/DefaultWorldGenerator.hpp b/src/voxels/DefaultWorldGenerator.hpp deleted file mode 100644 index a53770c8..00000000 --- a/src/voxels/DefaultWorldGenerator.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "typedefs.hpp" -#include "WorldGenerator.hpp" - -struct voxel; -class Content; - -class DefaultWorldGenerator : WorldGenerator { -public: - DefaultWorldGenerator(const Content* content) : WorldGenerator(content) { - } - - void generate(voxel* voxels, int x, int z, int seed); -}; diff --git a/src/voxels/FlatWorldGenerator.cpp b/src/voxels/FlatWorldGenerator.cpp deleted file mode 100644 index d97ab545..00000000 --- a/src/voxels/FlatWorldGenerator.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "FlatWorldGenerator.hpp" - -#include "content/Content.hpp" -#include "core_defs.hpp" -#include "Chunk.hpp" -#include "voxel.hpp" - -void FlatWorldGenerator::generate(voxel* voxels, int cx, int cz, int seed) { - for (int z = 0; z < CHUNK_D; z++) { - for (int x = 0; x < CHUNK_W; x++) { - for (int cur_y = 0; cur_y < CHUNK_H; cur_y++) { - int id = BLOCK_AIR; - blockstate state {}; - - if (cur_y == 2) { - id = idBazalt; - } else if (cur_y == 6) { - id = idGrassBlock; - } else if (cur_y > 2 && cur_y <= 5) { - id = idDirt; - } - - voxels[(cur_y * CHUNK_D + z) * CHUNK_W + x].id = id; - voxels[(cur_y * CHUNK_D + z) * CHUNK_W + x].state = state; - } - } - } -} diff --git a/src/voxels/FlatWorldGenerator.hpp b/src/voxels/FlatWorldGenerator.hpp deleted file mode 100644 index 8d10ae4b..00000000 --- a/src/voxels/FlatWorldGenerator.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "typedefs.hpp" -#include "WorldGenerator.hpp" - -struct voxel; -class Content; - -class FlatWorldGenerator : WorldGenerator { -public: - FlatWorldGenerator(const Content* content) : WorldGenerator(content) { - } - - void generate(voxel* voxels, int x, int z, int seed); -}; diff --git a/src/voxels/WorldGenerator.cpp b/src/voxels/WorldGenerator.cpp deleted file mode 100644 index 525a04ca..00000000 --- a/src/voxels/WorldGenerator.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "WorldGenerator.hpp" - -#include "content/Content.hpp" -#include "Block.hpp" -#include "Chunk.hpp" -#include "voxel.hpp" - -WorldGenerator::WorldGenerator(const Content* content) - : idStone(content->blocks.require("base:stone").rt.id), - idDirt(content->blocks.require("base:dirt").rt.id), - idGrassBlock(content->blocks.require("base:grass_block").rt.id), - idSand(content->blocks.require("base:sand").rt.id), - idWater(content->blocks.require("base:water").rt.id), - idWood(content->blocks.require("base:wood").rt.id), - idLeaves(content->blocks.require("base:leaves").rt.id), - idGrass(content->blocks.require("base:grass").rt.id), - idFlower(content->blocks.require("base:flower").rt.id), - idBazalt(content->blocks.require("base:bazalt").rt.id) { -} diff --git a/src/voxels/WorldGenerator.hpp b/src/voxels/WorldGenerator.hpp deleted file mode 100644 index 7a8c98f3..00000000 --- a/src/voxels/WorldGenerator.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -#include "typedefs.hpp" - -struct voxel; -class Content; - -class WorldGenerator { -protected: - blockid_t const idStone; - blockid_t const idDirt; - blockid_t const idGrassBlock; - blockid_t const idSand; - blockid_t const idWater; - blockid_t const idWood; - blockid_t const idLeaves; - blockid_t const idGrass; - blockid_t const idFlower; - blockid_t const idBazalt; -public: - WorldGenerator(const Content* content); - virtual ~WorldGenerator() = default; - - virtual void generate(voxel* voxels, int x, int z, int seed) = 0; -}; diff --git a/src/world/Level.cpp b/src/world/Level.cpp index 2c2eef24..19d3ecb1 100644 --- a/src/world/Level.cpp +++ b/src/world/Level.cpp @@ -87,7 +87,7 @@ void Level::loadMatrix(int32_t x, int32_t z, uint32_t radius) { (settings.chunks.loadDistance.get() + settings.chunks.padding.get()) * 2LL ); - if (chunks->w != diameter) { + if (chunks->getWidth() != diameter) { chunks->resize(diameter, diameter); } } diff --git a/src/world/World.cpp b/src/world/World.cpp index 79a31155..5267561d 100644 --- a/src/world/World.cpp +++ b/src/world/World.cpp @@ -15,7 +15,8 @@ #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" #include "voxels/ChunksStorage.hpp" -#include "WorldGenerators.hpp" +#include "world/generator/WorldGenerator.hpp" +#include "world/generator/GeneratorDef.hpp" #include "Level.hpp" static debug::Logger logger("world"); @@ -205,9 +206,6 @@ void WorldInfo::deserialize(const dv::value& root) { generator = root["generator"].asString(generator); seed = root["seed"].asInteger(seed); - if (generator.empty()) { - generator = WorldGenerators::getDefaultGeneratorID(); - } if (root.has("version")) { auto& verobj = root["version"]; major = verobj["major"].asInteger(); diff --git a/src/world/WorldGenerators.cpp b/src/world/WorldGenerators.cpp deleted file mode 100644 index c64ab756..00000000 --- a/src/world/WorldGenerators.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "WorldGenerators.hpp" - -#include - -#include "content/Content.hpp" -#include "voxels/FlatWorldGenerator.hpp" -#include "voxels/WorldGenerator.hpp" - -std::vector WorldGenerators::getGeneratorsIDs() { - std::vector ids; - for (auto& entry : generators) { - ids.push_back(entry.first); - } - return ids; -} - -std::string WorldGenerators::getDefaultGeneratorID() { - return "core:default"; -} - -std::unique_ptr WorldGenerators::createGenerator( - const std::string& id, const Content* content -) { - auto found = generators.find(id); - if (found == generators.end()) { - throw std::runtime_error("unknown generator id: " + id); - } - return std::unique_ptr(found->second(content)); -} diff --git a/src/world/WorldGenerators.hpp b/src/world/WorldGenerators.hpp deleted file mode 100644 index 91c3a121..00000000 --- a/src/world/WorldGenerators.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "voxels/WorldGenerator.hpp" - -class Content; - -typedef WorldGenerator* (*gen_constructor)(const Content*); - -class WorldGenerators { - static inline std::map generators; -public: - template - static void addGenerator(std::string id); - - static std::vector getGeneratorsIDs(); - - static std::string getDefaultGeneratorID(); - - static std::unique_ptr createGenerator( - const std::string& id, const Content* content - ); -}; - -template -void WorldGenerators::addGenerator(std::string id) { - generators[id] = [](const Content* content) { - return (WorldGenerator*)new T(content); - }; -} diff --git a/src/world/generator/GeneratorDef.cpp b/src/world/generator/GeneratorDef.cpp new file mode 100644 index 00000000..1dda81d9 --- /dev/null +++ b/src/world/generator/GeneratorDef.cpp @@ -0,0 +1,37 @@ +#include "GeneratorDef.hpp" + +#include "VoxelFragment.hpp" +#include "content/Content.hpp" +#include "util/stringutil.hpp" +#include "voxels/Block.hpp" + +VoxelStructure::VoxelStructure( + VoxelStructureMeta meta, + std::unique_ptr structure +) : fragments({std::move(structure)}), meta(std::move(meta)) {} + +GeneratorDef::GeneratorDef(std::string name) + : name(std::move(name)), caption(util::id_to_caption(name)) { +} + +void GeneratorDef::prepare(const Content* content) { + for (auto& biome : biomes) { + for (auto& layer : biome.groundLayers.layers) { + layer.rt.id = content->blocks.require(layer.block).rt.id; + } + for (auto& layer : biome.seaLayers.layers) { + layer.rt.id = content->blocks.require(layer.block).rt.id; + } + for (auto& plant : biome.plants.entries) { + plant.rt.id = content->blocks.require(plant.name).rt.id; + } + for (auto& structure : biome.structures.entries) { + const auto& found = structuresIndices.find(structure.name); + if (found == structuresIndices.end()) { + throw std::runtime_error( + "no structure "+util::quote(structure.name)+" found"); + } + structure.rt.id = found->second; + } + } +} diff --git a/src/world/generator/GeneratorDef.hpp b/src/world/generator/GeneratorDef.hpp new file mode 100644 index 00000000..d978a41c --- /dev/null +++ b/src/world/generator/GeneratorDef.hpp @@ -0,0 +1,224 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "typedefs.hpp" +#include "maths/Heightmap.hpp" +#include "StructurePlacement.hpp" + +class Content; +class VoxelFragment; +struct GeneratorDef; + +struct VoxelStructureMeta { + std::string name; +}; + +struct BlocksLayer { + /// @brief Layer block + std::string block; + + /// @brief Layer height. -1 is resizeable layer + int height; + + /// @brief Layer can present under the sea level (default: true) else will + /// extend the next layer + bool belowSeaLevel; + + struct { + /// @brief Layer block index + blockid_t id; + } rt; +}; + +/// @brief Set of blocks layers with additional info +struct BlocksLayers { + std::vector layers; + + /// @brief Total height of all layers after the resizeable one + uint lastLayersHeight; +}; + +struct BiomeParameter { + /// @brief Central parameter value for the biome + float value; + /// @brief Parameter score multiplier + /// (the higher the weight, the greater the chance of biome choosing) + float weight; +}; + +struct WeightedEntry { + std::string name; + float weight; + + struct { + size_t id; + } rt; + + bool operator>(const WeightedEntry& other) const { + return weight > other.weight; + } +}; + +struct BiomeElementList { + static inline float MIN_CHANCE = 1e-6f; + + /// @brief Entries sorted by weight descending. + std::vector entries; + /// @brief Sum of weight values + float weightsSum = 0.0f; + /// @brief Value generation chance + float chance; + + BiomeElementList() {} + + BiomeElementList(std::vector entries, float chance) + : entries(entries), chance(chance) { + for (const auto& entry : entries) { + weightsSum += entry.weight; + } + } + + /// @brief Choose value based on weight + /// @param rand some random value in range [0, 1) + /// @return *.index of chosen value + inline size_t choose(float rand, size_t def=0) const { + if (entries.empty() || rand > chance || chance < MIN_CHANCE) { + return def; + } + rand = rand / chance; + rand *= weightsSum; + for (const auto& entry : entries) { + rand -= entry.weight; + if (rand <= 0.0f) { + return entry.rt.id; + } + } + return entries[entries.size()-1].rt.id; + } +}; + +struct Biome { + /// @brief Biome name + std::string name; + + std::vector parameters; + + /// @brief Plant is a single-block structure randomly generating in world + BiomeElementList plants; + + BiomeElementList structures; + + BlocksLayers groundLayers; + + BlocksLayers seaLayers; +}; + +/// @brief Generator behaviour and settings interface +class GeneratorScript { +public: + virtual ~GeneratorScript() = default; + + /// @brief Generate a heightmap with values in range 0..1 + /// @param offset position of the heightmap in the world + /// @param size size of the heightmap + /// @param seed world seed + /// @param bpd blocks per dot + /// @return generated heightmap (can't be nullptr) + virtual std::shared_ptr generateHeightmap( + const glm::ivec2& offset, + const glm::ivec2& size, + uint64_t seed, + uint bpd + ) = 0; + + /// @brief Generate a biomes parameters maps + /// @param offset position of maps in the world + /// @param size maps size + /// @param seed world seed + /// @param bpd blocks per dot + /// @return generated maps (can't be nullptr) + virtual std::vector> generateParameterMaps( + const glm::ivec2& offset, + const glm::ivec2& size, + uint64_t seed, + uint bpd + ) = 0; + + /// @brief Generate a list of structures placements. Structures may be + /// placed to nearest N chunks also (position of out area), where N is + /// wide-structs-chunks-radius + /// @param offset position of the area + /// @param size size of the area (blocks) + /// @param seed world seed + /// @param chunkHeight chunk height to use as heights multiplier + virtual std::vector placeStructuresWide( + const glm::ivec2& offset, + const glm::ivec2& size, + uint64_t seed, + uint chunkHeight + ) = 0; + + /// @brief Generate a list of structures placements. Structures may be + /// placed to nearest chunks also (position of out area). + /// @param offset position of the area + /// @param size size of the area (blocks) + /// @param seed world seed + /// @param heightmap area heightmap + /// @param chunkHeight chunk height to use as heights multiplier + /// @return structure & line placements + virtual std::vector placeStructures( + const glm::ivec2& offset, const glm::ivec2& size, uint64_t seed, + const std::shared_ptr& heightmap, uint chunkHeight) = 0; +}; + +/// @brief Structure voxel fragments and metadata +struct VoxelStructure { + VoxelStructureMeta meta; + /// @brief voxel fragment and 3 pre-calculated rotated versions + std::array, 4> fragments; + + VoxelStructure( + VoxelStructureMeta meta, + std::unique_ptr structure + ); +}; + +/// @brief Generator information +struct GeneratorDef { + /// @brief Generator full name - packid:name + std::string name; + /// @brief Generator display name + std::string caption; + + std::unique_ptr script; + + /// @brief Sea level (top of seaLayers) + uint seaLevel = 0; + + /// @brief Number of biome parameters, that biome choosing depending on + uint biomeParameters = 0; + + /// @brief Biome parameter map blocks per dot + uint biomesBPD = 8; + + /// @brief Heightmap blocks per dot + uint heightsBPD = 4; + + /// @brief Number of chunks must be generated before and after wide + /// structures placement triggered + uint wideStructsChunksRadius = 3; + + std::unordered_map structuresIndices; + std::vector> structures; + std::vector biomes; + + GeneratorDef(std::string name); + GeneratorDef(const GeneratorDef&) = delete; + + void prepare(const Content* content); +}; diff --git a/src/world/generator/StructurePlacement.hpp b/src/world/generator/StructurePlacement.hpp new file mode 100644 index 00000000..2ec7185e --- /dev/null +++ b/src/world/generator/StructurePlacement.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +struct StructurePlacement { + int structure; + glm::ivec3 position; + uint8_t rotation; + + StructurePlacement(int structure, glm::ivec3 position, uint8_t rotation) + : structure(structure), + position(std::move(position)), + rotation(rotation) { + } +}; + +struct LinePlacement { + blockid_t block; + glm::ivec3 a; + glm::ivec3 b; + int radius; + + LinePlacement(blockid_t block, glm::ivec3 a, glm::ivec3 b, int radius) + : block(block), a(std::move(a)), b(std::move(b)), radius(radius) { + } +}; + +struct Placement { + int priority; + std::variant placement; + + Placement( + int priority, + std::variant placement + ) : priority(priority), placement(std::move(placement)) {} +}; diff --git a/src/world/generator/SurroundMap.cpp b/src/world/generator/SurroundMap.cpp new file mode 100644 index 00000000..07fa7087 --- /dev/null +++ b/src/world/generator/SurroundMap.cpp @@ -0,0 +1,71 @@ +#include "SurroundMap.hpp" + +#include +#include +#include + +SurroundMap::SurroundMap(int maxLevelRadius, int8_t maxLevel) + : areaMap((maxLevelRadius + maxLevel) * 2 + 1, + (maxLevelRadius + maxLevel) * 2 + 1), + levelCallbacks(maxLevel), + maxLevel(maxLevel) +{} + +void SurroundMap::setLevelCallback(int8_t level, LevelCallback callback) { + auto& wrapper = levelCallbacks.at(level - 1); + wrapper.callback = callback; + wrapper.active = callback != nullptr; +} + +void SurroundMap::setOutCallback(util::AreaMap2D::OutCallback callback) { + areaMap.setOutCallback(callback); +} + +void SurroundMap::upgrade(int x, int y, int8_t level) { + auto& callback = levelCallbacks[level - 1]; + int size = maxLevel - level + 1; + for (int ly = -size+1; ly < size; ly++) { + for (int lx = -size+1; lx < size; lx++) { + int posX = lx + x; + int posY = ly + y; + int8_t sourceLevel = areaMap.get(posX, posY, 0); + if (sourceLevel < level-1) { + throw std::runtime_error("invalid map state"); + } + if (sourceLevel >= level) { + continue; + } + areaMap.set(posX, posY, level); + if (callback.active) { + callback.callback(posX, posY); + } + } + } +} + +void SurroundMap::resize(int maxLevelRadius) { + areaMap.resize((maxLevelRadius + maxLevel) * 2 + 1, + (maxLevelRadius + maxLevel) * 2 + 1); +} + +void SurroundMap::completeAt(int x, int y) { + if (!areaMap.isInside(x - maxLevel + 1, y - maxLevel + 1) || + !areaMap.isInside(x + maxLevel - 1, y + maxLevel - 1)) { + throw std::invalid_argument( + "upgrade square is not fully inside of area"); + } + for (int8_t level = 1; level <= maxLevel; level++) { + upgrade(x, y, level); + } +} + +void SurroundMap::setCenter(int x, int y) { + areaMap.setCenter(x, y); +} + +int8_t SurroundMap::at(int x, int y) { + if (auto ptr = areaMap.getIf(x, y)) { + return *ptr; + } + throw std::invalid_argument("position is out of area"); +} diff --git a/src/world/generator/SurroundMap.hpp b/src/world/generator/SurroundMap.hpp new file mode 100644 index 00000000..4f04d63e --- /dev/null +++ b/src/world/generator/SurroundMap.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +#define GLM_ENABLE_EXPERIMENTAL +#include + +#include "typedefs.hpp" +#include "util/AreaMap2D.hpp" + +class SurroundMap { +public: + using LevelCallback = std::function; + struct LevelCallbackWrapper { + LevelCallback callback; + bool active = false; + }; +private: + util::AreaMap2D areaMap; + std::vector levelCallbacks; + int8_t maxLevel; + + void upgrade(int x, int y, int8_t level); +public: + SurroundMap(int maxLevelRadius, int8_t maxLevel); + + /// @brief Callback called on point level increments + void setLevelCallback(int8_t level, LevelCallback callback); + + /// @brief Callback called when non-zero value moves out of area + void setOutCallback(util::AreaMap2D::OutCallback callback); + + /// @brief Upgrade point to maxLevel + /// @throws std::invalid_argument - upgrade square is not fully inside + void completeAt(int x, int y); + + /// @brief Set map area center + void setCenter(int x, int y); + + void resize(int maxLevelRadius); + + /// @brief Get level at position + /// @throws std::invalid_argument - position is out of area + int8_t at(int x, int y); + + const util::AreaMap2D& getArea() const { + return areaMap; + } +}; diff --git a/src/world/generator/VoxelFragment.cpp b/src/world/generator/VoxelFragment.cpp new file mode 100644 index 00000000..70dda238 --- /dev/null +++ b/src/world/generator/VoxelFragment.cpp @@ -0,0 +1,200 @@ +#include "VoxelFragment.hpp" + +#include +#include + +#include "data/dv_util.hpp" +#include "content/Content.hpp" +#include "voxels/Block.hpp" +#include "voxels/ChunksStorage.hpp" +#include "voxels/VoxelsVolume.hpp" +#include "world/Level.hpp" +#include "core_defs.hpp" + +std::unique_ptr VoxelFragment::create( + Level* level, + const glm::ivec3& a, + const glm::ivec3& b, + bool crop, + bool entities +) { + auto start = glm::min(a, b); + auto size = glm::abs(a - b); + + if (crop) { + VoxelsVolume volume(size.x, size.y, size.z); + volume.setPosition(start.x, start.y, start.z); + level->chunksStorage->getVoxels(&volume); + + auto end = start + size; + + auto min = end; + auto max = start; + + for (int y = start.y; y < end.y; y++) { + for (int z = start.z; z < end.z; z++) { + for (int x = start.x; x < end.x; x++) { + if (volume.pickBlockId(x, y, z)) { + min = glm::min(min, {x, y, z}); + max = glm::max(max, {x+1, y+1, z+1}); + } + } + } + } + if (glm::min(min, max) == min) { + start = min; + size = max - min; + } + } + + VoxelsVolume volume(size.x, size.y, size.z); + volume.setPosition(start.x, start.y, start.z); + level->chunksStorage->getVoxels(&volume); + + auto volVoxels = volume.getVoxels(); + std::vector voxels(size.x * size.y * size.z); + + std::vector blockNames {CORE_AIR}; + std::unordered_map blocksRegistered {{0, 0}}; + auto contentIndices = level->content->getIndices(); + for (size_t i = 0 ; i < voxels.size(); i++) { + blockid_t id = volVoxels[i].id; + blockid_t index; + + auto found = blocksRegistered.find(id); + if (found == blocksRegistered.end()) { + const auto& def = contentIndices->blocks.require(id); + index = blockNames.size(); + blockNames.push_back(def.name); + blocksRegistered[id] = index; + } else { + index = found->second; + } + voxels[i].id = index; + voxels[i].state = volVoxels[i].state; + } + + return std::make_unique( + size, std::move(voxels), std::move(blockNames)); +} + +dv::value VoxelFragment::serialize() const { + auto root = dv::object(); + root["version"] = STRUCTURE_FORMAT_VERSION; + root["size"] = dv::to_value(size); + + auto& blockNamesArr = root.list("block-names"); + for (const auto& name : blockNames) { + blockNamesArr.add(name); + } + auto& voxelsArr = root.list("voxels"); + for (size_t i = 0; i < voxels.size(); i++) { + voxelsArr.add(voxels[i].id); + voxelsArr.add(blockstate2int(voxels[i].state)); + } + return root; +} + +void VoxelFragment::deserialize(const dv::value& src) { + size = glm::ivec3(); + dv::get_vec(src, "size", size); + + const auto& namesArr = src["block-names"]; + for (const auto& elem : namesArr) { + blockNames.push_back(elem.asString()); + } + + auto volume = size.x*size.y*size.z; + voxels.resize(volume); + + const auto& voxelsArr = src["voxels"]; + for (size_t i = 0; i < volume; i++) { + voxels[i].id = voxelsArr[i * 2].asInteger(); + voxels[i].state = int2blockstate(voxelsArr[i * 2 + 1].asInteger()); + } +} + +void VoxelFragment::crop() { + glm::ivec3 min = size; + glm::ivec3 max = {}; + + blockid_t air; + const auto& found = std::find(blockNames.begin(), blockNames.end(), CORE_AIR); + if (found == blockNames.end()) { + throw std::runtime_error(CORE_AIR+" is not found in fragment"); + } + air = found - blockNames.begin(); + + for (int y = 0; y < size.y; y++) { + for (int z = 0; z < size.z; z++) { + for (int x = 0; x < size.x; x++) { + if (voxels[vox_index(x, y, z, size.x, size.z)].id != air) { + min = glm::min(min, {x, y, z}); + max = glm::max(max, {x+1, y+1, z+1}); + } + } + } + } + if (glm::min(min, max) == min) { + auto newSize = max - min; + std::vector newVoxels(newSize.x * newSize.y * newSize.z); + for (int y = 0; y < newSize.y; y++) { + for (int z = 0; z < newSize.z; z++) { + for (int x = 0; x < newSize.x; x++) { + newVoxels[vox_index(x, y, z, newSize.x, newSize.z)] = + voxels[vox_index( + x + min.x, + y + min.y, + z + min.z, + size.x, + size.z + )]; + } + } + } + voxels = std::move(newVoxels); + size = newSize; + } +} + +void VoxelFragment::prepare(const Content& content) { + auto volume = size.x*size.y*size.z; + voxelsRuntime.resize(volume); + for (size_t i = 0; i < volume; i++) { + const auto& name = blockNames.at(voxels[i].id); + voxelsRuntime[i].id = content.blocks.require(name).rt.id; + voxelsRuntime[i].state = voxels[i].state; + } +} + +std::unique_ptr VoxelFragment::rotated(const Content& content) const { + std::vector newVoxels(voxels.size()); + + for (int y = 0; y < size.y; y++) { + for (int z = 0; z < size.z; z++) { + for (int x = 0; x < size.x; x++) { + auto& voxel = newVoxels[vox_index(size.z-z-1, y, x, size.z, size.x)]; + voxel = voxels[vox_index(x, y, z, size.x, size.z)]; + // swap X and Z segment bits + voxel.state.segment = ((voxel.state.segment & 0b001) << 2) + | (voxel.state.segment & 0b010) + | ((voxel.state.segment & 0b100) >> 2); + auto& def = content.blocks.require(blockNames[voxel.id]); + if (def.rotations.name == BlockRotProfile::PANE_NAME || + def.rotations.name == BlockRotProfile::PIPE_NAME){ + if (voxel.state.rotation < 4) { + voxel.state.rotation = (voxel.state.rotation + 3) & 0b11; + } + } + } + } + } + auto newStructure = std::make_unique( + // swap X and Z on 90 deg. rotation + glm::ivec3(size.z, size.y, size.x), + std::move(newVoxels), + blockNames + ); + newStructure->prepare(content); + return newStructure; +} diff --git a/src/world/generator/VoxelFragment.hpp b/src/world/generator/VoxelFragment.hpp new file mode 100644 index 00000000..95a5375b --- /dev/null +++ b/src/world/generator/VoxelFragment.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +#include "interfaces/Serializable.hpp" +#include "voxels/voxel.hpp" + +inline constexpr int STRUCTURE_FORMAT_VERSION = 1; + +class Level; +class Content; + +class VoxelFragment : public Serializable { + glm::ivec3 size; + + /// @brief Structure voxels indexed different to world content + std::vector voxels; + /// @brief Block names are used for indexing + std::vector blockNames; + + /// @brief Structure voxels built on prepare(...) call + std::vector voxelsRuntime; +public: + VoxelFragment() : size() {} + + VoxelFragment( + glm::ivec3 size, + std::vector voxels, + std::vector blockNames + ): size(size), + voxels(std::move(voxels)), + blockNames(std::move(blockNames)) + {} + + dv::value serialize() const override; + void deserialize(const dv::value& src) override; + void crop(); + + /// @brief Build runtime voxel indices + /// @param content world content + void prepare(const Content& content); + + /// @brief Create structure copy rotated 90 deg. clockwise + std::unique_ptr rotated(const Content& content) const; + + static std::unique_ptr create( + Level* level, + const glm::ivec3& a, + const glm::ivec3& b, + bool crop, + bool entities + ); + + const glm::ivec3& getSize() const { + return size; + } + + /// @return Voxels with indices valid to current world content + const std::vector& getRuntimeVoxels() { + assert(!voxelsRuntime.empty()); + return voxelsRuntime; + } +}; diff --git a/src/world/generator/WorldGenerator.cpp b/src/world/generator/WorldGenerator.cpp new file mode 100644 index 00000000..b0236ec2 --- /dev/null +++ b/src/world/generator/WorldGenerator.cpp @@ -0,0 +1,590 @@ +#include "WorldGenerator.hpp" + +#include +#include + +#include "maths/util.hpp" +#include "content/Content.hpp" +#include "voxels/Block.hpp" +#include "voxels/Chunk.hpp" +#include "GeneratorDef.hpp" +#include "VoxelFragment.hpp" +#include "util/timeutil.hpp" +#include "util/listutil.hpp" +#include "maths/voxmaths.hpp" +#include "maths/util.hpp" +#include "debug/Logger.hpp" + +static debug::Logger logger("world-generator"); + +/// @brief Max number of biome parameters +static inline constexpr uint MAX_PARAMETERS = 4; + +/// @brief Initial + wide_structs + biomes + heightmaps + complete +static inline constexpr uint BASIC_PROTOTYPE_LAYERS = 5; + +WorldGenerator::WorldGenerator( + const GeneratorDef& def, const Content* content, uint64_t seed +) + : def(def), + content(content), + seed(seed), + surroundMap(0, BASIC_PROTOTYPE_LAYERS + def.wideStructsChunksRadius * 2) +{ + uint levels = BASIC_PROTOTYPE_LAYERS + def.wideStructsChunksRadius * 2; + + surroundMap = SurroundMap(0, levels); + logger.info() << "total number of prototype levels is " << levels; + surroundMap.setOutCallback([this](int const x, int const z, int8_t) { + const auto& found = prototypes.find({x, z}); + if (found == prototypes.end()) { + logger.warning() << "unable to remove non-existing chunk prototype"; + return; + } + prototypes.erase({x, z}); + }); + surroundMap.setLevelCallback(1, [this](int const x, int const z) { + if (prototypes.find({x, z}) != prototypes.end()) { + return; + } + prototypes[{x, z}] = generatePrototype(x, z); + }); + surroundMap.setLevelCallback(def.wideStructsChunksRadius + 1, + [this](int const x, int const z) { + generateStructuresWide(requirePrototype(x, z), x, z); + }); + surroundMap.setLevelCallback(levels-3, [this](int const x, int const z) { + generateBiomes(requirePrototype(x, z), x, z); + }); + surroundMap.setLevelCallback(levels-2, [this](int const x, int const z) { + generateHeightmap(requirePrototype(x, z), x, z); + }); + surroundMap.setLevelCallback(levels-1, [this](int const x, int const z) { + generateStructures(requirePrototype(x, z), x, z); + }); + for (int i = 0; i < def.structures.size(); i++) { + // pre-calculate rotated structure variants + def.structures[i]->fragments[0]->prepare(*content); + for (int j = 1; j < 4; j++) { + def.structures[i]->fragments[j] = + def.structures[i]->fragments[j-1]->rotated(*content); + } + } +} + +WorldGenerator::~WorldGenerator() {} + +ChunkPrototype& WorldGenerator::requirePrototype(int x, int z) { + const auto& found = prototypes.find({x, z}); + if (found == prototypes.end()) { + throw std::runtime_error("prototype not found"); + } + return *found->second; +} + +static inline void generate_pole( + const BlocksLayers& layers, + int top, int bottom, + int seaLevel, + voxel* voxels, + int x, int z +) { + uint y = top; + uint layerExtension = 0; + for (const auto& layer : layers.layers) { + // skip layer if can't be generated under sea level + if (y < seaLevel && !layer.belowSeaLevel) { + layerExtension = std::max(0, layer.height); + continue; + } + int layerHeight = layer.height; + if (layerHeight == -1) { + // resizeable layer + layerHeight = y - layers.lastLayersHeight - bottom + 1; + } else { + layerHeight += layerExtension; + } + layerHeight = std::min(static_cast(layerHeight), y+1); + + for (uint i = 0; i < layerHeight; i++, y--) { + voxels[vox_index(x, y, z)].id = layer.rt.id; + } + layerExtension = 0; + } +} + +static inline const Biome* choose_biome( + const std::vector& biomes, + const std::vector>& maps, + uint x, uint z +) { + uint paramsCount = maps.size(); + float params[MAX_PARAMETERS]; + for (uint i = 0; i < paramsCount; i++) { + params[i] = maps[i]->getUnchecked(x, z); + } + const Biome* chosenBiome = nullptr; + float chosenScore = std::numeric_limits::infinity(); + for (const auto& biome : biomes) { + float score = 0.0f; + for (uint i = 0; i < paramsCount; i++) { + score += glm::abs((params[i] - biome.parameters[i].value) / + biome.parameters[i].weight); + } + if (score < chosenScore) { + chosenScore = score; + chosenBiome = &biome; + } + } + return chosenBiome; +} + +std::unique_ptr WorldGenerator::generatePrototype( + int chunkX, int chunkZ +) { + return std::make_unique(); +} + +inline AABB gen_chunk_aabb(int chunkX, int chunkZ) { + return AABB({chunkX * CHUNK_W, 0, chunkZ * CHUNK_D}, + {(chunkX + 1)*CHUNK_W, 256, (chunkZ + 1) * CHUNK_D}); +} + +void WorldGenerator::placeStructure( + const StructurePlacement& placement, int priority, + int chunkX, int chunkZ +) { + auto& structure = + *def.structures[placement.structure]->fragments[placement.rotation]; + auto position = + glm::ivec3(chunkX * CHUNK_W, 0, chunkZ * CHUNK_D) + placement.position; + auto size = structure.getSize() + glm::ivec3(0, CHUNK_H, 0); + AABB aabb(position, position + size); + for (int lcz = -1; lcz <= 1; lcz++) { + for (int lcx = -1; lcx <= 1; lcx++) { + const auto& found = prototypes.find({chunkX + lcx, chunkZ + lcz}); + if (found == prototypes.end()) { + continue; + } + auto& otherPrototype = *found->second; + auto chunkAABB = gen_chunk_aabb(chunkX + lcx, chunkZ + lcz); + if (chunkAABB.intersect(aabb)) { + otherPrototype.placements.emplace_back( + priority, + StructurePlacement { + placement.structure, + placement.position - + glm::ivec3(lcx * CHUNK_W, 0, lcz * CHUNK_D), + placement.rotation} + ); + } + } + } +} + +void WorldGenerator::placeLine(const LinePlacement& line, int priority) { + AABB aabb(line.a, line.b); + aabb.fix(); + aabb.a -= line.radius; + aabb.b += line.radius; + int cxa = floordiv(aabb.a.x, CHUNK_W); + int cza = floordiv(aabb.a.z, CHUNK_D); + int cxb = floordiv(aabb.b.x, CHUNK_W); + int czb = floordiv(aabb.b.z, CHUNK_D); + for (int cz = cza; cz <= czb; cz++) { + for (int cx = cxa; cx <= cxb; cx++) { + const auto& found = prototypes.find({cx, cz}); + if (found != prototypes.end()) { + found->second->placements.emplace_back(priority, line); + } + } + } +} + +void WorldGenerator::placeStructures( + const std::vector& placements, + ChunkPrototype& prototype, + int chunkX, + int chunkZ +) { + for (const auto& placement : placements) { + if (auto sp = std::get_if(&placement.placement)) { + if (sp->structure < 0 || sp->structure >= def.structures.size()) { + logger.error() << "invalid structure index " << sp->structure; + continue; + } + placeStructure(*sp, placement.priority, chunkX, chunkZ); + } else { + const auto& line = std::get(placement.placement); + placeLine(line, placement.priority); + } + } +} + +void WorldGenerator::generateStructuresWide( + ChunkPrototype& prototype, int chunkX, int chunkZ +) { + if (prototype.level >= ChunkPrototypeLevel::WIDE_STRUCTS) { + return; + } + auto placements = def.script->placeStructuresWide( + {chunkX * CHUNK_W, chunkZ * CHUNK_D}, {CHUNK_W, CHUNK_D}, seed, CHUNK_H + ); + placeStructures(placements, prototype, chunkX, chunkZ); + + prototype.level = ChunkPrototypeLevel::WIDE_STRUCTS; +} + +void WorldGenerator::generateStructures( + ChunkPrototype& prototype, int chunkX, int chunkZ +) { + if (prototype.level >= ChunkPrototypeLevel::STRUCTURES) { + return; + } + const auto& biomes = prototype.biomes; + const auto& heightmap = prototype.heightmap; + + auto placements = def.script->placeStructures( + {chunkX * CHUNK_W, chunkZ * CHUNK_D}, {CHUNK_W, CHUNK_D}, seed, + heightmap, CHUNK_H + ); + placeStructures(placements, prototype, chunkX, chunkZ); + + util::PseudoRandom structsRand; + structsRand.setSeed(chunkX, chunkZ); + + // Place structures defined in biome + auto heights = heightmap->getValues(); + for (uint z = 0; z < CHUNK_D; z++) { + for (uint x = 0; x < CHUNK_W; x++) { + float rand = structsRand.randFloat(); + const Biome* biome = biomes[z * CHUNK_W + x]; + int structureId = biome->structures.choose(rand, -1); + if (structureId == -1) { + continue; + } + uint8_t rotation = structsRand.randU32() % 4; + int height = heights[z * CHUNK_W + x] * CHUNK_H; + if (height < def.seaLevel) { + continue; + } + auto& structure = *def.structures[structureId]->fragments[rotation]; + glm::ivec3 position {x, height, z}; + position.x -= structure.getSize().x / 2; + position.z -= structure.getSize().z / 2; + placeStructure( + StructurePlacement { + structureId, + position, + rotation + }, + 1, + chunkX, + chunkZ + ); + } + } + prototype.level = ChunkPrototypeLevel::STRUCTURES; +} + +void WorldGenerator::generateBiomes( + ChunkPrototype& prototype, int chunkX, int chunkZ +) { + if (prototype.level >= ChunkPrototypeLevel::BIOMES) { + return; + } + uint bpd = def.biomesBPD; + auto biomeParams = def.script->generateParameterMaps( + {floordiv(chunkX * CHUNK_W, bpd), floordiv(chunkZ * CHUNK_D, bpd)}, + {floordiv(CHUNK_W, bpd)+1, floordiv(CHUNK_D, bpd)+1}, + seed, + bpd + ); + for (const auto& map : biomeParams) { + map->resize( + CHUNK_W + bpd, CHUNK_D + bpd, InterpolationType::LINEAR + ); + map->crop(0, 0, CHUNK_W, CHUNK_D); + } + const auto& biomes = def.biomes; + + auto chunkBiomes = std::make_unique(CHUNK_W*CHUNK_D); + for (uint z = 0; z < CHUNK_D; z++) { + for (uint x = 0; x < CHUNK_W; x++) { + chunkBiomes.get()[z * CHUNK_W + x] = + choose_biome(biomes, biomeParams, x, z); + } + } + prototype.biomes = std::move(chunkBiomes); + prototype.level = ChunkPrototypeLevel::BIOMES; +} + +void WorldGenerator::generateHeightmap( + ChunkPrototype& prototype, int chunkX, int chunkZ +) { + if (prototype.level >= ChunkPrototypeLevel::HEIGHTMAP) { + return; + } + uint bpd = def.heightsBPD; + prototype.heightmap = def.script->generateHeightmap( + {floordiv(chunkX * CHUNK_W, bpd), floordiv(chunkZ * CHUNK_D, bpd)}, + {floordiv(CHUNK_W, bpd)+1, floordiv(CHUNK_D, bpd)+1}, + seed, + bpd + ); + prototype.heightmap->resize( + CHUNK_W + bpd, CHUNK_D + bpd, InterpolationType::LINEAR + ); + prototype.heightmap->crop(0, 0, CHUNK_W, CHUNK_D); + prototype.level = ChunkPrototypeLevel::HEIGHTMAP; +} + +void WorldGenerator::update(int centerX, int centerY, int loadDistance) { + surroundMap.setCenter(centerX, centerY); + // 1 is safety padding preventing ChunksController rounding problem + surroundMap.resize(loadDistance + 1); + surroundMap.setCenter(centerX, centerY); +} + +void WorldGenerator::generatePlants( + const ChunkPrototype& prototype, + float* heights, + voxel* voxels, + int chunkX, + int chunkZ, + const Biome** biomes +) { + const auto& indices = content->getIndices()->blocks; + util::PseudoRandom plantsRand; + plantsRand.setSeed(chunkX, chunkZ); + + for (uint z = 0; z < CHUNK_D; z++) { + for (uint x = 0; x < CHUNK_W; x++) { + const Biome* biome = biomes[z * CHUNK_W + x]; + + int height = heights[z * CHUNK_W + x] * CHUNK_H; + height = std::max(0, height); + + if (height+1 > def.seaLevel) { + float rand = plantsRand.randFloat(); + blockid_t plant = biome->plants.choose(rand); + if (plant) { + auto& voxel = voxels[vox_index(x, height+1, z)]; + if (voxel.id) { + continue; + } + auto& groundVoxel = voxels[vox_index(x, height, z)]; + if (indices.get(groundVoxel.id)->rt.solid) { + voxel = {plant, {}}; + } + } + } + } + } +} + +void WorldGenerator::generateLand( + const ChunkPrototype& prototype, + float* values, + voxel* voxels, + int chunkX, + int chunkZ, + const Biome** biomes +) { + uint seaLevel = def.seaLevel; + for (uint z = 0; z < CHUNK_D; z++) { + for (uint x = 0; x < CHUNK_W; x++) { + const Biome* biome = biomes[z * CHUNK_W + x]; + + int height = values[z * CHUNK_W + x] * CHUNK_H; + height = std::max(0, height); + + const auto& groundLayers = biome->groundLayers; + const auto& seaLayers = biome->seaLayers; + + generate_pole(seaLayers, seaLevel, height, seaLevel, voxels, x, z); + generate_pole(groundLayers, height, 0, seaLevel, voxels, x, z); + } + } +} + +void WorldGenerator::generate(voxel* voxels, int chunkX, int chunkZ) { + surroundMap.completeAt(chunkX, chunkZ); + + const auto& prototype = requirePrototype(chunkX, chunkZ); + const auto values = prototype.heightmap->getValues(); + + uint seaLevel = def.seaLevel; + + std::memset(voxels, 0, sizeof(voxel) * CHUNK_VOL); + + const auto& indices = content->getIndices()->blocks; + const auto& biomes = prototype.biomes.get(); + for (uint z = 0; z < CHUNK_D; z++) { + for (uint x = 0; x < CHUNK_W; x++) { + const Biome* biome = biomes[z * CHUNK_W + x]; + + int height = values[z * CHUNK_W + x] * CHUNK_H; + height = std::max(0, height); + + const auto& groundLayers = biome->groundLayers; + const auto& seaLayers = biome->seaLayers; + + generate_pole(seaLayers, seaLevel, height, seaLevel, voxels, x, z); + generate_pole(groundLayers, height, 0, seaLevel, voxels, x, z); + } + } + generatePlacements(prototype, voxels, chunkX, chunkZ); + generatePlants(prototype, values, voxels, chunkX, chunkZ, biomes); + +#ifndef NDEBUG + for (uint i = 0; i < CHUNK_VOL; i++) { + blockid_t id = voxels[i].id; + if (indices.get(id) == nullptr) { + abort(); + } + } +#endif +} + +void WorldGenerator::generatePlacements( + const ChunkPrototype& prototype, voxel* voxels, int chunkX, int chunkZ +) { + auto placements = prototype.placements; + std::stable_sort( + placements.begin(), + placements.end(), + [](const auto& a, const auto& b) { + return a.priority < b.priority; + } + ); + for (const auto& placement : placements) { + if (auto structure = std::get_if(&placement.placement)) { + generateStructure(prototype, *structure, voxels, chunkX, chunkZ); + } else { + const auto& line = std::get(placement.placement); + generateLine(prototype, line, voxels, chunkX, chunkZ); + } + } +} + +void WorldGenerator::generateStructure( + const ChunkPrototype& prototype, + const StructurePlacement& placement, + voxel* voxels, + int chunkX, int chunkZ +) { + if (placement.structure < 0 || placement.structure >= def.structures.size()) { + logger.error() << "invalid structure index " << placement.structure; + return; + } + auto& generatingStructure = def.structures[placement.structure]; + auto& structure = *generatingStructure->fragments[placement.rotation]; + auto& structVoxels = structure.getRuntimeVoxels(); + const auto& offset = placement.position; + const auto& size = structure.getSize(); + + for (int y = 0; y < size.y; y++) { + int sy = y + offset.y; + if (sy < 0 || sy >= CHUNK_H) { + continue; + } + for (int z = 0; z < size.z; z++) { + int sz = z + offset.z; + if (sz < 0 || sz >= CHUNK_D) { + continue; + } + for (int x = 0; x < size.x; x++) { + int sx = x + offset.x; + if (sx < 0 || sx >= CHUNK_W) { + continue; + } + const auto& structVoxel = + structVoxels[vox_index(x, y, z, size.x, size.z)]; + if (structVoxel.id) { + if (structVoxel.id == BLOCK_STRUCT_AIR) { + voxels[vox_index(sx, sy, sz)] = {0, {}}; + } else { + voxels[vox_index(sx, sy, sz)] = structVoxel; + } + } + } + } + } +} + +void WorldGenerator::generateLine( + const ChunkPrototype& prototype, + const LinePlacement& line, + voxel* voxels, + int chunkX, int chunkZ +) { + const auto& indices = content->getIndices()->blocks; + + int cgx = chunkX * CHUNK_W; + int cgz = chunkZ * CHUNK_D; + + int const radius = line.radius; + + auto a = line.a; + auto b = line.b; + + int minX = std::max(0, std::min(a.x-radius-cgx, b.x-radius-cgx)); + int maxX = std::min(CHUNK_W, std::max(a.x+radius-cgx, b.x+radius-cgx)); + + int minZ = std::max(0, std::min(a.z-radius-cgz, b.z-radius-cgz)); + int maxZ = std::min(CHUNK_D, std::max(a.z+radius-cgz, b.z+radius-cgz)); + + int minY = std::max(0, std::min(a.y-radius, b.y-radius)); + int maxY = std::min(CHUNK_H, std::max(a.y+radius, b.y+radius)); + + for (int y = minY; y < maxY; y++) { + for (int z = minZ; z < maxZ; z++) { + for (int x = minX; x < maxX; x++) { + int gx = x + cgx; + int gz = z + cgz; + glm::ivec3 point {gx, y, gz}; + glm::ivec3 closest = util::closest_point_on_segment( + a, b, point + ); + if (y > 0 && + util::distance2(closest, point) <= radius * radius && + line.block == BLOCK_AIR + ) { + auto& voxel = voxels[vox_index(x, y, z)]; + if (!indices.require(voxel.id).replaceable) { + voxel = {line.block, {}}; + } + auto& below = voxels[vox_index(x, y-1, z)]; + glm::ivec3 closest2 = util::closest_point_on_segment( + a, b, {gx, y-1, gz} + ); + if (util::distance2(closest2, {gx, y-1, gz}) > radius*radius) { + const auto& def = indices.require(below.id); + if (def.rt.surfaceReplacement != below.id) { + below = {def.rt.surfaceReplacement, {}}; + } + } + } + } + } + } +} + +WorldGenDebugInfo WorldGenerator::createDebugInfo() const { + const auto& area = surroundMap.getArea(); + const auto& levels = area.getBuffer(); + auto values = std::make_unique(area.getWidth()*area.getHeight()); + + for (uint i = 0; i < levels.size(); i++) { + values[i] = levels[i]; + } + + return WorldGenDebugInfo { + area.getOffsetX(), + area.getOffsetY(), + static_cast(area.getWidth()), + static_cast(area.getHeight()), + std::move(values) + }; +} diff --git a/src/world/generator/WorldGenerator.hpp b/src/world/generator/WorldGenerator.hpp new file mode 100644 index 00000000..fa8a51e0 --- /dev/null +++ b/src/world/generator/WorldGenerator.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "constants.hpp" +#include "typedefs.hpp" +#include "voxels/voxel.hpp" +#include "SurroundMap.hpp" +#include "StructurePlacement.hpp" + +class Content; +struct GeneratorDef; +class Heightmap; +struct Biome; +class VoxelFragment; + +enum class ChunkPrototypeLevel { + VOID=0, WIDE_STRUCTS, BIOMES, HEIGHTMAP, STRUCTURES +}; + +struct ChunkPrototype { + ChunkPrototypeLevel level = ChunkPrototypeLevel::VOID; + + /// @brief chunk biomes matrix + std::unique_ptr biomes; + + /// @brief chunk heightmap + std::shared_ptr heightmap; + + std::vector placements; +}; + +struct WorldGenDebugInfo { + int areaOffsetX; + int areaOffsetY; + uint areaWidth; + uint areaHeight; + std::unique_ptr areaLevels; +}; + +/// @brief High-level world generation controller +class WorldGenerator { + /// @param def generator definition + const GeneratorDef& def; + /// @param content world content + const Content* content; + /// @param seed world seed + uint64_t seed; + /// @brief Chunk prototypes main storage + std::unordered_map> prototypes; + /// @brief Chunk prototypes loading surround map + SurroundMap surroundMap; + + /// @brief Generate chunk prototype (see ChunkPrototype) + /// @param x chunk position X divided by CHUNK_W + /// @param z chunk position Y divided by CHUNK_D + std::unique_ptr generatePrototype(int x, int z); + + ChunkPrototype& requirePrototype(int x, int z); + + void generateStructuresWide(ChunkPrototype& prototype, int x, int z); + + void generateStructures(ChunkPrototype& prototype, int x, int z); + + void generateBiomes(ChunkPrototype& prototype, int x, int z); + + void generateHeightmap(ChunkPrototype& prototype, int x, int z); + + void placeStructure( + const StructurePlacement& placement, int priority, + int chunkX, int chunkZ + ); + + void placeLine(const LinePlacement& line, int priority); + + void generatePlacements( + const ChunkPrototype& prototype, voxel* voxels, int x, int z + ); + void generateLine( + const ChunkPrototype& prototype, + const LinePlacement& placement, + voxel* voxels, + int x, int z + ); + void generateStructure( + const ChunkPrototype& prototype, + const StructurePlacement& placement, + voxel* voxels, + int x, int z + ); + void generatePlants( + const ChunkPrototype& prototype, + float* values, + voxel* voxels, + int x, + int z, + const Biome** biomes + ); + void generateLand( + const ChunkPrototype& prototype, + float* values, + voxel* voxels, + int x, + int z, + const Biome** biomes + ); + + void placeStructures( + const std::vector& placements, + ChunkPrototype& prototype, + int x, int z + ); +public: + WorldGenerator( + const GeneratorDef& def, + const Content* content, + uint64_t seed + ); + ~WorldGenerator(); + + void update(int centerX, int centerY, int loadDistance); + + /// @brief Generate complete chunk voxels + /// @param voxels destinatiopn chunk voxels buffer + /// @param x chunk position X divided by CHUNK_W + /// @param z chunk position Y divided by CHUNK_D + void generate(voxel* voxels, int x, int z); + + WorldGenDebugInfo createDebugInfo() const; +}; diff --git a/test/util/AreaMap2D.cpp b/test/util/AreaMap2D.cpp new file mode 100644 index 00000000..a4535926 --- /dev/null +++ b/test/util/AreaMap2D.cpp @@ -0,0 +1,78 @@ +#include +#include + +#include "util/AreaMap2D.hpp" + +TEST(AreaMap2D, BaseTest) { + util::AreaMap2D window({7, 5}); + window.setCenter(0, 0); + { + int i = 1; + for (int y = -2; y <= 2; y++) { + for (int x = -3; x <= 3; x++, i++) { + window.set(x, y, i); + } + } + } + EXPECT_EQ(window.count(), 7 * 5); + { + int i = 1; + for (int y = -2; y <= 2; y++) { + for (int x = -3; x <= 3; x++, i++) { + EXPECT_EQ(window.require(x, y), i); + } + } + } + window.set(0, 0, 0); + EXPECT_EQ(window.count(), 7 * 5 - 1); +} + + +TEST(AreaMap2D, ResizeTest) { + util::AreaMap2D window({7, 5}); + window.setCenter(0, 0); + { + int i = 1; + for (int y = -2; y <= 2; y++) { + for (int x = -3; x <= 3; x++, i++) { + window.set(x, y, i); + } + } + } + EXPECT_EQ(window.count(), 7 * 5); + window.resize(9, 7); + window.setCenter(0, 0); + EXPECT_EQ(window.count(), 7 * 5); + window.resize(7, 5); + + EXPECT_EQ(window.count(), 7 * 5); + { + int i = 1; + for (int y = -2; y <= 2; y++) { + for (int x = -3; x <= 3; x++, i++) { + EXPECT_EQ(window.require(x, y), i); + } + } + } +} + +TEST(AreaMap2D, TranslateWithOut) { + util::AreaMap2D window({7, 5}); + window.setCenter(0, 0); + { + int i = 1; + for (int y = -2; y <= 2; y++) { + for (int x = -3; x <= 3; x++, i++) { + window.set(x, y, i); + } + } + } + std::atomic_int outside = 0; + window.setOutCallback([&outside](auto, auto, auto) { + outside++; + }); + window.setCenter(-2, -1); + EXPECT_EQ(window.require(-3, -2), 1); + EXPECT_EQ(outside, 15); + EXPECT_EQ(window.count(), 20); +} diff --git a/test/world/generator/SurroundMap.cpp b/test/world/generator/SurroundMap.cpp new file mode 100644 index 00000000..6fb5d7ae --- /dev/null +++ b/test/world/generator/SurroundMap.cpp @@ -0,0 +1,78 @@ +#include +#include + +#include "world/generator/SurroundMap.hpp" + +TEST(SurroundMap, InitTest) { + int maxLevelZone = 50; + int x = 0; + int y = 0; + int8_t maxLevel = 5; + + SurroundMap map(maxLevelZone, maxLevel); + std::atomic_int affected = 0; + + map.setLevelCallback(1, [&affected](auto, auto) { + affected++; + }); + map.setCenter(0, 0); + map.completeAt(x, y); + EXPECT_EQ(affected, (maxLevel * 2 - 1) * (maxLevel * 2 - 1)); + + for (int ly = -maxLevel + 1; ly < maxLevel; ly++) { + for (int lx = -maxLevel + 1; lx < maxLevel; lx++) { + int levelExpected = maxLevel - std::max(std::abs(lx), std::abs(ly)); + EXPECT_EQ(map.at(x + lx, y + ly), levelExpected); + } + } + + affected = 0; + map.completeAt(x - 1, y); + EXPECT_EQ(affected, maxLevel * 2 - 1); +} + +#define VISUAL_TEST +#ifdef VISUAL_TEST + +#include + +#include "coders/png.hpp" +#include "graphics/core/ImageData.hpp" + +void visualize(const SurroundMap& map, int mul, int max) { + const auto& areaMap = map.getArea(); + int w = areaMap.getWidth(); + int h = areaMap.getHeight(); + int ox = areaMap.getOffsetX(); + int oy = areaMap.getOffsetY(); + + ImageData image(ImageFormat::rgb888, w, h); + ubyte* bytes = image.getData(); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int val = areaMap.get(x + ox, y + oy) * mul; + if (val && val / mul < max) { + val = val / 4 + 50; + } + bytes[(y * w + x) * 3] = val; + bytes[(y * w + x) * 3 + 1] = val; + bytes[(y * w + x) * 3 + 2] = val; + } + } + png::write_image("test.png", &image); +} + +TEST(SurroundMap, Visualize) { + int levels = 3; + SurroundMap map(50, levels); + map.setCenter(0, 0); + + for (int i = 0; i < 1000; i++) { + float x = glm::gaussRand(0.0f, 2.0f); + float y = glm::gaussRand(0.0f, 2.0f); + map.completeAt(x, y); + } + visualize(map, 30, levels); +} + +#endif