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