Merge pull request #295 from MihailRis/heightmaps

World Generators
This commit is contained in:
MihailRis 2024-10-15 04:30:39 +03:00 committed by GitHub
commit d6e6e32ef2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
152 changed files with 4748 additions and 1312 deletions

2
.gitignore vendored
View File

@ -6,6 +6,8 @@ Debug/voxel_engine
/build
/cmake-build-**
/screenshots
/export
/config
/out
/misc

1
res/config/defaults.toml Normal file
View File

@ -0,0 +1 @@
generator = "core:default"

View File

@ -0,0 +1,3 @@
{
"texture": "coal_ore"
}

View File

@ -1,4 +1,5 @@
{
"texture": "dirt",
"material": "base:ground"
}
"material": "base:ground",
"surface-replacement": "base:grass_block"
}

View File

@ -0,0 +1 @@
generator = "base:demo"

View File

@ -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"
]
}

View File

@ -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}
]

View File

@ -0,0 +1,3 @@
[
{"struct": "coal_ore0", "rarity": 4400}
]

View File

@ -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

View File

@ -0,0 +1,5 @@
tree0 = {}
tree1 = {}
tree2 = {}
tower = {}
coal_ore0 = {}

View File

@ -0,0 +1,4 @@
# 1 - temperature
# 2 - humidity
biome-parameters = 2
sea-level = 64

View File

@ -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

View File

@ -7,13 +7,6 @@
"models": [
"drop-item"
],
"shaders": [
"ui3d",
"entity",
"screen",
"background",
"skybox_gen"
],
"textures": [
"misc/moon",
"misc/sun",

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,8 @@
[flat]
parameters = []
layers = [
{height=-1, block="core:obstacle"}
]
sea-layers = [
{height=-1, block="core:obstacle"}
]

View File

@ -0,0 +1 @@
biome-parameters = 0

View File

@ -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("<button onclick='menu:back()'>@Back</button>")

View File

@ -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 ''

View File

@ -1,8 +1,13 @@
{
"shaders": [
"ui",
"ui3d",
"main",
"lines"
"lines",
"entity",
"screen",
"background",
"skybox_gen"
],
"textures": [
"gui/menubg",

View File

@ -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
)

View File

@ -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 (...)

289
res/scripts/stdmin.lua Normal file
View File

@ -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

View File

@ -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=Есть изменения в индексах! Конвертировать мир?

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -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();

View File

@ -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;

View File

@ -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<Block> blocks,
ContentUnitDefs<ItemDef> items,
ContentUnitDefs<EntityDef> entities,
ContentUnitDefs<GeneratorDef> generators,
UptrsMap<std::string, ContentPackRuntime> packs,
UptrsMap<std::string, BlockMaterial> blockMaterials,
UptrsMap<std::string, rigging::SkeletonConfig> 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]);

View File

@ -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<Block> blocks;
ContentUnitDefs<ItemDef> items;
ContentUnitDefs<EntityDef> entities;
ContentUnitDefs<GeneratorDef> generators;
std::unique_ptr<DrawGroups> const drawGroups;
ResourceIndicesSet resourceIndices {};
@ -198,6 +215,7 @@ public:
ContentUnitDefs<Block> blocks,
ContentUnitDefs<ItemDef> items,
ContentUnitDefs<EntityDef> entities,
ContentUnitDefs<GeneratorDef> generators,
UptrsMap<std::string, ContentPackRuntime> packs,
UptrsMap<std::string, BlockMaterial> blockMaterials,
UptrsMap<std::string, rigging::SkeletonConfig> skeletons,

View File

@ -72,6 +72,7 @@ std::unique_ptr<Content> 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<Content> 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;
}

View File

@ -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 <class T>
@ -67,6 +69,7 @@ public:
ContentUnitBuilder<Block> blocks {allNames, ContentType::BLOCK};
ContentUnitBuilder<ItemDef> items {allNames, ContentType::ITEM};
ContentUnitBuilder<EntityDef> entities {allNames, ContentType::ENTITY};
ContentUnitBuilder<GeneratorDef> generators {allNames, ContentType::GENERATOR};
ResourceIndicesSet resourceIndices {};
~ContentBuilder();

View File

@ -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<ContentPackRuntime>(
*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<std::tuple<std::string, std::string>>& 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<std::tuple<std::string, std::string>> ContentLoader::scanContent(
const ContentPack& pack, ContentType type
) {
std::vector<std::tuple<std::string, std::string>> 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<std::string, std::string, std::string> 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<std::pair<std::string, std::string>> 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<void(const fs::path&)> 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<size_t>(type)].addAlias(
name.asString(), alias
);
}
}

View File

@ -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<std::tuple<std::string, std::string>> scanContent(
const ContentPack& pack, ContentType type
);
void fixPackIndices();
void load();
};

