diff --git a/.github/workflows/windows-clang.yml b/.github/workflows/windows-clang.yml index af0b060ae..5dd91a576 100644 --- a/.github/workflows/windows-clang.yml +++ b/.github/workflows/windows-clang.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main", "dev" ] + branches: [ "main", "dev" ] workflow_call: inputs: build_name: diff --git a/doc/en/3d-text.md b/doc/en/3d-text.md index 6e957a37e..b0c9fd1f6 100644 --- a/doc/en/3d-text.md +++ b/doc/en/3d-text.md @@ -79,6 +79,14 @@ gfx.text3d.set_rotation(id: int, rotation: mat4) Sets the text rotation (Sets the rotated vectors X,Y). +```lua +gfx.text3d.get_entity(id: int) -> int +gfx.text3d.set_entity(id: int, entity: int) +``` + +Getter and setter for the entity to which the 3D text position is bound. +The position set via `gfx.text3d.set_pos` is then relative to the entity's position. + ```lua gfx.text3d.update_settings(id: int, preset: table) ``` diff --git a/doc/en/main-page.md b/doc/en/main-page.md index 16b11d1eb..65a22ee85 100644 --- a/doc/en/main-page.md +++ b/doc/en/main-page.md @@ -1,6 +1,7 @@ # Documentation -Documentation for 0.31. +> [!WARNING] +> Version is in development. Proceed to [Documentation for 0.31.](https://github.com/MihailRis/voxelcore/blob/release-0.31/doc/en/main-page.md) ## Sections diff --git a/doc/en/scripting/builtins/libgfx-posteffects.md b/doc/en/scripting/builtins/libgfx-posteffects.md index c624d557c..5e36683c4 100644 --- a/doc/en/scripting/builtins/libgfx-posteffects.md +++ b/doc/en/scripting/builtins/libgfx-posteffects.md @@ -41,8 +41,8 @@ gfx.posteffects.set_array( slot: int, -- parameter (array) name name: str - -- data string (use Bytearray_as_string function) - data: str + -- data + data: Bytearray ) -- Example of filling an array `#param vec3 u_arrayName[64]`: diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index 7b1f87a1a..878452928 100644 --- a/doc/en/scripting/ecs.md +++ b/doc/en/scripting/ecs.md @@ -180,6 +180,10 @@ rig:get_color() -> vec3 rig:set_color(color: vec3) ``` +> [!WARNING] +> When an entity moves outside the loaded area, it is removed, triggering the event. +> When it re-enters the loaded area, the entity is spawned again. + ## Component events ```lua diff --git a/doc/en/scripting/events.md b/doc/en/scripting/events.md index 504bde532..75180edc3 100644 --- a/doc/en/scripting/events.md +++ b/doc/en/scripting/events.md @@ -152,6 +152,17 @@ function on_block_interact(blockid, x, y, z, playerid) -> bool Called on block RMB click interaction. Prevents block placing if **true** returned. +```lua +function on_entity_spawn(uid) +``` +Called after an entity is spawned. + +```lua +function on_entity_despawn(uid) +``` + +Called before an entity is despawned. + ### Chunk Events (world.lua) ```lua @@ -198,11 +209,26 @@ function on_close(invid: int) Called on element removed from the screen. +## Content events + +Callbacks specified in *content.lua* script. + +```lua +function on_scripts_loading() +``` + +Called before the content pack scripts are loaded + +```lua +function on_content_loaded() +``` + +Called after the content pack has been fully loaded + ## HUD events Callbacks specified in *hud.lua* script. - ```lua function on_hud_open(playerid: int) ``` diff --git a/doc/ru/3d-text.md b/doc/ru/3d-text.md index a53075ca2..c2d209434 100644 --- a/doc/ru/3d-text.md +++ b/doc/ru/3d-text.md @@ -79,6 +79,14 @@ gfx.text3d.set_rotation(id: int, rotation: mat4) Устанавливает вращение текста (Устанавливает повернутые вектора X,Y). +```lua +gfx.text3d.get_entity(id: int) -> int +gfx.text3d.set_entity(id: int, entity: int) +``` + +Геттер и сеттер сущности, к позиции которой привязан 3D текст. +Позиция, устанавливаемая через `gfx.text3d.set_pos`, в таком случае, относительна позиции сущности. + ```lua gfx.text3d.update_settings(id: int, preset: table) ``` diff --git a/doc/ru/main-page.md b/doc/ru/main-page.md index ee8cc5bc1..772f327fd 100644 --- a/doc/ru/main-page.md +++ b/doc/ru/main-page.md @@ -1,6 +1,7 @@ # Документация -Документация версии 0.31. +> [!WARNING] +> Версия находится в разработке. Перейдите к [документации для 0.31.](https://github.com/MihailRis/voxelcore/blob/release-0.31/doc/ru/main-page.md) ## Разделы diff --git a/doc/ru/scripting/builtins/libgfx-posteffects.md b/doc/ru/scripting/builtins/libgfx-posteffects.md index 01c13d888..64fb6c064 100644 --- a/doc/ru/scripting/builtins/libgfx-posteffects.md +++ b/doc/ru/scripting/builtins/libgfx-posteffects.md @@ -41,8 +41,8 @@ gfx.posteffects.set_array( slot: int, -- имя параметра (массива) name: string - -- строка данных (используйте функцию Bytearray_as_string) - data: string + -- данные + data: Bytearray ) -- Пример заполнения массива `#param vec3 u_имяМассива[64]`: @@ -54,4 +54,4 @@ for i = 0, 63 do Bytearray.append(buffer, byteutil.pack("fff", x, y, z)) end gfx.posteffects.set_array(slot, "u_имяМассива", Bytearray_as_string(buffer)) -``` \ No newline at end of file +``` diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index 4f9d45f08..1a5f6ccf3 100644 --- a/doc/ru/scripting/ecs.md +++ b/doc/ru/scripting/ecs.md @@ -180,6 +180,10 @@ rig:get_color() -> vec3 rig:set_color(color: vec3) ``` +> [!WARNING] +> При выходе сущности за пределы зоны прогрузки она удаляется, вызывая события. +> При повторном попадании в зону прогрузки сущность спавнится заново. + ## События компонента ```lua diff --git a/doc/ru/scripting/events.md b/doc/ru/scripting/events.md index 116c73551..ff4bfb456 100644 --- a/doc/ru/scripting/events.md +++ b/doc/ru/scripting/events.md @@ -5,6 +5,7 @@ - [События предметов](#события-предметов) - [События мира](#события-мира) - [События макета](#события-макета) +- [События контента](#события-контента) - [События HUD](#события-hud) - [События ECS](ecs.md#события-компонента) - [Библиотека *Events*](#библиотека-events) @@ -155,6 +156,18 @@ function on_block_interact(blockid, x, y, z, playerid) -> bool Вызывается при нажатии на блок ПКМ. Предотвращает установку блоков, если возвращает `true` +```lua +function on_entity_spawn(uid) +``` + +Вызывается после спавна сущности + +```lua +function on_entity_despawn(uid) +``` + +Вызывается перед удалением сущности + ```lua function on_player_tick(playerid: int, tps: int) ``` @@ -189,7 +202,6 @@ function on_inventory_closed(invid: int, playerid: int) Вызывается при закрытии инвентаря. - ## События макета События прописываются в файле `layouts/имя_макета.xml.lua`. @@ -208,6 +220,22 @@ function on_close(invid: int) Вызывается при удалении элемента с экрана. +## События контента + +События жизненного цикла контент-пака прописываются в файле `scripts/content.lua` + +```lua +function on_scripts_loading() +``` + +Вызывается перед загрузкой скриптов контент-пака + +```lua +function on_content_loaded() +``` + +Вызывается после того, как контент-пак был полностью загружен + ## События HUD События связанные с игровым интерфейсом прописываются в файле `scripts/hud.lua` diff --git a/res/content/base/package.json b/res/content/base/package.json index a9214111b..71b4dca9f 100644 --- a/res/content/base/package.json +++ b/res/content/base/package.json @@ -1,6 +1,6 @@ { "id": "base", "title": "Base", - "version": "0.31", + "version": "0.32", "description": "basic content package" } diff --git a/res/content/base/scripts/components/drop.lua b/res/content/base/scripts/components/drop.lua index 2b798a386..39bd03ceb 100644 --- a/res/content/base/scripts/components/drop.lua +++ b/res/content/base/scripts/components/drop.lua @@ -2,7 +2,6 @@ local tsf = entity.transform local body = entity.rigidbody local rig = entity.skeleton -inair = true target = -1 timer = 0.3 @@ -20,40 +19,58 @@ if SAVED_DATA.item then dropitem.data = SAVED_DATA.data end -local DROP_SCALE = 0.3 -local scale = {1, 1, 1} -local rotation = mat4.rotate({ - math.random(), math.random(), math.random() -}, 360) - function on_save() SAVED_DATA.item = item.name(dropitem.id) SAVED_DATA.count = dropitem.count SAVED_DATA.data = dropitem.data end -do -- setup visuals - local matrix = mat4.idt() - rig:set_model(0, item.model_name(dropitem.id)) - local bodysize = math.min(scale[1], scale[2], scale[3]) * DROP_SCALE - body:set_size({scale[1] * DROP_SCALE, bodysize, scale[3] * DROP_SCALE}) - mat4.mul(matrix, rotation, matrix) - mat4.scale(matrix, scale, matrix) - rig:set_matrix(0, matrix) +if vc.is_client() then + local scale = {1, 1, 1} + local rotation = mat4.rotate({ + math.random(), math.random(), math.random() + }, 360) + + do + local matrix = mat4.idt() + rig:set_model(0, item.model_name(dropitem.id)) + local bodysize = math.min(scale[1], scale[2], scale[3]) + body:set_size({scale[1], bodysize, scale[3]}) + mat4.mul(matrix, rotation, matrix) + mat4.scale(matrix, scale, matrix) + rig:set_matrix(0, matrix) + end + + local inair = true + + function on_grounded() + local matrix = mat4.idt() + mat4.rotate(matrix, {0, 1, 0}, math.random() * 360, matrix) + mat4.rotate(matrix, {1, 0, 0}, 90, matrix) + mat4.scale(matrix, scale, matrix) + rig:set_matrix(0, matrix) + inair = false + end + + function on_fall() + inair = true + end + + function on_render() + if inair then + local dt = time.delta(); + + mat4.rotate(rotation, {0, 1, 0}, 240*dt, rotation) + mat4.rotate(rotation, {0, 0, 1}, 240*dt, rotation) + + local matrix = mat4.idt() + mat4.mul(matrix, rotation, matrix) + mat4.scale(matrix, scale, matrix) + rig:set_matrix(0, matrix) + end + end end -function on_grounded(force) - local matrix = mat4.idt() - mat4.rotate(matrix, {0, 1, 0}, math.random() * 360, matrix) - mat4.rotate(matrix, {1, 0, 0}, 90, matrix) - mat4.scale(matrix, scale, matrix) - rig:set_matrix(0, matrix) - inair = false -end - -function on_fall() - inair = true -end function on_sensor_enter(index, oid) local other = entities.get(oid) @@ -98,20 +115,6 @@ function on_sensor_exit(index, oid) end end -function on_render() - if inair then - local dt = time.delta(); - - mat4.rotate(rotation, {0, 1, 0}, 240*dt, rotation) - mat4.rotate(rotation, {0, 0, 1}, 240*dt, rotation) - - local matrix = mat4.idt() - mat4.mul(matrix, rotation, matrix) - mat4.scale(matrix, scale, matrix) - rig:set_matrix(0, matrix) - end -end - function on_update(tps) timer = timer - 1.0/tps diff --git a/res/content/base/scripts/components/falling_block.lua b/res/content/base/scripts/components/falling_block.lua index b6bbe4656..01c5bc5f2 100644 --- a/res/content/base/scripts/components/falling_block.lua +++ b/res/content/base/scripts/components/falling_block.lua @@ -1,6 +1,5 @@ local tsf = entity.transform local body = entity.rigidbody -local rig = entity.skeleton local blockid = ARGS.block local blockstates = ARGS.states or 0 @@ -12,7 +11,8 @@ else SAVED_DATA.states = blockstates end -do -- setup visuals +if vc.is_client() then-- setup visuals + local rig = entity.skeleton local id = block.index(blockid) local rotation = block.decompose_state(blockstates)[1] local textures = block.get_textures(id) diff --git a/res/content/base/scripts/components/player_animator.lua b/res/content/base/scripts/components/player_animator.lua index f82927ccd..6bd118906 100644 --- a/res/content/base/scripts/components/player_animator.lua +++ b/res/content/base/scripts/components/player_animator.lua @@ -1,5 +1,3 @@ -local tsf = entity.transform -local body = entity.rigidbody local rig = entity.skeleton local mob = entity:require_component("core:mob") diff --git a/res/content/base/scripts/grass_block.lua b/res/content/base/scripts/grass_block.lua index e58597cc4..fb7482c20 100644 --- a/res/content/base/scripts/grass_block.lua +++ b/res/content/base/scripts/grass_block.lua @@ -1,20 +1,26 @@ +local offsets = {} +for lx=-1,1 do + for ly=-1,1 do + for lz=-1,1 do + offsets[(lx * 3 + ly) * 3 + lz] = {lx, ly, lz} + end + end +end + function on_random_update(x, y, z) local dirtid = block.index('base:dirt'); if block.is_solid_at(x, y+1, z) then block.set(x, y, z, dirtid, 0) - else - local grassblockid = block.index('base:grass_block') - for lx=-1,1 do - for ly=-1,1 do - for lz=-1,1 do - if block.get(x + lx, y + ly, z + lz) == dirtid then - if not block.is_solid_at(x + lx, y + ly + 1, z + lz) then - block.set(x + lx, y + ly, z + lz, grassblockid, 0) - return - end - end - end - end + return + end + local grassblockid = block.index('base:grass_block') + for _, offset in ipairs(offsets) do + local nx, ny, nz = x + offset[1], y + offset[2], z + offset[3] + + if block.get(nx, ny, nz) == dirtid and + not block.is_solid_at(nx, ny + 1, nz) then + block.set(nx, ny, nz, grassblockid, 0) + return end end end diff --git a/res/content/base/scripts/world.lua b/res/content/base/scripts/world.lua index 7f468c291..419dcc769 100644 --- a/res/content/base/scripts/world.lua +++ b/res/content/base/scripts/world.lua @@ -1,5 +1,5 @@ function on_block_broken(id, x, y, z, playerid) - if gfx then + if vc.is_client() then local size = {block.get_size(id)} gfx.particles.emit({ x + size[1] * 0.5, diff --git a/res/modules/internal/gui_util.lua b/res/modules/internal/gui_util.lua index 0f53faaba..0ec47291e 100644 --- a/res/modules/internal/gui_util.lua +++ b/res/modules/internal/gui_util.lua @@ -24,7 +24,7 @@ function gui_util.parse_query(query) end --- @param query string page query string -function gui_util.load_page(query) +function gui_util.load_page(app, query) local name, args = gui_util.parse_query(query) for i = #gui_util.local_dispatchers, 1, -1 do local newname, newargs = gui_util.local_dispatchers[i](name, args) @@ -34,7 +34,7 @@ function gui_util.load_page(query) local filename = file.find(string.format("layouts/pages/%s.xml", name)) if filename then name = file.prefix(filename)..":pages/"..name - gui.load_document(filename, name, args, { app = __vc_app }) + gui.load_document(filename, name, args, { app = app }) return name end end diff --git a/res/preload.json b/res/preload.json index 00948068e..51acb129c 100644 --- a/res/preload.json +++ b/res/preload.json @@ -50,7 +50,10 @@ "gui/hud", "gui/entity", "gui/half_block", - "misc/blank" + { + "name": "", + "path": "textures/misc/blank" + } ], "fonts": [ { diff --git a/res/scripts/components/player.lua b/res/scripts/components/player.lua index e829f4b5a..4f2ec53a8 100644 --- a/res/scripts/components/player.lua +++ b/res/scripts/components/player.lua @@ -80,7 +80,7 @@ function on_physics_update(delta) local front = mat4.mul(rot, {0, 0, -1}) local pos = tsf:get_pos() - if hud and pid == hud.get_player() then + if vc.is_client() and pid == hud.get_player() then process_player_inputs(pid, rot, delta) end mob.look_at(vec3.add(pos, front)) diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua index 322f03db5..562fe19d1 100644 --- a/res/scripts/hud.lua +++ b/res/scripts/hud.lua @@ -1,37 +1,3 @@ -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(__vc_app.get_setting("graphics.ssao")) -end - local function update_hand() local skeleton = gfx.skeletons local pid = hud.get_player() @@ -123,8 +89,6 @@ function on_hud_open() end end) - configure_SSAO() - hud.default_hand_controller = update_hand end diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 900b71b2e..0e72ad86d 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -1,4 +1,5 @@ -local enable_experimental = __vc_app.get_setting("debug.enable-experimental") +local __app = __vc_app +local enable_experimental = __app.get_setting("debug.enable-experimental") ------------------------------------------------ ------ Extended kit of standard functions ------ @@ -119,12 +120,7 @@ local function complete_app_lib(app) end end -if app then - complete_app_lib(app) -elseif __vc_app then - complete_app_lib(__vc_app) -end - +complete_app_lib(__app) require "core:internal/maths_inline" require "core:internal/debugging" require "core:internal/audio_input" @@ -175,18 +171,18 @@ function start_coroutine(chunk, name) local co = coroutine.create(function() local status, error = xpcall(chunk, function(err) local fullmsg = "error: "..string.match(err, ": (.+)").."\n"..debug.traceback() - - if hud then - gui.alert(fullmsg, function() - if world.is_open() then - __vc_app.close_world() - else - __vc_app.reset_content() - menu:reset() - menu.page = "main" - end - end) + if vc.is_headless() then + return fullmsg end + gui.alert(fullmsg, function() + if world.is_open() then + __app.close_world() + else + __app.reset_content() + menu:reset() + menu.page = "main" + end + end) return fullmsg end) if not status then @@ -204,7 +200,7 @@ function __vc_start_app_script(path, name) if chunk == nil then error(err) end - local script_env = setmetatable({app = app or __vc_app}, {__index=_G}) + local script_env = setmetatable({app = __app}, {__index=_G}) chunk = setfenv(chunk, script_env) if name then start_coroutine(chunk, name) @@ -222,7 +218,7 @@ gui_util = require "core:internal/gui_util" Document = gui_util.Document Element = gui_util.Element RadioGroup = gui_util.RadioGroup -__vc_page_loader = gui_util.load_page +__vc_page_loader = function(...) return gui_util.load_page(__app, ...) end function __vc_get_document_node(docname, nodeid) return Element.new(docname, nodeid) @@ -384,6 +380,40 @@ 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 @@ -446,6 +476,8 @@ function __vc_on_hud_open() end end) hud.open_permanent("core:ingame_chat") + + configure_SSAO() end local Schedule = require "core:schedule" @@ -508,20 +540,27 @@ function __vc_on_world_save() 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() - file.__close_all_descriptors() + __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 core.get_core_token = audio.input.__get_core_token -local function __vc__process_post_runnables() +local __post_runnables = {} + +local function __process_post_runnables() if #__post_runnables > 0 then for _, func in ipairs(__post_runnables) do local status, result = xpcall(func, __vc__error) @@ -555,9 +594,9 @@ local function __vc__process_post_runnables() end end -function __process_post_runnables() +function __vc__process_post_runnables() __vc__is_post_runnable = true - local success, err = pcall(__vc__process_post_runnables) + local success, err = pcall(__process_post_runnables) if not success then debug.error("an error ocurred while processing post-runnables: ".. err) end @@ -590,4 +629,6 @@ end require "core:internal/deprecated" ffi = nil +__vc_app = nil __vc_lock_internal_modules() +__vc_lock_internal_modules = nil diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 4ae34b2fd..70e27f561 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -1,5 +1,17 @@ +local _vc_headless = __VC_HEADLESS +__VC_HEADLESS = nil + +vc = { + is_headless = function() + return _vc_headless + end, + is_client = function () + return not _vc_headless + end +} + local _ffi = ffi -local _debug_getinfo = debug.getinfo +local _crc32 = crc32 function crc32(bytes, chksum) chksum = chksum or 0 @@ -103,6 +115,8 @@ end ---------------------------------------------- +local _debug_getinfo = debug.getinfo + function debug.count_frames() local frames = 1 while true do @@ -178,7 +192,7 @@ function reload_module(name) end end -local internal_locked = false +local __internal_locked = false -- Load script with caching -- @@ -188,9 +202,12 @@ local internal_locked = false -- nocache - ignore cached script, load anyway function __load_script(path, nocache, env) local packname, filename = parse_path(path) + local is_internal = (packname == "res" or packname == "core") + and filename:find("modules/internal") == 1 - if internal_locked and (packname == "res" or packname == "core") - and filename:starts_with("modules/internal") then + nocache = nocache or is_internal + + if is_internal and __internal_locked then error("access to core:internal modules outside of [core]") end @@ -218,16 +235,19 @@ function __load_script(path, nocache, env) end function __vc_lock_internal_modules() - internal_locked = true + __internal_locked = true end +local __pack_envs = __vc__pack_envs +__vc__pack_envs = nil + function require(path) if not string.find(path, ':') then local prefix, _ = parse_path(_debug_getinfo(2).source) return require(prefix .. ':' .. path) end local prefix, file = parse_path(path) - local env = __vc__pack_envs[prefix] + local env = __pack_envs[prefix] return __load_script(prefix .. ":modules/" .. file .. ".lua", nil, env) end @@ -248,7 +268,7 @@ function __scripts_cleanup(non_reset_packs) __cached_scripts[k] = nil package.loaded[k] = nil end - __vc__pack_envs[packname] = nil + __pack_envs[packname] = nil ::continue:: end end diff --git a/src/constants.hpp b/src/constants.hpp index 1cf3e72ce..f43314f4a 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -6,7 +6,7 @@ #include inline constexpr int ENGINE_VERSION_MAJOR = 0; -inline constexpr int ENGINE_VERSION_MINOR = 31; +inline constexpr int ENGINE_VERSION_MINOR = 32; #ifdef NDEBUG inline constexpr bool ENGINE_DEBUG_BUILD = false; @@ -14,7 +14,7 @@ inline constexpr bool ENGINE_DEBUG_BUILD = false; inline constexpr bool ENGINE_DEBUG_BUILD = true; #endif // NDEBUG -inline const std::string ENGINE_VERSION_STRING = "0.31"; +inline const std::string ENGINE_VERSION_STRING = "0.32"; /// @brief world regions format version inline constexpr uint REGION_FORMAT_VERSION = 3; diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index e2a463cac..5120f5c59 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -334,6 +334,8 @@ void ContentLoader::load() { fixPackIndices(); + loadContentScript(*runtime); + auto folder = pack->folder; builder.defaults = paths.readCombinedObject( @@ -462,6 +464,20 @@ void ContentLoader::reloadScript(const Content& content, ItemDef& item) { load_script(content, item); } +void ContentLoader::loadContentScript(ContentPackRuntime& runtime) { + const auto& pack = runtime.getInfo(); + const auto& folder = pack.folder; + io::path scriptFile = folder / "scripts/content.lua"; + if (io::is_regular_file(scriptFile)) { + scripting::load_content_script( + runtime.getEnvironment(), + pack.id, + scriptFile, + pack.id + ":scripts/content.lua" + ); + } +} + void ContentLoader::loadWorldScript(ContentPackRuntime& runtime) { const auto& pack = runtime.getInfo(); const auto& folder = pack.folder; @@ -478,6 +494,7 @@ void ContentLoader::loadWorldScript(ContentPackRuntime& runtime) { } void ContentLoader::loadScripts(Content& content) { + scripting::on_scripts_loading(); load_scripts(content, content.blocks); load_scripts(content, content.items); @@ -486,7 +503,6 @@ void ContentLoader::loadScripts(Content& content) { const auto& pack = runtime->getInfo(); const auto& folder = pack.folder; - // Load main world script loadWorldScript(*runtime); // Load entity components @@ -501,6 +517,8 @@ void ContentLoader::loadScripts(Content& content) { ); }); } + + scripting::on_content_loaded(); } void ContentLoader::loadResources(ResourceType type, const dv::value& list) { diff --git a/src/content/ContentLoader.hpp b/src/content/ContentLoader.hpp index 3bf385c5b..aa65478ae 100644 --- a/src/content/ContentLoader.hpp +++ b/src/content/ContentLoader.hpp @@ -58,6 +58,7 @@ public: void load(); static void loadScripts(Content& content); + static void loadContentScript(ContentPackRuntime& pack); static void loadWorldScript(ContentPackRuntime& pack); static void reloadScript(const Content& content, Block& block); static void reloadScript(const Content& content, ItemDef& item); diff --git a/src/content/ContentPack.cpp b/src/content/ContentPack.cpp index d2ffdc9e5..ec7168a28 100644 --- a/src/content/ContentPack.cpp +++ b/src/content/ContentPack.cpp @@ -47,10 +47,6 @@ io::path ContentPack::getContentFile() const { return folder / CONTENT_FILENAME; } -bool ContentPack::is_pack(const io::path& folder) { - return io::is_regular_file(folder / PACKAGE_FILENAME); -} - std::optional ContentPack::loadStats() const { auto contentFile = getContentFile(); if (!io::exists(contentFile)) { @@ -85,6 +81,9 @@ static void checkContentPackId(const std::string& id, const io::path& folder) { ); } } + if (id == "none") { + throw contentpack_error(id, folder, "content-pack id is not specified"); + } if (std::find( ContentPack::RESERVED_NAMES.begin(), ContentPack::RESERVED_NAMES.end(), @@ -94,10 +93,71 @@ static void checkContentPackId(const std::string& id, const io::path& folder) { } } +static DependencyPack parse_dependency(std::string depName) { + auto level = DependencyLevel::REQUIRED; + switch (depName.at(0)) { + case '!': + depName = depName.substr(1); + break; + case '?': + depName = depName.substr(1); + level = DependencyLevel::OPTIONAL; + break; + case '~': + depName = depName.substr(1); + level = DependencyLevel::WEAK; + break; + } + + std::string depVer = "*"; + std::string depVerOperator = "="; + + size_t versionPos = depName.rfind("@"); + if (versionPos != std::string::npos) { + depVer = depName.substr(versionPos + 1); + depName = depName.substr(0, versionPos); + + if (depVer.size() >= 2) { + std::string op = depVer.substr(0, 2); + std::uint8_t op_size = 0; + + // Two symbol operators + if (op == ">=" || op == "<=") { + op_size = 2; + depVerOperator = op; + } + + // One symbol operators + else { + op = depVer.substr(0, 1); + + if (op == ">" || op == "<") { + op_size = 1; + depVerOperator = op; + } + } + + depVer = depVer.substr(op_size); + } else { + if (depVer == ">" || depVer == "<"){ + depVer = "*"; + } + } + } + + VersionOperator versionOperator; + if (VersionOperatorMeta.getItem(depVerOperator, versionOperator)) { + return DependencyPack{level, depName, depVer, versionOperator}; + } else { + throw std::runtime_error("invalid version operator"); + } +} + ContentPack ContentPack::read(const io::path& folder) { auto root = io::read_json(folder / PACKAGE_FILENAME); ContentPack pack; root.at("id").get(pack.id); + checkContentPackId(pack.id, folder); root.at("title").get(pack.title); root.at("version").get(pack.version); if (root.has("creators")) { @@ -115,81 +175,26 @@ ContentPack ContentPack::read(const io::path& folder) { root.at("source").get(pack.source); pack.folder = folder; + auto dependenciesList = root.at("dependencies"); + if (!dependenciesList) { + return pack; + } + if (auto found = root.at("dependencies")) { const auto& dependencies = *found; for (const auto& elem : dependencies) { std::string depName = elem.asString(); - auto level = DependencyLevel::REQUIRED; - switch (depName.at(0)) { - case '!': - depName = depName.substr(1); - break; - case '?': - depName = depName.substr(1); - level = DependencyLevel::OPTIONAL; - break; - case '~': - depName = depName.substr(1); - level = DependencyLevel::WEAK; - break; - } - - std::string depVer = "*"; - std::string depVerOperator = "="; - - size_t versionPos = depName.rfind("@"); - if (versionPos != std::string::npos) { - depVer = depName.substr(versionPos + 1); - depName = depName.substr(0, versionPos); - - if (depVer.size() >= 2) { - std::string op = depVer.substr(0, 2); - std::uint8_t op_size = 0; - - // Two symbol operators - if (op == ">=" || op == "<=") { - op_size = 2; - depVerOperator = op; - } - - // One symbol operators - else { - op = depVer.substr(0, 1); - - if (op == ">" || op == "<") { - op_size = 1; - depVerOperator = op; - } - } - - depVer = depVer.substr(op_size); - } else { - if (depVer == ">" || depVer == "<"){ - depVer = "*"; - } - } - } - - VersionOperator versionOperator; - if (VersionOperatorMeta.getItem(depVerOperator, versionOperator)) { - pack.dependencies.push_back( - {level, depName, depVer, versionOperator} - ); - } else { + try { + pack.dependencies.push_back(parse_dependency(std::move(depName))); + } catch (const std::runtime_error& err) { throw contentpack_error( - pack.id, folder, "invalid version operator" + pack.id, + folder, + "dependency parsing error: " + std::string(err.what()) ); } } } - - if (pack.id == "none") { - throw contentpack_error( - pack.id, folder, "content-pack id is not specified" - ); - } - checkContentPackId(pack.id, folder); - return pack; } @@ -200,15 +205,21 @@ void ContentPack::scanFolder( return; } for (const auto& packFolder : io::directory_iterator(folder)) { - if (!io::is_directory(packFolder)) continue; - if (!is_pack(packFolder)) continue; + if (!io::is_directory(packFolder)) { + continue; + } + auto packageFile = packFolder / PACKAGE_FILENAME; + if (!io::is_regular_file(packageFile)) { + continue; + } try { packs.push_back(read(packFolder)); } catch (const contentpack_error& err) { - std::cerr << "package.json error at " << err.getFolder().string(); - std::cerr << ": " << err.what() << std::endl; + logger.warning() << "package.json error at " + << err.getFolder().string() << ": " << err.what(); } catch (const std::runtime_error& err) { - std::cerr << err.what() << std::endl; + logger.error() << "reading " << packageFile.string() + << " error: " << err.what(); } } } diff --git a/src/content/ContentPack.hpp b/src/content/ContentPack.hpp index a4548d038..cd5f93dbc 100644 --- a/src/content/ContentPack.hpp +++ b/src/content/ContentPack.hpp @@ -85,7 +85,6 @@ struct ContentPack { static inline const io::path GENERATORS_FOLDER = "generators"; static const std::vector RESERVED_NAMES; - static bool is_pack(const io::path& folder); static ContentPack read(const io::path& folder); static void scanFolder( @@ -114,6 +113,14 @@ struct ContentPack { default: return ""; } } + + static std::string_view getPrefix(std::string_view path) { + size_t pos = path.find(':'); + if (pos == std::string::npos) { + return ""; + } + return path.substr(0, pos); + } }; struct WorldFuncsSet { @@ -127,6 +134,8 @@ struct WorldFuncsSet { bool onchunkremove; bool oninventoryopen; bool oninventoryclosed; + bool onentityspawn; + bool onentitydespawn; }; class ContentPackRuntime { diff --git a/src/graphics/render/TextNote.cpp b/src/graphics/render/TextNote.cpp index 127414c37..7c4797fd3 100644 --- a/src/graphics/render/TextNote.cpp +++ b/src/graphics/render/TextNote.cpp @@ -10,6 +10,14 @@ void TextNote::setText(std::wstring_view text) { this->text = text; } +void TextNote::setEntity(entityid_t eid) { + this->entity = eid; +} + +entityid_t TextNote::getEntity() const { + return entity; +} + const std::wstring& TextNote::getText() const { return text; } diff --git a/src/graphics/render/TextNote.hpp b/src/graphics/render/TextNote.hpp index 65a26ac17..0fc8f4afd 100644 --- a/src/graphics/render/TextNote.hpp +++ b/src/graphics/render/TextNote.hpp @@ -1,5 +1,6 @@ #pragma once +#include "constants.hpp" #include "presets/NotePreset.hpp" /// @brief 3D text instance @@ -7,6 +8,7 @@ class TextNote { std::wstring text; NotePreset preset; glm::vec3 position; + entityid_t entity = ENTITY_NONE; glm::vec3 xAxis {1, 0, 0}; glm::vec3 yAxis {0, 1, 0}; @@ -14,6 +16,8 @@ public: TextNote(std::wstring text, NotePreset preset, glm::vec3 position); void setText(std::wstring_view text); + void setEntity(entityid_t eid); + entityid_t getEntity() const; const std::wstring& getText() const; diff --git a/src/graphics/render/TextsRenderer.cpp b/src/graphics/render/TextsRenderer.cpp index ed822fc9f..ac93d1e43 100644 --- a/src/graphics/render/TextsRenderer.cpp +++ b/src/graphics/render/TextsRenderer.cpp @@ -10,12 +10,18 @@ #include "graphics/core/Shader.hpp" #include "graphics/core/DrawContext.hpp" #include "presets/NotePreset.hpp" +#include "world/Level.hpp" +#include "objects/Entities.hpp" +#include "objects/Entity.hpp" #include "constants.hpp" TextsRenderer::TextsRenderer( - Batch3D& batch, const Assets& assets, const Frustum& frustum + const Level& level, + Batch3D& batch, + const Assets& assets, + const Frustum& frustum ) - : batch(batch), assets(assets), frustum(frustum) { + : level(level), batch(batch), assets(assets), frustum(frustum) { } void TextsRenderer::renderNote( @@ -31,6 +37,13 @@ void TextsRenderer::renderNote( const auto& preset = note.getPreset(); auto pos = note.getPosition(); + entityid_t eid = note.getEntity(); + if (eid != ENTITY_NONE) { + if (auto entity = level.entities->get(eid)) { + pos += entity->getTransform().displayPos; + } + } + if (util::distance2(pos, camera.position) > util::sqr(preset.renderDistance / camera.zoom)) { return; diff --git a/src/graphics/render/TextsRenderer.hpp b/src/graphics/render/TextsRenderer.hpp index 629bc3cd2..77af2581a 100644 --- a/src/graphics/render/TextsRenderer.hpp +++ b/src/graphics/render/TextsRenderer.hpp @@ -12,8 +12,10 @@ class Batch3D; class Frustum; class TextNote; struct EngineSettings; +class Level; class TextsRenderer { + const Level& level; Batch3D& batch; const Assets& assets; const Frustum& frustum; @@ -31,7 +33,12 @@ class TextsRenderer { bool projected ); public: - TextsRenderer(Batch3D& batch, const Assets& assets, const Frustum& frustum); + TextsRenderer( + const Level& level, + Batch3D& batch, + const Assets& assets, + const Frustum& frustum + ); void render( const DrawContext& context, diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 9f173eb38..2e150e1f7 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -91,9 +91,11 @@ WorldRenderer::WorldRenderer( assets, level, *player.chunks, &engine.getSettings().graphics )), particles(std::make_unique( - assets, level, *player.chunks, engine.getSettings().graphics + assets, level, *player.chunks, engine.getSettings().graphics + )), + texts(std::make_unique( + level, *batch3d, assets, *frustumCulling )), - texts(std::make_unique(*batch3d, assets, *frustumCulling)), blockWraps(std::make_unique( assets, level.content, *player.chunks )) { diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 09ea33ce3..6442fcbad 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -350,6 +350,7 @@ static int l_start_debug_instance(lua::State* L) { } } auto projectPath = lua::isstring(L, 2) ? lua::require_lstring(L, 2) : ""; + auto outputPath = lua::isstring(L, 3) ? lua::require_lstring(L, 3) : ""; const auto& paths = engine->getPaths(); std::vector args { @@ -362,7 +363,10 @@ static int l_start_debug_instance(lua::State* L) { args.emplace_back(io::resolve(std::string(projectPath)).string()); } - platform::new_engine_instance(std::move(args)); + platform::new_engine_instance( + std::move(args), + outputPath.empty() ? "" : io::resolve(std::string(outputPath)) + ); return lua::pushinteger(L, port); } diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 0e53643db..58a2f2be1 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -1011,8 +1011,9 @@ static int l_gui_get_env(lua::State* L) { "document '" + std::string(name) + "' not found" ); } - lua::getglobal(L, lua::env_name(*doc->getEnvironment())); - return 1; + return lua::getregistry( + L, lua::ENVS_TABLE, lua::env_name(*doc->getEnvironment()) + ); } static int l_gui_str(lua::State* L) { diff --git a/src/logic/scripting/lua/libs/libposteffects.cpp b/src/logic/scripting/lua/libs/libposteffects.cpp index 6eedaa6c8..3d714203b 100644 --- a/src/logic/scripting/lua/libs/libposteffects.cpp +++ b/src/logic/scripting/lua/libs/libposteffects.cpp @@ -65,7 +65,7 @@ static int l_set_params(lua::State* L) { static int l_set_array(lua::State* L) { size_t index = static_cast(lua::tointeger(L, 1)); auto key = lua::require_string(L, 2); - auto data = lua::require_lstring(L, 3); + auto data = lua::bytearray_as_string(L, 3); auto effect = post_processing->getEffect(index); if (effect == nullptr) { return 0; diff --git a/src/logic/scripting/lua/libs/libtext3d.cpp b/src/logic/scripting/lua/libs/libtext3d.cpp index 310f5d78e..0c3957cce 100644 --- a/src/logic/scripting/lua/libs/libtext3d.cpp +++ b/src/logic/scripting/lua/libs/libtext3d.cpp @@ -95,6 +95,20 @@ static int l_set_rotation(lua::State* L) { return 0; } +static int l_get_entity(lua::State* L) { + if (auto note = renderer->texts->get(lua::tointeger(L, 1))) { + return lua::pushinteger(L, note->getEntity()); + } + return 0; +} + +static int l_set_entity(lua::State* L) { + if (auto note = renderer->texts->get(lua::tointeger(L, 1))) { + note->setEntity(lua::touinteger(L, 2)); + } + return 0; +} + const luaL_Reg text3dlib[] = { {"show", wrap_hud}, {"hide", wrap_hud}, @@ -108,5 +122,7 @@ const luaL_Reg text3dlib[] = { {"set_axis_y", wrap_hud}, {"set_rotation", wrap_hud}, {"update_settings", wrap_hud}, + {"get_entity", wrap_hud}, + {"set_entity", wrap_hud}, {nullptr, nullptr} }; diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index 5edb24fd9..bd2f0359b 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -64,12 +64,13 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "vec4", vec4lib); openlib(L, "yaml", yamllib); + openlib(L, "__vc_app", applib); + lua::getglobal(L, "__vc_app"); + lua::setregistry(L, "app"); + if (stateType == StateType::SCRIPT) { - openlib(L, "app", applib); - lua::getglobal(L, "app"); - lua::setglobal(L, "__vc_app"); - } else if (stateType == StateType::BASE) { - openlib(L, "__vc_app", applib); + lua::getregistry(L, "app"); + lua::setglobal(L, "app"); } if (stateType == StateType::BASE || stateType == StateType::SCRIPT) { openlib(L, "assets", assetslib); @@ -95,10 +96,18 @@ static void create_libs(State* L, StateType stateType) { } addfunc(L, "print", lua::wrap); - addfunc(L, "_crc32", lua::wrap); + addfunc(L, "crc32", lua::wrap); +} + +static int l_panic_handler(lua::State* L) { + logger.error() << "PANIC: unprotected error in call to Lua API: " << lua::tostring(L, -1); + logger.flush(); + abort(); } void lua::init_state(State* L, StateType stateType) { + lua_atpanic(L, l_panic_handler); + // Allowed standard libraries luaL_openlibs(L); @@ -112,21 +121,25 @@ void lua::init_state(State* L, StateType stateType) { setglobal(L, "io"); createtable(L, 0, 0); + pushvalue(L, -1); setglobal(L, "__vc__pack_envs"); + setregistry(L, lua::PACK_ENVS_TABLE); const char* removed_os[] { "execute", "exit", "remove", "rename", "setlocale", "tmpname", nullptr}; remove_lib_funcs(L, "os", removed_os); create_libs(L, stateType); + createtable(L, 0, 0); + setregistry(L, LAMBDAS_TABLE); + + createtable(L, 0, 0); + setregistry(L, CHUNKS_TABLE); + + createtable(L, 0, 0); pushglobals(L); - setglobal(L, env_name(0)); - - createtable(L, 0, 0); - setglobal(L, LAMBDAS_TABLE); - - createtable(L, 0, 0); - setglobal(L, CHUNKS_TABLE); + setfield(L, env_name(0)); + setregistry(L, ENVS_TABLE); initialize_libs_extends(L); @@ -144,6 +157,9 @@ void lua::initialize(const EnginePaths& paths, const CoreParameters& params) { ); 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() { diff --git a/src/logic/scripting/lua/lua_util.cpp b/src/logic/scripting/lua/lua_util.cpp index 6fdbe91ea..ded9cc79b 100644 --- a/src/logic/scripting/lua/lua_util.cpp +++ b/src/logic/scripting/lua/lua_util.cpp @@ -8,7 +8,7 @@ using namespace lua; -static int nextEnvironment = 1; +static int next_environment = 1; std::unordered_map lua::usertypeNames; @@ -20,7 +20,7 @@ int lua::userdata_destructor(lua::State* L) { } std::string lua::env_name(int env) { - return "_ENV" + util::mangleid(env); + return std::to_string(env); } int lua::pushvalue(State* L, const dv::value& value) { @@ -220,7 +220,7 @@ void lua::dump_stack(State* L) { static std::shared_ptr create_lambda_handler(State* L) { auto ptr = reinterpret_cast(topointer(L, -1)); auto name = util::mangleid(ptr); - requireglobal(L, LAMBDAS_TABLE); + requireregistry(L, LAMBDAS_TABLE); pushvalue(L, -2); setfield(L, name); pop(L, 2); @@ -229,7 +229,7 @@ static std::shared_ptr create_lambda_handler(State* L) { new std::string(name), [=](std::string* name) { auto L = lua::get_main_state(); - requireglobal(L, LAMBDAS_TABLE); + requireregistry(L, LAMBDAS_TABLE); pushnil(L); setfield(L, *name); pop(L); @@ -242,8 +242,9 @@ runnable lua::create_runnable(State* L) { auto funcptr = create_lambda_handler(L); return [=]() { auto L = lua::get_main_state(); - if (!get_from(L, LAMBDAS_TABLE, *funcptr, false)) + if (!get_from_registry(L, LAMBDAS_TABLE, *funcptr, false)) { return; + } call_nothrow(L, 0, 0); pop(L); }; @@ -252,8 +253,9 @@ runnable lua::create_runnable(State* L) { KeyCallback lua::create_simple_handler(State* L) { auto funcptr = create_lambda_handler(L); return [=]() -> bool { - if (!get_from(L, LAMBDAS_TABLE, *funcptr, false)) + if (!get_from_registry(L, LAMBDAS_TABLE, *funcptr, false)) { return false; + } int top = gettop(L) - 1; if (call_nothrow(L, 0)) { int nres = gettop(L) - top; @@ -271,8 +273,9 @@ KeyCallback lua::create_simple_handler(State* L) { scripting::common_func lua::create_lambda(State* L) { auto funcptr = create_lambda_handler(L); return [=](const std::vector& args) -> dv::value { - if (!get_from(L, LAMBDAS_TABLE, *funcptr, false)) + if (!get_from_registry(L, LAMBDAS_TABLE, *funcptr, false)) { return nullptr; + } int top = gettop(L) - 1; for (const auto& arg : args) { pushvalue(L, arg); @@ -294,8 +297,9 @@ scripting::common_func lua::create_lambda(State* L) { scripting::common_func lua::create_lambda_nothrow(State* L) { auto funcptr = create_lambda_handler(L); return [=](const std::vector& args) -> dv::value { - if (!get_from(L, LAMBDAS_TABLE, *funcptr, false)) + if (!get_from_registry(L, LAMBDAS_TABLE, *funcptr, false)) { return nullptr; + } int top = gettop(L) - 1; for (const auto& arg : args) { pushvalue(L, arg); @@ -313,8 +317,16 @@ scripting::common_func lua::create_lambda_nothrow(State* L) { }; } +static void store_env(lua::State* L, int id) { + requireregistry(L, ENVS_TABLE); + pushvalue(L, -2); + lua_remove(L, -3); + setfield(L, env_name(id)); + pop(L); +} + int lua::create_environment(State* L, int parent) { - int id = nextEnvironment++; + int id = next_environment++; // local env = {} createtable(L, 0, 1); @@ -331,24 +343,22 @@ int lua::create_environment(State* L, int parent) { setfield(L, "__index"); setmetatable(L); - // envname = env - setglobal(L, env_name(id)); + store_env(L, id); return id; } int lua::restore_pack_environment(lua::State* L, const std::string& packid) { - if(!lua::getglobal(L, "__vc__pack_envs")) { + if(!requireregistry(L, PACK_ENVS_TABLE)) { return -1; } - int id = nextEnvironment++; + int id = next_environment++; - if (lua::getfield(L, packid)) { - // envname = env - setglobal(L, env_name(id)); - lua::pop(L); + if (getfield(L, packid)) { + store_env(L, id); + pop(L); return id; } - lua::pop(L); + pop(L); return -1; } @@ -357,5 +367,5 @@ void lua::remove_environment(State* L, int id) { return; } pushnil(L); - setglobal(L, env_name(id)); + store_env(L, id); } diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index f87853256..a7881bef7 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -14,21 +14,35 @@ namespace lua { inline std::string LAMBDAS_TABLE = "$L"; // lambdas storage inline std::string CHUNKS_TABLE = "$C"; // precompiled lua chunks + inline std::string PACK_ENVS_TABLE = "$P"; + inline std::string ENVS_TABLE = "$E"; extern std::unordered_map usertypeNames; int userdata_destructor(lua::State* L); std::string env_name(int env); void dump_stack(lua::State*); - inline bool getglobal(lua::State* L, const std::string& name) { - lua_getglobal(L, name.c_str()); - if (isnil(L, -1)) { + inline bool isnoneornil(lua::State* L, int idx) { + return lua_isnoneornil(L, idx); + } + + inline bool getfield(lua::State* L, const std::string& name, int idx = -1) { + lua_getfield(L, idx, name.c_str()); + if (isnoneornil(L, -1)) { pop(L); return false; } return true; } + inline bool getglobal(lua::State* L, const std::string& name) { + return getfield(L, name, LUA_GLOBALSINDEX); + } + + inline bool getregistry(lua::State* L, const std::string& name) { + return getfield(L, name, LUA_REGISTRYINDEX); + } + inline int requireglobal(lua::State* L, const std::string& name) { if (getglobal(L, name)) { return 1; @@ -37,6 +51,24 @@ namespace lua { } } + inline int requireregistry(lua::State* L, const std::string& name) { + if (getregistry(L, name)) { + return 1; + } else { + throw std::runtime_error("registry entry " + name + " not found"); + } + } + + inline bool getregistry(lua::State* L, const std::string& name, const std::string& key) { + requireregistry(L, name); + if (getfield(L, key)) { + lua_remove(L, -2); + return true; + } + pop(L); + return false; + } + inline bool hasglobal(lua::State* L, const std::string& name) { lua_getglobal(L, name.c_str()); if (isnil(L, -1)) { @@ -203,9 +235,6 @@ namespace lua { inline int pushglobals(lua::State* L) { return pushvalue(L, LUA_GLOBALSINDEX); } - inline bool isnoneornil(lua::State* L, int idx) { - return lua_isnoneornil(L, idx); - } inline bool isboolean(lua::State* L, int idx) { return lua_isboolean(L, idx); } @@ -267,6 +296,9 @@ namespace lua { inline void setglobal(lua::State* L, const std::string& name) { lua_setglobal(L, name.c_str()); } + inline void setregistry(lua::State* L, const std::string& key) { + lua_setfield(L, LUA_REGISTRYINDEX, key.c_str()); + } template inline T* touserdata(lua::State* L, int idx) { if (void* rawptr = lua_touserdata(L, idx)) { @@ -461,15 +493,6 @@ namespace lua { [[nodiscard]] dv::value tovalue(lua::State*, int idx); - inline bool getfield(lua::State* L, const std::string& name, int idx = -1) { - lua_getfield(L, idx, name.c_str()); - if (isnoneornil(L, -1)) { - pop(L); - return false; - } - return true; - } - inline int requirefield( lua::State* L, const std::string& name, int idx = -1 ) { @@ -538,7 +561,7 @@ namespace lua { if (luaL_loadbuffer(L, src.c_str(), src.length(), file.c_str())) { throw luaerror(tostring(L, -1)); } - if (env && getglobal(L, env_name(env))) { + if (env && getregistry(L, ENVS_TABLE, env_name(env))) { lua_setfenv(L, -2); } } @@ -551,7 +574,46 @@ namespace lua { setfield(L, name); pop(L, 2); } else { - throw std::runtime_error("table " + tableName + " not found"); + throw std::runtime_error("global table " + tableName + " not found"); + } + } + + inline void store_in_registry( + lua::State* L, const std::string& tableName, const std::string& name + ) { + if (getregistry(L, tableName)) { + pushvalue(L, -2); + setfield(L, name); + pop(L, 2); + } else { + throw std::runtime_error("table " + tableName + " not found in registry"); + } + } + + inline int get_from( + lua::State* L, + const std::string& tableName, + const std::string& name, + bool required, + int idx, + std::string_view context + ) { + if (getfield(L, tableName, idx)) { + if (getfield(L, name)) { + return 1; + } else if (required) { + pop(L); + throw std::runtime_error( + std::string(context) + " table " + tableName + + " has no member " + name + ); + } + pop(L); + return 0; + } else { + throw std::runtime_error( + std::string(context) + " table " + tableName + " not found" + ); } } @@ -561,20 +623,20 @@ namespace lua { const std::string& name, bool required = false ) { - if (getglobal(L, tableName)) { - if (getfield(L, name)) { - return 1; - } else if (required) { - pop(L); - throw std::runtime_error( - "table " + tableName + " has no member " + name - ); - } - pop(L); - return 0; - } else { - throw std::runtime_error("table " + tableName + " not found"); - } + return get_from( + L, tableName, name, required, LUA_GLOBALSINDEX, "global" + ); + } + + inline int get_from_registry( + lua::State* L, + const std::string& tableName, + const std::string& name, + bool required = false + ) { + return get_from( + L, tableName, name, required, LUA_REGISTRYINDEX, "registry" + ); } int call(lua::State*, int argc, int nresults = -1); @@ -607,10 +669,7 @@ namespace lua { scripting::common_func create_lambda_nothrow(lua::State*); inline int pushenv(lua::State* L, int env) { - if (getglobal(L, env_name(env))) { - return 1; - } - return 0; + return getregistry(L, ENVS_TABLE, env_name(env)); } int create_environment(lua::State*, int parent); int restore_pack_environment(lua::State*, const std::string& packid); @@ -739,14 +798,14 @@ namespace lua { inline void read_bytes_from_table( lua::State* L, int tableIndex, std::vector& bytes ) { - if (!lua::istable(L, tableIndex)) { + if (!istable(L, tableIndex)) { throw std::runtime_error("table expected"); } else { - size_t size = lua::objlen(L, tableIndex); + size_t size = objlen(L, tableIndex); for (size_t i = 0; i < size; i++) { - lua::rawgeti(L, i + 1, tableIndex); - const int byte = lua::tointeger(L, -1); - lua::pop(L); + rawgeti(L, i + 1, tableIndex); + const int byte = tointeger(L, -1); + pop(L); if (byte < 0 || byte > 255) { throw std::runtime_error( "invalid byte '" + std::to_string(byte) + "'" @@ -758,11 +817,11 @@ namespace lua { } inline int create_bytearray(lua::State* L, const void* bytes, size_t size) { - lua::requireglobal(L, "Bytearray_construct"); - lua::pushlstring( + requireglobal(L, "Bytearray_construct"); + pushlstring( L, std::string_view(reinterpret_cast(bytes), size) ); - return lua::call(L, 1, 1); + return call(L, 1, 1); } inline int create_bytearray(lua::State* L, const std::vector& bytes) { @@ -770,12 +829,15 @@ namespace lua { } inline std::string_view bytearray_as_string(lua::State* L, int idx) { - lua::pushvalue(L, idx); - lua::requireglobal(L, "Bytearray_as_string"); - lua::pushvalue(L, -2); - lua::call(L, 1, 1); - auto view = lua::tolstring(L, -1); - lua::pop(L, 2); + 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; } } diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index e3c23ee1e..806f92c24 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -188,11 +188,7 @@ std::unique_ptr scripting::start_app_script(const io::path& script) { lua::pushstring(L, pack.id); lua::setfield(L, "PACK_ID"); - if(!lua::getglobal(L, "__vc__pack_envs")) { - lua::createtable(L, 0, 0); - lua::setglobal(L, "__vc__pack_envs"); - lua::pushvalue(L, -1); - } + lua::requireregistry(L, lua::PACK_ENVS_TABLE); lua::pushenv(L, id); lua::setfield(L, pack.id); lua::pop(L); @@ -248,7 +244,7 @@ std::unique_ptr scripting::start_app_script(const io::path& script) { void scripting::process_post_runnables() { auto L = lua::get_main_state(); - if (lua::getglobal(L, "__process_post_runnables")) { + if (lua::getglobal(L, "__vc__process_post_runnables")) { lua::call_nothrow(L, 0, 0); } } @@ -627,6 +623,26 @@ void scripting::on_ui_close(UiDocument* layout, Inventory* inventory) { }); } +void scripting::on_scripts_loading() { + auto L = lua::get_main_state(); + + for (auto& pack : content_control->getAllContentPacks()) { + lua::emit_event(L, pack.id + ":.onscriptsloading", [](auto L) { + return 0; + }); + } +} + +void scripting::on_content_loaded() { + auto L = lua::get_main_state(); + + for (auto& pack : content_control->getAllContentPacks()) { + lua::emit_event(L, pack.id + ":.oncontentloaded", [](auto L) { + return 0; + }); + } +} + bool scripting::register_event( int env, const std::string& name, const std::string& id ) { @@ -722,7 +738,7 @@ void scripting::load_entity_component( std::string src = io::read_string(file); logger.info() << "script (component) " << file.string(); lua::loadbuffer(L, *env, src, fileName); - lua::store_in(L, lua::CHUNKS_TABLE, name); + lua::store_in_registry(L, lua::CHUNKS_TABLE, name); } void scripting::load_world_script( @@ -761,6 +777,23 @@ void scripting::load_world_script( register_event(env, "on_inventory_open", prefix + ":.inventoryopen"); funcsset.oninventoryclosed = register_event(env, "on_inventory_closed", prefix + ":.inventoryclosed"); + funcsset.onentityspawn = + register_event(env, "on_entity_spawn", prefix + ":.entityspawn"); + funcsset.onentitydespawn = + register_event(env, "on_entity_despawn", prefix + ":.entitydespawn"); +} + +void scripting::load_content_script( + const scriptenv& senv, + const std::string& prefix, + const io::path& file, + const std::string& fileName +) { + int env = *senv; + lua::pop(lua::get_main_state(), load_script(env, "content", file, fileName)); + + register_event(env, "on_scripts_loading", prefix + ":.onscriptsloading"); + register_event(env, "on_content_loaded", prefix + ":.oncontentloaded"); } void scripting::load_layout_script( diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index db27e8873..9d29528b8 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -160,6 +160,12 @@ namespace scripting { /// @brief Called on UI view close void on_ui_close(UiDocument* layout, Inventory* inventory); + /// @brief Called on Content loading + void on_scripts_loading(); + + /// @brief Called on Content loading finish + void on_content_loaded(); + /// @brief Load script associated with a Block /// @param env environment /// @param prefix pack id @@ -221,6 +227,13 @@ namespace scripting { WorldFuncsSet& funcsset ); + void load_content_script( + const scriptenv& env, + const std::string& packid, + const io::path& file, + const std::string& fileName + ); + /// @brief Load script associated with an UiDocument /// @param env environment /// @param prefix pack id diff --git a/src/logic/scripting/scripting_entities.cpp b/src/logic/scripting/scripting_entities.cpp index c068d7007..375b382a9 100644 --- a/src/logic/scripting/scripting_entities.cpp +++ b/src/logic/scripting/scripting_entities.cpp @@ -1,14 +1,18 @@ #include "scripting.hpp" +#include "debug/Logger.hpp" #include "lua/lua_engine.hpp" #include "objects/Entities.hpp" #include "objects/EntityDef.hpp" #include "objects/Entity.hpp" #include "objects/Player.hpp" #include "util/stringutil.hpp" +#include "content/Content.hpp" +#include "content/ContentPack.hpp" using namespace scripting; +static debug::Logger logger("scripting-entities"); static inline const std::string STDCOMP = "stdcomp"; [[nodiscard]] static scriptenv create_component_environment( @@ -52,6 +56,24 @@ dv::value scripting::get_component_value( return nullptr; } +static void init_component_events( + lua::State* L, const scriptenv& env, UserComponent& component +) { + lua::pushenv(L, *env); + auto& funcsset = component.funcsset; + funcsset.on_grounded = lua::hasfield(L, "on_grounded"); + funcsset.on_fall = lua::hasfield(L, "on_fall"); + funcsset.on_despawn = lua::hasfield(L, "on_despawn"); + funcsset.on_sensor_enter = lua::hasfield(L, "on_sensor_enter"); + funcsset.on_sensor_exit = lua::hasfield(L, "on_sensor_exit"); + funcsset.on_save = lua::hasfield(L, "on_save"); + funcsset.on_aim_on = lua::hasfield(L, "on_aim_on"); + funcsset.on_aim_off = lua::hasfield(L, "on_aim_off"); + funcsset.on_attacked = lua::hasfield(L, "on_attacked"); + funcsset.on_used = lua::hasfield(L, "on_used"); + lua::pop(L); +} + static void create_component( lua::State* L, int entityIdx, @@ -59,11 +81,20 @@ static void create_component( const dv::value& args, const dv::value& saved ) { + auto parentEnv = get_root_environment(); + auto prefix = ContentPack::getPrefix(component.name); + const auto& content = *scripting::content; + if (auto runtime = content.getPackRuntime(std::string(prefix))) { + parentEnv = runtime->getEnvironment(); + } else { + logger.warning() << "pack environment is not available for " + << component.name; + } lua::pushvalue(L, entityIdx); auto compenv = create_component_environment( - get_root_environment(), -1, component.name + std::move(parentEnv), -1, component.name ); - lua::get_from(L, lua::CHUNKS_TABLE, component.name, true); + lua::get_from_registry(L, lua::CHUNKS_TABLE, component.name, true); lua::pushenv(L, *compenv); if (args != nullptr) { @@ -95,19 +126,8 @@ static void create_component( lua::setfenv(L); lua::call_nothrow(L, 0, 0); - lua::pushenv(L, *compenv); - auto& funcsset = component.funcsset; - funcsset.on_grounded = lua::hasfield(L, "on_grounded"); - funcsset.on_fall = lua::hasfield(L, "on_fall"); - funcsset.on_despawn = lua::hasfield(L, "on_despawn"); - funcsset.on_sensor_enter = lua::hasfield(L, "on_sensor_enter"); - funcsset.on_sensor_exit = lua::hasfield(L, "on_sensor_exit"); - funcsset.on_save = lua::hasfield(L, "on_save"); - funcsset.on_aim_on = lua::hasfield(L, "on_aim_on"); - funcsset.on_aim_off = lua::hasfield(L, "on_aim_off"); - funcsset.on_attacked = lua::hasfield(L, "on_attacked"); - funcsset.on_used = lua::hasfield(L, "on_used"); - lua::pop(L, 2); + init_component_events(L, compenv, component); + lua::pop(L); component.env = compenv; } @@ -129,6 +149,15 @@ void scripting::on_entity_spawn( for (auto& component : components) { create_component(L, -1, *component, args, saved); } + + for (auto& [packid, pack] : content->getPacks()) { + if (pack->worldfuncsset.onentityspawn) { + lua::emit_event(L, packid + ":.entityspawn", [&](lua::State* L) { + lua::pushinteger(L, eid); + return 1; + }); + } + } } static void process_entity_callback( @@ -170,9 +199,21 @@ void scripting::on_entity_despawn(const Entity& entity) { process_entity_callback( entity, "on_despawn", &EntityFuncsSet::on_despawn, nullptr ); + auto L = lua::get_main_state(); + entityid_t uid = entity.getUID(); + + for (auto& [packid, pack] : content->getPacks()) { + if (pack->worldfuncsset.onentitydespawn) { + lua::emit_event(L, packid + ":.entitydespawn", [&](lua::State* L) { + lua::pushinteger(L, uid); + return 1; + }); + } + } + lua::get_from(L, "stdcomp", "remove_Entity", true); - lua::pushinteger(L, entity.getUID()); + lua::pushinteger(L, uid); lua::call(L, 1, 0); } diff --git a/src/main.cpp b/src/main.cpp index ea6ad550e..77175bd8a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,7 +15,9 @@ static void sigterm_handler(int signum) { int main(int argc, char** argv) { #ifdef VC_BUILD_NAME - logger.info() << "build: " << VC_BUILD_NAME; + if constexpr (VC_BUILD_NAME[0]) { + logger.info() << "build: " << VC_BUILD_NAME; + } #endif CoreParameters coreParameters; diff --git a/src/util/platform.cpp b/src/util/platform.cpp index f9ecf070d..f113641e5 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -215,7 +215,9 @@ std::filesystem::path platform::get_executable_path() { #endif } -void platform::new_engine_instance(const std::vector& args) { +void platform::new_engine_instance( + const std::vector& args, std::filesystem::path outputFile +) { auto executable = get_executable_path(); #ifdef _WIN32 @@ -250,6 +252,24 @@ void platform::new_engine_instance(const std::vector& args) { auto commandString = toWString(ss.str()); STARTUPINFOW si = { sizeof(si) }; + if (!outputFile.empty()) { + si.dwFlags |= STARTF_USESTDHANDLES; + si.hStdOutput = CreateFileW( + toWString(outputFile.u8string()).data(), + GENERIC_WRITE, + FILE_SHARE_WRITE | FILE_SHARE_READ, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr + ); + if (si.hStdOutput == INVALID_HANDLE_VALUE) { + throw std::runtime_error( + "CreateFileW failed with code: " + + std::to_string(GetLastError()) + ); + } + } PROCESS_INFORMATION pi = { 0 }; DWORD flags = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS; // | CREATE_NO_WINDOW; @@ -280,7 +300,11 @@ void platform::new_engine_instance(const std::vector& args) { for (int i = 0; i < args.size(); i++) { ss << " " << util::quote(args[i]); } - ss << " >/dev/null &"; + if (outputFile.empty()) { + ss << " >/dev/null &"; + } else { + ss << " >" << util::quote(outputFile.u8string()) << " &"; + } auto command = ss.str(); logger.info() << command; @@ -302,4 +326,4 @@ bool platform::stdin_has_data() { fds.events = POLLIN; return poll(&fds, 1, 0) == 1; #endif -} \ No newline at end of file +} diff --git a/src/util/platform.hpp b/src/util/platform.hpp index 620178fc1..54c56a0c5 100644 --- a/src/util/platform.hpp +++ b/src/util/platform.hpp @@ -18,7 +18,9 @@ namespace platform { /// @brief Get current process running executable path std::filesystem::path get_executable_path(); /// @brief Run a separate engine instance with specified arguments - void new_engine_instance(const std::vector& args); + void new_engine_instance( + const std::vector& args, std::filesystem::path outputFile + ); /// @brief Open URL in web browser bool open_url(const std::string& url); /// @brief Check if stdin has input