diff --git a/doc/en/scripting/builtins/libgui.md b/doc/en/scripting/builtins/libgui.md index e3c109566..53d6e4ad2 100644 --- a/doc/en/scripting/builtins/libgui.md +++ b/doc/en/scripting/builtins/libgui.md @@ -149,16 +149,22 @@ gui.load_document( args: table ) -> str --- Loads a template into the layout +-- Loads and processes layout template from file gui.template( -- template name in /layouts/templates without path and extension name: str, -- variable table (can be used in markup) -- * Ex: -- * text in this case is the value from params with the text key - params: table, - -- table, available in events as the global variable DATA - [optional] data: table + params: table +) -> str + +-- Processes layout template from string +gui.process_template( + -- template source code + source: str, + -- variable table (same as gui.template) + params: table ) -> str ``` diff --git a/doc/ru/scripting/builtins/libgui.md b/doc/ru/scripting/builtins/libgui.md index a744639ea..9c2eb7418 100644 --- a/doc/ru/scripting/builtins/libgui.md +++ b/doc/ru/scripting/builtins/libgui.md @@ -147,17 +147,23 @@ gui.load_document( args: table -- параметры для события on_open ) -> string --- Загружает шаблон в лояут +-- Обрабатывает xml шаблон макета из файла gui.template( -- имя шаблона в /layouts/templates без пути и расширения name: string, -- таблица переменных (может быть использована в разметке) -- * Пр: -- * text в данном случае, это значение из params по ключу text - params: table, - -- таблица, доступная в событиях как глобальная переменная DATA - [опционально] data: table + params: table ) -> string + +-- Обрабатывает xml шаблон макета из строки +gui.process_template( + -- шаблон в виде строки + source: string, + -- таблица переменных, как в gui.template + params: table +) ``` ## Корневой документ diff --git a/res/layouts/pages/settings_audio.xml.lua b/res/layouts/pages/settings_audio.xml.lua index 1ff217534..475ccc51b 100644 --- a/res/layouts/pages/settings_audio.xml.lua +++ b/res/layouts/pages/settings_audio.xml.lua @@ -1,4 +1,4 @@ -function create_setting(id, name, step, postfix) +function create_trackbar_setting(id, name, step, postfix) local info = app.get_setting_info(id) postfix = postfix or "" document.root:add(gui.template("track_setting", { @@ -10,10 +10,10 @@ function create_setting(id, name, step, postfix) step=step, postfix=postfix })) - update_setting(app.get_setting(id), id, name, postfix) + update_trackbar_label(app.get_setting(id), id, name, postfix) end -function update_setting(x, id, name, postfix) +function update_trackbar_label(x, id, name, postfix) app.set_setting(id, x) -- updating label document[id..".L"].text = string.format( @@ -24,12 +24,22 @@ function update_setting(x, id, name, postfix) ) end +function update_checkbox_setting(id, value) + app.set_setting(id, value) + + local recording_enabled = app.get_setting("audio.recording-enabled") + document.input_device_select.enabled = recording_enabled + document.input_volume_inner.enabled = recording_enabled + document.input_volume_outer.enabled = recording_enabled +end + function create_checkbox(id, name, tooltip) tooltip = tooltip or '' document.root:add(string.format( - "%s", + "%s", id, app.str_setting(id), gui.str(tooltip, "settings"), gui.str(name, "settings") )) + update_checkbox_setting(id, app.get_setting(id)) end local initialized = false @@ -55,11 +65,12 @@ function on_open() prev_amplitude = amplitude end) end - create_setting("audio.volume-master", "Master Volume", 0.01) - create_setting("audio.volume-regular", "Regular Sounds", 0.01) - create_setting("audio.volume-ui", "UI Sounds", 0.01) - create_setting("audio.volume-ambient", "Ambient", 0.01) - create_setting("audio.volume-music", "Music", 0.01) + create_trackbar_setting("audio.volume-master", "Master Volume", 0.01) + create_trackbar_setting("audio.volume-regular", "Regular Sounds", 0.01) + create_trackbar_setting("audio.volume-ui", "UI Sounds", 0.01) + create_trackbar_setting("audio.volume-ambient", "Ambient", 0.01) + create_trackbar_setting("audio.volume-music", "Music", 0.01) + document.root:add("") document.root:add("".. @@ -70,5 +70,5 @@ function on_open() create_checkbox("camera.inertia", "Camera Inertia") create_checkbox("camera.fov-effects", "Camera FOV Effects") create_checkbox("display.limit-fps-iconified", "Limit Background FPS") - create_setting("graphics.gamma", "Gamma", 0.05, "", "graphics.gamma.tooltip") + create_trackbar_setting("graphics.gamma", "Gamma", 0.05, "", "graphics.gamma.tooltip") end diff --git a/res/layouts/pages/settings_graphics.xml.lua b/res/layouts/pages/settings_graphics.xml.lua index 806f48d0c..6a4596a04 100644 --- a/res/layouts/pages/settings_graphics.xml.lua +++ b/res/layouts/pages/settings_graphics.xml.lua @@ -1,4 +1,4 @@ -function create_setting(id, name, step, postfix, tooltip, changeonrelease) +function create_trackbar_setting(id, name, step, postfix, tooltip, changeonrelease) local info = app.get_setting_info(id) postfix = postfix or "" tooltip = tooltip or "" @@ -14,10 +14,10 @@ function create_setting(id, name, step, postfix, tooltip, changeonrelease) tooltip=tooltip, changeonrelease=changeonrelease })) - update_setting(app.get_setting(id), id, name, postfix) + update_trackbar_label(app.get_setting(id), id, name, postfix) end -function update_setting(x, id, name, postfix) +function update_trackbar_label(x, id, name, postfix) -- updating label document[id..".L"].text = string.format( "%s: %s%s", @@ -36,15 +36,15 @@ function create_checkbox(id, name, tooltip) end function on_open() - create_setting("chunks.load-distance", "Load Distance", 1) - create_setting("chunks.load-speed", "Load Speed", 1) - create_setting("graphics.fog-curve", "Fog Curve", 0.1) + create_trackbar_setting("chunks.load-distance", "Load Distance", 1) + create_trackbar_setting("chunks.load-speed", "Load Speed", 1) + create_trackbar_setting("graphics.fog-curve", "Fog Curve", 0.1) create_checkbox("graphics.backlight", "Backlight", "graphics.backlight.tooltip") create_checkbox("graphics.soft-lighting", "Soft lighting", "graphics.soft-lighting.tooltip") create_checkbox("graphics.dense-render", "Dense blocks render", "graphics.dense-render.tooltip") create_checkbox("graphics.advanced-render", "Advanced render", "graphics.advanced-render.tooltip") - create_setting("graphics.ssao", "SSAO", 1, "", "graphics.ssao.tooltip") - create_setting("graphics.shadows-quality", "Shadows quality", 1) - create_setting("graphics.clouds-quality", "Clouds quality", 1) + create_trackbar_setting("graphics.ssao", "SSAO", 1, "", "graphics.ssao.tooltip") + create_trackbar_setting("graphics.shadows-quality", "Shadows quality", 1) + create_trackbar_setting("graphics.clouds-quality", "Clouds quality", 1) end diff --git a/res/layouts/templates/track_setting.xml b/res/layouts/templates/track_setting.xml index f01b8f842..79cdb41c1 100644 --- a/res/layouts/templates/track_setting.xml +++ b/res/layouts/templates/track_setting.xml @@ -3,5 +3,5 @@ + sub-consumer='function(x) update_trackbar_label(x, "%{id}", "%{name}", "%{postfix}") end'/> diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index c7c756f1c..739bb7cea 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -215,6 +215,13 @@ FFIBytearray = { } table.merge(FFIBytearray, bytearray_methods) +local function FFIBytearray_as_ptr(bytes) + if type(bytes) == "cdata" then + return tostring(bytes.bytes):sub(27), bytes.size + end + return "0" +end + local function FFIBytearray_as_string(bytes) local t = type(bytes) if t == "cdata" then @@ -283,6 +290,7 @@ local FFIU32view = create_FFIview_class("FFIU32view", "uint32_t", 4) return { FFIBytearray = setmetatable(FFIBytearray, FFIBytearray), FFIBytearray_as_string = FFIBytearray_as_string, + FFIBytearray_as_ptr = FFIBytearray_as_ptr, FFIU16view = FFIU16view, FFII16view = FFII16view, FFIU32view = FFIU32view, diff --git a/res/modules/internal/console.lua b/res/modules/internal/console.lua new file mode 100644 index 000000000..5b4f6b69c --- /dev/null +++ b/res/modules/internal/console.lua @@ -0,0 +1,62 @@ + +--- Console library extension --- +console.cheats = {} + +local log_element = Document.new("core:console").log +function console.log(...) + local args = {...} + local text = '' + for i,v in ipairs(args) do + if i ~= 1 then + text = text..' '..v + else + text = text..v + end + end + log_element.caret = -1 + if log_element.caret > 0 then + text = '\n'..text + end + log_element:paste(text) +end + +local console_add_command = console.__add_command +console.__add_command = nil + +function console.add_command(scheme, description, handler, is_cheat) + console_add_command(scheme, description, handler) + if not is_cheat then return end + + local name = string.match(scheme, "^(%S+)") + if not name then + error("Incorrect command syntax, command name not found") + end + + table.insert_unique(console.cheats, name) +end + +function console.is_cheat(name) + if not table.has(console.get_commands_list(), name) then + error(string.format("command \"%s\" not found", name)) + end + + return table.has(console.cheats, name) +end + +function console.set_cheat(name, status) + local is_cheat = console.is_cheat(name) + if status and not is_cheat then + table.insert(console.cheats, name) + return true + elseif not status and is_cheat then + table.remove_value(console.cheats, name) + return true + end + + return false +end + +function console.chat(...) + console.log(...) + events.emit("core:chat", ...) +end diff --git a/res/modules/internal/extensions/table.lua b/res/modules/internal/extensions/table.lua index a1856d383..d2d665755 100644 --- a/res/modules/internal/extensions/table.lua +++ b/res/modules/internal/extensions/table.lua @@ -57,6 +57,18 @@ function table.merge(t1, t2) return t1 end +function table.extend(t1, t2) + for i, v in pairs(t2) do + if type(i) == "number" then + t1[#t1 + 1] = v + else + t1[i] = v + end + end + + return t1 +end + function table.map(t, func) for i, v in pairs(t) do t[i] = func(i, v) diff --git a/res/modules/internal/lifetime_events.lua b/res/modules/internal/lifetime_events.lua new file mode 100644 index 000000000..48a96c550 --- /dev/null +++ b/res/modules/internal/lifetime_events.lua @@ -0,0 +1,224 @@ +local __app = __vc_app +local __rules = rules + +local function configure_SSAO() + -- Temporary using slot to configure built-in SSAO effect + local slot = gfx.posteffects.index("core:ssao") + gfx.posteffects.set_effect(slot, "ssao") + + -- Generating random SSAO samples + local buffer = Bytearray(0) + for i = 0, 63 do + local x = math.random() * 2.0 - 1.0 + local y = math.random() * 2.0 - 1.0 + local z = math.random() * 2.0 + local len = math.sqrt(x * x + y * y + z * z) + if len > 0 then + x = x / len + y = y / len + z = z / len + end + Bytearray.append(buffer, byteutil.pack("fff", x, y, z)) + end + gfx.posteffects.set_array(slot, "u_ssaoSamples", Bytearray_as_string(buffer)) + + local function update_ssao_quality(value) + value = math.min(value, 3) + gfx.posteffects.set_params(slot, { + u_kernelSize = value * 16, + u_radius = 0.4 / value, + u_bias = 0.006 / value / value, + }) + end + events.on("core:setting.graphics.ssao.set", update_ssao_quality) + + update_ssao_quality(__app.get_setting("graphics.ssao")) +end + +local function __vc_on_hud_open() + local _hud_is_content_access = hud._is_content_access + local _hud_set_content_access = hud._set_content_access + local _hud_set_debug_cheats = hud._set_debug_cheats + + __rules.create("allow-cheats", true) + + __rules.create("allow-content-access", _hud_is_content_access(), function(value) + _hud_set_content_access(value) + end) + __rules.create("allow-flight", true, function(value) + input.set_enabled("player.flight", value) + end) + __rules.create("allow-noclip", true, function(value) + input.set_enabled("player.noclip", value) + end) + __rules.create("allow-attack", true, function(value) + input.set_enabled("player.attack", value) + end) + __rules.create("allow-destroy", true, function(value) + input.set_enabled("player.destroy", value) + end) + __rules.create("allow-cheat-movement", true, function(value) + input.set_enabled("movement.cheat", value) + end) + __rules.create("allow-fast-interaction", true, function(value) + input.set_enabled("player.fast_interaction", value) + end) + __rules.create("allow-debug-cheats", true, function(value) + _hud_set_debug_cheats(value) + end) + input.add_callback("devtools.console", function() + if menu.page ~= "" then + return + end + time.post_runnable(function() + hud.show_overlay("core:console", false, {"console"}) + end) + end) + input.add_callback("hud.chat", function() + if menu.page ~= "" then + return + end + time.post_runnable(function() + hud.show_overlay("core:console", false, {"chat"}) + end) + end) + input.add_callback("key:escape", function() + if menu.page ~= "" then + if not menu:back() then + menu:reset() + gui.set_active_frame("") + end + elseif hud.is_inventory_open() then + hud.close_inventory() + elseif gui.get_active_frame() then + gui.set_active_frame("") + else + hud.pause() + end + end) + hud.open_permanent("core:ingame_chat") + + configure_SSAO() +end + +local Schedule = require "core:schedule" +local ScheduleGroup_mt = { + __index = { + publish = function(self, schedule) + local id = self._next_schedule + self._schedules[id] = schedule + self._next_schedule = id + 1 + end, + tick = function(self, dt) + for id, schedule in pairs(self._schedules) do + schedule:tick(dt) + end + self.common:tick(dt) + end, + remove = function(self, id) + self._schedules[id] = nil + end, + } +} + +local function ScheduleGroup() + return setmetatable({ + _next_schedule = 1, + _schedules = {}, + common = Schedule() + }, ScheduleGroup_mt) +end + +local RULES_FILE = "world:rules.toml" +local function __vc_on_world_open() + time.schedules.world = ScheduleGroup() + + if not file.exists(RULES_FILE) then + return + end + local rule_values = toml.parse(file.read(RULES_FILE)) + for name, value in pairs(rule_values) do + __rules.set(name, value) + end +end + +local function __vc_on_world_tick(tps) + time.schedules.world:tick(1.0 / tps) +end + +local function __vc_process_before_quit() + block.__process_register_events() +end + +local function __vc_on_world_save() + local rule_values = {} + for name, rule in pairs(__rules.rules) do + rule_values[name] = rule.value + end + file.write(RULES_FILE, toml.tostring(rule_values)) +end + +local __close_all_descriptors = file.__close_all_descriptors +local __gui_util_reset_local = gui_util.__reset_local +local __stdcomp_reset = stdcomp.__reset +file.__close_all_descriptors = nil +gui_util.__reset_local = nil +stdcomp.reset = nil + +local function __vc_on_world_quit() + __rules.clear() + __gui_util_reset_local() + __stdcomp_reset() + __close_all_descriptors() +end + +local __post_runnables = {} +local fn_audio_reset_fetch_buffer = audio.__reset_fetch_buffer +audio.__reset_fetch_buffer = nil + +local __vc_update_coroutines = _G.__vc_update_coroutines + +local function __process_post_runnables() + if #__post_runnables > 0 then + for _, func in ipairs(__post_runnables) do + local status, result = xpcall(func, __vc__error) + if not status then + debug.error("error in post_runnable: "..result) + end + end + __post_runnables = {} + end + + __vc_update_coroutines() + + fn_audio_reset_fetch_buffer() + debug.pull_events() + network.__process_events() + if not hud or not hud.is_paused() then + block.__process_register_events() + block.__perform_ticks(time.delta()) + end +end + +function __vc__process_post_runnables() + __vc__is_post_runnable = true + local success, err = pcall(__process_post_runnables) + if not success then + debug.error("an error ocurred while processing post-runnables: ".. err) + end + __vc__is_post_runnable = false +end + +function time.post_runnable(runnable) + table.insert(__post_runnables, runnable) +end + +return { + __vc_on_hud_open = __vc_on_hud_open, + __vc_on_world_open = __vc_on_world_open, + __vc_on_world_tick = __vc_on_world_tick, + __vc_process_before_quit = __vc_process_before_quit, + __vc_on_world_save = __vc_on_world_save, + __vc_on_world_quit = __vc_on_world_quit, + __vc__process_post_runnables = __vc__process_post_runnables, +} diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 0e72ad86d..702d50072 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -12,7 +12,7 @@ function sleep(timesec) end end -function tb_frame_tostring(frame) +local function tb_frame_tostring(frame) local s = frame.short_src if frame.what ~= "C" then s = s .. ":" .. tostring(frame.currentline) @@ -192,6 +192,22 @@ function start_coroutine(chunk, name) __vc_named_coroutines[name] = co end +function __vc_update_coroutines() + local dead = {} + for name, co in pairs(__vc_named_coroutines) do + local success, err = coroutine.resume(co) + if not success then + debug.error(err) + end + if coroutine.status(co) == "dead" then + table.insert(dead, name) + end + end + for _, name in ipairs(dead) do + __vc_named_coroutines[name] = nil + end +end + function __vc_start_app_script(path, name) debug.log("starting application script "..path) @@ -257,71 +273,8 @@ do end end ---- Console library extension --- -console.cheats = {} - -local log_element = Document.new("core:console").log -function console.log(...) - local args = {...} - local text = '' - for i,v in ipairs(args) do - if i ~= 1 then - text = text..' '..v - else - text = text..v - end - end - log_element.caret = -1 - if log_element.caret > 0 then - text = '\n'..text - end - log_element:paste(text) -end - -local console_add_command = console.__add_command -console.__add_command = nil - -function console.add_command(scheme, description, handler, is_cheat) - console_add_command(scheme, description, handler) - if not is_cheat then return end - - local name = string.match(scheme, "^(%S+)") - if not name then - error("Incorrect command syntax, command name not found") - end - - table.insert_unique(console.cheats, name) -end - -function console.is_cheat(name) - if not table.has(console.get_commands_list(), name) then - error(string.format("command \"%s\" not found", name)) - end - - return table.has(console.cheats, name) -end - -function console.set_cheat(name, status) - local is_cheat = console.is_cheat(name) - if status and not is_cheat then - table.insert(console.cheats, name) - return true - elseif not status and is_cheat then - table.remove_value(console.cheats, name) - return true - end - - return false -end - -function console.chat(...) - console.log(...) - events.emit("core:chat", ...) -end - -function gui.template(name, params) - local text = file.read(file.find("layouts/templates/"..name..".xml")) - text = text:gsub("%%{([^}]+)}", function(n) +function gui.process_template(source, params) + local text = source:gsub("%%{([^}]+)}", function(n) local s = params[n] if s == nil then return @@ -341,6 +294,11 @@ function gui.template(name, params) return text end +function gui.template(name, params) + local text = file.read(file.find("layouts/templates/"..name..".xml")) + return gui.process_template(text, params) +end + session = require "core:internal/session" stdcomp = require "core:internal/stdcomp" entities.get = stdcomp.get_Entity @@ -365,7 +323,7 @@ if ffi.os == "Windows" then ffi.cdef[[ unsigned long GetCurrentProcessId(); ]] - + os.pid = ffi.C.GetCurrentProcessId() else ffi.cdef[[ @@ -378,236 +336,20 @@ end math.randomseed(time.uptime() * 1536227939) rules = require "core:internal/rules" -local _rules = rules - -local function configure_SSAO() - -- Temporary using slot to configure built-in SSAO effect - local slot = gfx.posteffects.index("core:ssao") - gfx.posteffects.set_effect(slot, "ssao") - - -- Generating random SSAO samples - local buffer = Bytearray(0) - for i = 0, 63 do - local x = math.random() * 2.0 - 1.0 - local y = math.random() * 2.0 - 1.0 - local z = math.random() * 2.0 - local len = math.sqrt(x * x + y * y + z * z) - if len > 0 then - x = x / len - y = y / len - z = z / len - end - Bytearray.append(buffer, byteutil.pack("fff", x, y, z)) - end - gfx.posteffects.set_array(slot, "u_ssaoSamples", Bytearray_as_string(buffer)) - - local function update_ssao_quality(value) - value = math.min(value, 3) - gfx.posteffects.set_params(slot, { - u_kernelSize = value * 16, - u_radius = 0.4 / value, - u_bias = 0.006 / value / value, - }) - end - events.on("core:setting.graphics.ssao.set", update_ssao_quality) - - update_ssao_quality(__app.get_setting("graphics.ssao")) -end - -function __vc_on_hud_open() - local _hud_is_content_access = hud._is_content_access - local _hud_set_content_access = hud._set_content_access - local _hud_set_debug_cheats = hud._set_debug_cheats - - _rules.create("allow-cheats", true) - - _rules.create("allow-content-access", _hud_is_content_access(), function(value) - _hud_set_content_access(value) - end) - _rules.create("allow-flight", true, function(value) - input.set_enabled("player.flight", value) - end) - _rules.create("allow-noclip", true, function(value) - input.set_enabled("player.noclip", value) - end) - _rules.create("allow-attack", true, function(value) - input.set_enabled("player.attack", value) - end) - _rules.create("allow-destroy", true, function(value) - input.set_enabled("player.destroy", value) - end) - _rules.create("allow-cheat-movement", true, function(value) - input.set_enabled("movement.cheat", value) - end) - _rules.create("allow-fast-interaction", true, function(value) - input.set_enabled("player.fast_interaction", value) - end) - _rules.create("allow-debug-cheats", true, function(value) - _hud_set_debug_cheats(value) - end) - input.add_callback("devtools.console", function() - if menu.page ~= "" then - return - end - time.post_runnable(function() - hud.show_overlay("core:console", false, {"console"}) - end) - end) - input.add_callback("hud.chat", function() - if menu.page ~= "" then - return - end - time.post_runnable(function() - hud.show_overlay("core:console", false, {"chat"}) - end) - end) - input.add_callback("key:escape", function() - if menu.page ~= "" then - if not menu:back() then - menu:reset() - gui.set_active_frame("") - end - elseif hud.is_inventory_open() then - hud.close_inventory() - elseif gui.get_active_frame() then - gui.set_active_frame("") - else - hud.pause() - end - end) - hud.open_permanent("core:ingame_chat") - - configure_SSAO() -end - -local Schedule = require "core:schedule" - -local ScheduleGroup_mt = { - __index = { - publish = function(self, schedule) - local id = self._next_schedule - self._schedules[id] = schedule - self._next_schedule = id + 1 - end, - tick = function(self, dt) - for id, schedule in pairs(self._schedules) do - schedule:tick(dt) - end - self.common:tick(dt) - end, - remove = function(self, id) - self._schedules[id] = nil - end, - } -} - -local function ScheduleGroup() - return setmetatable({ - _next_schedule = 1, - _schedules = {}, - common = Schedule() - }, ScheduleGroup_mt) -end - time.schedules = {} -local RULES_FILE = "world:rules.toml" -function __vc_on_world_open() - time.schedules.world = ScheduleGroup() +table.merge(_G, require "core:internal/lifetime_events") - if not file.exists(RULES_FILE) then - return - end - local rule_values = toml.parse(file.read(RULES_FILE)) - for name, value in pairs(rule_values) do - _rules.set(name, value) - end -end - -function __vc_on_world_tick(tps) - time.schedules.world:tick(1.0 / tps) -end - -function __vc_process_before_quit() - block.__process_register_events() -end - -function __vc_on_world_save() - local rule_values = {} - for name, rule in pairs(rules.rules) do - rule_values[name] = rule.value - end - file.write(RULES_FILE, toml.tostring(rule_values)) -end - -local __close_all_descriptors = file.__close_all_descriptors -local __gui_util_reset_local = gui_util.__reset_local -local __stdcomp_reset = stdcomp.__reset -file.__close_all_descriptors = nil -gui_util.__reset_local = nil -stdcomp.reset = nil - -function __vc_on_world_quit() - _rules.clear() - __gui_util_reset_local() - __stdcomp_reset() - __close_all_descriptors() -end - -local fn_audio_reset_fetch_buffer = audio.__reset_fetch_buffer -audio.__reset_fetch_buffer = nil core.get_core_token = audio.input.__get_core_token -local __post_runnables = {} +require "core:internal/console" +require "core:internal/deprecated" -local function __process_post_runnables() - if #__post_runnables > 0 then - for _, func in ipairs(__post_runnables) do - local status, result = xpcall(func, __vc__error) - if not status then - debug.error("error in post_runnable: "..result) - end - end - __post_runnables = {} - end - local dead = {} - for name, co in pairs(__vc_named_coroutines) do - local success, err = coroutine.resume(co) - if not success then - debug.error(err) - end - if coroutine.status(co) == "dead" then - table.insert(dead, name) - end - end - for _, name in ipairs(dead) do - __vc_named_coroutines[name] = nil - end +------------------------------------------ +---- Unsafe functions cleanup section ---- +------------------------------------------ - fn_audio_reset_fetch_buffer() - debug.pull_events() - network.__process_events() - if not hud or not hud.is_paused() then - block.__process_register_events() - block.__perform_ticks(time.delta()) - end -end - -function __vc__process_post_runnables() - __vc__is_post_runnable = true - local success, err = pcall(__process_post_runnables) - if not success then - debug.error("an error ocurred while processing post-runnables: ".. err) - end - __vc__is_post_runnable = false -end - -function time.post_runnable(runnable) - table.insert(__post_runnables, runnable) -end - --- Hide unsafe debug.* functions local removed_names = { "getregistry", "getupvalue", "setupvalue", "upvalueid", "upvaluejoin", "sethook", "gethook", "getinfo" @@ -626,9 +368,9 @@ debug.getinfo = function(lvl, fields) return debuginfo end -require "core:internal/deprecated" - ffi = nil __vc_app = nil __vc_lock_internal_modules() __vc_lock_internal_modules = nil +__vc_update_coroutines = nil +__VC_SCRIPT_NAME = "" diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 70e27f561..2868dd634 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -1,13 +1,18 @@ local _vc_headless = __VC_HEADLESS +local _vc_project_args = __VC_PROJECT_ARGS __VC_HEADLESS = nil +__VC_PROJECT_ARGS = nil vc = { is_headless = function() return _vc_headless end, - is_client = function () + is_client = function() return not _vc_headless - end + end, + get_setting = __vc_app.get_setting, + get_setting_str = __vc_app.get_setting_str, + get_setting_info = __vc_app.get_setting_info, } local _ffi = ffi @@ -297,9 +302,14 @@ require "core:internal/extensions/file" require "core:internal/extensions/table" require "core:internal/extensions/string" +vc.get_project_arg = function (name) + return _vc_project_args[name] +end + local bytearray = require "core:internal/bytearray" Bytearray = bytearray.FFIBytearray Bytearray_as_string = bytearray.FFIBytearray_as_string +Bytearray_as_ptr = bytearray.FFIBytearray_as_ptr U16view = bytearray.FFIU16view I16view = bytearray.FFII16view U32view = bytearray.FFIU32view diff --git a/res/texts/en_US.txt b/res/texts/en_US.txt index a6c9c5391..ff44ec754 100644 --- a/res/texts/en_US.txt +++ b/res/texts/en_US.txt @@ -18,6 +18,7 @@ devtools.output=Output # Tooltips audio.acoustic-effects.tooltip=Acoustic effects may not be supported by your system +audio.recording-enabled.tooltip=Enables audio recording features graphics.gamma.tooltip=Lighting brightness curve graphics.backlight.tooltip=Backlight to prevent total darkness graphics.dense-render.tooltip=Enables transparency in blocks like leaves diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index 66dc40a42..ed3bb9686 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -39,6 +39,7 @@ pack.remove-confirm=Удалить весь поставляемый паком/ # Подсказки audio.acoustic-effects.tooltip=Акустические эффекты могут не поддерживаться системой +audio.recording-enabled.tooltip=Включает функции записи звука graphics.gamma.tooltip=Кривая яркости освещения graphics.backlight.tooltip=Подсветка, предотвращающая полную темноту graphics.dense-render.tooltip=Включает прозрачность блоков, таких как листья @@ -114,6 +115,7 @@ settings.Microphone=Микрофон settings.microphone.None=Нет settings.Clouds quality=Качество облаков settings.Acoustic effects=Акустические эффекты +settings.Microphone access=Доступ к микрофону # Управление chunks.reload=Перезагрузить Чанки diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index cfc84ce00..d6e32d089 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -190,18 +190,34 @@ void audio::initialize( audio::get_channel(channel.name)->setVolume(value * value); }, true)); } - objects_keeper.keepAlive(settings.acousticEffects.observe([=](auto value) { + objects_keeper.keepAlive(settings.acousticEffects.observe([=](bool value) { if (value) return; backend->setAcoustics(audio::Acoustics {}); }, true)); - if (inputEnabled) { - ::input_device = backend->openInputDevice("", 44100, 1, 16); - ::input_enabled = true; - } - if (::input_device) { - ::input_device->startCapture(); - } + ::input_enabled = inputEnabled; + + objects_keeper.keepAlive(settings.recordingEnabled.observe([&settings](bool enabled) { + if (!::input_enabled) { + return; + } + if (!enabled && ::input_device == nullptr) { + return; + } + if (enabled) { + logger.info() << "recording enabled; input device is " + << settings.inputDevice.get(); + ::input_device = backend->openInputDevice( + settings.inputDevice.get(), 44100, 1, 16 + ); + ::input_device->startCapture(); + } else { + if (::input_device) { + ::input_device->stopCapture(); + } + ::input_device = nullptr; + } + }, true)); } InputDevice* audio::get_input_device() { diff --git a/src/engine/CoreParameters.hpp b/src/engine/CoreParameters.hpp index b482f2bb5..b9f585f47 100644 --- a/src/engine/CoreParameters.hpp +++ b/src/engine/CoreParameters.hpp @@ -2,6 +2,7 @@ #include #include +#include struct CoreParameters { bool headless = false; @@ -13,4 +14,5 @@ struct CoreParameters { std::filesystem::path projectFolder; std::string debugServerString; int tps = 20; + std::unordered_map projectArgs; }; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 89a345ff6..879d95f0b 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -189,7 +189,7 @@ void Engine::initialize(CoreParameters coreParameters) { keepAlive(settings.audio.inputDevice.observe([](auto name) { audio::set_input_device(name == "auto" ? "" : name); - }, true)); + })); project->loadProjectStartScript(); if (!params.headless) { diff --git a/src/frontend/UiDocument.cpp b/src/frontend/UiDocument.cpp index a202c0d96..7e0c9bfc6 100644 --- a/src/frontend/UiDocument.cpp +++ b/src/frontend/UiDocument.cpp @@ -61,16 +61,19 @@ std::unique_ptr UiDocument::read( const scriptenv& penv, const std::string& name, const io::path& file, - const std::string& fileName + const std::string& fileName, + scriptenv&& env ) { const std::string text = io::read_string(file); auto xmldoc = xml::parse(file.string(), text); - auto env = penv == nullptr - ? scripting::create_doc_environment(scripting::get_root_environment(), name) - : scripting::create_doc_environment(penv, name); + if (env == nullptr) { + env = penv == nullptr + ? scripting::create_doc_environment(scripting::get_root_environment(), name) + : scripting::create_doc_environment(penv, name); + } - gui::UiXmlReader reader(gui, scriptenv(env)); + gui::UiXmlReader reader(gui, env); auto view = reader.readXML(file.string(), *xmldoc->getRoot()); view->setId("root"); UiDocScript script {}; @@ -82,9 +85,3 @@ std::unique_ptr UiDocument::read( } return std::make_unique(name, script, view, env); } - -std::shared_ptr UiDocument::readElement( - gui::GUI& gui, const io::path& file, const std::string& fileName -) { - return read(gui, nullptr, file.name(), file, fileName)->getRoot(); -} diff --git a/src/frontend/UiDocument.hpp b/src/frontend/UiDocument.hpp index 57077bec0..5b40ca8ca 100644 --- a/src/frontend/UiDocument.hpp +++ b/src/frontend/UiDocument.hpp @@ -50,9 +50,7 @@ public: const scriptenv& parent_env, const std::string& name, const io::path& file, - const std::string& fileName - ); - static std::shared_ptr readElement( - gui::GUI&, const io::path& file, const std::string& fileName + const std::string& fileName, + scriptenv&& env = nullptr ); }; diff --git a/src/graphics/ui/elements/CheckBox.cpp b/src/graphics/ui/elements/CheckBox.cpp index 8694dc085..fe3a342dd 100644 --- a/src/graphics/ui/elements/CheckBox.cpp +++ b/src/graphics/ui/elements/CheckBox.cpp @@ -57,6 +57,10 @@ FullCheckBox::FullCheckBox( add(checkbox); + auto& checkboxActions = actions; label->setMargin(glm::vec4(5.f, 5.f, 0.f, 0.f)); + label->listenAction(UIAction::CLICK, [&checkboxActions](auto& gui) { + checkboxActions.notify(UIAction::CLICK, gui); + }); add(label); } diff --git a/src/graphics/ui/gui_xml.cpp b/src/graphics/ui/gui_xml.cpp index 33d57aa9a..e91b4553f 100644 --- a/src/graphics/ui/gui_xml.cpp +++ b/src/graphics/ui/gui_xml.cpp @@ -889,8 +889,8 @@ static std::shared_ptr read_iframe( return iframe; } -UiXmlReader::UiXmlReader(gui::GUI& gui, scriptenv&& env) - : gui(gui), env(std::move(env)) { +UiXmlReader::UiXmlReader(gui::GUI& gui, const scriptenv& env) + : gui(gui), env(env) { contextStack.emplace(""); add("image", read_image); add("canvas", read_canvas); @@ -922,8 +922,7 @@ void UiXmlReader::addIgnore(const std::string& tag) { ignored.insert(tag); } -std::shared_ptr UiXmlReader::readUINode(const xml::xmlelement& element -) { +std::shared_ptr UiXmlReader::readUINode(const xml::xmlelement& element) { if (element.has("if")) { const auto& cond = element.attr("if").getText(); if (cond.empty() || cond == "false" || cond == "nil") return nullptr; diff --git a/src/graphics/ui/gui_xml.hpp b/src/graphics/ui/gui_xml.hpp index 4a8a301ff..733503526 100644 --- a/src/graphics/ui/gui_xml.hpp +++ b/src/graphics/ui/gui_xml.hpp @@ -20,9 +20,9 @@ namespace gui { std::unordered_set ignored; std::stack contextStack; std::string filename; - scriptenv env; + const scriptenv& env; public: - UiXmlReader(gui::GUI& gui, scriptenv&& env); + UiXmlReader(gui::GUI& gui, const scriptenv& env); void add(const std::string& tag, uinode_reader reader); bool hasReader(const std::string& tag) const; diff --git a/src/io/settings_io.cpp b/src/io/settings_io.cpp index c66b5c451..8e8b0a2dc 100644 --- a/src/io/settings_io.cpp +++ b/src/io/settings_io.cpp @@ -38,6 +38,7 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) { builder.addSection("audio"); builder.add("enabled", &settings.audio.enabled, false); + builder.add("recording-enabled", &settings.audio.recordingEnabled); builder.add("volume-master", &settings.audio.volumeMaster); builder.add("volume-regular", &settings.audio.volumeRegular); builder.add("volume-ui", &settings.audio.volumeUI); @@ -101,6 +102,7 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) { builder.addSection("system"); builder.add("max-bg-asset-loaders", &settings.system.maxBgAssetLoaders); builder.add("preserve-assets-during-frame", &settings.system.preserveAssetsDuringFrame); + builder.add("direct-scripting-data-access", &settings.system.directScriptingDataAccess); } dv::value SettingsHandler::getValue(const std::string& name) const { diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 58a2f2be1..cdd3dad28 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -1116,28 +1116,34 @@ static int l_gui_load_document(lua::State* L) { auto args = lua::tovalue(L, 3); auto prefix = filename.entryPoint(); - auto env = scripting::get_root_environment(); + auto parentEnv = scripting::get_root_environment(); if (content) { if (auto runtime = content->getPackRuntime(prefix)) { - env = runtime->getEnvironment(); + parentEnv = runtime->getEnvironment(); } } - auto documentPtr = UiDocument::read( - engine->getGUI(), std::move(env), alias, filename, filename.string() - ); - auto document = documentPtr.get(); - engine->requireAssets().store(std::move(documentPtr), alias); - + auto env = scripting::create_doc_environment(parentEnv, alias); // namespace extension if (lua::istable(L, 4)) { - if (lua::get_from(L, "table", "merge")) { - lua::pushenv(L, *document->getEnvironment()); + if (lua::get_from(L, "table", "extend")) { + lua::pushenv(L, *env); lua::pushvalue(L, 4); lua::call(L, 2, 0); lua::pop(L); } } + auto documentPtr = UiDocument::read( + engine->getGUI(), + nullptr, + alias, + filename, + filename.string(), + std::move(env) + ); + auto document = documentPtr.get(); + engine->requireAssets().store(std::move(documentPtr), alias); + scripting::on_ui_open(document, {args}); return 0; } diff --git a/src/logic/scripting/lua/lua_commons.hpp b/src/logic/scripting/lua/lua_commons.hpp index bb3163c86..8e77cbbc0 100644 --- a/src/logic/scripting/lua/lua_commons.hpp +++ b/src/logic/scripting/lua/lua_commons.hpp @@ -17,6 +17,11 @@ #error LuaJIT required #endif +#ifndef LUA_TCDATA +// unsafe, use for filtering only +#define LUA_TCDATA 10 +#endif + namespace lua { class luaerror : public std::runtime_error { public: diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index bd2f0359b..afb2f42eb 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -15,8 +15,12 @@ #include "usertypes/lua_type_pcmstream.hpp" #include "engine/Engine.hpp" -static debug::Logger logger("lua-state"); -static lua::State* main_thread = nullptr; +namespace { + debug::Logger logger("lua-state"); + lua::State* main_thread = nullptr; + bool headless_mode = false; + const std::unordered_map* project_args; +} using namespace lua; @@ -146,20 +150,53 @@ void lua::init_state(State* L, StateType stateType) { newusertype(L); newusertype(L); newusertype(L); + + pushboolean(L, headless_mode); + setglobal(L, "__VC_HEADLESS"); + + createtable(L, 0, project_args->size()); + for (const auto& [key, value] : *project_args) { + pushstring(L, value); + setfield(L, key); + } + setglobal(L, "__VC_PROJECT_ARGS"); + + auto file = "res:scripts/stdmin.lua"; + auto src = io::read_string(file); + lua::pop(L, lua::execute(L, 0, src, "core:scripts/stdmin.lua")); + + newusertype(L); + if (getglobal(L, "random")) { + if (getglobal(L, "__vc_Random")) { + setfield(L, "Random"); + } + pop(L); + } + newusertype(L); + if (getglobal(L, "audio")) { + if (getglobal(L, "__vc_PCMStream")) { + setfield(L, "PCMStream"); + } + pop(L); + } + + if (stateType == StateType::GENERATOR) { + pushnil(L); + setglobal(L, "ffi"); + } } void lua::initialize(const EnginePaths& paths, const CoreParameters& params) { logger.info() << LUA_VERSION; logger.info() << LUAJIT_VERSION; + headless_mode = params.headless; + project_args = ¶ms.projectArgs; main_thread = create_state( paths, params.headless ? StateType::SCRIPT : StateType::BASE ); lua::pushstring(main_thread, params.scriptFile.stem().u8string()); lua::setglobal(main_thread, "__VC_SCRIPT_NAME"); - - lua::pushboolean(main_thread, params.headless); - lua::setglobal(main_thread, "__VC_HEADLESS"); } void lua::finalize() { @@ -191,29 +228,5 @@ State* lua::create_state(const EnginePaths& paths, StateType stateType) { throw luaerror("could not initialize Lua state"); } init_state(L, stateType); - - auto file = "res:scripts/stdmin.lua"; - auto src = io::read_string(file); - lua::pop(L, lua::execute(L, 0, src, "core:scripts/stdmin.lua")); - - newusertype(L); - if (getglobal(L, "random")) { - if (getglobal(L, "__vc_Random")) { - setfield(L, "Random"); - } - pop(L); - } - newusertype(L); - if (getglobal(L, "audio")) { - if (getglobal(L, "__vc_PCMStream")) { - setfield(L, "PCMStream"); - } - pop(L); - } - - if (stateType == StateType::GENERATOR) { - pushnil(L); - setglobal(L, "ffi"); - } return L; } diff --git a/src/logic/scripting/lua/lua_util.cpp b/src/logic/scripting/lua/lua_util.cpp index ded9cc79b..e28e6ad37 100644 --- a/src/logic/scripting/lua/lua_util.cpp +++ b/src/logic/scripting/lua/lua_util.cpp @@ -5,6 +5,8 @@ #include #include "util/stringutil.hpp" +#include "engine/Engine.hpp" +#include "debug/Logger.hpp" using namespace lua; @@ -12,6 +14,8 @@ static int next_environment = 1; std::unordered_map lua::usertypeNames; +static debug::Logger logger("lua-util"); + int lua::userdata_destructor(lua::State* L) { if (auto obj = touserdata(L, 1)) { obj->~Userdata(); @@ -369,3 +373,55 @@ void lua::remove_environment(State* L, int id) { pushnil(L); store_env(L, id); } + +static inline std::string_view bytearray_as_string_indirect(lua::State* L, int idx) { + requireglobal(L, "Bytearray_as_string"); + pushvalue(L, -2); + call(L, 1, 1); + auto view = tolstring(L, -1); + pop(L, 2); + return view; +} + +std::string_view lua::bytearray_as_string(lua::State* L, int idx) { + const auto& settings = scripting::engine->getSettings(); + + int luaType = type(L, idx); + if (luaType == LUA_TSTRING) { + return tolstring(L, idx); + } else if (luaType == LUA_TTABLE) { + return bytearray_as_string_indirect(L, idx); + } + pushvalue(L, idx); + + if (settings.system.directScriptingDataAccess.get()) { + requireglobal(L, "Bytearray_as_ptr"); + pushvalue(L, -2); + call(L, 1, 2); + auto view = tolstring(L, -2); + if (view == "0") { + if (luaType == LUA_TCDATA) { + logger.error() << "FFI-based Bytearray_as_ptr returned null-pointer"; + } + pop(L, 2); + return bytearray_as_string_indirect(L, idx); + } + uint64_t size = touinteger(L, -1); + auto ptr = (const char*)std::stoull(std::string(view), nullptr, 16); + pop(L, 3); + return std::string_view(ptr, size); + } else { + return bytearray_as_string_indirect(L, idx); + } +} + +void lua::loadbuffer( + lua::State* L, int env, const std::string& src, const std::string& file +) { + if (luaL_loadbuffer(L, src.c_str(), src.length(), file.c_str())) { + throw luaerror(tostring(L, -1)); + } + if (env && getregistry(L, ENVS_TABLE, env_name(env))) { + lua_setfenv(L, -2); + } +} diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index a7881bef7..d78afaad1 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -555,16 +555,9 @@ namespace lua { return lua_setfenv(L, idx); } - inline void loadbuffer( + void loadbuffer( lua::State* L, int env, const std::string& src, const std::string& file - ) { - if (luaL_loadbuffer(L, src.c_str(), src.length(), file.c_str())) { - throw luaerror(tostring(L, -1)); - } - if (env && getregistry(L, ENVS_TABLE, env_name(env))) { - lua_setfenv(L, -2); - } - } + ); inline void store_in( lua::State* L, const std::string& tableName, const std::string& name @@ -828,16 +821,5 @@ namespace lua { return create_bytearray(L, bytes.data(), bytes.size()); } - inline std::string_view bytearray_as_string(lua::State* L, int idx) { - if (type(L, idx) == LUA_TSTRING) { - return tolstring(L, idx); - } - pushvalue(L, idx); - requireglobal(L, "Bytearray_as_string"); - pushvalue(L, -2); - call(L, 1, 1); - auto view = tolstring(L, -1); - pop(L, 2); - return view; - } + std::string_view bytearray_as_string(lua::State* L, int idx); } diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 806f92c24..37597b058 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -141,7 +141,7 @@ std::unique_ptr scripting::load_client_project_script( auto source = io::read_string(script); auto env = create_environment(nullptr); lua::pushenv(L, *env); - if (lua::getglobal(L, "__vc_app")) { + if (lua::getregistry(L, "app")) { lua::setfield(L, "app"); } lua::pop(L); diff --git a/src/settings.hpp b/src/settings.hpp index 782330f14..5220ca328 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -10,6 +10,7 @@ struct AudioSettings { /// @brief try to initialize AL backend FlagSetting enabled {true}; + FlagSetting recordingEnabled {false}; NumberSetting volumeMaster {1.0f, 0.0f, 1.0f, setting_format::percent}; NumberSetting volumeRegular {1.0f, 0.0f, 1.0f, setting_format::percent}; @@ -126,6 +127,8 @@ struct SystemSettings { IntegerSetting maxBgAssetLoaders {3, -4, 16}; /// @brief Preserve assets during current frame from being destroyed FlagSetting preserveAssetsDuringFrame {true}; + /// @brief Allow luajit cdata objects direct data access optimizations + FlagSetting directScriptingDataAccess {true}; }; struct EngineSettings { diff --git a/src/util/ArgsReader.hpp b/src/util/ArgsReader.hpp index 4c8453766..c13c1dd3e 100644 --- a/src/util/ArgsReader.hpp +++ b/src/util/ArgsReader.hpp @@ -26,6 +26,13 @@ namespace util { return last[0] == '-'; } + bool isNextKeywordArg() const { + if (pos >= argc) { + throw std::runtime_error("unexpected end"); + } + return argv[pos][0] == '-'; + } + std::string next() { if (pos >= argc) { throw std::runtime_error("unexpected end"); diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index e41a0ea57..7f34bcea4 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -97,11 +97,32 @@ static bool perform_keyword( throw std::runtime_error("unknown argument " + keyword); } +static void parse_project_args(util::ArgsReader& reader, CoreParameters& params) { + while (reader.hasNext()) { + std::string key = reader.next(); + if (!reader.isKeywordArg()) { + std::cerr << "-- argument expected" << std::endl; + return; + } + key = key.substr(2); + if (!reader.hasNext() || reader.isNextKeywordArg()) { + params.projectArgs[std::move(key)] = ""; + continue; + } + std::string value = reader.next(); + params.projectArgs[std::move(key)] = std::move(value); + } +} + bool parse_cmdline(int argc, char** argv, CoreParameters& params) { util::ArgsReader reader(argc, argv); reader.skip(); while (reader.hasNext()) { std::string token = reader.next(); + if (token == "--") { + parse_project_args(reader, params); + return true; + } if (reader.isKeywordArg()) { if (!perform_keyword(reader, token, params)) { return false;