View File

@ -6,6 +6,7 @@
#include <vector>
#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<std::string> 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 {

View File

@ -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 };

View File

@ -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<BlocksLayer> 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<WeightedEntry> 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<WeightedEntry>());
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<BiomeParameter> 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<std::unique_ptr<VoxelStructure>> load_structures(
const fs::path& structuresFile
) {
auto structuresDir = structuresFile.parent_path() / fs::path("fragments");
auto map = files::read_object(structuresFile);
std::vector<std::unique_ptr<VoxelStructure>> 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<VoxelFragment>();
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<VoxelStructure>(
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");
}

View File

@ -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;
}
}

View File

@ -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";

View File

@ -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) {

View File

@ -5,8 +5,8 @@
#include <glm/glm.hpp>
namespace dv {
template <int n>
inline dv::value to_value(glm::vec<n, float> vec) {
template <int n, typename T>
inline dv::value to_value(glm::vec<n, T> 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 <int n, int m>
inline dv::value to_value(glm::mat<n, m, float> mat) {
template <int n, int m, typename T>
inline dv::value to_value(glm::mat<n, m, T> 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 <int n>
void get_vec(const dv::value& map, const std::string& key, glm::vec<n, float>& vec) {
template <int n, typename T>
void get_vec(const dv::value& map, const std::string& key, glm::vec<n, T>& 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<T>()) {
vec[i] = list[i].asNumber();
} else {
vec[i] = list[i].asInteger();
}
}
}

View File

@ -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 <iostream>
@ -51,11 +48,6 @@ static debug::Logger logger("engine");
namespace fs = std::filesystem;
static void add_world_generators() {
WorldGenerators::addGenerator<DefaultWorldGenerator>("core:default");
WorldGenerators::addGenerator<FlatWorldGenerator>("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<PathsRoot> 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<PathsRoot> resRoots {
{"core", corePack.folder}
};
for (auto& pack : contentPacks) {
resRoots.push_back({pack.id, pack.folder});
ContentLoader(&pack, contentBuilder).load();
}
resPaths = std::make_unique<ResPaths>(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<ResPaths>(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<ResPaths>(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<ContentPack> Engine::getAllContentPacks() {
auto packs = getContentPacks();
packs.insert(packs.begin(), ContentPack::createCore(paths));
return packs;
}
std::vector<ContentPack>& Engine::getContentPacks() {
return contentPacks;
}

View File

@ -130,6 +130,8 @@ public:
/// @brief Get selected content packs
std::vector<ContentPack>& getContentPacks();
std::vector<ContentPack> getAllContentPacks();
std::vector<std::string>& getBasePacks();
/// @brief Get current screen

View File

@ -5,6 +5,7 @@
#include <vector>
#include "debug/Logger.hpp"
#include "coders/json.hpp"
#include "coders/byte_utils.hpp"
#include "coders/rle.hpp"
#include "coders/binary_json.hpp"

View File

@ -10,11 +10,15 @@
#include <utility>
#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<ContentPack>* contentPacks) {
this->contentPacks = contentPacks;
}
std::tuple<std::string, std::string> 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<std::filesystem::path> 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;
}

View File

@ -4,7 +4,9 @@
#include <stdexcept>
#include <string>
#include <vector>
#include <tuple>
#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<std::string, std::string> 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<std::filesystem::path> listdir(const std::string& folder) const;
std::vector<std::string> 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:

View File

@ -165,3 +165,36 @@ std::vector<std::string> files::read_list(const fs::path& filename) {
}
return lines;
}
#include <map>
#include "coders/json.hpp"
#include "coders/toml.hpp"
using DecodeFunc = dv::value(*)(std::string_view, std::string_view);
static std::map<fs::path, DecodeFunc> 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());
}
}

View File

@ -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<std::string> 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);
}

16
src/files/util.hpp Normal file
View File

@ -0,0 +1,16 @@
#pragma once
#include <string>
#include <stdexcept>
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();
}
}

View File

