Compare commits

...

25 Commits

Author SHA1 Message Date
Lars Müller
790efb06b5
Merge 743199f6a4b88f3355e66eb84ac57152dde5a374 into 9a1501ae89ffe79c38dbd6756c9e7ed647dd7dc1 2024-06-28 00:55:34 +02:00
grorp
9a1501ae89
CIrrDeviceSDL: Fix numpad key events not having correct KeyInput.Char (#14780)
Allows you to change viewing range using numpad +/- again. This fix also works with the current unreleased version of SDL 3.

The keycodes for numpad keys (both SDL keycodes and Irrlicht keycodes) are not the same as the keycodes for the equivalent non-numpad keys and don't correspond to chars, so I mapped them to chars manually.

Since I think the resolution of https://github.com/minetest/minetest/issues/13770 was "just disable numlock", I made sure to only do this for the numpad number keys if numlock is enabled.
2024-06-27 14:44:44 +02:00
Erich Schubert
514e106414
Fix missing newline before Markdown list (#14783)
Renders incorrectly e.g. on https://api.minetest.net/spatial-vectors/
2024-06-26 22:21:18 +02:00
grorp
4c001bd248
Make button sprites (scrollbar arrows) DPI-aware (#14772) 2024-06-26 15:25:27 +02:00
1F616EMO~nya
fb6ceb2664
Properly escape Markdown markups at minetest.string_to_area (#14774)
Co-authored-by: DS <ds.desour@proton.me>
2024-06-24 22:14:16 +02:00
aminothere
50da26da91
Document alternative 'persist' key in noise parameter table (#14762) 2024-06-24 22:12:42 +02:00
kromka-chleba
28857841aa
Fix math.round floating point bug (#14757) 2024-06-24 20:57:06 +01:00
rubenwardy
157d129e30
Fix unnecessary content refreshing (#14705) 2024-06-24 20:56:37 +01:00
Yoruma
9ab447843b
Clarify "dtime" in API (#14758) 2024-06-22 10:59:58 +02:00
Lars Mueller
743199f6a4 Use range queries for raycasts and collisions 2024-06-07 13:15:52 +02:00
Lars Mueller
267dc09f8b Use separate type for sizes 2024-06-07 13:15:52 +02:00
Lars Mueller
bffba43644 Extend unit test 2024-06-07 13:15:52 +02:00
Lars Mueller
1ae476181f Trivial optimization for static entities 2024-06-07 13:15:52 +02:00
Lars Mueller
e305d2c138 Fix bugs 2024-06-07 13:15:52 +02:00
Lars Mueller
2bcb715386 Fix yet another wraparound issue 2024-06-07 13:15:52 +02:00
Lars Mueller
50d5ab3241 Fix potential wraparound issue 2024-06-07 13:15:52 +02:00
Lars Mueller
d33da30fed Eliminate risk of unwanted wraparound 2024-06-07 13:15:52 +02:00
Lars Mueller
8954059561 Some cleanup, add brief explanation 2024-06-07 13:15:52 +02:00
Lars Mueller
fcfabbe1e6 stuff 2024-06-07 13:15:52 +02:00
Lars Mueller
f9fc8cf1b2 Update benchmark to include very large case 2024-06-07 13:15:52 +02:00
Lars Mueller
c3345bec54 hook the thing up 2024-06-07 13:15:52 +02:00
Lars Mueller
08751de016 let there be fix 2024-06-07 13:15:52 +02:00
Lars Mueller
bcbe072b66 Add failing test 2024-06-07 13:15:52 +02:00
Lars Mueller
2701f63883 Deletion 2024-06-07 13:15:52 +02:00
Lars Mueller
957d6e1874 Dynamic k-d-tree firstest draft 2024-06-07 13:15:52 +02:00
40 changed files with 1117 additions and 318 deletions

@ -240,12 +240,15 @@ function math.factorial(x)
return v
end
function math.round(x)
if x >= 0 then
return math.floor(x + 0.5)
if x < 0 then
local int = math.ceil(x)
local frac = x - int
return int - ((frac <= -0.5) and 1 or 0)
end
return math.ceil(x - 0.5)
local int = math.floor(x)
local frac = x - int
return int + ((frac >= 0.5) and 1 or 0)
end
local formspec_escapes = {

@ -176,3 +176,17 @@ describe("formspec_escape", function()
assert.equal("\\[Hello\\\\\\[", core.formspec_escape("[Hello\\["))
end)
end)
describe("math", function()
it("round()", function()
assert.equal(0, math.round(0))
assert.equal(10, math.round(10.3))
assert.equal(11, math.round(10.5))
assert.equal(11, math.round(10.7))
assert.equal(-10, math.round(-10.3))
assert.equal(-11, math.round(-10.5))
assert.equal(-11, math.round(-10.7))
assert.equal(0, math.round(0.49999999999999994))
assert.equal(0, math.round(-0.49999999999999994))
end)
end)

@ -133,6 +133,8 @@ local function start_install(package, reason)
conf:set("release", package.release)
conf:write()
end
pkgmgr.reload_by_type(package.type)
end
end
@ -146,7 +148,6 @@ local function start_install(package, reason)
start_install(next.package, next.reason)
end
ui.update()
end
@ -427,8 +428,9 @@ end
function contentdb.update_paths()
pkgmgr.load_all()
local mod_hash = {}
pkgmgr.refresh_globals()
for _, mod in pairs(pkgmgr.global_mods:get_list()) do
local cdb_id = pkgmgr.get_contentdb_id(mod)
if cdb_id then
@ -437,7 +439,6 @@ function contentdb.update_paths()
end
local game_hash = {}
pkgmgr.update_gamelist()
for _, game in pairs(pkgmgr.games) do
local cdb_id = pkgmgr.get_contentdb_id(game)
if cdb_id then
@ -446,7 +447,7 @@ function contentdb.update_paths()
end
local txp_hash = {}
for _, txp in pairs(pkgmgr.get_texture_packs()) do
for _, txp in pairs(pkgmgr.texture_packs) do
local cdb_id = pkgmgr.get_contentdb_id(txp)
if cdb_id then
txp_hash[contentdb.aliases[cdb_id] or cdb_id] = txp

@ -110,7 +110,7 @@ pkgmgr = {}
-- @param modpack Currently processing modpack or nil/"" if none (recursion)
function pkgmgr.get_mods(path, virtual_path, listing, modpack)
local mods = core.get_dir_list(path, true)
local added = {}
for _, name in ipairs(mods) do
if name:sub(1, 1) ~= "." then
local mod_path = path .. DIR_DELIM .. name
@ -120,6 +120,7 @@ function pkgmgr.get_mods(path, virtual_path, listing, modpack)
parent_dir = path,
}
listing[#listing + 1] = toadd
added[#added + 1] = toadd
-- Get config file
local mod_conf
@ -150,8 +151,6 @@ function pkgmgr.get_mods(path, virtual_path, listing, modpack)
toadd.virtual_path = mod_virtual_path
toadd.type = "mod"
pkgmgr.update_translations({ toadd })
-- Check modpack.txt
-- Note: modpack.conf is already checked above
local modpackfile = io.open(mod_path .. DIR_DELIM .. "modpack.txt")
@ -171,6 +170,8 @@ function pkgmgr.get_mods(path, virtual_path, listing, modpack)
end
end
pkgmgr.update_translations(added)
if not modpack then
-- Sort all when the recursion is done
table.sort(listing, function(a, b)
@ -180,12 +181,13 @@ function pkgmgr.get_mods(path, virtual_path, listing, modpack)
end
--------------------------------------------------------------------------------
function pkgmgr.get_texture_packs()
function pkgmgr.reload_texture_packs()
local txtpath = core.get_texturepath()
local txtpath_system = core.get_texturepath_share()
local retval = {}
load_texture_packs(txtpath, retval)
-- on portable versions these two paths coincide. It avoids loading the path twice
if txtpath ~= txtpath_system then
load_texture_packs(txtpath_system, retval)
@ -197,11 +199,13 @@ function pkgmgr.get_texture_packs()
return a.title:lower() < b.title:lower()
end)
return retval
pkgmgr.texture_packs = retval
end
--------------------------------------------------------------------------------
function pkgmgr.get_all()
pkgmgr.load_all()
local result = {}
for _, mod in pairs(pkgmgr.global_mods:get_list()) do
@ -210,7 +214,7 @@ function pkgmgr.get_all()
for _, game in pairs(pkgmgr.games) do
result[#result + 1] = game
end
for _, txp in pairs(pkgmgr.get_texture_packs()) do
for _, txp in pairs(pkgmgr.texture_packs) do
result[#result + 1] = txp
end
@ -288,7 +292,7 @@ end
function pkgmgr.render_packagelist(render_list, use_technical_names, with_icon)
if not render_list then
if not pkgmgr.global_mods then
pkgmgr.refresh_globals()
pkgmgr.reload_global_mods()
end
render_list = pkgmgr.global_mods
end
@ -549,6 +553,7 @@ function pkgmgr.get_worldconfig(worldpath)
end
--------------------------------------------------------------------------------
-- Caller is responsible for reloading content types (see reload_by_type)
function pkgmgr.install_dir(expected_type, path, basename, targetpath)
assert(type(expected_type) == "string")
assert(type(path) == "string")
@ -615,12 +620,6 @@ function pkgmgr.install_dir(expected_type, path, basename, targetpath)
fgettext_ne("Failed to install $1 to $2", basename, targetpath)
end
if basefolder.type == "game" then
pkgmgr.update_gamelist()
else
pkgmgr.refresh_globals()
end
return targetpath, nil
end
@ -742,7 +741,7 @@ function pkgmgr.comparemod(elem1,elem2)
end
--------------------------------------------------------------------------------
function pkgmgr.refresh_globals()
function pkgmgr.reload_global_mods()
local function is_equal(element,uid) --uid match
if element.name == uid then
return true
@ -774,7 +773,7 @@ function pkgmgr.get_game_mods(gamespec, retval)
end
--------------------------------------------------------------------------------
function pkgmgr.update_gamelist()
function pkgmgr.reload_games()
pkgmgr.games = core.get_games()
table.sort(pkgmgr.games, function(a, b)
return a.title:lower() < b.title:lower()
@ -782,6 +781,32 @@ function pkgmgr.update_gamelist()
pkgmgr.update_translations(pkgmgr.games)
end
--------------------------------------------------------------------------------
function pkgmgr.reload_by_type(type)
if type == "game" then
pkgmgr.reload_games()
elseif type == "txp" then
pkgmgr.reload_texture_packs()
elseif type == "mod" or type == "modpack" then
pkgmgr.reload_global_mods()
else
error("Unknown package type: " .. type)
end
end
--------------------------------------------------------------------------------
function pkgmgr.load_all()
if not pkgmgr.global_mods then
pkgmgr.reload_global_mods()
end
if not pkgmgr.games then
pkgmgr.reload_games()
end
if not pkgmgr.texture_packs then
pkgmgr.reload_texture_packs()
end
end
--------------------------------------------------------------------------------
function pkgmgr.update_translations(list)
for _, item in ipairs(list) do
@ -831,4 +856,4 @@ end
--------------------------------------------------------------------------------
-- read initial data
--------------------------------------------------------------------------------
pkgmgr.update_gamelist()
pkgmgr.reload_games()

@ -46,6 +46,9 @@ local function reset()
function core.get_texturepath()
return txp_dir
end
function core.get_texturepath_share()
return txp_dir
end
function core.get_modpath()
return mods_dir
end
@ -59,13 +62,6 @@ local function reset()
setfenv(loadfile("builtin/common/misc_helpers.lua"), env)()
setfenv(loadfile("builtin/mainmenu/content/pkgmgr.lua"), env)()
function env.pkgmgr.update_gamelist()
table.insert(calls, { "update_gamelist" })
end
function env.pkgmgr.refresh_globals()
table.insert(calls, { "refresh_globals" })
end
function env.assert_calls(list)
assert.are.same(list, calls)
end
@ -113,7 +109,6 @@ describe("install_dir", function()
env.assert_calls({
{ "delete_dir", mods_dir .. "/mymod" },
{ "copy_dir", "/tmp/123", mods_dir .. "/mymod", false },
{ "refresh_globals" },
})
end)
@ -129,7 +124,6 @@ describe("install_dir", function()
env.assert_calls({
{ "delete_dir", mods_dir .. "/mymod" },
{ "copy_dir", "/tmp/123", mods_dir .. "/mymod", false },
{ "refresh_globals" },
})
end)
@ -145,7 +139,6 @@ describe("install_dir", function()
env.assert_calls({
{ "delete_dir", games_dir .. "/mygame" },
{ "copy_dir", "/tmp/123", games_dir .. "/mygame", false },
{ "update_gamelist" },
})
end)
@ -161,7 +154,6 @@ describe("install_dir", function()
env.assert_calls({
{ "delete_dir", mods_dir .. "/123" },
{ "copy_dir", "/tmp/123", mods_dir .. "/123", false },
{ "refresh_globals" },
})
end)
@ -188,7 +180,6 @@ describe("install_dir", function()
env.assert_calls({
{ "delete_dir", "/tmp/alt-target" },
{ "copy_dir", "/tmp/123", "/tmp/alt-target", false },
{ "refresh_globals" },
})
end)
@ -238,6 +229,5 @@ describe("install_dir", function()
path, message = env.pkgmgr.install_dir("txp", "/tmp/123", "name", nil)
assert.is._not._nil(path)
assert.is._nil(message)
end)
end)

@ -72,9 +72,6 @@ end
local function has_packages_from_cdb()
pkgmgr.refresh_globals()
pkgmgr.update_gamelist()
for _, content in pairs(pkgmgr.get_all()) do
if pkgmgr.get_contentdb_id(content) then
return true
@ -127,9 +124,6 @@ function update_detector.get_all()
return {}
end
pkgmgr.refresh_globals()
pkgmgr.update_gamelist()
local ret = {}
local all_content = pkgmgr.get_all()
for _, content in ipairs(all_content) do

@ -37,11 +37,7 @@ local function delete_content_buttonhandler(this, fields)
gamedata.errormessage = fgettext_ne("pkgmgr: failed to delete \"$1\"", this.data.content.path)
end
if this.data.content.type == "game" then
pkgmgr.update_gamelist()
else
pkgmgr.refresh_globals()
end
pkgmgr.reload_by_type(this.data.content.type)
else
gamedata.errormessage = fgettext_ne("pkgmgr: invalid path \"$1\"", this.data.content.path)
end

@ -45,7 +45,7 @@ local function rename_modpack_buttonhandler(this, fields)
local oldpath = this.data.mod.path
local targetpath = this.data.mod.parent_dir .. DIR_DELIM .. fields["te_modpack_name"]
os.rename(oldpath, targetpath)
pkgmgr.refresh_globals()
pkgmgr.reload_global_mods()
pkgmgr.selected_mod = pkgmgr.global_mods:get_current_index(
pkgmgr.global_mods:raw_index_by_uid(fields["te_modpack_name"]))

@ -22,16 +22,27 @@ local component_funcs = dofile(core.get_mainmenu_path() .. DIR_DELIM ..
local shadows_component = dofile(core.get_mainmenu_path() .. DIR_DELIM ..
"settings" .. DIR_DELIM .. "shadows_component.lua")
local full_settings = settingtypes.parse_config_file(false, true)
local loaded = false
local full_settings
local info_icon_path = core.formspec_escape(defaulttexturedir .. "settings_info.png")
local reset_icon_path = core.formspec_escape(defaulttexturedir .. "settings_reset.png")
local all_pages = {}
local page_by_id = {}
local filtered_pages = all_pages
local filtered_page_by_id = page_by_id
local function get_setting_info(name)
for _, entry in ipairs(full_settings) do
if entry.type ~= "category" and entry.name == name then
return entry
end
end
return nil
end
local function add_page(page)
assert(type(page.id) == "string")
assert(type(page.title) == "string")
@ -46,49 +57,6 @@ local function add_page(page)
end
local change_keys = {
query_text = "Controls",
requires = {
keyboard_mouse = true,
},
get_formspec = function(self, avail_w)
local btn_w = math.min(avail_w, 3)
return ("button[0,0;%f,0.8;btn_change_keys;%s]"):format(btn_w, fgettext("Controls")), 0.8
end,
on_submit = function(self, fields)
if fields.btn_change_keys then
core.show_keys_menu()
end
end,
}
add_page({
id = "accessibility",
title = fgettext_ne("Accessibility"),
content = {
"language",
{ heading = fgettext_ne("General") },
"font_size",
"chat_font_size",
"gui_scaling",
"hud_scaling",
"show_nametag_backgrounds",
{ heading = fgettext_ne("Chat") },
"console_height",
"console_alpha",
"console_color",
{ heading = fgettext_ne("Controls") },
"autojump",
"safe_dig_and_place",
{ heading = fgettext_ne("Movement") },
"arm_inertia",
"view_bobbing_amount",
"fall_bobbing_amount",
},
})
local function load_settingtypes()
local page = nil
local section = nil
@ -129,94 +97,134 @@ local function load_settingtypes()
end
end
end
load_settingtypes()
table.insert(page_by_id.controls_keyboard_and_mouse.content, 1, change_keys)
do
local content = page_by_id.graphics_and_audio_shaders.content
local idx = table.indexof(content, "enable_dynamic_shadows")
table.insert(content, idx, shadows_component)
end
local function get_setting_info(name)
for _, entry in ipairs(full_settings) do
if entry.type ~= "category" and entry.name == name then
return entry
end
local function load()
if loaded then
return
end
loaded = true
full_settings = settingtypes.parse_config_file(false, true)
local change_keys = {
query_text = "Controls",
requires = {
keyboard_mouse = true,
},
get_formspec = function(self, avail_w)
local btn_w = math.min(avail_w, 3)
return ("button[0,0;%f,0.8;btn_change_keys;%s]"):format(btn_w, fgettext("Controls")), 0.8
end,
on_submit = function(self, fields)
if fields.btn_change_keys then
core.show_keys_menu()
end
end,
}
add_page({
id = "accessibility",
title = fgettext_ne("Accessibility"),
content = {
"language",
{ heading = fgettext_ne("General") },
"font_size",
"chat_font_size",
"gui_scaling",
"hud_scaling",
"show_nametag_backgrounds",
{ heading = fgettext_ne("Chat") },
"console_height",
"console_alpha",
"console_color",
{ heading = fgettext_ne("Controls") },
"autojump",
"safe_dig_and_place",
{ heading = fgettext_ne("Movement") },
"arm_inertia",
"view_bobbing_amount",
"fall_bobbing_amount",
},
})
load_settingtypes()
table.insert(page_by_id.controls_keyboard_and_mouse.content, 1, change_keys)
do
local content = page_by_id.graphics_and_audio_shaders.content
local idx = table.indexof(content, "enable_dynamic_shadows")
table.insert(content, idx, shadows_component)
end
return nil
-- These must not be translated, as they need to show in the local
-- language no matter the user's current language.
-- This list must be kept in sync with src/unsupported_language_list.txt.
get_setting_info("language").option_labels = {
[""] = fgettext_ne("(Use system language)"),
--ar = " [ar]", blacklisted
be = "Беларуская [be]",
bg = "Български [bg]",
ca = "Català [ca]",
cs = "Česky [cs]",
cy = "Cymraeg [cy]",
da = "Dansk [da]",
de = "Deutsch [de]",
--dv = " [dv]", blacklisted
el = "Ελληνικά [el]",
en = "English [en]",
eo = "Esperanto [eo]",
es = "Español [es]",
et = "Eesti [et]",
eu = "Euskara [eu]",
fi = "Suomi [fi]",
fil = "Wikang Filipino [fil]",
fr = "Français [fr]",
gd = "Gàidhlig [gd]",
gl = "Galego [gl]",
--he = " [he]", blacklisted
--hi = " [hi]", blacklisted
hu = "Magyar [hu]",
id = "Bahasa Indonesia [id]",
it = "Italiano [it]",
ja = "日本語 [ja]",
jbo = "Lojban [jbo]",
kk = "Қазақша [kk]",
--kn = " [kn]", blacklisted
ko = "한국어 [ko]",
ky = "Kırgızca / Кыргызча [ky]",
lt = "Lietuvių [lt]",
lv = "Latviešu [lv]",
mn = "Монгол [mn]",
mr = "मराठी [mr]",
ms = "Bahasa Melayu [ms]",
--ms_Arab = " [ms_Arab]", blacklisted
nb = "Norsk Bokmål [nb]",
nl = "Nederlands [nl]",
nn = "Norsk Nynorsk [nn]",
oc = "Occitan [oc]",
pl = "Polski [pl]",
pt = "Português [pt]",
pt_BR = "Português do Brasil [pt_BR]",
ro = "Română [ro]",
ru = "Русский [ru]",
sk = "Slovenčina [sk]",
sl = "Slovenščina [sl]",
sr_Cyrl = "Српски [sr_Cyrl]",
sr_Latn = "Srpski (Latinica) [sr_Latn]",
sv = "Svenska [sv]",
sw = "Kiswahili [sw]",
--th = " [th]", blacklisted
tr = "Türkçe [tr]",
tt = "Tatarça [tt]",
uk = "Українська [uk]",
vi = "Tiếng Việt [vi]",
zh_CN = "中文 (简体) [zh_CN]",
zh_TW = "正體中文 (繁體) [zh_TW]",
}
end
-- These must not be translated, as they need to show in the local
-- language no matter the user's current language.
-- This list must be kept in sync with src/unsupported_language_list.txt.
get_setting_info("language").option_labels = {
[""] = fgettext_ne("(Use system language)"),
--ar = " [ar]", blacklisted
be = "Беларуская [be]",
bg = "Български [bg]",
ca = "Català [ca]",
cs = "Česky [cs]",
cy = "Cymraeg [cy]",
da = "Dansk [da]",
de = "Deutsch [de]",
--dv = " [dv]", blacklisted
el = "Ελληνικά [el]",
en = "English [en]",
eo = "Esperanto [eo]",
es = "Español [es]",
et = "Eesti [et]",
eu = "Euskara [eu]",
fi = "Suomi [fi]",
fil = "Wikang Filipino [fil]",
fr = "Français [fr]",
gd = "Gàidhlig [gd]",
gl = "Galego [gl]",
--he = " [he]", blacklisted
--hi = " [hi]", blacklisted
hu = "Magyar [hu]",
id = "Bahasa Indonesia [id]",
it = "Italiano [it]",
ja = "日本語 [ja]",
jbo = "Lojban [jbo]",
kk = "Қазақша [kk]",
--kn = " [kn]", blacklisted
ko = "한국어 [ko]",
ky = "Kırgızca / Кыргызча [ky]",
lt = "Lietuvių [lt]",
lv = "Latviešu [lv]",
mn = "Монгол [mn]",
mr = "मराठी [mr]",
ms = "Bahasa Melayu [ms]",
--ms_Arab = " [ms_Arab]", blacklisted
nb = "Norsk Bokmål [nb]",
nl = "Nederlands [nl]",
nn = "Norsk Nynorsk [nn]",
oc = "Occitan [oc]",
pl = "Polski [pl]",
pt = "Português [pt]",
pt_BR = "Português do Brasil [pt_BR]",
ro = "Română [ro]",
ru = "Русский [ru]",
sk = "Slovenčina [sk]",
sl = "Slovenščina [sl]",
sr_Cyrl = "Српски [sr_Cyrl]",
sr_Latn = "Srpski (Latinica) [sr_Latn]",
sv = "Svenska [sv]",
sw = "Kiswahili [sw]",
--th = " [th]", blacklisted
tr = "Türkçe [tr]",
tt = "Tatarça [tt]",
uk = "Українська [uk]",
vi = "Tiếng Việt [vi]",
zh_CN = "中文 (简体) [zh_CN]",
zh_TW = "正體中文 (繁體) [zh_TW]",
}
-- See if setting matches keywords
local function get_setting_match_weight(entry, query_keywords)
local setting_score = 0
@ -734,6 +742,7 @@ end
function create_settings_dlg()
load()
local dlg = dialog_create("dlg_settings", get_formspec, buttonhandler, eventhandler)
dlg.data.page_id = update_filtered_pages("")

@ -439,9 +439,9 @@ function settingtypes.parse_config_file(read_all, parse_mods)
end
-- Parse mods
pkgmgr.load_all()
local mods_category_initialized = false
local mods = {}
pkgmgr.get_mods(core.get_modpath(), "mods", mods)
local mods = pkgmgr.global_mods:get_list()
table.sort(mods, function(a, b) return a.name < b.name end)
for _, mod in ipairs(mods) do

@ -29,16 +29,11 @@ end
local packages_raw, packages
local function update_packages()
if not pkgmgr.global_mods then
pkgmgr.refresh_globals()
end
if not pkgmgr.games then
pkgmgr.update_gamelist()
end
pkgmgr.load_all()
packages_raw = {}
table.insert_all(packages_raw, pkgmgr.games)
table.insert_all(packages_raw, pkgmgr.get_texture_packs())
table.insert_all(packages_raw, pkgmgr.texture_packs)
table.insert_all(packages_raw, pkgmgr.global_mods:get_list())
local function get_data()
@ -207,6 +202,7 @@ local function handle_doubleclick(pkg)
core.settings:set("texture_path", pkg.path)
end
packages = nil
pkgmgr.reload_texture_packs()
mm_game_theme.init()
mm_game_theme.set_engine()
@ -271,6 +267,7 @@ local function handle_buttons(tabview, fields, tabname, tabdata)
core.settings:set("texture_path", txp_path)
packages = nil
pkgmgr.reload_texture_packs()
mm_game_theme.init()
mm_game_theme.set_engine()

@ -314,7 +314,8 @@ Minetest namespace reference
Call these functions only at load time!
* `minetest.register_globalstep(function(dtime))`
* Called every client environment step, usually interval of 0.1s
* Called every client environment step
* `dtime` is the time since last execution in seconds.
* `minetest.register_on_mods_loaded(function())`
* Called just after mods have finished loading.
* `minetest.register_on_shutdown(function())`

@ -3902,6 +3902,7 @@ Operators
---------
Operators can be used if all of the involved vectors have metatables:
* `v1 == v2`:
* Returns whether `v1` and `v2` are identical.
* `-v`:
@ -4007,8 +4008,9 @@ Helper functions
* X1, Y1, ... Z2 are coordinates
* `relative_to`: Optional. If set to a position, each coordinate
can use the tilde notation for relative positions
* Tilde notation: "~": Relative coordinate
"~<number>": Relative coordinate plus <number>
* Tilde notation
* `"~"`: Relative coordinate
* `"~<number>"`: Relative coordinate plus `<number>`
* Example: `minetest.string_to_area("(1,2,3) (~5,~-5,~)", {x=10,y=10,z=10})`
returns `{x=1,y=2,z=3}, {x=15,y=5,z=10}`
* `minetest.formspec_escape(string)`: returns a string
@ -4346,6 +4348,8 @@ the previous octave.
This may need to be tuned when altering `lacunarity`; when doing so consider
that a common medium value is 1 / lacunarity.
Instead of `persistence`, the key `persist` may be used to the same effect.
### `lacunarity`
Each additional octave has a 'wavelength' that is the 'wavelength' of the
@ -5714,7 +5718,8 @@ Global callback registration functions
Call these functions only at load time!
* `minetest.register_globalstep(function(dtime))`
* Called every server step, usually interval of 0.1s
* Called every server step, usually interval of 0.1s.
* `dtime` is the time since last execution in seconds.
* `minetest.register_on_mods_loaded(function())`
* Called after mods have finished loading and before the media is cached or the
aliases handled.

@ -200,7 +200,7 @@ class IGUIButton : public IGUIElement
\param loop: True if the animation should loop, false if not
\param scale: True if the sprite should scale to button size, false if not */
virtual void setSprite(EGUI_BUTTON_STATE state, s32 index,
video::SColor color = video::SColor(255, 255, 255, 255), bool loop = false, bool scale = false) = 0;
video::SColor color = video::SColor(255, 255, 255, 255), bool loop = false) = 0;
//! Get the sprite-index for the given state or -1 when no sprite is set
virtual s32 getSpriteIndex(EGUI_BUTTON_STATE state) const = 0;
@ -211,9 +211,6 @@ class IGUIButton : public IGUIElement
//! Returns if the sprite in the given state does loop
virtual bool getSpriteLoop(EGUI_BUTTON_STATE state) const = 0;
//! Returns if the sprite in the given state is scaled
virtual bool getSpriteScale(EGUI_BUTTON_STATE state) const = 0;
//! Sets if the button should behave like a push button.
/** Which means it can be in two states: Normal or Pressed. With a click on the button,
the user can change the state of the button. */

@ -374,6 +374,12 @@ const c8 *const GUISkinFontNames[EGDF_COUNT + 1] = {
class IGUISkin : virtual public IReferenceCounted
{
public:
//! returns display density scaling factor
virtual float getScale() const = 0;
//! sets display density scaling factor
virtual void setScale(float scale) = 0;
//! returns default color
virtual video::SColor getColor(EGUI_DEFAULT_COLOR color) const = 0;

@ -7,6 +7,7 @@
#include "irrMath.h"
#include <functional>
#include <array>
namespace irr
{
@ -32,6 +33,9 @@ class vector3d
//! Constructor with the same value for all elements
explicit constexpr vector3d(T n) :
X(n), Y(n), Z(n) {}
//! Array - vector conversion
constexpr vector3d(const std::array<T, 3> &arr) :
X(arr[0]), Y(arr[1]), Z(arr[2]) {}
// operators
@ -181,6 +185,26 @@ class vector3d
return *this;
}
std::array<T, 3> toArray() const {
return {X, Y, Z};
}
vector3d<T> min(const T min_component) const {
return vector3d<T>(
std::min(X, min_component),
std::min(Y, min_component),
std::min(Z, min_component)
);
}
vector3d<T> max(const T max_component) const {
return vector3d<T>(
std::max(X, max_component),
std::max(Y, max_component),
std::max(Z, max_component)
);
}
//! Get length of the vector.
T getLength() const { return core::squareroot(X * X + Y * Y + Z * Z); }

@ -75,12 +75,11 @@ void CGUIButton::setSpriteBank(IGUISpriteBank *sprites)
SpriteBank = sprites;
}
void CGUIButton::setSprite(EGUI_BUTTON_STATE state, s32 index, video::SColor color, bool loop, bool scale)
void CGUIButton::setSprite(EGUI_BUTTON_STATE state, s32 index, video::SColor color, bool loop)
{
ButtonSprites[(u32)state].Index = index;
ButtonSprites[(u32)state].Color = color;
ButtonSprites[(u32)state].Loop = loop;
ButtonSprites[(u32)state].Scale = scale;
}
//! Get the sprite-index for the given state or -1 when no sprite is set
@ -101,12 +100,6 @@ bool CGUIButton::getSpriteLoop(EGUI_BUTTON_STATE state) const
return ButtonSprites[(u32)state].Loop;
}
//! Returns if the sprite in the given state is scaled
bool CGUIButton::getSpriteScale(EGUI_BUTTON_STATE state) const
{
return ButtonSprites[(u32)state].Scale;
}
//! called if an event happened.
bool CGUIButton::OnEvent(const SEvent &event)
{
@ -294,19 +287,26 @@ void CGUIButton::draw()
void CGUIButton::drawSprite(EGUI_BUTTON_STATE state, u32 startTime, const core::position2di &center)
{
u32 stateIdx = (u32)state;
s32 spriteIdx = ButtonSprites[stateIdx].Index;
if (spriteIdx == -1)
return;
if (ButtonSprites[stateIdx].Index != -1) {
if (ButtonSprites[stateIdx].Scale) {
const video::SColor colors[] = {ButtonSprites[stateIdx].Color, ButtonSprites[stateIdx].Color, ButtonSprites[stateIdx].Color, ButtonSprites[stateIdx].Color};
SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, AbsoluteRect,
&AbsoluteClippingRect, colors,
os::Timer::getTime() - startTime, ButtonSprites[stateIdx].Loop);
} else {
SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, center,
&AbsoluteClippingRect, ButtonSprites[stateIdx].Color, startTime, os::Timer::getTime(),
ButtonSprites[stateIdx].Loop, true);
}
}
u32 rectIdx = SpriteBank->getSprites()[spriteIdx].Frames[0].rectNumber;
core::rect<s32> srcRect = SpriteBank->getPositions()[rectIdx];
IGUISkin *skin = Environment->getSkin();
s32 scale = std::max(std::floor(skin->getScale()), 1.0f);
core::rect<s32> rect(center, srcRect.getSize() * scale);
rect -= rect.getSize() / 2;
const video::SColor colors[] = {
ButtonSprites[stateIdx].Color,
ButtonSprites[stateIdx].Color,
ButtonSprites[stateIdx].Color,
ButtonSprites[stateIdx].Color,
};
SpriteBank->draw2DSprite(spriteIdx, rect, &AbsoluteClippingRect, colors,
os::Timer::getTime() - startTime, ButtonSprites[stateIdx].Loop);
}
EGUI_BUTTON_IMAGE_STATE CGUIButton::getImageState(bool pressed) const

@ -92,7 +92,7 @@ class CGUIButton : public IGUIButton
*/
virtual void setSprite(EGUI_BUTTON_STATE state, s32 index,
video::SColor color = video::SColor(255, 255, 255, 255),
bool loop = false, bool scale = false) override;
bool loop = false) override;
//! Get the sprite-index for the given state or -1 when no sprite is set
s32 getSpriteIndex(EGUI_BUTTON_STATE state) const override;
@ -103,9 +103,6 @@ class CGUIButton : public IGUIButton
//! Returns if the sprite in the given state does loop
bool getSpriteLoop(EGUI_BUTTON_STATE state) const override;
//! Returns if the sprite in the given state is scaled
bool getSpriteScale(EGUI_BUTTON_STATE state) const override;
//! Sets if the button should behave like a push button. Which means it
//! can be in two states: Normal or Pressed. With a click on the button,
//! the user can change the state of the button.
@ -158,19 +155,18 @@ class CGUIButton : public IGUIButton
struct ButtonSprite
{
ButtonSprite() :
Index(-1), Loop(false), Scale(false)
Index(-1), Loop(false)
{
}
bool operator==(const ButtonSprite &other) const
{
return Index == other.Index && Color == other.Color && Loop == other.Loop && Scale == other.Scale;
return Index == other.Index && Color == other.Color && Loop == other.Loop;
}
s32 Index;
video::SColor Color;
bool Loop;
bool Scale;
};
ButtonSprite ButtonSprites[EGBS_COUNT];

@ -24,6 +24,12 @@ class CGUISkin : public IGUISkin
//! destructor
virtual ~CGUISkin();
//! returns display density scaling factor
virtual float getScale() const override { return Scale; }
//! sets display density scaling factor
virtual void setScale(float scale) override { Scale = scale; }
//! returns default color
video::SColor getColor(EGUI_DEFAULT_COLOR color) const override;
@ -210,6 +216,7 @@ class CGUISkin : public IGUISkin
EGUI_SKIN_TYPE getType() const override;
private:
float Scale = 1.0f;
video::SColor Colors[EGDC_COUNT];
s32 Sizes[EGDS_COUNT];
u32 Icons[EGDI_COUNT];

@ -129,9 +129,9 @@ EM_BOOL CIrrDeviceSDL::MouseLeaveCallback(int eventType, const EmscriptenMouseEv
}
#endif
bool CIrrDeviceSDL::keyIsKnownSpecial(EKEY_CODE key)
bool CIrrDeviceSDL::keyIsKnownSpecial(EKEY_CODE irrlichtKey)
{
switch (key) {
switch (irrlichtKey) {
// keys which are known to have safe special character interpretation
// could need changes over time (removals and additions!)
case KEY_RETURN:
@ -189,24 +189,68 @@ bool CIrrDeviceSDL::keyIsKnownSpecial(EKEY_CODE key)
}
}
int CIrrDeviceSDL::findCharToPassToIrrlicht(int assumedChar, EKEY_CODE key)
int CIrrDeviceSDL::findCharToPassToIrrlicht(uint32_t sdlKey, EKEY_CODE irrlichtKey, bool numlock)
{
switch (irrlichtKey) {
// special cases that always return a char regardless of how the SDL keycode
// looks
switch (key) {
case KEY_RETURN:
case KEY_ESCAPE:
return (int)key;
return (int)irrlichtKey;
// This is necessary for keys on the numpad because they don't use the same
// keycodes as their non-numpad versions (whose keycodes correspond to chars),
// but have their own SDL keycodes and their own Irrlicht keycodes (which
// don't correspond to chars).
case KEY_MULTIPLY:
return '*';
case KEY_ADD:
return '+';
case KEY_SUBTRACT:
return '-';
case KEY_DIVIDE:
return '/';
default:
break;
}
if (numlock) {
// Number keys on the numpad are also affected, but we only want them
// to produce number chars when numlock is enabled.
switch (irrlichtKey) {
case KEY_NUMPAD0:
return '0';
case KEY_NUMPAD1:
return '1';
case KEY_NUMPAD2:
return '2';
case KEY_NUMPAD3:
return '3';
case KEY_NUMPAD4:
return '4';
case KEY_NUMPAD5:
return '5';
case KEY_NUMPAD6:
return '6';
case KEY_NUMPAD7:
return '7';
case KEY_NUMPAD8:
return '8';
case KEY_NUMPAD9:
return '9';
default:
break;
}
}
// SDL in-place ORs values with no character representation with 1<<30
// https://wiki.libsdl.org/SDL2/SDLKeycodeLookup
if (assumedChar & (1 << 30))
// This also affects the numpad keys btw.
if (sdlKey & (1 << 30))
return 0;
switch (key) {
switch (irrlichtKey) {
case KEY_PRIOR:
case KEY_NEXT:
case KEY_HOME:
@ -218,7 +262,7 @@ int CIrrDeviceSDL::findCharToPassToIrrlicht(int assumedChar, EKEY_CODE key)
case KEY_NUMLOCK:
return 0;
default:
return assumedChar;
return sdlKey;
}
}
@ -825,7 +869,8 @@ bool CIrrDeviceSDL::run()
irrevent.KeyInput.PressedDown = (SDL_event.type == SDL_KEYDOWN);
irrevent.KeyInput.Shift = (SDL_event.key.keysym.mod & KMOD_SHIFT) != 0;
irrevent.KeyInput.Control = (SDL_event.key.keysym.mod & KMOD_CTRL) != 0;
irrevent.KeyInput.Char = findCharToPassToIrrlicht(mp.SDLKey, key);
irrevent.KeyInput.Char = findCharToPassToIrrlicht(mp.SDLKey, key,
(SDL_event.key.keysym.mod & KMOD_NUM) != 0);
postEventFromUser(irrevent);
} break;

@ -273,10 +273,10 @@ class CIrrDeviceSDL : public CIrrDeviceStub
#endif
// Check if a key is a known special character with no side effects on text boxes.
static bool keyIsKnownSpecial(EKEY_CODE key);
static bool keyIsKnownSpecial(EKEY_CODE irrlichtKey);
// Return the Char that should be sent to Irrlicht for the given key (either the one passed in or 0).
static int findCharToPassToIrrlicht(int assumedChar, EKEY_CODE key);
static int findCharToPassToIrrlicht(uint32_t sdlKey, EKEY_CODE irrlichtKey, bool numlock);
// Check if a text box is in focus. Enable or disable SDL_TEXTINPUT events only if in focus.
void resetReceiveTextInputEvents();

@ -55,7 +55,7 @@ class ActiveObjectMgr
for (auto &it : m_active_objects.iter()) {
if (!it.second)
continue;
m_active_objects.remove(it.first);
removeObject(it.first);
}
} while (!m_active_objects.empty());
}

@ -105,7 +105,11 @@ void benchGetObjectsInArea(Catch::Benchmark::Chronometer &meter)
TEST_CASE("ActiveObjectMgr") {
BENCH_INSIDE_RADIUS(200)
BENCH_INSIDE_RADIUS(1450)
BENCH_INSIDE_RADIUS(10000)
BENCH_IN_AREA(200)
BENCH_IN_AREA(1450)
BENCH_IN_AREA(10000)
}
// TODO benchmark active object manager update costs

@ -352,6 +352,7 @@ void ClientLauncher::config_guienv()
float density = rangelim(g_settings->getFloat("gui_scaling"), 0.5f, 20) *
RenderingEngine::getDisplayDensity();
skin->setScale(density);
skin->setSize(gui::EGDS_CHECK_BOX_WIDTH, (s32)(17.0f * density));
skin->setSize(gui::EGDS_SCROLLBAR_SIZE, (s32)(21.0f * density));
skin->setSize(gui::EGDS_WINDOW_BUTTON_WIDTH, (s32)(15.0f * density));

@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "collision.h"
#include <cmath>
#include "irr_aabb3d.h"
#include "mapblock.h"
#include "map.h"
#include "nodedef.h"
@ -281,13 +282,14 @@ static void add_object_boxes(Environment *env,
}
};
// Calculate distance by speed, add own extent and 1.5m of tolerance
const f32 distance = speed_f.getLength() * dtime +
box_0.getExtent().getLength() + 1.5f * BS;
const f32 tolerance = 1.5f * BS; // TODO increase tolerance
#ifndef SERVER
ClientEnvironment *c_env = dynamic_cast<ClientEnvironment*>(env);
if (c_env) {
// Calculate distance by speed, add own extent and tolerance
const f32 distance = speed_f.getLength() * dtime +
box_0.getExtent().getLength() + tolerance;
std::vector<DistanceSortedActiveObject> clientobjects;
c_env->getActiveObjects(pos_f, distance, clientobjects);
@ -326,9 +328,14 @@ static void add_object_boxes(Environment *env,
return false;
};
// Calculate distance by speed, add own extent and tolerance
const v3f movement = speed_f * dtime;
const v3f min = pos_f + box_0.MinEdge - v3f(tolerance) + movement.min(0);
const v3f max = pos_f + box_0.MaxEdge + v3f(tolerance) + movement.max(0);
// nothing is put into this vector
std::vector<ServerActiveObject*> s_objects;
s_env->getObjectsInsideRadius(s_objects, pos_f, distance, include_obj_cb);
s_env->getObjectsInArea(s_objects, aabb3f(min, max), include_obj_cb);
}
}
}

@ -89,12 +89,11 @@ void GUIButton::setSpriteBank(IGUISpriteBank* sprites)
SpriteBank = sprites;
}
void GUIButton::setSprite(EGUI_BUTTON_STATE state, s32 index, video::SColor color, bool loop, bool scale)
void GUIButton::setSprite(EGUI_BUTTON_STATE state, s32 index, video::SColor color, bool loop)
{
ButtonSprites[(u32)state].Index = index;
ButtonSprites[(u32)state].Color = color;
ButtonSprites[(u32)state].Loop = loop;
ButtonSprites[(u32)state].Scale = scale;
}
//! Get the sprite-index for the given state or -1 when no sprite is set
@ -115,12 +114,6 @@ bool GUIButton::getSpriteLoop(EGUI_BUTTON_STATE state) const
return ButtonSprites[(u32)state].Loop;
}
//! Returns if the sprite in the given state is scaled
bool GUIButton::getSpriteScale(EGUI_BUTTON_STATE state) const
{
return ButtonSprites[(u32)state].Scale;
}
//! called if an event happened.
bool GUIButton::OnEvent(const SEvent& event)
{
@ -354,23 +347,26 @@ void GUIButton::draw()
void GUIButton::drawSprite(EGUI_BUTTON_STATE state, u32 startTime, const core::position2di& center)
{
u32 stateIdx = (u32)state;
s32 spriteIdx = ButtonSprites[stateIdx].Index;
if (spriteIdx == -1)
return;
if (ButtonSprites[stateIdx].Index != -1)
{
if ( ButtonSprites[stateIdx].Scale )
{
const video::SColor colors[] = {ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color};
SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, AbsoluteRect.UpperLeftCorner,
&AbsoluteClippingRect, colors[0], // FIXME: remove [0]
porting::getTimeMs()-startTime, ButtonSprites[stateIdx].Loop);
}
else
{
SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, center,
&AbsoluteClippingRect, ButtonSprites[stateIdx].Color, startTime, porting::getTimeMs(),
ButtonSprites[stateIdx].Loop, true);
}
}
u32 rectIdx = SpriteBank->getSprites()[spriteIdx].Frames[0].rectNumber;
core::rect<s32> srcRect = SpriteBank->getPositions()[rectIdx];
IGUISkin *skin = Environment->getSkin();
s32 scale = std::max(std::floor(skin->getScale()), 1.0f);
core::rect<s32> rect(center, srcRect.getSize() * scale);
rect -= rect.getSize() / 2;
const video::SColor colors[] = {
ButtonSprites[stateIdx].Color,
ButtonSprites[stateIdx].Color,
ButtonSprites[stateIdx].Color,
ButtonSprites[stateIdx].Color,
};
SpriteBank->draw2DSprite(spriteIdx, rect, &AbsoluteClippingRect, colors,
porting::getTimeMs() - startTime, ButtonSprites[stateIdx].Loop);
}
EGUI_BUTTON_IMAGE_STATE GUIButton::getImageState(bool pressed) const

@ -92,7 +92,7 @@ class GUIButton : public gui::IGUIButton
*/
virtual void setSprite(gui::EGUI_BUTTON_STATE state, s32 index,
video::SColor color=video::SColor(255,255,255,255),
bool loop=false, bool scale=false) override;
bool loop=false) override;
//! Get the sprite-index for the given state or -1 when no sprite is set
virtual s32 getSpriteIndex(gui::EGUI_BUTTON_STATE state) const override;
@ -103,9 +103,6 @@ class GUIButton : public gui::IGUIButton
//! Returns if the sprite in the given state does loop
virtual bool getSpriteLoop(gui::EGUI_BUTTON_STATE state) const override;
//! Returns if the sprite in the given state is scaled
virtual bool getSpriteScale(gui::EGUI_BUTTON_STATE state) const override;
//! Sets if the button should behave like a push button. Which means it
//! can be in two states: Normal or Pressed. With a click on the button,
//! the user can change the state of the button.
@ -230,13 +227,12 @@ class GUIButton : public gui::IGUIButton
{
bool operator==(const ButtonSprite &other) const
{
return Index == other.Index && Color == other.Color && Loop == other.Loop && Scale == other.Scale;
return Index == other.Index && Color == other.Color && Loop == other.Loop;
}
s32 Index = -1;
video::SColor Color;
bool Loop = false;
bool Scale = false;
};
ButtonSprite ButtonSprites[gui::EGBS_COUNT];

@ -27,6 +27,12 @@ namespace gui
//! destructor
virtual ~GUISkin();
//! returns display density scaling factor
virtual float getScale() const { return Scale; }
//! sets display density scaling factor
virtual void setScale(float scale) { Scale = scale; }
//! returns default color
virtual video::SColor getColor(EGUI_DEFAULT_COLOR color) const;
@ -292,6 +298,7 @@ namespace gui
private:
float Scale = 1.0f;
video::SColor Colors[EGDC_COUNT];
s32 Sizes[EGDS_COUNT];
u32 Icons[EGDI_COUNT];

@ -41,7 +41,7 @@ void ActiveObjectMgr::clearIf(const std::function<bool(ServerActiveObject *, u16
continue;
if (cb(it.second.get(), it.first)) {
// Remove reference from m_active_objects
m_active_objects.remove(it.first);
removeObject(it.first);
}
}
}
@ -83,16 +83,18 @@ bool ActiveObjectMgr::registerObject(std::unique_ptr<ServerActiveObject> obj)
return false;
}
if (objectpos_over_limit(obj->getBasePosition())) {
v3f p = obj->getBasePosition();
const v3f pos = obj->getBasePosition();
if (objectpos_over_limit(pos)) {
warningstream << "Server::ActiveObjectMgr::addActiveObjectRaw(): "
<< "object position (" << p.X << "," << p.Y << "," << p.Z
<< "object position (" << pos.X << "," << pos.Y << "," << pos.Z
<< ") outside maximum range" << std::endl;
return false;
}
auto obj_id = obj->getId();
m_active_objects.put(obj_id, std::move(obj));
m_spatial_index.insert(pos.toArray(), obj_id);
assert(m_spatial_index.size() == m_active_objects.size());
auto new_size = m_active_objects.size();
verbosestream << "Server::ActiveObjectMgr::addActiveObjectRaw(): "
@ -115,42 +117,45 @@ void ActiveObjectMgr::removeObject(u16 id)
if (!ok) {
infostream << "Server::ActiveObjectMgr::removeObject(): "
<< "id=" << id << " not found" << std::endl;
} else {
m_spatial_index.remove(id);
}
}
void ActiveObjectMgr::updatePos(const v3f &pos, u16 id) {
// laziggy solution: only update if we already know the object
if (m_active_objects.get(id) != nullptr)
m_spatial_index.update(pos.toArray(), id);
}
void ActiveObjectMgr::getObjectsInsideRadius(const v3f &pos, float radius,
std::vector<ServerActiveObject *> &result,
std::function<bool(ServerActiveObject *obj)> include_obj_cb)
{
float r2 = radius * radius;
for (auto &activeObject : m_active_objects.iter()) {
ServerActiveObject *obj = activeObject.second.get();
if (!obj)
continue;
const v3f &objectpos = obj->getBasePosition();
if (objectpos.getDistanceFromSQ(pos) > r2)
continue;
float r_squared = radius * radius;
m_spatial_index.rangeQuery((pos - v3f(radius)).toArray(), (pos + v3f(radius)).toArray(), [&](auto objPos, u16 id) {
if (v3f(objPos).getDistanceFromSQ(pos) > r_squared)
return;
auto obj = m_active_objects.get(id).get();
if (!obj)
return;
if (!include_obj_cb || include_obj_cb(obj))
result.push_back(obj);
}
});
}
void ActiveObjectMgr::getObjectsInArea(const aabb3f &box,
std::vector<ServerActiveObject *> &result,
std::function<bool(ServerActiveObject *obj)> include_obj_cb)
{
for (auto &activeObject : m_active_objects.iter()) {
ServerActiveObject *obj = activeObject.second.get();
m_spatial_index.rangeQuery(box.MinEdge.toArray(), box.MaxEdge.toArray(), [&](auto _, u16 id) {
auto obj = m_active_objects.get(id).get();
if (!obj)
continue;
const v3f &objectpos = obj->getBasePosition();
if (!box.isPointInside(objectpos))
continue;
return;
if (!include_obj_cb || include_obj_cb(obj))
result.push_back(obj);
}
});
}
void ActiveObjectMgr::getAddedActiveObjectsAroundPos(v3f player_pos, f32 radius,

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <vector>
#include "../activeobjectmgr.h"
#include "serveractiveobject.h"
#include "util/k_d_tree.h"
namespace server
{
@ -38,6 +39,8 @@ class ActiveObjectMgr final : public ::ActiveObjectMgr<ServerActiveObject>
bool registerObject(std::unique_ptr<ServerActiveObject> obj) override;
void removeObject(u16 id) override;
void updatePos(const v3f &pos, u16 id);
void getObjectsInsideRadius(const v3f &pos, float radius,
std::vector<ServerActiveObject *> &result,
std::function<bool(ServerActiveObject *obj)> include_obj_cb);
@ -48,5 +51,7 @@ class ActiveObjectMgr final : public ::ActiveObjectMgr<ServerActiveObject>
void getAddedActiveObjectsAroundPos(v3f player_pos, f32 radius,
f32 player_radius, const std::set<u16> &current_objects,
std::vector<u16> &added_objects);
private:
DynamicKdTrees<3, f32, u16> m_spatial_index;
};
} // namespace server

@ -162,7 +162,7 @@ void LuaEntitySAO::step(float dtime, bool send_recommended)
// Each frame, parent position is copied if the object is attached, otherwise it's calculated normally
// If the object gets detached this comes into effect automatically from the last known origin
if (auto *parent = getParent()) {
m_base_position = parent->getBasePosition();
setBasePosition(parent->getBasePosition());
m_velocity = v3f(0,0,0);
m_acceleration = v3f(0,0,0);
} else {
@ -171,7 +171,7 @@ void LuaEntitySAO::step(float dtime, bool send_recommended)
box.MinEdge *= BS;
box.MaxEdge *= BS;
f32 pos_max_d = BS*0.25; // Distance per iteration
v3f p_pos = m_base_position;
v3f p_pos = getBasePosition();
v3f p_velocity = m_velocity;
v3f p_acceleration = m_acceleration;
moveresult = collisionMoveSimple(m_env, m_env->getGameDef(),
@ -181,11 +181,11 @@ void LuaEntitySAO::step(float dtime, bool send_recommended)
moveresult_p = &moveresult;
// Apply results
m_base_position = p_pos;
setBasePosition(p_pos);
m_velocity = p_velocity;
m_acceleration = p_acceleration;
} else {
m_base_position += (m_velocity + m_acceleration * 0.5f * dtime) * dtime;
addPos((m_velocity + m_acceleration * 0.5f * dtime) * dtime);
m_velocity += dtime * m_acceleration;
}
@ -228,7 +228,7 @@ void LuaEntitySAO::step(float dtime, bool send_recommended)
} else if(m_last_sent_position_timer > 0.2){
minchange = 0.05*BS;
}
float move_d = m_base_position.getDistanceFrom(m_last_sent_position);
float move_d = getBasePosition().getDistanceFrom(m_last_sent_position);
move_d += m_last_sent_move_precision;
float vel_d = m_velocity.getDistanceFrom(m_last_sent_velocity);
if (move_d > minchange || vel_d > minchange ||
@ -252,7 +252,7 @@ std::string LuaEntitySAO::getClientInitializationData(u16 protocol_version)
os << serializeString16(m_init_name); // name
writeU8(os, 0); // is_player
writeU16(os, getId()); //id
writeV3F32(os, m_base_position);
writeV3F32(os, getBasePosition());
writeV3F32(os, m_rotation);
writeU16(os, m_hp);
@ -381,7 +381,7 @@ void LuaEntitySAO::setPos(const v3f &pos)
{
if(isAttached())
return;
m_base_position = pos;
setBasePosition(pos);
sendPosition(false, true);
}
@ -389,7 +389,7 @@ void LuaEntitySAO::moveTo(v3f pos, bool continuous)
{
if(isAttached())
return;
m_base_position = pos;
setBasePosition(pos);
if(!continuous)
sendPosition(true, true);
}
@ -403,7 +403,7 @@ std::string LuaEntitySAO::getDescription()
{
std::ostringstream oss;
oss << "LuaEntitySAO \"" << m_init_name << "\" ";
auto pos = floatToInt(m_base_position, BS);
auto pos = floatToInt(getBasePosition(), BS);
oss << "at " << pos;
return oss.str();
}
@ -521,10 +521,10 @@ void LuaEntitySAO::sendPosition(bool do_interpolate, bool is_movement_end)
// Send attachment updates instantly to the client prior updating position
sendOutdatedData();
m_last_sent_move_precision = m_base_position.getDistanceFrom(
m_last_sent_move_precision = getBasePosition().getDistanceFrom(
m_last_sent_position);
m_last_sent_position_timer = 0;
m_last_sent_position = m_base_position;
m_last_sent_position = getBasePosition();
m_last_sent_velocity = m_velocity;
//m_last_sent_acceleration = m_acceleration;
m_last_sent_rotation = m_rotation;
@ -532,7 +532,7 @@ void LuaEntitySAO::sendPosition(bool do_interpolate, bool is_movement_end)
float update_interval = m_env->getSendRecommendedInterval();
std::string str = generateUpdatePositionCommand(
m_base_position,
getBasePosition(),
m_velocity,
m_acceleration,
m_rotation,
@ -552,8 +552,8 @@ bool LuaEntitySAO::getCollisionBox(aabb3f *toset) const
toset->MinEdge = m_prop.collisionbox.MinEdge * BS;
toset->MaxEdge = m_prop.collisionbox.MaxEdge * BS;
toset->MinEdge += m_base_position;
toset->MaxEdge += m_base_position;
toset->MinEdge += getBasePosition();
toset->MaxEdge += getBasePosition();
return true;
}

@ -86,11 +86,10 @@ std::string PlayerSAO::getDescription()
void PlayerSAO::addedToEnvironment(u32 dtime_s)
{
ServerActiveObject::addedToEnvironment(dtime_s);
ServerActiveObject::setBasePosition(m_base_position);
m_player->setPlayerSAO(this);
m_player->setPeerId(m_peer_id_initial);
m_peer_id_initial = PEER_ID_INEXISTENT; // don't try to use it again.
m_last_good_position = m_base_position;
m_last_good_position = getBasePosition();
}
// Called before removing from environment
@ -116,7 +115,7 @@ std::string PlayerSAO::getClientInitializationData(u16 protocol_version)
os << serializeString16(m_player->getName()); // name
writeU8(os, 1); // is_player
writeS16(os, getId()); // id
writeV3F32(os, m_base_position);
writeV3F32(os, getBasePosition());
writeV3F32(os, m_rotation);
writeU16(os, getHP());
@ -195,7 +194,7 @@ void PlayerSAO::step(float dtime, bool send_recommended)
// Sequence of damage points, starting 0.1 above feet and progressing
// upwards in 1 node intervals, stopping below top damage point.
for (float dam_height = 0.1f; dam_height < dam_top; dam_height++) {
v3s16 p = floatToInt(m_base_position +
v3s16 p = floatToInt(getBasePosition() +
v3f(0.0f, dam_height * BS, 0.0f), BS);
MapNode n = m_env->getMap().getNode(p);
const ContentFeatures &c = m_env->getGameDef()->ndef()->get(n);
@ -207,7 +206,7 @@ void PlayerSAO::step(float dtime, bool send_recommended)
}
// Top damage point
v3s16 ptop = floatToInt(m_base_position +
v3s16 ptop = floatToInt(getBasePosition() +
v3f(0.0f, dam_top * BS, 0.0f), BS);
MapNode ntop = m_env->getMap().getNode(ptop);
const ContentFeatures &c = m_env->getGameDef()->ndef()->get(ntop);
@ -285,7 +284,7 @@ void PlayerSAO::step(float dtime, bool send_recommended)
if (isAttached())
pos = m_last_good_position;
else
pos = m_base_position;
pos = getBasePosition();
std::string str = generateUpdatePositionCommand(
pos,
@ -344,7 +343,7 @@ std::string PlayerSAO::generateUpdatePhysicsOverrideCommand() const
void PlayerSAO::setBasePosition(v3f position)
{
if (m_player && position != m_base_position)
if (m_player && position != getBasePosition())
m_player->setDirty(true);
// This needs to be ran for attachments too
@ -629,7 +628,7 @@ bool PlayerSAO::checkMovementCheat()
if (m_is_singleplayer ||
isAttached() ||
g_settings->getBool("disable_anticheat")) {
m_last_good_position = m_base_position;
m_last_good_position = getBasePosition();
return false;
}
@ -694,7 +693,7 @@ bool PlayerSAO::checkMovementCheat()
if (player_max_jump < 0.0001f)
player_max_jump = 0.0001f;
v3f diff = (m_base_position - m_last_good_position);
v3f diff = (getBasePosition() - m_last_good_position);
float d_vert = diff.Y;
diff.Y = 0;
float d_horiz = diff.getLength();
@ -710,7 +709,7 @@ bool PlayerSAO::checkMovementCheat()
}
if (m_move_pool.grab(required_time)) {
m_last_good_position = m_base_position;
m_last_good_position = getBasePosition();
} else {
const float LAG_POOL_MIN = 5.0;
float lag_pool_max = m_env->getMaxLagEstimate() * 2.0;
@ -732,8 +731,8 @@ bool PlayerSAO::getCollisionBox(aabb3f *toset) const
toset->MinEdge = m_prop.collisionbox.MinEdge * BS;
toset->MaxEdge = m_prop.collisionbox.MaxEdge * BS;
toset->MinEdge += m_base_position;
toset->MaxEdge += m_base_position;
toset->MinEdge += getBasePosition();
toset->MaxEdge += getBasePosition();
return true;
}

@ -182,7 +182,7 @@ class PlayerSAO : public UnitSAO
void finalize(RemotePlayer *player, const std::set<std::string> &privs);
v3f getEyePosition() const { return m_base_position + getEyeOffset(); }
v3f getEyePosition() const { return getBasePosition() + getEyeOffset(); }
v3f getEyeOffset() const;
float getZoomFOV() const;

@ -31,6 +31,13 @@ ServerActiveObject::ServerActiveObject(ServerEnvironment *env, v3f pos):
{
}
void ServerActiveObject::setBasePosition(v3f pos) {
bool changed = m_base_position != pos;
m_base_position = pos;
if (changed && m_env) // HACK this doesn't feel right; *when* is m_env null?
ServerEnvironment_updatePos(m_env, pos, getId());
}
float ServerActiveObject::getMinimumSavedMovement()
{
return 2.0*BS;

@ -44,6 +44,7 @@ Some planning
*/
class ServerEnvironment;
void ServerEnvironment_updatePos(ServerEnvironment *senv, const v3f &pos, u16 id);
struct ItemStack;
struct ToolCapabilities;
struct ObjectProperties;
@ -77,7 +78,7 @@ class ServerActiveObject : public ActiveObject
Some simple getters/setters
*/
v3f getBasePosition() const { return m_base_position; }
void setBasePosition(v3f pos){ m_base_position = pos; }
void setBasePosition(v3f pos);
ServerEnvironment* getEnv(){ return m_env; }
/*
@ -244,7 +245,6 @@ class ServerActiveObject : public ActiveObject
virtual void onDetach(int parent_id) {}
ServerEnvironment *m_env;
v3f m_base_position;
std::unordered_set<u32> m_attached_particle_spawners;
/*
@ -272,4 +272,6 @@ class ServerActiveObject : public ActiveObject
Queue of messages to be sent to the client
*/
std::queue<ActiveObjectMessage> m_messages_out;
private:
v3f m_base_position; // setBasePosition updates index and MUST be called
};

@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <stack>
#include <utility>
#include "serverenvironment.h"
#include "irr_aabb3d.h"
#include "settings.h"
#include "log.h"
#include "mapblock.h"
@ -1871,10 +1872,12 @@ void ServerEnvironment::getSelectedActiveObjects(
return false;
};
aabb3f search_area(shootline_on_map.start - 5 * BS, shootline_on_map.end + 5 * BS);
search_area.repair();
// Use "logic in callback" pattern to avoid useless vector filling
std::vector<ServerActiveObject*> tmp;
getObjectsInsideRadius(tmp, shootline_on_map.getMiddle(),
0.5 * shootline_on_map.getLength() + 5 * BS, process);
getObjectsInArea(tmp, search_area, process);
}
/*
@ -2528,3 +2531,8 @@ bool ServerEnvironment::migrateAuthDatabase(
}
return true;
}
// HACK
void ServerEnvironment_updatePos(ServerEnvironment *senv, const v3f &pos, u16 id) {
senv->updatePos(pos, id);
}

@ -334,6 +334,10 @@ class ServerEnvironment final : public Environment
// Find the daylight value at pos with a Depth First Search
u8 findSunlight(v3s16 pos) const;
void updatePos(const v3f &pos, u16 id) {
return m_ao_manager.updatePos(pos, id);
}
// Find all active objects inside a radius around a point
void getObjectsInsideRadius(std::vector<ServerActiveObject *> &objects, const v3f &pos, float radius,
std::function<bool(ServerActiveObject *obj)> include_obj_cb)
@ -513,3 +517,6 @@ class ServerEnvironment final : public Environment
std::unique_ptr<ServerActiveObject> createSAO(ActiveObjectType type, v3f pos,
const std::string &data);
};
// HACK
void ServerEnvironment_updatePos(ServerEnvironment *senv, const v3f &pos, u16 id);

@ -10,6 +10,7 @@ set (UNITTEST_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/test_connection.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_craft.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_datastructures.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_k_d_tree.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_filesys.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_inventory.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_irrptr.cpp

@ -0,0 +1,145 @@
// Copyright (C) 2024 Lars Müller
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "noise.h"
#include "test.h"
#include "util/k_d_tree.h"
#include <algorithm>
#include <unordered_set>
class TestKdTree : public TestBase
{
public:
TestKdTree() { TestManager::registerTestModule(this); }
const char *getName() { return "TestKdTree"; }
void runTests(IGameDef *gamedef);
// TODO basic small cube test
void singleUpdate();
void randomOps();
};
template<uint8_t Dim, typename Component, typename Id>
class ObjectVector {
public:
using Point = std::array<Component, Dim>;
void insert(const Point &p, Id id) {
entries.push_back(Entry{p, id});
}
void remove(Id id) {
const auto it = std::find_if(entries.begin(), entries.end(), [&](const auto &e) {
return e.id == id;
});
assert(it != entries.end());
entries.erase(it);
}
void update(const Point &p, Id id) {
remove(id);
insert(p, id);
}
template<typename F>
void rangeQuery(const Point &min, const Point &max, const F &cb) {
for (const auto &e : entries) {
for (uint8_t d = 0; d < Dim; ++d)
if (e.point[d] < min[d] || e.point[d] > max[d])
goto next;
cb(e.point, e.id); // TODO check
next: {}
}
}
private:
struct Entry {
Point point;
Id id;
};
std::vector<Entry> entries;
};
static TestKdTree g_test_instance;
void TestKdTree::runTests(IGameDef *gamedef)
{
rawstream << "-------- k-d-tree" << std::endl;
TEST(singleUpdate);
TEST(randomOps);
}
void TestKdTree::singleUpdate() {
DynamicKdTrees<3, u16, u16> kds;
for (u16 i = 1; i <= 5; ++i)
kds.insert({i, i, i}, i);
for (u16 i = 1; i <= 5; ++i) {
u16 j = i - 1;
kds.update({j, j, j}, i);
}
}
void TestKdTree::randomOps() {
PseudoRandom pr(814538);
ObjectVector<3, f32, u16> objvec;
DynamicKdTrees<3, f32, u16> kds;
const auto randPos = [&]() {
std::array<f32, 3> point;
for (uint8_t d = 0; d < 3; ++d)
point[d] = pr.range(-1000, 1000);
return point;
};
for (u16 id = 1; id < 1000; ++id) {
const auto point = randPos();
objvec.insert(point, id);
kds.insert(point, id);
}
const auto testRandomQuery = [&]() {
std::array<f32, 3> min, max;
for (uint8_t d = 0; d < 3; ++d) {
min[d] = pr.range(-1500, 1500);
max[d] = min[d] + pr.range(1, 2500);
}
std::unordered_set<u16> expected_ids;
objvec.rangeQuery(min, max, [&](auto _, u16 id) {
UASSERT(expected_ids.count(id) == 0);
expected_ids.insert(id);
});
kds.rangeQuery(min, max, [&](auto point, u16 id) {
UASSERT(expected_ids.count(id) == 1);
expected_ids.erase(id);
});
UASSERT(expected_ids.empty());
};
const auto testRandomQueries = [&]() {
for (int i = 0; i < 1000; ++i) {
testRandomQuery();
}
};
testRandomQueries();
for (u16 id = 1; id < 800; ++id) {
objvec.remove(id);
kds.remove(id);
}
testRandomQueries();
for (u16 id = 800; id < 1000; ++id) {
const auto point = randPos();
objvec.update(point, id);
kds.update(point, id);
}
testRandomQueries();
// Finally, empty the structure(s)
for (u16 id = 800; id < 1000; ++id) {
objvec.remove(id);
kds.remove(id);
testRandomQuery();
}
}

499
src/util/k_d_tree.h Normal file

@ -0,0 +1,499 @@
// Copyright (C) 2024 Lars Müller
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <unordered_map>
#include <vector>
#include <memory>
/*
This implements a dynamic forest of static k-d-trees.
A k-d-tree is a k-dimensional binary search tree.
On the i-th level of the tree, you split by the (i mod k)-th coordinate.
Building a balanced k-d-tree for n points is done in O(n log n) time:
Points are stored in a matrix, identified by indices.
These indices are presorted by all k axes.
To split, you simply pick the pivot index in the appropriate index array,
and mark all points left to it by index in a bitset.
This lets you then split the indices sorted by the other axes,
while preserving the sorted order.
This however only gives us a static spatial index.
To make it dynamic, we keep a "forest" of k-d-trees of sizes of successive powers of two.
When we insert a new tree, we check whether there already is a k-d-tree of the same size.
If this is the case, we merge with that tree, giving us a tree of twice the size,
and so on, until we find a free size.
This means our "forest" corresponds to a bit pattern,
where a set bit means a non-empty tree.
Inserting a point is equivalent to incrementing this bit pattern.
To handle deletions, we simply mark the appropriate point as deleted using another bitset.
When more than half the points have been deleted,
we shrink the structure by removing all deleted points.
This is equivalent to shifting down the "bit pattern" by one.
There are plenty variations that could be explored:
* Keeping a small amount of points in a small pool to make updates faster -
avoid building and querying small k-d-trees.
This might be useful if the overhead for small sizes hurts performance.
* Keeping fewer trees to make queries faster, at the expense of updates.
* More eagerly removing entries marked as deleted (for example, on merge).
* Replacing the array-backed structure with a structure of dynamically allocated nodes.
This would make it possible to "let trees get out of shape".
* Shrinking the structure currently sorts the live points by all axes,
not leveraging the existing presorting of the subsets.
Cleverly done filtering followed by sorted merges should enable linear time.
*/
using Idx = uint16_t; // TODO unify with Id
// Use a separate, larger type for sizes than for indices
// to make sure there are no wraparounds when we approach the limit.
// This hardly affects performance or memory usage;
// the core arrays still only store indices.
using Size = uint32_t;
// TODO more doc comments
// TODO profile and tweak knobs
// TODO cleanup (split up in header and impl among other things)
template<uint8_t Dim, typename Component>
class Points {
public:
using Point = std::array<Component, Dim>;
//! Empty
Points() : n(0), coords(nullptr) {}
//! Allocating constructor; leaves coords uninitialized!
Points(Size n) : n(n), coords(new Component[Dim * n]) {}
//! Copying constructor
Points(Size n, const std::array<Component const *, Dim> &coords) : Points(n) {
for (uint8_t d = 0; d < Dim; ++d)
std::copy(coords[d], coords[d] + n, begin(d));
}
Size size() const {
return n;
}
void assign(Idx start, const Points &from) {
for (uint8_t d = 0; d < Dim; ++d)
std::copy(from.begin(d), from.end(d), begin(d) + start);
}
Point getPoint(Idx i) const {
Point point;
for (uint8_t d = 0; d < Dim; ++d)
point[d] = begin(d)[i];
return point;
}
void setPoint(Idx i, const Point &point) {
for (uint8_t d = 0; d < Dim; ++d)
begin(d)[i] = point[d];
}
Component *begin(uint8_t d) {
return coords.get() + d * static_cast<std::size_t>(n);
}
Component *end(uint8_t d) {
return begin(d) + n;
}
const Component *begin(uint8_t d) const {
return coords.get() + d * static_cast<std::size_t>(n);
}
const Component *end(uint8_t d) const {
return begin(d) + n;
}
private:
Size n;
std::unique_ptr<Component[]> coords;
};
template<uint8_t Dim>
class SortedIndices {
public:
//! empty
SortedIndices() : indices() {}
//! uninitialized indices
static SortedIndices newUninitialized(Size n) {
return SortedIndices(Points<Dim, Idx>(n));
}
//! Identity permutation on all axes
SortedIndices(Size n) : indices(n) {
for (uint8_t d = 0; d < Dim; ++d)
for (Idx i = 0; i < n; ++i)
indices.begin(d)[i] = i;
}
Size size() const {
return indices.size();
}
bool empty() const {
return size() == 0;
}
struct SplitResult {
SortedIndices left, right;
Idx pivot;
};
//! Splits the sorted indices in the middle along the specified axis,
//! partitioning them into left (<=), the pivot, and right (>=).
SplitResult split(uint8_t axis, std::vector<bool> &markers) const {
const auto begin = indices.begin(axis);
Idx left_n = indices.size() / 2;
const auto mid = begin + left_n;
// Mark all points to be partitioned left
for (auto it = begin; it != mid; ++it)
markers[*it] = true;
SortedIndices left(left_n);
std::copy(begin, mid, left.indices.begin(axis));
SortedIndices right(indices.size() - left_n - 1);
std::copy(mid + 1, indices.end(axis), right.indices.begin(axis));
for (uint8_t d = 0; d < Dim; ++d) {
if (d == axis)
continue;
auto left_ptr = left.indices.begin(d);
auto right_ptr = right.indices.begin(d);
for (auto it = indices.begin(d); it != indices.end(d); ++it) {
if (*it != *mid) { // ignore pivot
if (markers[*it])
*(left_ptr++) = *it;
else
*(right_ptr++) = *it;
}
}
}
// Unmark points, since we want to reuse the storage for markers
for (auto it = begin; it != mid; ++it)
markers[*it] = false;
return SplitResult{std::move(left), std::move(right), *mid};
}
Idx *begin(uint8_t d) {
return indices.begin(d);
}
Idx *end(uint8_t d) {
return indices.end(d);
}
const Idx *begin(uint8_t d) const {
return indices.begin(d);
}
const Idx *end(uint8_t d) const {
return indices.end(d);
}
private:
SortedIndices(Points<Dim, Idx> &&indices) : indices(std::move(indices)) {}
Points<Dim, Idx> indices;
};
template<uint8_t Dim, class Component>
class SortedPoints {
public:
SortedPoints() : points(), indices() {}
//! Single point
SortedPoints(const std::array<Component, Dim> &point) : points(1), indices(1) {
points.setPoint(0, point);
}
//! Sort points
SortedPoints(Size n, const std::array<Component const *, Dim> ptrs)
: points(n, ptrs), indices(n)
{
for (uint8_t d = 0; d < Dim; ++d) {
const auto coord = points.begin(d);
std::sort(indices.begin(d), indices.end(d), [&](auto i, auto j) {
return coord[i] < coord[j];
});
}
}
//! Merge two sets of sorted points
SortedPoints(const SortedPoints &a, const SortedPoints &b)
: points(a.size() + b.size())
{
const auto n = points.size();
indices = SortedIndices<Dim>::newUninitialized(n);
for (uint8_t d = 0; d < Dim; ++d) {
points.assign(0, a.points);
points.assign(a.points.size(), b.points);
const auto coord = points.begin(d);
auto a_ptr = a.indices.begin(d);
auto b_ptr = b.indices.begin(d);
auto dst_ptr = indices.begin(d);
while (a_ptr != a.indices.end(d) && b_ptr != b.indices.end(d)) {
const auto i = *a_ptr;
const auto j = *b_ptr + a.size();
if (coord[i] <= coord[j]) {
*(dst_ptr++) = i;
++a_ptr;
} else {
*(dst_ptr++) = j;
++b_ptr;
}
}
while (a_ptr != a.indices.end(d))
*(dst_ptr++) = *(a_ptr++);
while (b_ptr != b.indices.end(d))
*(dst_ptr++) = a.size() + *(b_ptr++);
}
}
Size size() const {
// technically redundant with indices.size(),
// but that is irrelevant
return points.size();
}
Points<Dim, Component> points;
SortedIndices<Dim> indices;
};
template<uint8_t Dim, class Component, class Id>
class KdTree {
public:
using Point = std::array<Component, Dim>;
//! Empty tree
KdTree()
: items()
, ids(nullptr)
, tree(nullptr)
, deleted()
{}
//! Build a tree containing just a single point
KdTree(const Point &point, const Id &id)
: items(point)
, ids(std::make_unique<Id[]>(1))
, tree(std::make_unique<Idx[]>(1))
, deleted(1)
{
tree[0] = 0;
ids[0] = id;
}
//! Build a tree
KdTree(Size n, Id const *ids, std::array<Component const *, Dim> pts)
: items(n, pts)
, ids(std::make_unique<Id[]>(n))
, tree(std::make_unique<Idx[]>(n))
, deleted(n)
{
std::copy(ids, ids + n, this->ids.get());
init(0, 0, items.indices);
}
//! Merge two trees. Both trees are assumed to have a power of two size.
KdTree(const KdTree &a, const KdTree &b)
: items(a.items, b.items)
{
tree = std::make_unique<Idx[]>(cap());
ids = std::make_unique<Id[]>(cap());
std::copy(a.ids.get(), a.ids.get() + a.cap(), ids.get());
std::copy(b.ids.get(), b.ids.get() + b.cap(), ids.get() + a.cap());
// Note: Initialize `deleted` *before* calling `init`,
// since `init` abuses the `deleted` marks as left/right marks.
deleted = std::vector<bool>(cap());
init(0, 0, items.indices);
std::copy(a.deleted.begin(), a.deleted.end(), deleted.begin());
std::copy(b.deleted.begin(), b.deleted.end(), deleted.begin() + a.items.size());
}
// TODO ray proximity query
template<typename F>
void rangeQuery(const Point &min, const Point &max,
const F &cb) const {
rangeQuery(0, 0, min, max, cb);
}
void remove(Idx internalIdx) {
assert(!deleted[internalIdx]);
deleted[internalIdx] = true;
}
template<class F>
void foreach(F cb) const {
for (Idx i = 0; i < cap(); ++i)
if (!deleted[i])
cb(i, items.points.getPoint(i), ids[i]);
}
//! Capacity, not size, since some items may be marked as deleted
Size cap() const {
return items.size();
}
//! "Empty" as in "never had anything"
bool empty() const {
return cap() == 0;
}
private:
void init(Idx root, uint8_t axis, const SortedIndices<Dim> &sorted) {
// Temporarily abuse "deleted" marks as left/right marks
const auto split = sorted.split(axis, deleted);
tree[root] = split.pivot;
const auto next_axis = (axis + 1) % Dim;
if (!split.left.empty())
init(2 * root + 1, next_axis, split.left);
if (!split.right.empty())
init(2 * root + 2, next_axis, split.right);
}
template<typename F>
// Note: root is of type std::size_t to avoid issues with wraparound
void rangeQuery(std::size_t root, uint8_t split,
const Point &min, const Point &max,
const F &cb) const {
if (root >= cap())
return;
const auto ptid = tree[root];
const auto coord = items.points.begin(split)[ptid];
const auto leftChild = 2*root + 1;
const auto rightChild = 2*root + 2;
const auto nextSplit = (split + 1) % Dim;
if (min[split] > coord) {
rangeQuery(rightChild, nextSplit, min, max, cb);
} else if (max[split] < coord) {
rangeQuery(leftChild, nextSplit, min, max, cb);
} else {
rangeQuery(rightChild, nextSplit, min, max, cb);
rangeQuery(leftChild, nextSplit, min, max, cb);
if (deleted[ptid])
return;
const auto point = items.points.getPoint(ptid);
for (uint8_t d = 0; d < Dim; ++d)
if (point[d] < min[d] || point[d] > max[d])
return;
cb(point, ids[ptid]);
}
}
SortedPoints<Dim, Component> items;
std::unique_ptr<Id[]> ids;
std::unique_ptr<Idx[]> tree;
// vector because this has the template specialization we want
// and i'm too lazy to implement bitsets myself right now
// just to shave off 16 redundant bytes (len + cap)
std::vector<bool> deleted;
};
template<uint8_t Dim, class Component, class Id>
class DynamicKdTrees {
using Tree = KdTree<Dim, Component, Id>;
public:
using Point = typename Tree::Point;
void insert(const std::array<Component, Dim> &point, Id id) {
Tree tree(point, id);
for (uint8_t tree_idx = 0;; ++tree_idx) {
if (tree_idx == trees.size()) {
trees.push_back(std::move(tree));
updateDelEntries(tree_idx);
break;
}
if (trees[tree_idx].empty()) {
trees[tree_idx] = std::move(tree);
updateDelEntries(tree_idx);
break;
}
tree = Tree(tree, trees[tree_idx]);
trees[tree_idx] = std::move(Tree());
}
++n_entries;
}
void remove(Id id) {
const auto it = del_entries.find(id);
assert(it != del_entries.end());
trees.at(it->second.tree_idx).remove(it->second.in_tree);
del_entries.erase(it);
++deleted;
if (deleted >= (n_entries+1)/2) // "shift out" the last tree
shrink_to_half();
}
void update(const Point &newPos, Id id) {
remove(id);
insert(newPos, id);
}
template<typename F>
void rangeQuery(const Point &min, const Point &max,
const F &cb) const {
for (const auto &tree : trees)
tree.rangeQuery(min, max, cb);
}
Size size() const {
return n_entries - deleted;
}
private:
void updateDelEntries(uint8_t tree_idx) {
trees[tree_idx].foreach([&](Idx in_tree_idx, auto _, Id id) {
del_entries[id] = {tree_idx, in_tree_idx};
});
}
// Shrink to half the size, equivalent to shifting down the "bit pattern".
void shrink_to_half() {
assert(n_entries >= deleted);
assert(n_entries - deleted == (n_entries >> 1));
n_entries -= deleted;
deleted = 0;
// Reset map, freeing memory (instead of clearing)
del_entries = std::unordered_map<Id, DelEntry>();
// Collect all live points and corresponding IDs.
const auto live_ids = std::make_unique<Id[]>(n_entries);
Points<Dim, Component> live_points(n_entries);
Size i = 0;
for (const auto &tree : trees) {
tree.foreach([&](Idx _, auto point, Id id) {
assert(i < n_entries);
live_points.setPoint(static_cast<Idx>(i), point);
live_ids[i] = id;
++i;
});
}
assert(i == n_entries);
// Construct a new forest.
// The "tree pattern" will effectively just be shifted down by one.
auto id_ptr = live_ids.get();
std::array<Component const *, Dim> point_ptrs;
Size n = 1;
for (uint8_t d = 0; d < Dim; ++d)
point_ptrs[d] = live_points.begin(d);
for (uint8_t tree_idx = 0; tree_idx < trees.size() - 1; ++tree_idx, n *= 2) {
Tree tree;
if (!trees[tree_idx+1].empty()) {
tree = std::move(Tree(n, id_ptr, point_ptrs));
id_ptr += n;
for (uint8_t d = 0; d < Dim; ++d)
point_ptrs[d] += n;
}
trees[tree_idx] = std::move(tree);
updateDelEntries(tree_idx);
}
trees.pop_back(); // "shift out" tree with the most elements
}
// This could even use an array instead of a vector,
// since the number of trees is guaranteed to be logarithmic in the max of Idx
std::vector<Tree> trees;
struct DelEntry {
uint8_t tree_idx;
Idx in_tree;
};
std::unordered_map<Id, DelEntry> del_entries;
Size n_entries = 0;
Size deleted = 0;
};