@ -84,7 +84,7 @@ std::shared_ptr<UINode> 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([=]() {

View File

@ -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<InventoryView> 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<ImageData>(
ImageFormat::rgba8888, WORLDGEN_IMG_SIZE, WORLDGEN_IMG_SIZE
)) {
contentAccess = createContentAccess();
contentAccess->setId("hud.content-access");
contentAccessPanel = std::make_shared<Panel>(
@ -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(
"<image src='"+DEBUG_WORLDGEN_IMAGE+
"' pos='0' size='256' gravity='top-right' margin='0,20,0,0'/>"
), 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<Texture>(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

View File

@ -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<gui::UINode> secondUI = nullptr;
std::unique_ptr<ImageData> debugImgWorldGen;
std::shared_ptr<gui::InventoryView> createContentAccess();
std::shared_ptr<gui::InventoryView> 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<Inventory> getBlockInventory();
/// @brief Runtime updating debug visualization texture
inline static std::string DEBUG_WORLDGEN_IMAGE = "#debug.img.worldgen";
};

View File

@ -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<const GLvoid*>(data));

View File

@ -16,6 +16,8 @@ public:
void setNearestFilter();
virtual void reload(const ImageData& image) override;
virtual std::unique_ptr<ImageData> readData() override;
virtual uint getId() const override;

View File

@ -20,6 +20,8 @@ public:
virtual void bind() = 0;
virtual void unbind() = 0;
virtual void reload(const ImageData& image) = 0;
virtual std::unique_ptr<ImageData> readData() = 0;
virtual uint getWidth() const {

View File

@ -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<size_t> 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<float>(CHUNK_W) - 0.5f;
float pz = camera->position.z / static_cast<float>(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);

View File

@ -15,7 +15,7 @@ using inventories_map = std::unordered_map<int64_t, std::shared_ptr<Inventory>>;
class Inventories {
Level& level;
inventories_map map;
PseudoRandom random;
util::PseudoRandom random;
public:
Inventories(Level& level);
~Inventories();

View File

@ -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;

View File

@ -14,6 +14,8 @@ class Inventory : public Serializable {
int64_t id;
std::vector<ItemStack> slots;
public:
Inventory() = default;
Inventory(int64_t id, size_t size);
Inventory(const Inventory& orig);

View File

@ -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;

View File

@ -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;
}

View File

@ -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<WorldGenerator>(
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();

View File

@ -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();
}
};

View File

@ -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;

View File

@ -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:

View File

@ -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;

View File

@ -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();
}

View File

@ -34,5 +34,6 @@ public:
Player* getPlayer();
BlocksController* getBlocksController();
ChunksController* getChunksController();
PlayerController* getPlayerController();
};

View File

@ -3,7 +3,7 @@
#include <exception>
#include <string>
#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[];

View File

@ -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());

View File

@ -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<l_new_world>},
{"open_world", lua::wrap<l_open_world>},
@ -203,6 +185,4 @@ const luaL_Reg corelib[] = {
{"str_setting", lua::wrap<l_str_setting>},
{"get_setting_info", lua::wrap<l_get_setting_info>},
{"quit", lua::wrap<l_quit>},
{"get_default_generator", lua::wrap<l_get_default_generator>},
{"get_generators", lua::wrap<l_get_generators>},
{NULL, NULL}};

View File

@ -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<lua::Bytearray>(L, -1)) {
if (auto bytearray = lua::touserdata<lua::LuaBytearray>(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<l_file_exists>},
{"find", lua::wrap<l_file_find>},
@ -265,4 +273,5 @@ const luaL_Reg filelib[] = {
{"write", lua::wrap<l_file_write>},
{"gzip_compress", lua::wrap<l_file_gzip_compress>},
{"gzip_decompress", lua::wrap<l_file_gzip_decompress>},
{"read_combined_list", lua::wrap<l_file_read_combined_list>},
{NULL, NULL}};

View File

@ -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<lua::LuaVoxelFragment>(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<lua::LuaVoxelFragment>(
L, std::shared_ptr<VoxelFragment>(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<VoxelFragment>();
fragment->deserialize(map);
return lua::newuserdata<lua::LuaVoxelFragment>(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<l_create_fragment>},
{"save_fragment", lua::wrap<l_save_fragment>},
{"load_fragment", lua::wrap<l_load_fragment>},
{"get_generators", lua::wrap<l_get_generators>},
{"get_default_generator", lua::wrap<l_get_default_generator>},
{NULL, NULL}};

View File

@ -77,7 +77,7 @@ static int l_mul(lua::State* L) {
/// transformed copy of matrix mat4.<func>(matrix: float[16], vec: float[3],
/// dst: float[16]) -> sets dst to transformed version of matrix
template <glm::mat4 (*func)(const glm::mat4&, const glm::vec3&)>
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<l_idt>},
{"mul", lua::wrap<l_mul>},
{"scale", lua::wrap<l_transform_func<glm::scale>>},
{"scale", lua::wrap<l_binop_func<glm::scale>>},
{"rotate", lua::wrap<l_rotate>},
{"translate", lua::wrap<l_transform_func<glm::translate>>},
{"translate", lua::wrap<l_binop_func<glm::translate>>},
{"inverse", lua::wrap<l_inverse>},
{"transpose", lua::wrap<l_transpose>},
{"determinant", lua::wrap<l_determinant>},

View File

@ -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() + "/");
}

Some files were not shown because too many files have changed in this diff Show More