From a428e1f628be885d926c07b0d20b7210fdeca6ac Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 29 Jun 2026 23:54:28 +0300 Subject: [PATCH 01/63] add test human model --- res/models/human.vcm | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 res/models/human.vcm diff --git a/res/models/human.vcm b/res/models/human.vcm new file mode 100644 index 000000000..534fc047a --- /dev/null +++ b/res/models/human.vcm @@ -0,0 +1,30 @@ +@bone name 'body' { + @box from (-0.2,0,-0.125) to (0.2,0.5,0.15) + @bone name 'head' move (0,0.5,0) { + @box from (-0.2,0,-0.2) to (0.2,0.4,0.2) + } + @bone name 'leg_left' move (-0.125, 0, 0) rotate (-0,0,0) { + @box from (-0.075, -0.45, -0.125) to (0.125, 0.0, 0.15) + @bone name 'leg_left_btm' move (0, -0.45, 0) rotate (0,0,0) { + @box from (-0.075, -0.45, -0.125) to (0.125, 0.0, 0.15) + } + } + @bone name 'leg_right' move (0.125, 0.0, 0) rotate (0,0,0) { + @box from (-0.125, -0.45, -0.125) to (0.075, 0.0, 0.15) + @bone name 'leg_right_btm' move (0, -0.45, 0) rotate (0,0,0) { + @box from (-0.125, -0.45, -0.125) to (0.075, 0.0, 0.15) + } + } + @bone name 'hand_left' move (-0.325, 0.5, 0) rotate (-0,0,0) { + @box from (-0.075, -0.45, -0.125) to (0.125, 0.0, 0.15) + @bone name 'hand_left_btm' move (0, -0.45, 0) rotate (-0,0,0) { + @box from (-0.075, -0.45, -0.125) to (0.125, 0.0, 0.15) + } + } + @bone name 'hand_right' move (0.325, 0.5, 0) rotate (0,0,0) { + @box from (-0.125, -0.45, -0.125) to (0.075, 0.0, 0.15) + @bone name 'hand_right_btm' move (0, -0.45, 0) rotate (-0,0,0) { + @box from (-0.125, -0.45, -0.125) to (0.075, 0.0, 0.15) + } + } +} \ No newline at end of file From 086067cfa95dfbf8c1f4f9c0b9453f520dcdeadf Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 29 Jun 2026 23:55:52 +0300 Subject: [PATCH 02/63] add declarative skeleton animation component (WIP) --- res/scripts/components/animator.lua | 154 ++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 res/scripts/components/animator.lua diff --git a/res/scripts/components/animator.lua b/res/scripts/components/animator.lua new file mode 100644 index 000000000..2bcaadc6b --- /dev/null +++ b/res/scripts/components/animator.lua @@ -0,0 +1,154 @@ +local tsf = entity.transform +local rig = entity.skeleton +local body = entity.rigidbody + + +local body_idx = rig:index("body") + +local leg_left = rig:index("leg_left") +local leg_right = rig:index("leg_right") + +local leg_left_btm = rig:index("leg_left_btm") +local leg_right_btm = rig:index("leg_right_btm") + +local hand_left = rig:index("hand_left") +local hand_right = rig:index("hand_right") + +local hand_left_btm = rig:index("hand_left_btm") +local hand_right_btm = rig:index("hand_right_btm") + + +local tm_offset = random.random(10) +local prev_speed = 0.0 + +local CH_TRANSLATE = 1 +local CH_ROTATE = 2 + +local AX_X = 1 +local AX_Y = 2 +local AX_Z = 3 + +local function compile_track(bone_index, lines) + local env = { + mat4 = mat4, + } + table.extend(env, math) + + local code = "" + local translation = {false, false, false} + local rotation = {false, false, false} + for i, line in ipairs(lines) do + code = code .. "\n local l" .. i .. " = (" .. line.expression .. ")" + if line.channel == CH_TRANSLATE then + translation[line.axis] = i + elseif line.channel == CH_ROTATE then + rotation[line.axis] = i + end + end + + code = code .. "\n mat4.idt(dst)" + if translation[1] or translation[2] or translation[3] then + code = code .. "\n mat4.translate(dst, {" .. + (translation[1] and ("l" .. translation[1]) or '0').. ", " .. + (translation[2] and ("l" .. translation[2]) or '0').. ", " .. + (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" + end + + for axis, var in ipairs(rotation) do + if var then + code = code .. "\n mat4.rotate(dst, {" + .. ((axis == 1) and '1' or '0') .. ", " + .. ((axis == 2) and '1' or '0') .. ", " + .. ((axis == 3) and '1' or '0') .. "}, l" .. var .. ", dst)" + end + end + + local src = "return function(dst, t, m)" .. code .. "\n return dst\nend" + local generator, err = load(src, "", "bt", env) + if not generator then + error(err) + end + print(src) + return { + bone_index = bone_index, + generator = generator() + } +end + +local tracks = { + compile_track(body_idx, {{ + channel = CH_TRANSLATE, + axis = AX_Y, + expression = "sin(t * 2) * 0.1 * m" + }}), + compile_track(leg_left, {{ + channel = CH_ROTATE, + axis = AX_X, + expression = "sin(t) * 45 * m" + }}), + compile_track(leg_right, {{ + channel = CH_ROTATE, + axis = AX_X, + expression = "-sin(t) * 45 * m" + }}), + compile_track(leg_left_btm, {{ + channel = CH_ROTATE, + axis = AX_X, + expression = "(-sin(t * 2) * 45 - 45) * m" + }}), + compile_track(leg_right_btm, {{ + channel = CH_ROTATE, + axis = AX_X, + expression = "(-sin(t * 2) * 45 - 45) * m" + }}), + compile_track(hand_left, {{ + channel = CH_ROTATE, + axis = AX_X, + expression = "-sin(t) * 45 * m" + }}), + compile_track(hand_right, {{ + channel = CH_ROTATE, + axis = AX_X, + expression = "sin(t) * 45 * m" + }}), + compile_track(hand_left_btm, {{ + channel = CH_ROTATE, + axis = AX_X, + expression = "(sin(t * 2) * 45 + 45) * m" + }}), + compile_track(hand_right_btm, {{ + channel = CH_ROTATE, + axis = AX_X, + expression = "(sin(t * 2) * 45 + 45) * m" + }}), +} + +function on_render() + local tm = time.uptime() * 10 + tm_offset + local speed = vec3.length(body:get_vel()) / 7.0 + local delta = time.delta() + + prev_speed = prev_speed * (1.0 - delta * 10) + speed * delta * 10 + speed = prev_speed + + local ttm = time.precise_time() + local matrix = mat4.idt() + for i, track in ipairs(tracks) do + rig:set_matrix(track.bone_index, track.generator(matrix, tm, speed)) + end + + -- rig:set_matrix(body_idx, mat4.translate({0, math.sin(tm * 2) * 0.1 * speed, 0})) + + -- rig:set_matrix(leg_left, mat4.rotate({1, 0, 0}, (math.sin(tm) * 45) * speed)) + -- rig:set_matrix(leg_right, mat4.rotate({1, 0, 0}, (-math.sin(tm) * 45) * speed)) + + -- rig:set_matrix(leg_left_btm, mat4.rotate({1, 0, 0}, (-math.sin(tm * 2) * 45 - 45) * speed)) + -- rig:set_matrix(leg_right_btm, mat4.rotate({1, 0, 0}, (-math.sin(tm * 2) * 45 - 45) * speed)) + + -- rig:set_matrix(hand_left, mat4.rotate({1, 0, 0}, (-math.sin(tm) * 45) * speed)) + -- rig:set_matrix(hand_right, mat4.rotate({1, 0, 0}, (math.sin(tm) * 45) * speed)) + + -- rig:set_matrix(hand_left_btm, mat4.rotate({1, 0, 0}, (math.sin(tm * 2) * 45 + 45) * speed)) + -- rig:set_matrix(hand_right_btm, mat4.rotate({1, 0, 0}, (math.sin(tm * 2) * 45 + 45) * speed)) + print(math.floor((time.precise_time() - ttm) * 1e6), "mcs") +end From 7fdefe5625d72f4516eface0515da6af15808840 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 30 Jun 2026 00:40:07 +0300 Subject: [PATCH 03/63] add mat4.idt to maths_inline.lua --- res/modules/internal/maths_inline.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/res/modules/internal/maths_inline.lua b/res/modules/internal/maths_inline.lua index 36c0aecfe..ecb77df97 100644 --- a/res/modules/internal/maths_inline.lua +++ b/res/modules/internal/maths_inline.lua @@ -238,3 +238,17 @@ function vec2.mix(a, b, t, dest) } end end + +function mat4.idt(dst) + if dst then + for i=1,16 do + dst[i] = (i % 5 == 1) and 1 or 0 + end + end + return { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + } +end From 8ce0218c4c9181c8389b50634c70597b5fa7fcdd Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 30 Jun 2026 01:29:12 +0300 Subject: [PATCH 04/63] add mat4.rotate to maths_inline.lua --- res/modules/internal/maths_inline.lua | 77 +++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/res/modules/internal/maths_inline.lua b/res/modules/internal/maths_inline.lua index ecb77df97..12b3243ea 100644 --- a/res/modules/internal/maths_inline.lua +++ b/res/modules/internal/maths_inline.lua @@ -1,3 +1,8 @@ +local sin = math.sin +local cos = math.cos +local sqrt = math.sqrt +local rad = math.rad + -- =================================================== -- -- ====================== vec3 ======================= -- -- =================================================== -- @@ -252,3 +257,75 @@ function mat4.idt(dst) 0, 0, 0, 1 } end + +function mat4.rotate(...) + local argc = select("#", ...) + + local matrix, axis, angle, dst + + if argc == 2 then + axis, angle = ... + matrix = mat4.idt() + elseif argc == 3 then + matrix, axis, angle = ... + elseif argc == 4 then + matrix, axis, angle, dst = ... + else + error("invalid arguments number (2, 3 or 4 expected)") + end + + angle = rad(angle) + + local x = axis[1] + local y = axis[2] + local z = axis[3] + + local len = sqrt(x*x + y*y + z*z) + if len == 0 then + error("rotation axis has zero length") + end + + x = x / len + y = y / len + z = z / len + + local c = cos(angle) + local s = sin(angle) + local t = 1 - c + + local r00 = t*x*x + c + local r01 = t*x*y - s*z + local r02 = t*x*z + s*y + + local r10 = t*x*y + s*z + local r11 = t*y*y + c + local r12 = t*y*z - s*x + + local r20 = t*x*z - s*y + local r21 = t*y*z + s*x + local r22 = t*z*z + c + + local out = dst or {} + + out[1] = matrix[1]*r00 + matrix[5]*r10 + matrix[9]*r20 + out[2] = matrix[2]*r00 + matrix[6]*r10 + matrix[10]*r20 + out[3] = matrix[3]*r00 + matrix[7]*r10 + matrix[11]*r20 + out[4] = matrix[4]*r00 + matrix[8]*r10 + matrix[12]*r20 + + out[5] = matrix[1]*r01 + matrix[5]*r11 + matrix[9]*r21 + out[6] = matrix[2]*r01 + matrix[6]*r11 + matrix[10]*r21 + out[7] = matrix[3]*r01 + matrix[7]*r11 + matrix[11]*r21 + out[8] = matrix[4]*r01 + matrix[8]*r11 + matrix[12]*r21 + + out[9] = matrix[1]*r02 + matrix[5]*r12 + matrix[9]*r22 + out[10] = matrix[2]*r02 + matrix[6]*r12 + matrix[10]*r22 + out[11] = matrix[3]*r02 + matrix[7]*r12 + matrix[11]*r22 + out[12] = matrix[4]*r02 + matrix[8]*r12 + matrix[12]*r22 + + out[13] = matrix[13] + out[14] = matrix[14] + out[15] = matrix[15] + out[16] = matrix[16] + + return out +end From b2b889bd7c4bb21d7cdf28441dc9edb32861bb65 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 30 Jun 2026 01:30:15 +0300 Subject: [PATCH 05/63] feat: codegen whole track to one function --- res/scripts/components/animator.lua | 120 +++++++++++++++++++--------- 1 file changed, 81 insertions(+), 39 deletions(-) diff --git a/res/scripts/components/animator.lua b/res/scripts/components/animator.lua index 2bcaadc6b..6531fe784 100644 --- a/res/scripts/components/animator.lua +++ b/res/scripts/components/animator.lua @@ -28,17 +28,38 @@ local AX_X = 1 local AX_Y = 2 local AX_Z = 3 -local function compile_track(bone_index, lines) - local env = { - mat4 = mat4, - } - table.extend(env, math) +local patterns = { + {name="sint", pattern="sin(t)"}, + {name="sint2", pattern="sin(t * 2)"}, +} +local function process_expression(src, memoised) + for i, pattern in ipairs(patterns) do + local pattern_safe = string.pattern_safe(pattern.pattern) + if src:find(pattern_safe) then + memoised[pattern.name] = pattern.pattern + src = src:gsub(pattern_safe, pattern.name) + end + end + return src +end + +local env = { + mat4 = mat4, + X = {1, 0, 0}, + Y = {0, 1, 0}, + Z = {0, 0, 1}, + blank = core.blank, + DST = mat4.idt(), +} +table.extend(env, math) + +local function codegen_track(lines, memoised) local code = "" local translation = {false, false, false} local rotation = {false, false, false} for i, line in ipairs(lines) do - code = code .. "\n local l" .. i .. " = (" .. line.expression .. ")" + code = code .. "\n local l" .. i .. " = (" .. process_expression(line.expression, memoised) .. ")" if line.channel == CH_TRANSLATE then translation[line.axis] = i elseif line.channel == CH_ROTATE then @@ -54,75 +75,95 @@ local function compile_track(bone_index, lines) (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" end + local axis_names = {"X", "Y", "Z"} for axis, var in ipairs(rotation) do if var then - code = code .. "\n mat4.rotate(dst, {" - .. ((axis == 1) and '1' or '0') .. ", " - .. ((axis == 2) and '1' or '0') .. ", " - .. ((axis == 3) and '1' or '0') .. "}, l" .. var .. ", dst)" + code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. ", l" .. var .. ", dst)" end end - local src = "return function(dst, t, m)" .. code .. "\n return dst\nend" + return code +end + +local function compile_track(linesets) + local code = "" + local memoised = {} + + for i, lineset in ipairs(linesets) do + local lineset_code = codegen_track(lineset.lines, memoised) + code = code .. "\n do" .. lineset_code .. "\n end\n" .. + " rig:set_matrix(" .. lineset.bone_index .. ", dst)\n" + -- " blank(" .. lineset.bone_index .. ", dst)\n" + end + + local memoised_code = "" + for name, expression in pairs(memoised) do + memoised_code = memoised_code .. "\n local " .. name .. " = " .. expression + end + + if #memoised_code > 0 then + code = memoised_code .. "\n" .. code + end + + local src = "return function(rig, t, m)\n local dst = DST\n" .. code .. "\nend" + print(src) local generator, err = load(src, "", "bt", env) if not generator then error(err) end - print(src) - return { - bone_index = bone_index, - generator = generator() - } + return generator() end -local tracks = { - compile_track(body_idx, {{ +local linesets = { + {bone_index=body_idx, lines={{ channel = CH_TRANSLATE, axis = AX_Y, expression = "sin(t * 2) * 0.1 * m" - }}), - compile_track(leg_left, {{ + }}}, + {bone_index=leg_left, lines={{ channel = CH_ROTATE, axis = AX_X, expression = "sin(t) * 45 * m" - }}), - compile_track(leg_right, {{ + }}}, + {bone_index=leg_right, lines={{ channel = CH_ROTATE, axis = AX_X, expression = "-sin(t) * 45 * m" - }}), - compile_track(leg_left_btm, {{ + }}}, + {bone_index=leg_left_btm, lines={{ channel = CH_ROTATE, axis = AX_X, expression = "(-sin(t * 2) * 45 - 45) * m" - }}), - compile_track(leg_right_btm, {{ + }}}, + {bone_index=leg_right_btm, lines={{ channel = CH_ROTATE, axis = AX_X, expression = "(-sin(t * 2) * 45 - 45) * m" - }}), - compile_track(hand_left, {{ + }}}, + {bone_index=hand_left, lines={{ channel = CH_ROTATE, axis = AX_X, expression = "-sin(t) * 45 * m" - }}), - compile_track(hand_right, {{ + }}}, + {bone_index=hand_right, lines={{ channel = CH_ROTATE, axis = AX_X, expression = "sin(t) * 45 * m" - }}), - compile_track(hand_left_btm, {{ + }}}, + {bone_index=hand_left_btm, lines={{ channel = CH_ROTATE, axis = AX_X, expression = "(sin(t * 2) * 45 + 45) * m" - }}), - compile_track(hand_right_btm, {{ + }}}, + {bone_index=hand_right_btm, lines={{ channel = CH_ROTATE, axis = AX_X, expression = "(sin(t * 2) * 45 + 45) * m" - }}), + }}}, } +local track = compile_track(linesets) + function on_render() local tm = time.uptime() * 10 + tm_offset local speed = vec3.length(body:get_vel()) / 7.0 @@ -132,10 +173,11 @@ function on_render() speed = prev_speed local ttm = time.precise_time() - local matrix = mat4.idt() - for i, track in ipairs(tracks) do - rig:set_matrix(track.bone_index, track.generator(matrix, tm, speed)) - end + -- local matrix = mat4.idt() + track(rig, tm, speed) + -- for i, track in ipairs(tracks) do + -- rig:set_matrix(track.bone_index, track.generator(matrix, tm, speed)) + -- end -- rig:set_matrix(body_idx, mat4.translate({0, math.sin(tm * 2) * 0.1 * speed, 0})) From 9d3a60328f7a44e0361a31e2e8c5b9079282810b Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 30 Jun 2026 22:53:22 +0300 Subject: [PATCH 06/63] fix dead skeleton model references --- src/logic/scripting/lua/libs/lib__skeleton.cpp | 6 +++--- src/objects/rigging.cpp | 12 +++++++----- src/objects/rigging.hpp | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/logic/scripting/lua/libs/lib__skeleton.cpp b/src/logic/scripting/lua/libs/lib__skeleton.cpp index 2fb5d4fa0..f1f058b23 100644 --- a/src/logic/scripting/lua/libs/lib__skeleton.cpp +++ b/src/logic/scripting/lua/libs/lib__skeleton.cpp @@ -42,7 +42,7 @@ static int l_get_model(lua::State* L) { auto& rigConfig = *skeleton->config; auto index = index_range_check(*skeleton, lua::tointeger(L, 2)); const auto& modelOverride = skeleton->modelOverrides[index]; - if (!modelOverride.model) { + if (!modelOverride.model.expired()) { return lua::pushstring(L, modelOverride.name); } return lua::pushstring(L, rigConfig.getBones()[index]->model.name); @@ -55,9 +55,9 @@ static int l_set_model(lua::State* L) { auto index = index_range_check(*skeleton, lua::tointeger(L, 2)); auto& modelOverride = skeleton->modelOverrides[index]; if (lua::isnoneornil(L, 3)) { - modelOverride = {"", nullptr, true}; + modelOverride = {"", {}, true}; } else { - modelOverride = {lua::require_string(L, 3), nullptr, true}; + modelOverride = {lua::require_string(L, 3), {}, true}; } } return 0; diff --git a/src/objects/rigging.cpp b/src/objects/rigging.cpp index f686bfbcf..757ea090c 100644 --- a/src/objects/rigging.cpp +++ b/src/objects/rigging.cpp @@ -13,7 +13,7 @@ using namespace rigging; void ModelReference::refresh(const Assets& assets) { if (updateFlag) { - model = assets.get(name); + model = assets.getShared(name); updateFlag = false; } } @@ -29,14 +29,14 @@ Bone::Bone( name(std::move(name)), bones(std::move(bones)), offset(offset), - model({model, nullptr, true}) { + model({model, {}, true}) { } void Bone::setModel(const std::string& name) { if (model.name == name) { return; } - model = {name, nullptr, true}; + model = {name, {}, true}; } Skeleton::Skeleton(std::shared_ptr config) @@ -184,12 +184,14 @@ void SkeletonConfig::render( continue; } node->model.refresh(assets); - auto model = node->model.model; + auto model = node->model.model.lock().get(); // TODO: cache model pointer during frame auto& modelOverride = skeleton.modelOverrides.at(i); if (modelOverride.updateFlag) { modelOverride.refresh(assets); } - model = modelOverride.model ? modelOverride.model : model; + if (auto foundOverride = modelOverride.model.lock()) { + model = foundOverride.get(); + } if (model) { batch.draw( skeleton.calculated.matrices[i], diff --git a/src/objects/rigging.hpp b/src/objects/rigging.hpp index 9e7fc75f6..82066e132 100644 --- a/src/objects/rigging.hpp +++ b/src/objects/rigging.hpp @@ -33,8 +33,8 @@ namespace rigging { struct ModelReference { std::string name; - model::Model* model; - bool updateFlag; + std::weak_ptr model; + bool updateFlag = false; void refresh(const Assets& assets); }; From 465b298a127168ce6606d43d4968382e2e0d268e Mon Sep 17 00:00:00 2001 From: Onran Date: Wed, 1 Jul 2026 20:34:35 +0900 Subject: [PATCH 07/63] fix Skeleton::setConfig --- src/objects/rigging.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/objects/rigging.cpp b/src/objects/rigging.cpp index 757ea090c..075cd8680 100644 --- a/src/objects/rigging.cpp +++ b/src/objects/rigging.cpp @@ -88,12 +88,22 @@ void Skeleton::deserialize(const dv::value& root) { void Skeleton::setConfig(std::shared_ptr rigConfig) { config = std::move(rigConfig); + + const auto& bones = config->getBones(); + pose.matrices.resize( - config->getBones().size(), glm::mat4(1.0f) + bones.size(), glm::mat4(1.0f) ); calculated.matrices.resize( - config->getBones().size(), glm::mat4(1.0f) + bones.size(), glm::mat4(1.0f) ); + + modelOverrides.resize(bones.size()); + flags.resize(bones.size()); + + for (size_t i = 0; i < bones.size(); i++) { + flags[i].visible = true; + } } static void get_all_nodes(std::vector& nodes, Bone* node) { From 45c78609b8c5ae85a0f821f31cce9a19b20d3cc9 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 5 Jul 2026 13:32:48 +0300 Subject: [PATCH 08/63] add curves: constant, linear, bezier --- res/modules/animation.lua | 53 ++++++++++++ res/scripts/components/animator.lua | 130 ++++++++++++++++++++++++++-- 2 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 res/modules/animation.lua diff --git a/res/modules/animation.lua b/res/modules/animation.lua new file mode 100644 index 000000000..0da781692 --- /dev/null +++ b/res/modules/animation.lua @@ -0,0 +1,53 @@ +local this = {} + +local function bezier(a, b, c, d, u) + local s = 1 - u + return s * s * s * a + + 3 * s * s * u * b + + 3 * s * u * u * c + + u * u * u * d +end + +local function bezier_derivative(a, b, c, d, u) + local s = 1 - u + return + 3 * s * s * (b - a) + + 6 * s * u * (c - b) + + 3 * u * u * (d - c) +end + +function this.bezier_interpolation(k0, k1, t) + local frame = k0.frame + t * (k1.frame - k0.frame) + + local u = t + + for i=1,8 do + local x = bezier( + k0.frame, + k0.rx, + k1.lx, + k1.frame, + u + ) + + local dx = bezier_derivative( + k0.frame, + k0.rx, + k1.lx, + k1.frame, + u + ) + if math.abs(dx) < 1e-8 then + break + end + + u = u - (x - frame) / dx + + if u < 0 then u = 0 end + if u > 1 then u = 1 end + end + + return bezier(k0.value, k0.ry, k1.ly, k1.value, u) +end + +return this diff --git a/res/scripts/components/animator.lua b/res/scripts/components/animator.lua index 6531fe784..9b5c15022 100644 --- a/res/scripts/components/animator.lua +++ b/res/scripts/components/animator.lua @@ -1,8 +1,10 @@ +local animation_util = require "core:animation" + local tsf = entity.transform local rig = entity.skeleton local body = entity.rigidbody - +local head_idx = rig:index("head") local body_idx = rig:index("body") local leg_left = rig:index("leg_left") @@ -28,6 +30,10 @@ local AX_X = 1 local AX_Y = 2 local AX_Z = 3 +local INT_CONST = 1 +local INT_LINEAR = 2 +local INT_BEZIER = 3 + local patterns = { {name="sint", pattern="sin(t)"}, {name="sint2", pattern="sin(t * 2)"}, @@ -44,22 +50,64 @@ local function process_expression(src, memoised) return src end +local function key_neighbors(keys, frame) + local left = 1 + local right = #keys + + while left <= right do + local mid = math.floor((left + right) / 2) + + if keys[mid].frame < frame then + left = mid + 1 + elseif keys[mid].frame > frame then + right = mid - 1 + else + return mid, mid + end + end + if left > #keys then + left = #keys + end + return right, left +end + local env = { mat4 = mat4, X = {1, 0, 0}, Y = {0, 1, 0}, Z = {0, 0, 1}, - blank = core.blank, DST = mat4.idt(), + value_at = function(keys, frame, interp) + local left, right = key_neighbors(keys, frame) + if left == right then + return keys[left].value + end + left = keys[left] + if interp == INT_CONST then + return left.value + end + right = keys[right] + local t = (frame - left.frame) / (right.frame - left.frame) + if interp == INT_BEZIER then + return animation_util.bezier_interpolation(left, right, t) + end + return left.value * (1.0 - t) + right.value * t + end, } table.extend(env, math) -local function codegen_track(lines, memoised) +local function codegen_track(lines, memoised, keysets) local code = "" local translation = {false, false, false} local rotation = {false, false, false} for i, line in ipairs(lines) do - code = code .. "\n local l" .. i .. " = (" .. process_expression(line.expression, memoised) .. ")" + if line.expression then + code = code .. "\n local l" .. i .. " = (" .. process_expression(line.expression, memoised) .. ")" + elseif line.keys then + keysets[i] = line.keys + code = code .. "\n local l" .. i .. " = value_at(keysets["..i.."], t * 3 % 60, " .. line.interp .. ")" + end + if line.channel == CH_TRANSLATE then translation[line.axis] = i elseif line.channel == CH_ROTATE then @@ -88,9 +136,10 @@ end local function compile_track(linesets) local code = "" local memoised = {} + local keysets = {} for i, lineset in ipairs(linesets) do - local lineset_code = codegen_track(lineset.lines, memoised) + local lineset_code = codegen_track(lineset.lines, memoised, keysets) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " rig:set_matrix(" .. lineset.bone_index .. ", dst)\n" -- " blank(" .. lineset.bone_index .. ", dst)\n" @@ -107,7 +156,7 @@ local function compile_track(linesets) local src = "return function(rig, t, m)\n local dst = DST\n" .. code .. "\nend" print(src) - local generator, err = load(src, "", "bt", env) + local generator, err = load(src, "", "bt", table.extend({keysets = keysets}, env)) if not generator then error(err) end @@ -160,6 +209,71 @@ local linesets = { axis = AX_X, expression = "(sin(t * 2) * 45 + 45) * m" }}}, + + {bone_index=head_idx, lines={{ + channel = CH_TRANSLATE, + axis = AX_X, + interp = INT_BEZIER, + keys = { + { + frame = 0, + value = 2.37319803237915, + lx = -18.666540145874, + ly = 2.3008770942688, + rx = 57.5497436523438, + ry = 2.58115172386169, + }, { + frame = 60, + value = -2.95866346359253, + lx = 38.4764099121094, + ly = 9.56476593017578, + rx = 76.9985656738281, + ry = -12.8492240905762, + } + } + },{ + channel = CH_TRANSLATE, + axis = AX_Z, + interp = INT_BEZIER, + keys = { + { + frame = 0, + value = -0.65947163105011, + lx = -18.6666660308838, + ly = -0.65947163105011, + rx = 20.6666660308838, + ry = -0.65947163105011, + }, { + frame = 60, + value = 1.03530943393707, + lx = 38.4764099121094, + ly = 13.5587406158447, + rx = 76.9985809326172, + ry = -8.85525798797607, + } + } + },{ + channel = CH_TRANSLATE, + axis = AX_Y, + interp = INT_BEZIER, + keys = { + { + frame = 0, + value = 0.697231292724609, + lx = -18.6666660308838, + ly = 0.697231292724609, + rx = 20.6666660308838, + ry = 0.697231292724609, + }, { + frame = 60, + value = -0.432764053344727, + lx = 38.4764099121094, + ly = 12.0906667709351, + rx = 76.9985809326172, + ry = -10.3233318328857, + } + } + }}}, } local track = compile_track(linesets) @@ -172,7 +286,7 @@ function on_render() prev_speed = prev_speed * (1.0 - delta * 10) + speed * delta * 10 speed = prev_speed - local ttm = time.precise_time() + -- local ttm = time.precise_time() -- local matrix = mat4.idt() track(rig, tm, speed) -- for i, track in ipairs(tracks) do @@ -192,5 +306,5 @@ function on_render() -- rig:set_matrix(hand_left_btm, mat4.rotate({1, 0, 0}, (math.sin(tm * 2) * 45 + 45) * speed)) -- rig:set_matrix(hand_right_btm, mat4.rotate({1, 0, 0}, (math.sin(tm * 2) * 45 + 45) * speed)) - print(math.floor((time.precise_time() - ttm) * 1e6), "mcs") + -- print(math.floor((time.precise_time() - ttm) * 1e6), "mcs") end From 87310dd38f848ca059b627fc0b6d1d2503d05071 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 5 Jul 2026 19:00:49 +0300 Subject: [PATCH 09/63] fix maths_inline version of mat4.rotate --- res/modules/internal/maths_inline.lua | 61 ++++++++++++++------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/res/modules/internal/maths_inline.lua b/res/modules/internal/maths_inline.lua index 12b3243ea..e02fa96fe 100644 --- a/res/modules/internal/maths_inline.lua +++ b/res/modules/internal/maths_inline.lua @@ -276,11 +276,9 @@ function mat4.rotate(...) angle = rad(angle) - local x = axis[1] - local y = axis[2] - local z = axis[3] + local x, y, z = axis[1], axis[2], axis[3] - local len = sqrt(x*x + y*y + z*z) + local len = sqrt(x * x + y * y + z * z) if len == 0 then error("rotation axis has zero length") end @@ -293,39 +291,44 @@ function mat4.rotate(...) local s = sin(angle) local t = 1 - c - local r00 = t*x*x + c - local r01 = t*x*y - s*z - local r02 = t*x*z + s*y + local r00 = t * x * x + c + local r01 = t * x * y - s * z + local r02 = t * x * z + s * y - local r10 = t*x*y + s*z - local r11 = t*y*y + c - local r12 = t*y*z - s*x + local r10 = t * x * y + s * z + local r11 = t * y * y + c + local r12 = t * y * z - s * x - local r20 = t*x*z - s*y - local r21 = t*y*z + s*x - local r22 = t*z*z + c + local r20 = t * x * z - s * y + local r21 = t * y * z + s * x + local r22 = t * z * z + c local out = dst or {} - out[1] = matrix[1]*r00 + matrix[5]*r10 + matrix[9]*r20 - out[2] = matrix[2]*r00 + matrix[6]*r10 + matrix[10]*r20 - out[3] = matrix[3]*r00 + matrix[7]*r10 + matrix[11]*r20 - out[4] = matrix[4]*r00 + matrix[8]*r10 + matrix[12]*r20 + local m1, m2, m3, m4 = matrix[1], matrix[2], matrix[3], matrix[4] + local m5, m6, m7, m8 = matrix[5], matrix[6], matrix[7], matrix[8] + local m9, m10, m11, m12 = matrix[9], matrix[10], matrix[11], matrix[12] + local m13, m14, m15, m16 = matrix[13], matrix[14], matrix[15], matrix[16] - out[5] = matrix[1]*r01 + matrix[5]*r11 + matrix[9]*r21 - out[6] = matrix[2]*r01 + matrix[6]*r11 + matrix[10]*r21 - out[7] = matrix[3]*r01 + matrix[7]*r11 + matrix[11]*r21 - out[8] = matrix[4]*r01 + matrix[8]*r11 + matrix[12]*r21 + out[1] = m1 * r00 + m5 * r10 + m9 * r20 + out[2] = m2 * r00 + m6 * r10 + m10 * r20 + out[3] = m3 * r00 + m7 * r10 + m11 * r20 + out[4] = m4 * r00 + m8 * r10 + m12 * r20 - out[9] = matrix[1]*r02 + matrix[5]*r12 + matrix[9]*r22 - out[10] = matrix[2]*r02 + matrix[6]*r12 + matrix[10]*r22 - out[11] = matrix[3]*r02 + matrix[7]*r12 + matrix[11]*r22 - out[12] = matrix[4]*r02 + matrix[8]*r12 + matrix[12]*r22 + out[5] = m1 * r01 + m5 * r11 + m9 * r21 + out[6] = m2 * r01 + m6 * r11 + m10 * r21 + out[7] = m3 * r01 + m7 * r11 + m11 * r21 + out[8] = m4 * r01 + m8 * r11 + m12 * r21 - out[13] = matrix[13] - out[14] = matrix[14] - out[15] = matrix[15] - out[16] = matrix[16] + out[9] = m1 * r02 + m5 * r12 + m9 * r22 + out[10] = m2 * r02 + m6 * r12 + m10 * r22 + out[11] = m3 * r02 + m7 * r12 + m11 * r22 + out[12] = m4 * r02 + m8 * r12 + m12 * r22 + + out[13] = m13 + out[14] = m14 + out[15] = m15 + out[16] = m16 return out end From 2bd587451cc866a7016521c57489965e74aca7fc Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 5 Jul 2026 22:12:28 +0300 Subject: [PATCH 10/63] move common functions to core:animation module --- res/modules/animation.lua | 146 ++++++++++++- res/scripts/components/animator.lua | 328 ++++------------------------ res/scripts/components/mob.lua | 33 ++- 3 files changed, 197 insertions(+), 310 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 0da781692..7dea2a4ff 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -1,5 +1,16 @@ local this = {} +local CH_TRANSLATE = 1 +local CH_ROTATE = 2 + +local AX_X = 1 +local AX_Y = 2 +local AX_Z = 3 + +local INT_CONST = 1 +local INT_LINEAR = 2 +local INT_BEZIER = 3 + local function bezier(a, b, c, d, u) local s = 1 - u return s * s * s * a + @@ -16,9 +27,8 @@ local function bezier_derivative(a, b, c, d, u) 3 * u * u * (d - c) end -function this.bezier_interpolation(k0, k1, t) +local function bezier_interpolation(k0, k1, t) local frame = k0.frame + t * (k1.frame - k0.frame) - local u = t for i=1,8 do @@ -29,7 +39,6 @@ function this.bezier_interpolation(k0, k1, t) k1.frame, u ) - local dx = bezier_derivative( k0.frame, k0.rx, @@ -50,4 +59,135 @@ function this.bezier_interpolation(k0, k1, t) return bezier(k0.value, k0.ry, k1.ly, k1.value, u) end +local patterns = { + {name="sint", pattern="sin(t)"}, + {name="sint2", pattern="sin(t * 2)"}, +} + +local function process_expression(src, memoised) + for i, pattern in ipairs(patterns) do + local pattern_safe = string.pattern_safe(pattern.pattern) + if src:find(pattern_safe) then + memoised[pattern.name] = pattern.pattern + src = src:gsub(pattern_safe, pattern.name) + end + end + return src +end + +local function key_neighbors(keys, frame) + local left = 1 + local right = #keys + + while left <= right do + local mid = math.floor((left + right) / 2) + + if keys[mid].frame < frame then + left = mid + 1 + elseif keys[mid].frame > frame then + right = mid - 1 + else + return mid, mid + end + end + if left > #keys then + left = #keys + end + return right, left +end + +local env = { + mat4 = mat4, + X = {1, 0, 0}, + Y = {0, 1, 0}, + Z = {0, 0, 1}, + DST = mat4.idt(), + value_at = function(keys, frame, interp) + local left, right = key_neighbors(keys, frame) + if left == right then + return keys[left].value + end + left = keys[left] + if interp == INT_CONST then + return left.value + end + right = keys[right] + local t = (frame - left.frame) / (right.frame - left.frame) + if interp == INT_BEZIER then + return bezier_interpolation(left, right, t) + end + return left.value * (1.0 - t) + right.value * t + end, +} +table.extend(env, math) + +local function codegen_track(lines, memoised, keysets) + local code = "" + local translation = {false, false, false} + local rotation = {false, false, false} + for i, line in ipairs(lines) do + if line.expression then + code = code .. "\n local l" .. i .. " = (" .. + process_expression(line.expression, memoised) .. ")" + elseif line.keys then + keysets[i] = line.keys + code = code .. string.format( + "\n local l%d = value_at(keysets[%d], t * 3 %% 60, %s)", + i, i, line.interp) + end + + if line.channel == CH_TRANSLATE then + translation[line.axis] = i + elseif line.channel == CH_ROTATE then + rotation[line.axis] = i + end + end + + code = code .. "\n mat4.idt(dst)" + if translation[1] or translation[2] or translation[3] then + code = code .. "\n mat4.translate(dst, {" .. + (translation[1] and ("l" .. translation[1]) or '0').. ", " .. + (translation[2] and ("l" .. translation[2]) or '0').. ", " .. + (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" + end + + local axis_names = {"X", "Y", "Z"} + for axis, var in ipairs(rotation) do + if var then + code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. + ",l" .. var .. ", dst)" + end + end + + return code +end + +function this.compile_track(linesets) + local code = "" + local memoised = {} + local keysets = {} + + for i, lineset in ipairs(linesets) do + local lineset_code = codegen_track(lineset.lines, memoised, keysets) + code = code .. "\n do" .. lineset_code .. "\n end\n" .. + " rig:set_matrix(" .. lineset.bone_index .. ", dst)\n" + end + + local memoised_code = "" + for name, expression in pairs(memoised) do + memoised_code = memoised_code .. "\n local " .. name .. " = " .. expression + end + + if #memoised_code > 0 then + code = memoised_code .. "\n" .. code + end + + local src = "return function(rig, t, m)\n local dst = DST\n" .. code .. "\nend" + local generator, err = load(src, "", "bt", table.extend({keysets = keysets}, env)) + if not generator then + error(err) + end + return generator() +end + return this diff --git a/res/scripts/components/animator.lua b/res/scripts/components/animator.lua index 9b5c15022..0c7710f81 100644 --- a/res/scripts/components/animator.lua +++ b/res/scripts/components/animator.lua @@ -1,310 +1,60 @@ -local animation_util = require "core:animation" - local tsf = entity.transform local rig = entity.skeleton local body = entity.rigidbody -local head_idx = rig:index("head") -local body_idx = rig:index("body") +local states = {} -local leg_left = rig:index("leg_left") -local leg_right = rig:index("leg_right") - -local leg_left_btm = rig:index("leg_left_btm") -local leg_right_btm = rig:index("leg_right_btm") - -local hand_left = rig:index("hand_left") -local hand_right = rig:index("hand_right") - -local hand_left_btm = rig:index("hand_left_btm") -local hand_right_btm = rig:index("hand_right_btm") - - -local tm_offset = random.random(10) -local prev_speed = 0.0 - -local CH_TRANSLATE = 1 -local CH_ROTATE = 2 - -local AX_X = 1 -local AX_Y = 2 -local AX_Z = 3 - -local INT_CONST = 1 -local INT_LINEAR = 2 -local INT_BEZIER = 3 - -local patterns = { - {name="sint", pattern="sin(t)"}, - {name="sint2", pattern="sin(t * 2)"}, -} - -local function process_expression(src, memoised) - for i, pattern in ipairs(patterns) do - local pattern_safe = string.pattern_safe(pattern.pattern) - if src:find(pattern_safe) then - memoised[pattern.name] = pattern.pattern - src = src:gsub(pattern_safe, pattern.name) - end - end - return src +function create_state(name, track, max_intencity) + states[name] = { + track = track, + timer = 0.0, + fade_time = 0.0, + fade_timer = 0.0, + fade_in = false, + max_intencity = max_intencity, + } end -local function key_neighbors(keys, frame) - local left = 1 - local right = #keys +local current_state = nil - while left <= right do - local mid = math.floor((left + right) / 2) - - if keys[mid].frame < frame then - left = mid + 1 - elseif keys[mid].frame > frame then - right = mid - 1 - else - return mid, mid - end +function set_state(name, fade_time) + local state = states[name] + if state == current_state then + return end - if left > #keys then - left = #keys + if current_state then + current_state.fade_in = false + current_state.fade_time = fade_time + current_state.fade_timer = 0.0 + end + current_state = state + if state then + state.timer = 0.0 + state.fade_in = true + state.fade_time = fade_time + state.fade_timer = 0.0 end - return right, left end -local env = { - mat4 = mat4, - X = {1, 0, 0}, - Y = {0, 1, 0}, - Z = {0, 0, 1}, - DST = mat4.idt(), - value_at = function(keys, frame, interp) - local left, right = key_neighbors(keys, frame) - if left == right then - return keys[left].value - end - left = keys[left] - if interp == INT_CONST then - return left.value - end - right = keys[right] - local t = (frame - left.frame) / (right.frame - left.frame) - if interp == INT_BEZIER then - return animation_util.bezier_interpolation(left, right, t) - end - return left.value * (1.0 - t) + right.value * t - end, -} -table.extend(env, math) +local function update(delta) + for key, state in pairs(states) do + state.timer = state.timer + delta -local function codegen_track(lines, memoised, keysets) - local code = "" - local translation = {false, false, false} - local rotation = {false, false, false} - for i, line in ipairs(lines) do - if line.expression then - code = code .. "\n local l" .. i .. " = (" .. process_expression(line.expression, memoised) .. ")" - elseif line.keys then - keysets[i] = line.keys - code = code .. "\n local l" .. i .. " = value_at(keysets["..i.."], t * 3 % 60, " .. line.interp .. ")" + local intensity = state.fade_in and 1.0 or 0.0 + if state.fade_timer < state.fade_time then + state.fade_timer = state.fade_timer + delta + intensity = math.min(1.0, state.fade_timer / state.fade_time) + if not state.fade_in then + intensity = 1.0 - intensity + end end - if line.channel == CH_TRANSLATE then - translation[line.axis] = i - elseif line.channel == CH_ROTATE then - rotation[line.axis] = i + if state.track and intensity > 0.0 then + state.track(rig, state.timer, intensity * state.max_intencity) end end - - code = code .. "\n mat4.idt(dst)" - if translation[1] or translation[2] or translation[3] then - code = code .. "\n mat4.translate(dst, {" .. - (translation[1] and ("l" .. translation[1]) or '0').. ", " .. - (translation[2] and ("l" .. translation[2]) or '0').. ", " .. - (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" - end - - local axis_names = {"X", "Y", "Z"} - for axis, var in ipairs(rotation) do - if var then - code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. ", l" .. var .. ", dst)" - end - end - - return code end -local function compile_track(linesets) - local code = "" - local memoised = {} - local keysets = {} - - for i, lineset in ipairs(linesets) do - local lineset_code = codegen_track(lineset.lines, memoised, keysets) - code = code .. "\n do" .. lineset_code .. "\n end\n" .. - " rig:set_matrix(" .. lineset.bone_index .. ", dst)\n" - -- " blank(" .. lineset.bone_index .. ", dst)\n" - end - - local memoised_code = "" - for name, expression in pairs(memoised) do - memoised_code = memoised_code .. "\n local " .. name .. " = " .. expression - end - - if #memoised_code > 0 then - code = memoised_code .. "\n" .. code - end - - local src = "return function(rig, t, m)\n local dst = DST\n" .. code .. "\nend" - print(src) - local generator, err = load(src, "", "bt", table.extend({keysets = keysets}, env)) - if not generator then - error(err) - end - return generator() -end - -local linesets = { - {bone_index=body_idx, lines={{ - channel = CH_TRANSLATE, - axis = AX_Y, - expression = "sin(t * 2) * 0.1 * m" - }}}, - {bone_index=leg_left, lines={{ - channel = CH_ROTATE, - axis = AX_X, - expression = "sin(t) * 45 * m" - }}}, - {bone_index=leg_right, lines={{ - channel = CH_ROTATE, - axis = AX_X, - expression = "-sin(t) * 45 * m" - }}}, - {bone_index=leg_left_btm, lines={{ - channel = CH_ROTATE, - axis = AX_X, - expression = "(-sin(t * 2) * 45 - 45) * m" - }}}, - {bone_index=leg_right_btm, lines={{ - channel = CH_ROTATE, - axis = AX_X, - expression = "(-sin(t * 2) * 45 - 45) * m" - }}}, - {bone_index=hand_left, lines={{ - channel = CH_ROTATE, - axis = AX_X, - expression = "-sin(t) * 45 * m" - }}}, - {bone_index=hand_right, lines={{ - channel = CH_ROTATE, - axis = AX_X, - expression = "sin(t) * 45 * m" - }}}, - {bone_index=hand_left_btm, lines={{ - channel = CH_ROTATE, - axis = AX_X, - expression = "(sin(t * 2) * 45 + 45) * m" - }}}, - {bone_index=hand_right_btm, lines={{ - channel = CH_ROTATE, - axis = AX_X, - expression = "(sin(t * 2) * 45 + 45) * m" - }}}, - - {bone_index=head_idx, lines={{ - channel = CH_TRANSLATE, - axis = AX_X, - interp = INT_BEZIER, - keys = { - { - frame = 0, - value = 2.37319803237915, - lx = -18.666540145874, - ly = 2.3008770942688, - rx = 57.5497436523438, - ry = 2.58115172386169, - }, { - frame = 60, - value = -2.95866346359253, - lx = 38.4764099121094, - ly = 9.56476593017578, - rx = 76.9985656738281, - ry = -12.8492240905762, - } - } - },{ - channel = CH_TRANSLATE, - axis = AX_Z, - interp = INT_BEZIER, - keys = { - { - frame = 0, - value = -0.65947163105011, - lx = -18.6666660308838, - ly = -0.65947163105011, - rx = 20.6666660308838, - ry = -0.65947163105011, - }, { - frame = 60, - value = 1.03530943393707, - lx = 38.4764099121094, - ly = 13.5587406158447, - rx = 76.9985809326172, - ry = -8.85525798797607, - } - } - },{ - channel = CH_TRANSLATE, - axis = AX_Y, - interp = INT_BEZIER, - keys = { - { - frame = 0, - value = 0.697231292724609, - lx = -18.6666660308838, - ly = 0.697231292724609, - rx = 20.6666660308838, - ry = 0.697231292724609, - }, { - frame = 60, - value = -0.432764053344727, - lx = 38.4764099121094, - ly = 12.0906667709351, - rx = 76.9985809326172, - ry = -10.3233318328857, - } - } - }}}, -} - -local track = compile_track(linesets) - function on_render() - local tm = time.uptime() * 10 + tm_offset - local speed = vec3.length(body:get_vel()) / 7.0 - local delta = time.delta() - - prev_speed = prev_speed * (1.0 - delta * 10) + speed * delta * 10 - speed = prev_speed - - -- local ttm = time.precise_time() - -- local matrix = mat4.idt() - track(rig, tm, speed) - -- for i, track in ipairs(tracks) do - -- rig:set_matrix(track.bone_index, track.generator(matrix, tm, speed)) - -- end - - -- rig:set_matrix(body_idx, mat4.translate({0, math.sin(tm * 2) * 0.1 * speed, 0})) - - -- rig:set_matrix(leg_left, mat4.rotate({1, 0, 0}, (math.sin(tm) * 45) * speed)) - -- rig:set_matrix(leg_right, mat4.rotate({1, 0, 0}, (-math.sin(tm) * 45) * speed)) - - -- rig:set_matrix(leg_left_btm, mat4.rotate({1, 0, 0}, (-math.sin(tm * 2) * 45 - 45) * speed)) - -- rig:set_matrix(leg_right_btm, mat4.rotate({1, 0, 0}, (-math.sin(tm * 2) * 45 - 45) * speed)) - - -- rig:set_matrix(hand_left, mat4.rotate({1, 0, 0}, (-math.sin(tm) * 45) * speed)) - -- rig:set_matrix(hand_right, mat4.rotate({1, 0, 0}, (math.sin(tm) * 45) * speed)) - - -- rig:set_matrix(hand_left_btm, mat4.rotate({1, 0, 0}, (math.sin(tm * 2) * 45 + 45) * speed)) - -- rig:set_matrix(hand_right_btm, mat4.rotate({1, 0, 0}, (math.sin(tm * 2) * 45 + 45) * speed)) - -- print(math.floor((time.precise_time() - ttm) * 1e6), "mcs") + update(time.delta()) end diff --git a/res/scripts/components/mob.lua b/res/scripts/components/mob.lua index 92c2a3723..9f824600f 100644 --- a/res/scripts/components/mob.lua +++ b/res/scripts/components/mob.lua @@ -40,7 +40,7 @@ local function angle_delta(a, b) return normalize_angle(a - b) end -local dir = mat4.mul(tsf:get_rot(), {0, 0, -1}) +local mob_dir = mat4.mul(tsf:get_rot(), {0, 0, -1}) local flight = false function jump(multiplier) @@ -87,12 +87,12 @@ function look_at(point, change_dir) local pos = tsf:get_pos() local viewdir = vec3.normalize(vec3.sub(point, pos)) - local dot = vec3.dot(viewdir, dir) + local dot = vec3.dot(viewdir, mob_dir) if dot < 0.0 and not change_dir then viewdir = mat4.mul(tsf:get_rot(), {0, 0, -1}) else - dir[1] = dir[1] * 0.8 + viewdir[1] * 0.13 - dir[3] = dir[3] * 0.8 + viewdir[3] * 0.13 + mob_dir[1] = mob_dir[1] * 0.8 + viewdir[1] * 0.13 + mob_dir[3] = mob_dir[3] * 0.8 + viewdir[3] * 0.13 end if not headIndex then @@ -112,39 +112,36 @@ end function follow_waypoints(pathfinding) pathfinding = pathfinding or entity:require_component("core:pathfinding") - local pos = tsf:get_pos() + + local pos = tsf:get_pos() local waypoint = pathfinding.next_waypoint() if not waypoint then return end local speed = props.movement_speed local vel = body:get_vel() - dir = vec3.sub( + mob_dir = vec3.sub( vec3.add(waypoint, {0.5, 0, 0.5}), {pos[1], math.floor(pos[2]), pos[3]} ) - local upper = dir[2] > 0 - dir[2] = 0.0 - vec3.normalize(dir, dir) - move_horizontal(speed, {dir[1], dir[3]}, vel) + local upper = mob_dir[2] > 0 + mob_dir[2] = 0.0 + vec3.normalize(mob_dir, mob_dir) + move_horizontal(speed, {mob_dir[1], mob_dir[3]}, vel) if upper and body:is_grounded() then jump(1.0) end end -function set_dir(new_dir) - dir = new_dir -end +function set_dir(new_dir) mob_dir = new_dir end -function get_dir() - return dir -end +function get_dir() return mob_dir end function is_flight() return flight end function set_flight(flag) flight = flag end -local prev_angle = (vec2.angle({dir[3], dir[1]})) % 360 +local prev_angle = (vec2.angle({mob_dir[3], mob_dir[1]})) % 360 function on_physics_update(delta) local grounded = body:is_grounded() @@ -154,7 +151,7 @@ function on_physics_update(delta) (flight or not grounded) and props.air_damping or props.ground_damping ) - local new_angle = (vec2.angle({dir[3], dir[1]})) % 360 + local new_angle = (vec2.angle({mob_dir[3], mob_dir[1]})) % 360 local angle = prev_angle local adelta = angle_delta( From 2e69df0a24a4e66b4b6e0d496bfceca544ce1b10 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Jul 2026 00:32:17 +0300 Subject: [PATCH 11/63] cleanup & update linesets --- res/modules/animation.lua | 4 ++-- src/logic/scripting/lua/libs/libxml.cpp | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 7dea2a4ff..21189989d 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -167,10 +167,10 @@ function this.compile_track(linesets) local memoised = {} local keysets = {} - for i, lineset in ipairs(linesets) do + for bone, lineset in pairs(linesets) do local lineset_code = codegen_track(lineset.lines, memoised, keysets) code = code .. "\n do" .. lineset_code .. "\n end\n" .. - " rig:set_matrix(" .. lineset.bone_index .. ", dst)\n" + " rig:set_matrix(rig:index(" .. string.escape(bone) .. "), dst)\n" end local memoised_code = "" diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp index 70ccc36bb..b28b0a5fd 100644 --- a/src/logic/scripting/lua/libs/libxml.cpp +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -2,7 +2,6 @@ #include "api_lua.hpp" static const char* TAG_ATTR = "#"; -static const char* DEFAULT_ROOT_TAG = "root"; static int push_xml(lua::State* L, const xml::xmlelement& elem) { if (elem.isText()) { From 57c9da2eb192246d4aa7d79ed8828c1168331c81 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Jul 2026 22:25:46 +0300 Subject: [PATCH 12/63] add animation.parse_track --- res/modules/animation.lua | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 21189989d..0c6d6f3bf 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -162,6 +162,44 @@ local function codegen_track(lines, memoised, keysets) return code end +local action_to_channel = { + move = CH_TRANSLATE, + rotate = CH_ROTATE +} + +function this.parse_track(root) + local linesets = {} + for i, node in ipairs(root) do + if type(node) == "string" then + goto continue + end + local tag = node['#'] + local bone = node.bone + + local lineset = linesets[bone] + if not lineset then + lineset = {lines = {}} + linesets[bone] = lineset + end + + local line + if node.func then + line = { + axis = ("xyz"):find(node.by), + channel = action_to_channel[tag], + expression = node.func, + } + elseif #node > 0 then + error("not implemented") + end + + table.insert(lineset.lines, line) + ::continue:: + end + return linesets +end + + function this.compile_track(linesets) local code = "" local memoised = {} From ff0d7d094fc726c4f4ed9fdcfb82d2d016748574 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Jul 2026 23:46:45 +0300 Subject: [PATCH 13/63] feat: vca keyframes support --- res/modules/animation.lua | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 0c6d6f3bf..93e7a13e1 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -167,6 +167,12 @@ local action_to_channel = { rotate = CH_ROTATE } +local curve_to_interp = { + const = INT_CONST, + linear = INT_LINEAR, + bezier = INT_BEZIER, +} + function this.parse_track(root) local linesets = {} for i, node in ipairs(root) do @@ -182,14 +188,29 @@ function this.parse_track(root) linesets[bone] = lineset end - local line + local line = { + axis = ("xyz"):find(node.by), + channel = action_to_channel[tag] + } if node.func then - line = { - axis = ("xyz"):find(node.by), - channel = action_to_channel[tag], - expression = node.func, - } - elseif #node > 0 then + line.expression = node.func + elseif node.curve then + line.interp = curve_to_interp[node.curve] + line.keys = {} + for j, key_node in ipairs(node) do + local keyframe = { + frame = tonumber(key_node.frame), + value = tonumber(key_node.value), + } + if line.interp == INT_BEZIER then + keyframe.lx = tonumber(key_node.lx) + keyframe.ly = tonumber(key_node.ly) + keyframe.rx = tonumber(key_node.rx) + keyframe.ry = tonumber(key_node.ry) + end + table.insert(line.keys, keyframe) + end + else error("not implemented") end From 462159954508fc8f293014aa311ed88a49a2da95 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 00:07:27 +0300 Subject: [PATCH 14/63] add anmation.load_vca --- res/modules/animation.lua | 46 ++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 93e7a13e1..aaa1dd584 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -3,14 +3,12 @@ local this = {} local CH_TRANSLATE = 1 local CH_ROTATE = 2 -local AX_X = 1 -local AX_Y = 2 -local AX_Z = 3 - local INT_CONST = 1 local INT_LINEAR = 2 local INT_BEZIER = 3 +local TRACE_CODEGEN = false + local function bezier(a, b, c, d, u) local s = 1 - u return s * s * s * a + @@ -32,20 +30,9 @@ local function bezier_interpolation(k0, k1, t) local u = t for i=1,8 do - local x = bezier( - k0.frame, - k0.rx, - k1.lx, - k1.frame, - u - ) - local dx = bezier_derivative( - k0.frame, - k0.rx, - k1.lx, - k1.frame, - u - ) + local x = bezier(k0.frame, k0.rx, k1.lx, k1.frame, u) + local dx = bezier_derivative(k0.frame, k0.rx, k1.lx, k1.frame, u) + if math.abs(dx) < 1e-8 then break end @@ -173,7 +160,7 @@ local curve_to_interp = { bezier = INT_BEZIER, } -function this.parse_track(root) +local function parse_track(root) local linesets = {} for i, node in ipairs(root) do if type(node) == "string" then @@ -221,7 +208,7 @@ function this.parse_track(root) end -function this.compile_track(linesets) +function this.compile_track(linesets, track_name) local code = "" local memoised = {} local keysets = {} @@ -242,6 +229,11 @@ function this.compile_track(linesets) end local src = "return function(rig, t, m)\n local dst = DST\n" .. code .. "\nend" + + if TRACE_CODEGEN then + debug.log("["..string.escape(track_name or "nil").." codegen trace]:\n"..code) + end + local generator, err = load(src, "", "bt", table.extend({keysets = keysets}, env)) if not generator then error(err) @@ -249,4 +241,18 @@ function this.compile_track(linesets) return generator() end +local cached_tracks = {} + +function this.load_vca(filepath) + local track = cached_tracks[filepath] + if track then + return track + end + local source = file.read(filepath) + local raw_track = parse_track(xml.parse_vcd(source, "track")) + track = this.compile_track(raw_track, filepath) + cached_tracks[filepath] = track + return track +end + return this From a0a1175d9b307a35695202af4d2470a4280ce3d1 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 00:19:32 +0300 Subject: [PATCH 15/63] update doc/*/scripting/filesystem.md --- doc/en/scripting/filesystem.md | 2 +- doc/ru/scripting/filesystem.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/scripting/filesystem.md b/doc/en/scripting/filesystem.md index 12a190daa..29da88194 100644 --- a/doc/en/scripting/filesystem.md +++ b/doc/en/scripting/filesystem.md @@ -100,7 +100,7 @@ xml.tostring(data: XML, multiline: bool = true) -> str -- Parses XML xml.parse(code: str) -> XML --- Parses the VCD (VoxelCore Declaration) format used by the VCM format. +-- Parses the VCD (VoxelCore Declaration) format used by VCM and VCA formats. -- root_tag - a root element with the specified tag will be implicitly created, -- containing all root elements xml.parse_vcd(code: str, root_tag: str = "root") -> XML diff --git a/doc/ru/scripting/filesystem.md b/doc/ru/scripting/filesystem.md index a3bee78a6..546722101 100644 --- a/doc/ru/scripting/filesystem.md +++ b/doc/ru/scripting/filesystem.md @@ -100,7 +100,7 @@ xml.tostring(data: XML, multiline: bool = true) -> str -- Парсит XML xml.parse(code: str) -> XML --- Парсит формат VCD (VoxelCore Declaration), используемый форматом VCM. +-- Парсит формат VCD (VoxelCore Declaration), используемый форматами VCM и VCA. -- root_tag - будет неявно создан корневой элемент с указанным тегом, -- содержащий все корневые элементы xml.parse_vcd(code: str, root_tag: str | nil = nil) -> XML From 8242d299ed68c8f27e691e464562e43c936788da Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 15:08:55 +0300 Subject: [PATCH 16/63] fix components order & add skeleton:get_matrix overload & add skeleton:reset_pose --- res/modules/internal/stdcomp.lua | 17 ++++++++++------- src/logic/scripting/lua/libs/lib__skeleton.cpp | 15 ++++++++++++++- src/logic/scripting/scripting_entities.cpp | 5 +++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/res/modules/internal/stdcomp.lua b/res/modules/internal/stdcomp.lua index 4278f14f6..ebc314615 100644 --- a/res/modules/internal/stdcomp.lua +++ b/res/modules/internal/stdcomp.lua @@ -48,8 +48,9 @@ end local Skeleton = {__index={ get_model=function(self, i) return __skeleton.get_model(self.eid, i) end, set_model=function(self, i, s) return __skeleton.set_model(self.eid, i, s) end, - get_matrix=function(self, i) return __skeleton.get_matrix(self.eid, i) end, + get_matrix=function(self, i, ...) return __skeleton.get_matrix(self.eid, i, ...) end, set_matrix=function(self, i, m) return __skeleton.set_matrix(self.eid, i, m) end, + reset_pose=function(self) return __skeleton.reset_pose(self.eid) end, get_texture=function(self, s) return __skeleton.get_texture(self.eid, s) end, set_texture=function(self, s, s2) return __skeleton.set_texture(self.eid, s, s2) end, index=function(self, s) return __skeleton.index(self.eid, s) end, @@ -84,7 +85,7 @@ local Entity = {__index={ def_name=function(self) return entities.def_name(entities.get_def(self.eid)) end, get_player=function(self) return entities.get_player(self.eid) end, set_enabled=function(self, name, flag) - local comp = self.components[name] + local comp = self.components[name] if comp then if flag then if comp.__disabled and comp.on_enable then @@ -110,6 +111,7 @@ return { entity.rigidbody = new_Rigidbody(eid) entity.skeleton = new_Skeleton(eid) entity.components = {} + entity.ordered_components = {} entities[eid] = entity; return entity end, @@ -120,6 +122,7 @@ return { local entity = entities[eid] if entity then entity.components = nil + entity.ordered_components = nil entities[eid] = nil; end end, @@ -128,10 +131,10 @@ return { if uid % parts ~= part then goto continue end - for _, component in pairs(entity.components) do + for _, component in ipairs(entity.ordered_components) do local callback = component.on_update if not component.__disabled and callback then - local result, err = pcall(callback, tps) + local _, err = pcall(callback, tps) if err then debug.error(err) end @@ -142,7 +145,7 @@ return { end, physics_update = function(delta) for uid, entity in pairs(entities) do - for _, component in pairs(entity.components) do + for _, component in ipairs(entity.ordered_components) do local callback = component.on_physics_update if not component.__disabled and callback then local result, err = pcall(callback, delta) @@ -155,10 +158,10 @@ return { end, render = function(delta) for _,entity in pairs(entities) do - for _, component in pairs(entity.components) do + for _, component in ipairs(entity.ordered_components) do local callback = component.on_render if not component.__disabled and callback then - local result, err = pcall(callback, delta) + local _, err = pcall(callback, delta) if err then debug.error(err) end diff --git a/src/logic/scripting/lua/libs/lib__skeleton.cpp b/src/logic/scripting/lua/libs/lib__skeleton.cpp index f1f058b23..fb70ecbcb 100644 --- a/src/logic/scripting/lua/libs/lib__skeleton.cpp +++ b/src/logic/scripting/lua/libs/lib__skeleton.cpp @@ -66,7 +66,11 @@ static int l_set_model(lua::State* L) { static int l_get_matrix(lua::State* L) { if (auto skeleton = get_skeleton(L)) { auto index = index_range_check(*skeleton, lua::tointeger(L, 2)); - return lua::pushmat4(L, skeleton->pose.matrices[index]); + if (lua::istable(L, 3)) { + lua::setmat4(L, 3, skeleton->pose.matrices[index]); + } else { + return lua::pushmat4(L, skeleton->pose.matrices[index]); + } } return 0; } @@ -79,6 +83,14 @@ static int l_set_matrix(lua::State* L) { return 0; } +static int l_reset_pose(lua::State* L) { + if (auto skeleton = get_skeleton(L)) { + auto& pose = skeleton->pose; + std::fill(pose.matrices.begin(), pose.matrices.end(), glm::mat4(1.0f)); + } + return 0; +} + static int l_get_texture(lua::State* L) { if (auto skeleton = get_skeleton(L)) { const auto& found = skeleton->textures.find(lua::require_string(L, 2)); @@ -169,6 +181,7 @@ const luaL_Reg skeletonlib[] = { {"set_model", lua::wrap}, {"get_matrix", lua::wrap}, {"set_matrix", lua::wrap}, + {"reset_pose", lua::wrap}, {"get_texture", lua::wrap}, {"set_texture", lua::wrap}, {"index", lua::wrap}, diff --git a/src/logic/scripting/scripting_entities.cpp b/src/logic/scripting/scripting_entities.cpp index ca9ed6db1..9853925d0 100644 --- a/src/logic/scripting/scripting_entities.cpp +++ b/src/logic/scripting/scripting_entities.cpp @@ -37,6 +37,11 @@ static inline const std::string STDCOMP = "stdcomp"; lua::setfield(L, name); lua::pop(L); } + if (lua::getfield(L, "ordered_components")) { + lua::pushenv(L, id); + lua::rawseti(L, lua::objlen(L, -2) + 1); + lua::pop(L); + } lua::pop(L); return std::shared_ptr(new int(id), [=](int* id) { //-V508 From f507665c6889f814389674b611970a73a7ee6c3e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 18:55:49 +0300 Subject: [PATCH 17/63] move rigging to src/animation --- src/{objects => animation}/rigging.cpp | 0 src/{objects => animation}/rigging.hpp | 0 src/assets/AssetsLoader.cpp | 2 +- src/assets/assetload_funcs.cpp | 22 ++++++------ src/coders/vcm.hpp | 7 ++-- src/content/ContentControl.cpp | 14 ++++---- src/graphics/render/HandsRenderer.cpp | 12 +++---- src/graphics/render/NamedSkeletons.cpp | 2 +- src/graphics/render/WorldRenderer.cpp | 2 +- .../scripting/lua/libs/lib__skeleton.cpp | 2 +- src/logic/scripting/lua/libs/libentity.cpp | 8 ++--- src/objects/Entities.cpp | 2 +- src/objects/Entity.cpp | 8 ++--- src/objects/Player.cpp | 35 ++++++++++--------- 14 files changed, 58 insertions(+), 58 deletions(-) rename src/{objects => animation}/rigging.cpp (100%) rename src/{objects => animation}/rigging.hpp (100%) diff --git a/src/objects/rigging.cpp b/src/animation/rigging.cpp similarity index 100% rename from src/objects/rigging.cpp rename to src/animation/rigging.cpp diff --git a/src/objects/rigging.hpp b/src/animation/rigging.hpp similarity index 100% rename from src/objects/rigging.hpp rename to src/animation/rigging.hpp diff --git a/src/assets/AssetsLoader.cpp b/src/assets/AssetsLoader.cpp index 23b316e6b..5dfb45b9e 100644 --- a/src/assets/AssetsLoader.cpp +++ b/src/assets/AssetsLoader.cpp @@ -1,5 +1,6 @@ #include "AssetsLoader.hpp" +#include "animation/rigging.hpp" #include "assetload_funcs.hpp" #include "Assets.hpp" #include "coders/commons.hpp" @@ -15,7 +16,6 @@ #include "items/ItemDef.hpp" #include "logic/scripting/scripting.hpp" #include "objects/EntityDef.hpp" -#include "objects/rigging.hpp" #include "util/ThreadPool.hpp" #include "voxels/Block.hpp" diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index 74a298933..1098c7a51 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -1,13 +1,11 @@ #include "assetload_funcs.hpp" -#include -#include -#include -#include - +#include "animation/rigging.hpp" +#include "Assets.hpp" +#include "AssetsLoader.hpp" #include "audio/audio.hpp" -#include "coders/GLSLExtension.hpp" #include "coders/commons.hpp" +#include "coders/GLSLExtension.hpp" #include "coders/imageio.hpp" #include "coders/json.hpp" #include "coders/obj.hpp" @@ -17,19 +15,21 @@ #include "constants.hpp" #include "debug/Logger.hpp" #include "engine/EnginePaths.hpp" -#include "io/io.hpp" #include "frontend/UiDocument.hpp" +#include "graphics/commons/Model.hpp" #include "graphics/core/Atlas.hpp" #include "graphics/core/Font.hpp" #include "graphics/core/ImageData.hpp" #include "graphics/core/Shader.hpp" #include "graphics/core/Texture.hpp" #include "graphics/core/TextureAnimation.hpp" -#include "graphics/commons/Model.hpp" -#include "objects/rigging.hpp" +#include "io/io.hpp" #include "util/stringutil.hpp" -#include "Assets.hpp" -#include "AssetsLoader.hpp" + +#include +#include +#include +#include static debug::Logger logger("assetload-funcs"); diff --git a/src/coders/vcm.hpp b/src/coders/vcm.hpp index 4f97ea8d3..9c45c2400 100644 --- a/src/coders/vcm.hpp +++ b/src/coders/vcm.hpp @@ -1,13 +1,12 @@ #pragma once -#include +#include "animation/rigging.hpp" +#include "graphics/commons/Model.hpp" + #include #include #include -#include "graphics/commons/Model.hpp" -#include "objects/rigging.hpp" - namespace rigging { class SkeletonConfig; } diff --git a/src/content/ContentControl.cpp b/src/content/ContentControl.cpp index 6daf48dc0..5d74702de 100644 --- a/src/content/ContentControl.cpp +++ b/src/content/ContentControl.cpp @@ -1,16 +1,16 @@ #include "ContentControl.hpp" -#include "io/io.hpp" -#include "engine/EnginePaths.hpp" +#include "animation/rigging.hpp" #include "Content.hpp" -#include "ContentPack.hpp" #include "ContentBuilder.hpp" #include "ContentLoader.hpp" -#include "PacksManager.hpp" -#include "objects/rigging.hpp" -#include "devtools/Project.hpp" -#include "logic/scripting/scripting.hpp" +#include "ContentPack.hpp" #include "core_defs.hpp" +#include "devtools/Project.hpp" +#include "engine/EnginePaths.hpp" +#include "io/io.hpp" +#include "logic/scripting/scripting.hpp" +#include "PacksManager.hpp" static void load_configs(Input* input, const io::path& root) { auto configFolder = root / "config"; diff --git a/src/graphics/render/HandsRenderer.cpp b/src/graphics/render/HandsRenderer.cpp index 8e2f757b2..776365e17 100644 --- a/src/graphics/render/HandsRenderer.cpp +++ b/src/graphics/render/HandsRenderer.cpp @@ -1,14 +1,14 @@ #include "HandsRenderer.hpp" +#include "animation/rigging.hpp" +#include "content/Content.hpp" +#include "graphics/commons/Model.hpp" +#include "ModelBatch.hpp" +#include "window/Camera.hpp" + #include #include -#include "ModelBatch.hpp" -#include "content/Content.hpp" -#include "graphics/commons/Model.hpp" -#include "objects/rigging.hpp" -#include "window/Camera.hpp" - using namespace rigging; HandsRenderer::HandsRenderer( diff --git a/src/graphics/render/NamedSkeletons.cpp b/src/graphics/render/NamedSkeletons.cpp index c30278a9c..c3f681736 100644 --- a/src/graphics/render/NamedSkeletons.cpp +++ b/src/graphics/render/NamedSkeletons.cpp @@ -1,6 +1,6 @@ #include "NamedSkeletons.hpp" -#include "objects/rigging.hpp" +#include "animation/rigging.hpp" using namespace rigging; diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 2e150e1f7..ae19dac76 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -1,5 +1,6 @@ #include "WorldRenderer.hpp" +#include "animation/rigging.hpp" #include "assets/Assets.hpp" #include "assets/assets_util.hpp" #include "content/Content.hpp" @@ -15,7 +16,6 @@ #include "maths/voxmaths.hpp" #include "objects/Entities.hpp" #include "objects/Player.hpp" -#include "objects/rigging.hpp" #include "settings.hpp" #include "voxels/Block.hpp" #include "voxels/Chunk.hpp" diff --git a/src/logic/scripting/lua/libs/lib__skeleton.cpp b/src/logic/scripting/lua/libs/lib__skeleton.cpp index fb70ecbcb..c1cab88db 100644 --- a/src/logic/scripting/lua/libs/lib__skeleton.cpp +++ b/src/logic/scripting/lua/libs/lib__skeleton.cpp @@ -1,6 +1,6 @@ -#include "objects/rigging.hpp" #include "libentity.hpp" +#include "animation/rigging.hpp" #include "engine/Engine.hpp" #include "graphics/render/WorldRenderer.hpp" #include "graphics/render/NamedSkeletons.hpp" diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index 110aee123..3d850fdc5 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -1,20 +1,20 @@ #include "libentity.hpp" +#include "animation/rigging.hpp" #include "assets/Assets.hpp" #include "content/Content.hpp" #include "content/ContentPack.hpp" #include "engine/Engine.hpp" #include "engine/EnginePaths.hpp" #include "objects/Entities.hpp" -#include "objects/EntityDef.hpp" #include "objects/Entity.hpp" -#include "objects/Rigidbody.hpp" +#include "objects/EntityDef.hpp" #include "objects/Player.hpp" -#include "objects/rigging.hpp" +#include "objects/Rigidbody.hpp" #include "physics/Hitbox.hpp" -#include "voxels/Chunks.hpp" #include "voxels/Block.hpp" #include "voxels/blocks_agent.hpp" +#include "voxels/Chunks.hpp" #include "window/Camera.hpp" #include diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index fafd99fe5..9f3e62c72 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -1,6 +1,7 @@ #define VC_ENABLE_REFLECTION #include "Entities.hpp" +#include "animation/rigging.hpp" #include "assets/Assets.hpp" #include "content/Content.hpp" #include "data/dv_util.hpp" @@ -17,7 +18,6 @@ #include "maths/rays.hpp" #include "maths/util.hpp" #include "physics/PhysicsSolver.hpp" -#include "rigging.hpp" #include "world/Level.hpp" #include diff --git a/src/objects/Entity.cpp b/src/objects/Entity.cpp index 6629d2981..9af1ea80b 100644 --- a/src/objects/Entity.cpp +++ b/src/objects/Entity.cpp @@ -1,12 +1,12 @@ #include "Entity.hpp" -#include "Transform.hpp" -#include "Rigidbody.hpp" -#include "ScriptComponents.hpp" +#include "animation/rigging.hpp" #include "Entities.hpp" #include "EntityDef.hpp" -#include "rigging.hpp" #include "logic/scripting/scripting.hpp" +#include "Rigidbody.hpp" +#include "ScriptComponents.hpp" +#include "Transform.hpp" #include diff --git a/src/objects/Player.cpp b/src/objects/Player.cpp index c0fe61744..eb9023b0b 100644 --- a/src/objects/Player.cpp +++ b/src/objects/Player.cpp @@ -1,27 +1,28 @@ #include "Player.hpp" -#include #define GLM_ENABLE_EXPERIMENTAL + +#include "animation/rigging.hpp" +#include "content/ContentReport.hpp" +#include "data/dv_util.hpp" +#include "debug/Logger.hpp" +#include "Entities.hpp" +#include "Entity.hpp" +#include "items/Inventory.hpp" +#include "physics/Hitbox.hpp" +#include "physics/PhysicsSolver.hpp" +#include "util/stringutil.hpp" +#include "voxels/Chunks.hpp" +#include "window/Camera.hpp" +#include "world/generator/GeneratorDef.hpp" +#include "world/Level.hpp" +#include "world/World.hpp" + +#include #include #include #include -#include "content/ContentReport.hpp" -#include "items/Inventory.hpp" -#include "Entities.hpp" -#include "Entity.hpp" -#include "rigging.hpp" -#include "physics/Hitbox.hpp" -#include "physics/PhysicsSolver.hpp" -#include "voxels/Chunks.hpp" -#include "window/Camera.hpp" -#include "world/Level.hpp" -#include "world/World.hpp" -#include "world/generator/GeneratorDef.hpp" -#include "data/dv_util.hpp" -#include "debug/Logger.hpp" -#include "util/stringutil.hpp" - static debug::Logger logger("player"); constexpr int SPAWN_ATTEMPTS_PER_UPDATE = 64; From e69490ca52b123610ea665d6c92ec894480e88d2 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 28 Jul 2026 00:36:07 +0300 Subject: [PATCH 18/63] add debug/debug.hpp --- src/debug/debug.hpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/debug/debug.hpp diff --git a/src/debug/debug.hpp b/src/debug/debug.hpp new file mode 100644 index 000000000..bdc79a646 --- /dev/null +++ b/src/debug/debug.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +template +std::ostream& operator<<(std::ostream& out, const glm::vec& vec) { + out << "{"; + for (int i = 0; i < N; i++) { + if (i > 0) { + out << ", "; + } + out << vec[i]; + } + out << "}"; + return out; +} From ecd95f114705e1b1a6472bc155d813b2148110ac Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 28 Jul 2026 22:09:37 +0300 Subject: [PATCH 19/63] add .back(), .front() to util::stack_vector --- src/util/stack_vector.hpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/util/stack_vector.hpp b/src/util/stack_vector.hpp index 30e0b8f9c..715ca3717 100644 --- a/src/util/stack_vector.hpp +++ b/src/util/stack_vector.hpp @@ -103,6 +103,22 @@ namespace util { return data_.ptr()[index]; } + T& front() { + return data_.ptr()[0]; + } + + T& back() { + return data_.ptr()[size_ - 1]; + } + + const T& front() const { + return data_.ptr()[0]; + } + + const T& back() const { + return data_.ptr()[size_ - 1]; + } + int size() const { return size_; } From bc22e6e365661b0521d98f2a00fe82f2a0485674 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 9 Aug 2026 18:03:57 +0300 Subject: [PATCH 20/63] update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c0b0e1c9d..4d0155832 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ appimage-build/ # libs /libs/ /vcpkg_installed/ + +# vcstudio +.vcstudio From 9fff79f8e293b37630fd7b3017fb8614369520b6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 19 Aug 2026 20:51:40 +0300 Subject: [PATCH 21/63] make entity skeleton optional --- src/animation/rigging.cpp | 29 +++++++++++----------- src/logic/scripting/lua/libs/libentity.cpp | 4 +++ src/objects/Entities.cpp | 10 ++++++-- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/animation/rigging.cpp b/src/animation/rigging.cpp index 075cd8680..ceaed3365 100644 --- a/src/animation/rigging.cpp +++ b/src/animation/rigging.cpp @@ -41,12 +41,15 @@ void Bone::setModel(const std::string& name) { Skeleton::Skeleton(std::shared_ptr config) : config(config), - pose(config->getBones().size()), - calculated(config->getBones().size()), - flags(config->getBones().size()), + pose(config ? config->getBones().size() : 0), + calculated(config ? config->getBones().size() : 0), + flags(config ? config->getBones().size() : 0), textures(), - modelOverrides(config->getBones().size()), + modelOverrides(config ? config->getBones().size() : 0), visible(true) { + if (config == nullptr) { + return; + } const auto& bones = config->getBones(); for (size_t i = 0; i < bones.size(); i++) { flags[i].visible = true; @@ -89,19 +92,15 @@ void Skeleton::deserialize(const dv::value& root) { void Skeleton::setConfig(std::shared_ptr rigConfig) { config = std::move(rigConfig); - const auto& bones = config->getBones(); + int bonesCount = config ? config->getBones().size() : 0; + + pose.matrices.resize(bonesCount, glm::mat4(1.0f)); + calculated.matrices.resize(bonesCount, glm::mat4(1.0f)); - pose.matrices.resize( - bones.size(), glm::mat4(1.0f) - ); - calculated.matrices.resize( - bones.size(), glm::mat4(1.0f) - ); + modelOverrides.resize(bonesCount); + flags.resize(bonesCount); - modelOverrides.resize(bones.size()); - flags.resize(bones.size()); - - for (size_t i = 0; i < bones.size(); i++) { + for (size_t i = 0; i < bonesCount; i++) { flags[i].visible = true; } } diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index dd4e3dfaa..2d3eeecea 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -116,6 +116,10 @@ static int l_set_skeleton(lua::State* L) { return 0; } if (auto entity = get_entity(L, 1)) { + if (lua::isnil(L, 2)) { + entity->setRig(nullptr); + return 0; + } std::string skeletonName = lua::require_string(L, 2); auto rigConfig = assets->getShared(skeletonName); if (rigConfig == nullptr) { diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index fd7cc0b92..c0146552a 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -59,7 +59,11 @@ entityid_t Entities::spawn( if (assets) { skeleton = assets->get(def.skeletonName); if (skeleton == nullptr) { - throw std::runtime_error("skeleton " + def.skeletonName + " not found"); + if (def.skeletonName == def.name) { + logger.warning() << "skeleton " + def.skeletonName + " not found"; + } else { + throw std::runtime_error("skeleton " + def.skeletonName + " not found"); + } } } entityid_t id; @@ -100,7 +104,9 @@ entityid_t Entities::spawn( auto& scripting = registry->emplace(entity); if (assets) { - registry->emplace(entity, skeleton->instance()); + registry->emplace( + entity, skeleton ? skeleton->instance() : rigging::Skeleton(nullptr) + ); } for (auto& instance : def.components) { From c2be0c2eeb82404b19c4d28ab2b8435bf9f70ddd Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 19 Aug 2026 20:56:41 +0300 Subject: [PATCH 22/63] update doc/*/scripting/ecs.md --- doc/en/scripting/ecs.md | 2 +- doc/ru/scripting/ecs.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index 9636cf763..0dcaa6985 100644 --- a/doc/en/scripting/ecs.md +++ b/doc/en/scripting/ecs.md @@ -17,7 +17,7 @@ entity:def_name() -> str -- Returns the name of the entity skeleton entity:get_skeleton() -> str -- Replaces the entity skeleton -entity:set_skeleton(name: str) +entity:set_skeleton(name: str | nil) -- Returns the unique entity identifier entity:get_uid() -> int diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index cb4a79bec..6856be274 100644 --- a/doc/ru/scripting/ecs.md +++ b/doc/ru/scripting/ecs.md @@ -17,7 +17,7 @@ entity:def_name() -> str -- Возращает имя скелета сущности entity:get_skeleton() -> str -- Заменяет скелет сущности -entity:set_skeleton(name: str) +entity:set_skeleton(name: str | nil) -- Возращает уникальный идентификатор сущности entity:get_uid() -> int From 834a0e85d87d673f82ec0bb8697c9bbc3aa02d73 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 19 Aug 2026 23:38:00 +0300 Subject: [PATCH 23/63] add 'configure' VCA tag --- res/modules/animation.lua | 70 ++++++++++++++++++++--------- res/scripts/components/animator.lua | 2 +- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index aaa1dd584..dd89da237 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -9,6 +9,8 @@ local INT_BEZIER = 3 local TRACE_CODEGEN = false +local DEFAULT_FPS = 60 + local function bezier(a, b, c, d, u) local s = 1 - u return s * s * s * a + @@ -160,13 +162,52 @@ local curve_to_interp = { bezier = INT_BEZIER, } +local function parse_configure(raw_track, node) + if node.fps then + raw_track.fps = node.fps + end + if node.frames then + raw_track.duration = node.frames / node.fps + elseif node.duration then + raw_track.duration = node.duration + end +end + +local function parse_curve(line, node) + line.interp = curve_to_interp[node.curve] + line.keys = {} + for j, key_node in ipairs(node) do + local keyframe = { + frame = tonumber(key_node.frame), + value = tonumber(key_node.value), + } + if line.interp == INT_BEZIER then + keyframe.lx = tonumber(key_node.lx) + keyframe.ly = tonumber(key_node.ly) + keyframe.rx = tonumber(key_node.rx) + keyframe.ry = tonumber(key_node.ry) + end + table.insert(line.keys, keyframe) + end +end + local function parse_track(root) - local linesets = {} + local raw_track = { + duration = math.huge, + fps = DEFAULT_FPS, + linesets = {}, + } + local linesets = raw_track.linesets for i, node in ipairs(root) do if type(node) == "string" then goto continue end local tag = node['#'] + if tag == "configure" then + parse_configure(raw_track, node) + goto continue + end + local bone = node.bone local lineset = linesets[bone] @@ -182,21 +223,7 @@ local function parse_track(root) if node.func then line.expression = node.func elseif node.curve then - line.interp = curve_to_interp[node.curve] - line.keys = {} - for j, key_node in ipairs(node) do - local keyframe = { - frame = tonumber(key_node.frame), - value = tonumber(key_node.value), - } - if line.interp == INT_BEZIER then - keyframe.lx = tonumber(key_node.lx) - keyframe.ly = tonumber(key_node.ly) - keyframe.rx = tonumber(key_node.rx) - keyframe.ry = tonumber(key_node.ry) - end - table.insert(line.keys, keyframe) - end + parse_curve(line, node) else error("not implemented") end @@ -204,16 +231,16 @@ local function parse_track(root) table.insert(lineset.lines, line) ::continue:: end - return linesets + return raw_track end -function this.compile_track(linesets, track_name) +function this.compile_track(raw_track, track_name) local code = "" local memoised = {} local keysets = {} - for bone, lineset in pairs(linesets) do + for bone, lineset in pairs(raw_track.linesets) do local lineset_code = codegen_track(lineset.lines, memoised, keysets) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " rig:set_matrix(rig:index(" .. string.escape(bone) .. "), dst)\n" @@ -238,7 +265,10 @@ function this.compile_track(linesets, track_name) if not generator then error(err) end - return generator() + return { + duration = raw_track.duration, + func = generator(), + } end local cached_tracks = {} diff --git a/res/scripts/components/animator.lua b/res/scripts/components/animator.lua index 0c7710f81..cec1cc3b9 100644 --- a/res/scripts/components/animator.lua +++ b/res/scripts/components/animator.lua @@ -50,7 +50,7 @@ local function update(delta) end if state.track and intensity > 0.0 then - state.track(rig, state.timer, intensity * state.max_intencity) + state.track.func(rig, state.timer, intensity * state.max_intencity) end end end From e544d797e210931ff48082b91116436520ec077c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 19 Aug 2026 23:50:09 +0300 Subject: [PATCH 24/63] fix --- res/modules/animation.lua | 22 ++++++++++++++-------- res/scripts/components/animator.lua | 4 +--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index dd89da237..bc2b29fb5 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -110,7 +110,7 @@ local env = { } table.extend(env, math) -local function codegen_track(lines, memoised, keysets) +local function codegen_track(raw_track, lines, memoised, keysets) local code = "" local translation = {false, false, false} local rotation = {false, false, false} @@ -121,8 +121,8 @@ local function codegen_track(lines, memoised, keysets) elseif line.keys then keysets[i] = line.keys code = code .. string.format( - "\n local l%d = value_at(keysets[%d], t * 3 %% 60, %s)", - i, i, line.interp) + "\n local l%d = value_at(keysets[%d], t * %s, %s)", + i, i, raw_track.fps, line.interp) end if line.channel == CH_TRANSLATE then @@ -241,27 +241,33 @@ function this.compile_track(raw_track, track_name) local keysets = {} for bone, lineset in pairs(raw_track.linesets) do - local lineset_code = codegen_track(lineset.lines, memoised, keysets) + local lineset_code = codegen_track( + raw_track, lineset.lines, memoised, keysets) + code = code .. "\n do" .. lineset_code .. "\n end\n" .. " rig:set_matrix(rig:index(" .. string.escape(bone) .. "), dst)\n" end local memoised_code = "" for name, expression in pairs(memoised) do - memoised_code = memoised_code .. "\n local " .. name .. " = " .. expression + memoised_code = memoised_code .. "\n local " .. name .. " = " + .. expression end if #memoised_code > 0 then code = memoised_code .. "\n" .. code end - local src = "return function(rig, t, m)\n local dst = DST\n" .. code .. "\nend" + local src = "return function(rig, t, m)\n local dst = DST\n" + .. code .. "\nend" if TRACE_CODEGEN then - debug.log("["..string.escape(track_name or "nil").." codegen trace]:\n"..code) + debug.log("[".. + string.escape(track_name or "nil").." codegen trace]:\n"..src) end - local generator, err = load(src, "", "bt", table.extend({keysets = keysets}, env)) + local generator, err = load( + src, "", "bt", table.extend({keysets = keysets}, env)) if not generator then error(err) end diff --git a/res/scripts/components/animator.lua b/res/scripts/components/animator.lua index cec1cc3b9..dc80066db 100644 --- a/res/scripts/components/animator.lua +++ b/res/scripts/components/animator.lua @@ -1,6 +1,4 @@ -local tsf = entity.transform local rig = entity.skeleton -local body = entity.rigidbody local states = {} @@ -37,7 +35,7 @@ function set_state(name, fade_time) end local function update(delta) - for key, state in pairs(states) do + for _, state in pairs(states) do state.timer = state.timer + delta local intensity = state.fade_in and 1.0 or 0.0 From 0a29765fb4f56627db2c51f65179cc813da67939 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 20 Aug 2026 00:05:27 +0300 Subject: [PATCH 25/63] add VCA syntax highlighting --- res/devtools/syntax/vca.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 res/devtools/syntax/vca.toml diff --git a/res/devtools/syntax/vca.toml b/res/devtools/syntax/vca.toml new file mode 100644 index 000000000..369630a09 --- /dev/null +++ b/res/devtools/syntax/vca.toml @@ -0,0 +1,6 @@ +language = "VCA" +extensions = ["vca"] +line-comment = "#" +keywords = [ + "bone", "by", "func", "curve", "fps", "duration", "frames" +] From ce068790c1c2fbf2647098cc9808b20b85a36bda Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 25 Aug 2026 22:30:09 +0300 Subject: [PATCH 26/63] cleanup AssetsLoader a bit --- src/assets/Assets.hpp | 20 +++--- src/assets/AssetsLoader.cpp | 16 ++--- src/assets/AssetsLoader.hpp | 4 +- src/assets/assetload_funcs.cpp | 106 +++++++++++++++--------------- src/assets/assetload_funcs.hpp | 18 ++--- src/logic/scripting/scripting.cpp | 2 + 6 files changed, 86 insertions(+), 80 deletions(-) diff --git a/src/assets/Assets.hpp b/src/assets/Assets.hpp index b9faa877f..10c71093b 100644 --- a/src/assets/Assets.hpp +++ b/src/assets/Assets.hpp @@ -1,5 +1,9 @@ #pragma once +#include "util/stringutil.hpp" +#include "util/ObjectsKeeper.hpp" +#include "graphics/core/TextureAnimation.hpp" + #include #include #include @@ -11,10 +15,6 @@ #include #include -#include "util/stringutil.hpp" -#include "util/ObjectsKeeper.hpp" -#include "graphics/core/TextureAnimation.hpp" - class Assets; enum class AssetType { @@ -31,12 +31,12 @@ enum class AssetType { namespace assetload { /// @brief final work to do in the main thread - using postfunc = std::function; + using postfunc = std::function; - using setupfunc = std::function; + using setupfunc = std::function; template - void assets_setup(const Assets*); + void assets_setup(const Assets&); class error : public std::runtime_error { AssetType type; @@ -145,7 +145,7 @@ public: void setup() { for (auto& setupFunc : setupFuncs) { - setupFunc(this); + setupFunc(*this); } } @@ -155,8 +155,8 @@ public: }; template -void assetload::assets_setup(const Assets* assets) { - if (auto mapPtr = assets->getMap()) { +void assetload::assets_setup(const Assets& assets) { + if (auto mapPtr = assets.getMap()) { for (const auto& entry : **mapPtr) { static_cast(entry.second.get())->setup(); } diff --git a/src/assets/AssetsLoader.cpp b/src/assets/AssetsLoader.cpp index 1005a6b69..0043ea04d 100644 --- a/src/assets/AssetsLoader.cpp +++ b/src/assets/AssetsLoader.cpp @@ -79,8 +79,8 @@ void AssetsLoader::loadNext() { try { aloader_func loader = getLoader(entry.tag); auto postfunc = - loader(this, paths, entry.filename, entry.alias, entry.config); - postfunc(&assets); + loader(*this, paths, entry.filename, entry.alias, entry.config); + postfunc(assets); } catch (const parsing_error& err) { error = err.errorLog(); } catch (const std::runtime_error& err) { @@ -361,17 +361,17 @@ const ResPaths& AssetsLoader::getPaths() const { } class LoaderWorker : public util::Worker { - AssetsLoader* loader; + AssetsLoader& loader; public: - LoaderWorker(AssetsLoader* loader) : loader(loader) { + LoaderWorker(AssetsLoader& loader) : loader(loader) { } assetload::postfunc operator()(const aloader_entry& entry ) override { - aloader_func loadfunc = loader->getLoader(entry.tag); + aloader_func loadfunc = loader.getLoader(entry.tag); return loadfunc( loader, - loader->getPaths(), + loader.getPaths(), entry.filename, entry.alias, entry.config @@ -383,8 +383,8 @@ std::shared_ptr AssetsLoader::startTask(runnable onDone, int maxWorkers) { auto pool = std::make_shared>( "assets-loader-pool", - [=]() { return std::make_unique(this); }, - [this](const assetload::postfunc& func) { func(&assets); }, + [=]() { return std::make_unique(*this); }, + [this](const assetload::postfunc& func) { func(assets); }, maxWorkers ); pool->setOnComplete(std::move(onDone)); diff --git a/src/assets/AssetsLoader.hpp b/src/assets/AssetsLoader.hpp index d919d1f08..891cc0cfc 100644 --- a/src/assets/AssetsLoader.hpp +++ b/src/assets/AssetsLoader.hpp @@ -72,7 +72,7 @@ struct ModelCfg : AssetCfg { using aloader_func = std::function< assetload:: - postfunc(AssetsLoader*, const ResPaths&, const std::string&, const std::string&, std::shared_ptr)>; + postfunc(AssetsLoader&, const ResPaths&, const std::string&, const std::string&, std::shared_ptr)>; struct aloader_entry { AssetType tag; @@ -99,6 +99,8 @@ class AssetsLoader { void processPreloadConfigs(const Content* content); public: AssetsLoader(Engine& engine, Assets& assets, const ResPaths& paths); + AssetsLoader(const AssetsLoader&) = delete; + void addLoader(AssetType tag, aloader_func func); /// @brief Enqueue asset load diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index 1098c7a51..2c173fdf1 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -35,8 +35,8 @@ static debug::Logger logger("assetload-funcs"); namespace fs = std::filesystem; -static bool load_animation( - Assets* assets, +static bool load_animated_texture( + Assets& assets, const ResPaths& paths, const std::string& atlasName, const std::string& directory, @@ -45,7 +45,7 @@ static bool load_animation( ); assetload::postfunc assetload::texture( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& filename, const std::string& name, @@ -54,12 +54,12 @@ assetload::postfunc assetload::texture( auto actualFile = paths.find(filename + ".png"); try { std::shared_ptr image(imageio::read(actualFile)); - return [name, image, actualFile](auto assets) { - assets->store(Texture::from(image.get()), name); + return [name, image, actualFile](auto& assets) { + assets.store(Texture::from(image.get()), name); }; } catch (const std::runtime_error& err) { logger.error() << actualFile.string() << ": " << err.what(); - return [](auto) {}; + return [](auto&) {}; } } @@ -86,7 +86,7 @@ static auto read_program(const ResPaths& paths, const std::string& filename) { } assetload::postfunc assetload::shader( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& filename, const std::string& name, @@ -99,8 +99,8 @@ assetload::postfunc assetload::shader( io::path vertexFile = paths.find(filename + ".glslv"); io::path fragmentFile = paths.find(filename + ".glslf"); - return [=](auto assets) { - assets->store( + return [=](auto& assets) { + assets.store( Shader::create( {vertexFile.string(), vertex}, {fragmentFile.string(), fragment} @@ -111,7 +111,7 @@ assetload::postfunc assetload::shader( } assetload::postfunc assetload::posteffect( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& file, const std::string& name, @@ -131,7 +131,7 @@ assetload::postfunc assetload::posteffect( std::string vertexSource = std::move(vertex.code); std::string fragmentSource = std::move(fragment.code); - return [=](auto assets) { + return [=](auto& assets) { auto program = Shader::create( {effectFile.string(), vertexSource}, {effectFile.string(), fragmentSource} @@ -140,7 +140,7 @@ assetload::postfunc assetload::posteffect( if (settings) { advanced = dynamic_cast(settings.get())->advanced; } - assets->store( + assets.store( std::make_shared(advanced, std::move(program), params), name ); @@ -160,7 +160,7 @@ static bool append_atlas(AtlasBuilder& atlas, const io::path& file) { } assetload::postfunc assetload::atlas( - AssetsLoader* loader, + AssetsLoader& loader, const ResPaths& paths, const std::string& directory, const std::string& name, @@ -171,13 +171,13 @@ assetload::postfunc assetload::atlas( for (const auto& file : paths.listdir(directory)) { if (!imageio::is_read_supported(file.extension())) continue; - loader->add( + loader.add( AssetType::TEXTURE, directory + "/" + file.stem(), name + "/" + file.stem() ); } - return [](auto){}; + return [](auto&){}; } AtlasBuilder builder; for (const auto& file : paths.listdir(directory)) { @@ -186,17 +186,17 @@ assetload::postfunc assetload::atlas( } std::set names = builder.getNames(); Atlas* atlas = builder.build(ATLAS_EXTRUSION, false).release(); - return [=](auto assets) { + return [=](Assets& assets) { atlas->prepare(); - assets->store(std::unique_ptr(atlas), name); + assets.store(std::unique_ptr(atlas), name); for (const auto& file : names) { - load_animation(assets, paths, name, directory, file, atlas); + load_animated_texture(assets, paths, name, directory, file, atlas); } }; } assetload::postfunc assetload::font( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& filename, const std::string& name, @@ -206,15 +206,15 @@ assetload::postfunc assetload::font( auto ext = fs::path(filename).extension().string(); if (ext == ".ttf" || ext == ".otf") { logger.info() << "loading vector font " << util::quote(filename); - return [=](Assets* assets) { + return [=](Assets& assets) { using FontFile = vector_fonts::FontFile; FontFile* fontFile; - if ((fontFile = assets->get(filename)) == nullptr) { + if ((fontFile = assets.get(filename)) == nullptr) { auto fontFilePtr = vector_fonts::load_font(paths.find(filename).string()); fontFile = fontFilePtr.get(); - assets->store(fontFilePtr, filename); + assets.store(fontFilePtr, filename); } - assets->store(fontFile->createInstance(cfg ? cfg->size : 16), name); + assets.store(fontFile->createInstance(cfg ? cfg->size : 16), name); }; } @@ -230,24 +230,24 @@ assetload::postfunc assetload::font( pages->push_back(nullptr); } } - return [=](auto assets) { - assets->store(Font::createBitmapFont(std::move(*pages)), name); + return [=](auto& assets) { + assets.store(Font::createBitmapFont(std::move(*pages)), name); }; } assetload::postfunc assetload::layout( - AssetsLoader*, + AssetsLoader&, const ResPaths&, const std::string& file, const std::string& name, const std::shared_ptr& config ) { - return [=](auto assets) { + return [=](auto& assets) { try { auto cfg = std::dynamic_pointer_cast(config); size_t pos = name.find(':'); auto prefix = name.substr(0, pos); - assets->store( + assets.store( UiDocument::read( *cfg->gui, cfg->env, @@ -265,7 +265,7 @@ assetload::postfunc assetload::layout( }; } assetload::postfunc assetload::sound( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& file, const std::string& name, @@ -307,17 +307,17 @@ assetload::postfunc assetload::sound( } auto sound = baseSound.release(); - return [=](auto assets) { - assets->store(std::unique_ptr(sound), name); + return [=](auto& assets) { + assets.store(std::unique_ptr(sound), name); }; } -static void request_textures(AssetsLoader* loader, const model::Model& model) { +static void request_textures(AssetsLoader& loader, const model::Model& model) { for (auto& mesh : model.meshes) { if (mesh.texture.find('$') == std::string::npos && mesh.texture.find(':') == std::string::npos) { auto filename = TEXTURES_FOLDER + "/" + mesh.texture; - loader->add( + loader.add( AssetType::TEXTURE, filename, mesh.texture, nullptr ); } @@ -325,7 +325,7 @@ static void request_textures(AssetsLoader* loader, const model::Model& model) { } assetload::postfunc assetload::model( - AssetsLoader* loader, + AssetsLoader& loader, const ResPaths& paths, const std::string& file, const std::string& name, @@ -337,7 +337,9 @@ assetload::postfunc assetload::model( if (io::exists(path)) { auto bytes = io::read_bytes_buffer(path); auto modelVEC3 = std::make_shared(vec3::load(path.string(), bytes)); - return [loader, name, cfg, modelVEC3=std::move(modelVEC3)](Assets* assets) { + return [&loader, name, cfg, modelVEC3 = std::move(modelVEC3)]( + Assets& assets + ) { if (cfg && cfg->squashed) { model::Model fullModel; for (auto& entry : modelVEC3->models) { @@ -347,7 +349,7 @@ assetload::postfunc assetload::model( fullModel.merge(std::move(model)); } request_textures(loader, fullModel); - assets->store( + assets.store( std::make_unique(fullModel), name ); @@ -360,7 +362,7 @@ assetload::postfunc assetload::model( if (name != modelName) { fullName += "." + modelName; } - assets->store( + assets.store( std::make_unique(model.model), fullName ); @@ -374,9 +376,9 @@ assetload::postfunc assetload::model( auto text = io::read_string(path); try { auto model = obj::parse(path.string(), text).release(); - return [=](Assets* assets) { + return [=, &loader](Assets& assets) { request_textures(loader, *model); - assets->store(std::unique_ptr(model), name); + assets.store(std::unique_ptr(model), name); }; } catch (const parsing_error& err) { std::cerr << err.errorLog() << std::endl; @@ -409,22 +411,22 @@ assetload::postfunc assetload::model( if (vcmModel.parts.size() == 1 || (cfg && cfg->squashed)) { auto modelPtr = std::make_unique(std::move(vcmModel.squash())).release(); - return [=](Assets* assets) { + return [=, &loader](Assets& assets) { auto model = std::unique_ptr(modelPtr); request_textures(loader, *model); - assets->store(std::move(model), name); + assets.store(std::move(model), name); logger.info() << "store model " << util::quote(name); }; } else { auto vcmModelPtr = std::make_unique(std::move(vcmModel)).release(); - return [=](Assets* assets) { + return [=](Assets& assets) { auto vcmModel = std::unique_ptr(vcmModelPtr); for (auto& [partName, model] : vcmModel->parts) { auto fullName = name + "." + partName; logger.info() << "store model part " << util::quote(partName) << " as " << util::quote(fullName); - assets->store( + assets.store( std::make_unique(std::move(model)), fullName ); @@ -433,7 +435,7 @@ assetload::postfunc assetload::model( bone->setModel(name + "." + bone->model.name); } logger.info() << "store skeleton " << util::quote(name); - assets->store( + assets.store( std::make_unique( std::move(*vcmModel->skeleton) ), @@ -448,13 +450,13 @@ assetload::postfunc assetload::model( } assetload::postfunc assetload::skeleton( - AssetsLoader* loader, + AssetsLoader& loader, const ResPaths& paths, const std::string& file, const std::string& name, const std::shared_ptr& settings ) { - return [=](auto assets) { + return [&loader, &paths, file, name](auto& assets) { auto path = paths.find(file + ".json"); std::string text = io::read_string(path); auto skeleton = rigging::SkeletonConfig::parse(text, file, name); @@ -465,12 +467,12 @@ assetload::postfunc assetload::skeleton( model = model.substr(0, pos); } if (!model.empty()) { - loader->add( + loader.add( AssetType::MODEL, MODELS_FOLDER + "/" + model, model ); } } - assets->store(std::move(skeleton), name); + assets.store(std::move(skeleton), name); }; } @@ -552,8 +554,8 @@ inline bool contains( return false; } -static bool load_animation( - Assets* assets, +static bool load_animated_texture( + Assets& assets, const ResPaths& paths, const std::string& atlasName, const std::string& directory, @@ -591,10 +593,10 @@ static bool load_animation( auto animation = create_animation( srcAtlas.get(), dstAtlas, name, builder.getNames(), frameList ); - assets->store( + assets.store( std::move(srcAtlas), atlasName + "/" + name + "_animation" ); - assets->store(animation); + assets.store(animation); return true; } return true; diff --git a/src/assets/assetload_funcs.hpp b/src/assets/assetload_funcs.hpp index 50a255bb2..036d8ef6b 100644 --- a/src/assets/assetload_funcs.hpp +++ b/src/assets/assetload_funcs.hpp @@ -14,56 +14,56 @@ struct AssetCfg; /// @brief see AssetsLoader.h: aloader_func namespace assetload { postfunc texture( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& filename, const std::string& name, const std::shared_ptr& settings ); postfunc shader( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& filename, const std::string& name, const std::shared_ptr& settings ); postfunc atlas( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& directory, const std::string& name, const std::shared_ptr& settings ); postfunc font( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& filename, const std::string& name, const std::shared_ptr& settings ); postfunc layout( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& file, const std::string& name, const std::shared_ptr& settings ); postfunc sound( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& file, const std::string& name, const std::shared_ptr& settings ); postfunc model( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& file, const std::string& name, const std::shared_ptr& settings ); postfunc posteffect( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& file, const std::string& name, @@ -71,7 +71,7 @@ namespace assetload { ); postfunc skeleton( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& file, const std::string& name, diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 15da3697e..c59dc0a9a 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -298,6 +298,8 @@ void scripting::on_content_load(Content* content) { } catch (const std::exception&) { lua::pushnil(L); lua::setglobal(L, "__vc_app"); + lua::pushnil(L); + lua::setglobal(L, "__vc_internals"); throw; } lua::pushnil(L); From df5bc1f9fed90c565ea0796bda4078f7d2b36e98 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 25 Aug 2026 22:33:31 +0300 Subject: [PATCH 27/63] cleanup --- src/assets/AssetsLoader.hpp | 21 +++++++++++++-------- src/assets/assetload_funcs.hpp | 4 ++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/assets/AssetsLoader.hpp b/src/assets/AssetsLoader.hpp index 891cc0cfc..7d495a3fb 100644 --- a/src/assets/AssetsLoader.hpp +++ b/src/assets/AssetsLoader.hpp @@ -1,5 +1,12 @@ #pragma once +#include "delegates.hpp" +#include "interfaces/Task.hpp" +#include "typedefs.hpp" +#include "Assets.hpp" +#include "data/dv.hpp" +#include "io/fwd.hpp" + #include #include #include @@ -8,13 +15,6 @@ #include #include -#include "delegates.hpp" -#include "interfaces/Task.hpp" -#include "typedefs.hpp" -#include "Assets.hpp" -#include "data/dv.hpp" -#include "io/fwd.hpp" - class ResPaths; class AssetsLoader; class Content; @@ -72,7 +72,12 @@ struct ModelCfg : AssetCfg { using aloader_func = std::function< assetload:: - postfunc(AssetsLoader&, const ResPaths&, const std::string&, const std::string&, std::shared_ptr)>; + postfunc( + AssetsLoader&, + const ResPaths&, + const std::string&, + const std::string&, + std::shared_ptr)>; struct aloader_entry { AssetType tag; diff --git a/src/assets/assetload_funcs.hpp b/src/assets/assetload_funcs.hpp index 036d8ef6b..085d45742 100644 --- a/src/assets/assetload_funcs.hpp +++ b/src/assets/assetload_funcs.hpp @@ -1,10 +1,10 @@ #pragma once +#include "Assets.hpp" + #include #include -#include "Assets.hpp" - class ResPaths; class Assets; class AssetsLoader; From 104c74e6537f571260387e270448a32e9cabe610 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 25 Aug 2026 23:09:14 +0300 Subject: [PATCH 28/63] feat: animation track as asset (WIP) --- res/modules/animation.lua | 12 +++++++-- res/scripts/components/animator.lua | 6 +++-- res/scripts/stdlib.lua | 2 ++ src/assets/Assets.hpp | 1 + src/assets/AssetsLoader.cpp | 38 +++++++++++++++++------------ src/assets/assetload_funcs.cpp | 15 ++++++++++++ src/assets/assetload_funcs.hpp | 7 ++++++ src/constants.hpp | 9 ++++--- src/logic/scripting/scripting.cpp | 15 ++++++++++++ src/logic/scripting/scripting.hpp | 5 ++++ 10 files changed, 86 insertions(+), 24 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index bc2b29fb5..b7cce0fce 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -279,7 +279,7 @@ end local cached_tracks = {} -function this.load_vca(filepath) +function this.load_vca(filepath, identifier) local track = cached_tracks[filepath] if track then return track @@ -287,8 +287,16 @@ function this.load_vca(filepath) local source = file.read(filepath) local raw_track = parse_track(xml.parse_vcd(source, "track")) track = this.compile_track(raw_track, filepath) - cached_tracks[filepath] = track + cached_tracks[identifier] = track return track end +function this.get_track(identifier) + return cached_tracks[identifier] +end + +function __vc_internals.load_vca_animation(filepath, identifier) + this.load_vca(filepath, identifier) +end + return this diff --git a/res/scripts/components/animator.lua b/res/scripts/components/animator.lua index dc80066db..810ea7648 100644 --- a/res/scripts/components/animator.lua +++ b/res/scripts/components/animator.lua @@ -2,9 +2,10 @@ local rig = entity.skeleton local states = {} -function create_state(name, track, max_intencity) +function create_state(name, track_id, max_intencity) states[name] = { - track = track, + track = nil, + track_id = track_id, timer = 0.0, fade_time = 0.0, fade_timer = 0.0, @@ -47,6 +48,7 @@ local function update(delta) end end + state.track = state.track or animation.get_track(state.track_id) if state.track and intensity > 0.0 then state.track.func(rig, state.timer, intensity * state.max_intencity) end diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 56d86bf8f..d2fdc59ab 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -332,6 +332,8 @@ end world.raycast = entities.__world_raycast entities.__world_raycast = nil +animation = require "core:animation" + __vc_scripts_registry = require "core:internal/scripts_registry" file.open = require "core:internal/stream_providers/file" diff --git a/src/assets/Assets.hpp b/src/assets/Assets.hpp index 10c71093b..768ab0b17 100644 --- a/src/assets/Assets.hpp +++ b/src/assets/Assets.hpp @@ -18,6 +18,7 @@ class Assets; enum class AssetType { + ANIMATION, TEXTURE, SHADER, FONT, diff --git a/src/assets/AssetsLoader.cpp b/src/assets/AssetsLoader.cpp index 0043ea04d..eb23385fe 100644 --- a/src/assets/AssetsLoader.cpp +++ b/src/assets/AssetsLoader.cpp @@ -26,17 +26,20 @@ namespace fs = std::filesystem; static debug::Logger logger("assets-loader"); -AssetsLoader::AssetsLoader(Engine& engine, Assets& assets, const ResPaths& paths) +AssetsLoader::AssetsLoader( + Engine& engine, Assets& assets, const ResPaths& paths +) : engine(engine), assets(assets), paths(paths) { - addLoader(AssetType::SHADER, assetload::shader); - addLoader(AssetType::TEXTURE, assetload::texture); - addLoader(AssetType::FONT, assetload::font); + addLoader(AssetType::ANIMATION, assetload::animation); addLoader(AssetType::ATLAS, assetload::atlas); + addLoader(AssetType::FONT, assetload::font); addLoader(AssetType::LAYOUT, assetload::layout); - addLoader(AssetType::SOUND, assetload::sound); addLoader(AssetType::MODEL, assetload::model); addLoader(AssetType::POST_EFFECT, assetload::posteffect); + addLoader(AssetType::SHADER, assetload::shader); addLoader(AssetType::SKELETON, assetload::skeleton); + addLoader(AssetType::SOUND, assetload::sound); + addLoader(AssetType::TEXTURE, assetload::texture); } void AssetsLoader::addLoader(AssetType tag, aloader_func func) { @@ -127,24 +130,26 @@ void AssetsLoader::tryAddSound(const std::string& name) { static std::string assets_def_folder(AssetType tag) { switch (tag) { - case AssetType::FONT: - return FONTS_FOLDER; - case AssetType::SHADER: - return SHADERS_FOLDER; - case AssetType::TEXTURE: - return TEXTURES_FOLDER; + case AssetType::ANIMATION: + return ANIMATION_FOLDER; case AssetType::ATLAS: return TEXTURES_FOLDER; + case AssetType::FONT: + return FONTS_FOLDER; case AssetType::LAYOUT: return LAYOUTS_FOLDER; - case AssetType::SOUND: - return SOUNDS_FOLDER; case AssetType::MODEL: return MODELS_FOLDER; case AssetType::POST_EFFECT: return POST_EFFECTS_FOLDER; + case AssetType::SHADER: + return SHADERS_FOLDER; case AssetType::SKELETON: return SKELETONS_FOLDER; + case AssetType::SOUND: + return SOUNDS_FOLDER; + case AssetType::TEXTURE: + return TEXTURES_FOLDER; } return ""; } @@ -221,14 +226,15 @@ void AssetsLoader::processPreloadList(AssetType tag, const dv::value& list) { void AssetsLoader::processPreloadConfig(const io::path& file) { auto root = io::read_json(file); + processPreloadList(AssetType::ANIMATION, root["animation"]); processPreloadList(AssetType::ATLAS, root["atlases"]); processPreloadList(AssetType::FONT, root["fonts"]); - processPreloadList(AssetType::SHADER, root["shaders"]); - processPreloadList(AssetType::TEXTURE, root["textures"]); - processPreloadList(AssetType::SOUND, root["sounds"]); processPreloadList(AssetType::MODEL, root["models"]); processPreloadList(AssetType::POST_EFFECT, root["post-effects"]); + processPreloadList(AssetType::SHADER, root["shaders"]); processPreloadList(AssetType::SKELETON, root["skeletons"]); + processPreloadList(AssetType::SOUND, root["sounds"]); + processPreloadList(AssetType::TEXTURE, root["textures"]); // layouts are loaded automatically } diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index 2c173fdf1..f406cef45 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -23,6 +23,7 @@ #include "graphics/core/Shader.hpp" #include "graphics/core/Texture.hpp" #include "graphics/core/TextureAnimation.hpp" +#include "logic/scripting/scripting.hpp" #include "io/io.hpp" #include "util/stringutil.hpp" @@ -44,6 +45,20 @@ static bool load_animated_texture( Atlas* dstAtlas ); +assetload::postfunc assetload::animation( + AssetsLoader&, + const ResPaths& paths, + const std::string& filename, + const std::string& name, + const std::shared_ptr& +) { + auto path = paths.find(filename + ".vca"); + if (io::is_regular_file(path)) { + scripting::load_vca_animation(path, name); + } + return [](auto&) {}; +} + assetload::postfunc assetload::texture( AssetsLoader&, const ResPaths& paths, diff --git a/src/assets/assetload_funcs.hpp b/src/assets/assetload_funcs.hpp index 085d45742..9ccc601ab 100644 --- a/src/assets/assetload_funcs.hpp +++ b/src/assets/assetload_funcs.hpp @@ -13,6 +13,13 @@ struct AssetCfg; /// @brief see AssetsLoader.h: aloader_func namespace assetload { + postfunc animation( + AssetsLoader&, + const ResPaths& paths, + const std::string& filename, + const std::string& name, + const std::shared_ptr& settings + ); postfunc texture( AssetsLoader&, const ResPaths& paths, diff --git a/src/constants.hpp b/src/constants.hpp index 5bacfe078..03900b9e3 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -75,13 +75,14 @@ inline constexpr int DEFAULT_FONT_SIZE = 16; inline constexpr int DEFAULT_PRE_RENDER_FONT_PAGES = 1; // must be at least 1 -inline const std::string SHADERS_FOLDER = "shaders"; -inline const std::string TEXTURES_FOLDER = "textures"; +inline const std::string ANIMATION_FOLDER = "animation"; inline const std::string FONTS_FOLDER = "fonts"; inline const std::string LAYOUTS_FOLDER = "layouts"; -inline const std::string SOUNDS_FOLDER = "sounds"; inline const std::string MODELS_FOLDER = "models"; -inline const std::string SKELETONS_FOLDER = "skeletons"; inline const std::string POST_EFFECTS_FOLDER = "shaders/effects"; +inline const std::string SHADERS_FOLDER = "shaders"; +inline const std::string SKELETONS_FOLDER = "skeletons"; +inline const std::string SOUNDS_FOLDER = "sounds"; +inline const std::string TEXTURES_FOLDER = "textures"; inline const std::string FONT_DEFAULT = "normal"; diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index c59dc0a9a..661bec651 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -844,6 +844,21 @@ void scripting::load_layout_script( lua::pop(L); } +void scripting::load_vca_animation( + const io::path& file, + const std::string& identifier +) { + auto L = lua::get_main_state(); + if (lua::get_from_registry(L, "internals", "load_vca_animation", true)) { + lua::pushlstring(L, file.string()); + lua::pushlstring(L, identifier); + if (lua::call(L, 2, 0)) { + lua::pop(L); + } + lua::pop(L); + } +} + void scripting::close() { lua::finalize(); content = nullptr; diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index 0044213cd..e774864ef 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -251,6 +251,11 @@ namespace scripting { const std::string& fileName, UiDocScript& script ); + + void load_vca_animation( + const io::path& file, + const std::string& identifier + ); /// @brief Finalize lua state. Using scripting after will lead to Lua panic void close(); From 103ca99d818dac5f4dd4caccf42bf2f3895abd2e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 30 Aug 2026 20:15:34 +0300 Subject: [PATCH 29/63] cleanup AssetsLoader --- src/assets/Assets.hpp | 11 --- src/assets/AssetsLoader.cpp | 121 ++++++++++++++++---------------- src/assets/AssetsLoader.hpp | 32 ++++----- src/engine/AssetsManagement.cpp | 9 ++- 4 files changed, 80 insertions(+), 93 deletions(-) diff --git a/src/assets/Assets.hpp b/src/assets/Assets.hpp index 768ab0b17..68f802047 100644 --- a/src/assets/Assets.hpp +++ b/src/assets/Assets.hpp @@ -71,7 +71,6 @@ class Assets { using assets_map = std::unordered_map>; std::unordered_map assets; - std::vector setupFuncs; public: Assets(util::ObjectsKeeper* vault); Assets(const Assets&) = delete; @@ -143,16 +142,6 @@ public: } return &mapIter->second; } - - void setup() { - for (auto& setupFunc : setupFuncs) { - setupFunc(*this); - } - } - - void addSetupFunc(assetload::setupfunc setupfunc) { - setupFuncs.push_back(setupfunc); - } }; template diff --git a/src/assets/AssetsLoader.cpp b/src/assets/AssetsLoader.cpp index eb23385fe..207dab3a1 100644 --- a/src/assets/AssetsLoader.cpp +++ b/src/assets/AssetsLoader.cpp @@ -120,14 +120,6 @@ static void add_layouts( } } -void AssetsLoader::tryAddSound(const std::string& name) { - if (name.empty()) { - return; - } - std::string file = SOUNDS_FOLDER + "/" + name; - add(AssetType::SOUND, file, name); -} - static std::string assets_def_folder(AssetType tag) { switch (tag) { case AssetType::ANIMATION: @@ -165,7 +157,8 @@ void AssetsLoader::processPreload( } std::shared_ptr config = nullptr; map.at("path").get(path); - logger.debug() << "processing preload " << util::quote(name) << " path: " << util::quote(path); + logger.debug() << "processing preload " << util::quote(name) + << " path: " << util::quote(path); switch (tag) { case AssetType::SOUND: { bool keepPCM = false; @@ -269,65 +262,73 @@ static void add_variant(AssetsLoader& loader, const Variant& variant) { } } -void AssetsLoader::addDefaults(AssetsLoader& loader, const Content* content) { - loader.processPreloadConfigs(content); - if (content) { - for (auto& entry : content->getBlockMaterials()) { - auto& material = *entry.second; - loader.tryAddSound(material.stepsSound); - loader.tryAddSound(material.placeSound); - loader.tryAddSound(material.breakSound); - loader.tryAddSound(material.hitSound); +void AssetsLoader::addDefaults(const Content* content) { + processPreloadConfigs(content); + if (content == nullptr) { + return; + } + auto tryAddSound = [this](const std::string& name){ + if (name.empty()) { + return; } + std::string file = SOUNDS_FOLDER + "/" + name; + add(AssetType::SOUND, file, name); + }; + for (auto& entry : content->getBlockMaterials()) { + auto& material = *entry.second; + tryAddSound(material.stepsSound); + tryAddSound(material.placeSound); + tryAddSound(material.breakSound); + tryAddSound(material.hitSound); + } - for (auto& entry : content->getPacks()) { - auto pack = entry.second.get(); - auto& info = pack->getInfo(); - io::path folder = info.folder / "layouts"; - add_layouts(pack->getEnvironment(), info.id, folder, loader); - } + for (auto& entry : content->getPacks()) { + auto pack = entry.second.get(); + auto& info = pack->getInfo(); + io::path folder = info.folder / "layouts"; + add_layouts(pack->getEnvironment(), info.id, folder, *this); + } - for (const auto& entry : content->getPacks()) { - io::path skeletonsDir = entry.first + ":skeletons"; - if (!io::is_directory(skeletonsDir)) { - continue; - } - for (const auto& file : io::directory_iterator(skeletonsDir)) { - loader.add( - AssetType::SKELETON, - (file.parent() / file.stem()).string(), - entry.first + ":" + file.stem() - ); - } + for (const auto& entry : content->getPacks()) { + io::path skeletonsDir = entry.first + ":skeletons"; + if (!io::is_directory(skeletonsDir)) { + continue; } + for (const auto& file : io::directory_iterator(skeletonsDir)) { + add( + AssetType::SKELETON, + (file.parent() / file.stem()).string(), + entry.first + ":" + file.stem() + ); + } + } - for (const auto& [_, def] : content->blocks.getDefs()) { - if (def->variants) { - for (const auto& variant : def->variants->variants) { - add_variant(loader, variant); - } - } else { - add_variant(loader, def->defaults); + for (const auto& [_, def] : content->blocks.getDefs()) { + if (def->variants) { + for (const auto& variant : def->variants->variants) { + add_variant(*this, variant); } + } else { + add_variant(*this, def->defaults); } - for (const auto& [_, def] : content->items.getDefs()) { - if (def->modelName.find(':') == std::string::npos) { - loader.add( - AssetType::MODEL, - MODELS_FOLDER + "/" + def->modelName, - def->modelName - ); - } + } + for (const auto& [_, def] : content->items.getDefs()) { + if (def->modelName.find(':') == std::string::npos) { + add( + AssetType::MODEL, + MODELS_FOLDER + "/" + def->modelName, + def->modelName + ); } - for (const auto& [_, def] : content->entities.getDefs()) { - if (def->skeletonName.find(':') == std::string::npos) { - // expecting a VCM with skeleton - loader.add( - AssetType::MODEL, - MODELS_FOLDER + "/" + def->skeletonName, - def->skeletonName - ); - } + } + for (const auto& [_, def] : content->entities.getDefs()) { + if (def->skeletonName.find(':') == std::string::npos) { + // expecting a VCM with skeleton + add( + AssetType::MODEL, + MODELS_FOLDER + "/" + def->skeletonName, + def->skeletonName + ); } } } diff --git a/src/assets/AssetsLoader.hpp b/src/assets/AssetsLoader.hpp index 7d495a3fb..48676d6ed 100644 --- a/src/assets/AssetsLoader.hpp +++ b/src/assets/AssetsLoader.hpp @@ -87,21 +87,6 @@ struct aloader_entry { }; class AssetsLoader { - Engine& engine; - Assets& assets; - std::map loaders; - std::queue entries; - std::set> enqueued; - const ResPaths& paths; - - void tryAddSound(const std::string& name); - - void processPreload( - AssetType tag, const std::string& name, const dv::value& map - ); - void processPreloadList(AssetType tag, const dv::value& list); - void processPreloadConfig(const io::path& file); - void processPreloadConfigs(const Content* content); public: AssetsLoader(Engine& engine, Assets& assets, const ResPaths& paths); AssetsLoader(const AssetsLoader&) = delete; @@ -132,9 +117,8 @@ public: aloader_func getLoader(AssetType tag); /// @brief Enqueue core and content assets - /// @param loader target loader /// @param content engine content - static void addDefaults(AssetsLoader& loader, const Content* content); + void addDefaults(const Content* content); static bool loadExternalTexture( AssetsLoader& loader, @@ -144,4 +128,18 @@ public: Assets& getAssets(); Engine& getEngine(); +private: + Engine& engine; + Assets& assets; + std::map loaders; + std::queue entries; + std::set> enqueued; + const ResPaths& paths; + + void processPreload( + AssetType tag, const std::string& name, const dv::value& map + ); + void processPreloadList(AssetType tag, const dv::value& list); + void processPreloadConfig(const io::path& file); + void processPreloadConfigs(const Content* content); }; diff --git a/src/engine/AssetsManagement.cpp b/src/engine/AssetsManagement.cpp index 13df11220..a71382835 100644 --- a/src/engine/AssetsManagement.cpp +++ b/src/engine/AssetsManagement.cpp @@ -53,11 +53,11 @@ void AssetsManagement::loadAssets(Content* content) { logger.info() << "loading assets"; Shader::preprocessor->setPaths(&paths.resPaths); - auto new_assets = std::make_unique( + auto newAssets = std::make_unique( settings.system.preserveAssetsDuringFrame.get() ? &assetsVault : nullptr ); - AssetsLoader loader(engine, *new_assets, paths.resPaths); - AssetsLoader::addDefaults(loader, content); + AssetsLoader loader(engine, *newAssets, paths.resPaths); + loader.addDefaults(content); // no need // correct log messages order is more useful @@ -73,11 +73,10 @@ void AssetsManagement::loadAssets(Content* content) { loader.loadNext(); } } - assets = std::move(new_assets); + assets = std::move(newAssets); if (content) { ModelsGenerator::prepare(*content, *assets); } - assets->setup(); engine.getGUI().onAssetsLoad(assets.get()); } From c8605fe78bbbbb55735ea7f5cd0b01e6075e34ef Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 30 Aug 2026 23:37:58 +0300 Subject: [PATCH 30/63] feat: reloading assets for textures, models and animation --- src/assets/Assets.cpp | 4 +- src/assets/Assets.hpp | 23 +++- src/assets/AssetsLoader.cpp | 32 ++++- src/assets/AssetsLoader.hpp | 14 +- src/assets/assetload_funcs.cpp | 227 +++++++++++++++++++-------------- 5 files changed, 190 insertions(+), 110 deletions(-) diff --git a/src/assets/Assets.cpp b/src/assets/Assets.cpp index 3e0776747..5b306a375 100644 --- a/src/assets/Assets.cpp +++ b/src/assets/Assets.cpp @@ -1,6 +1,8 @@ #include "Assets.hpp" +#include "AssetsLoader.hpp" +#include "io/path.hpp" -Assets::Assets(util::ObjectsKeeper* vault) : vault(vault) { +Assets::Assets(util::ObjectsKeeper* vault) : vault(vault), assetsLoadInfo(std::make_unique()) { } Assets::~Assets() { diff --git a/src/assets/Assets.hpp b/src/assets/Assets.hpp index 68f802047..5858e17c5 100644 --- a/src/assets/Assets.hpp +++ b/src/assets/Assets.hpp @@ -3,6 +3,7 @@ #include "util/stringutil.hpp" #include "util/ObjectsKeeper.hpp" #include "graphics/core/TextureAnimation.hpp" +#include "io/fwd.hpp" #include #include @@ -16,6 +17,8 @@ #include class Assets; +struct AssetsLoadInfo; +class AssetsLoader; enum class AssetType { ANIMATION, @@ -34,8 +37,6 @@ namespace assetload { /// @brief final work to do in the main thread using postfunc = std::function; - using setupfunc = std::function; - template void assets_setup(const Assets&); @@ -63,15 +64,13 @@ namespace assetload { return reason; } }; + } class Assets { - util::ObjectsKeeper* vault; - std::vector animations; - - using assets_map = std::unordered_map>; - std::unordered_map assets; public: + using assets_map = std::unordered_map>; + Assets(util::ObjectsKeeper* vault); Assets(const Assets&) = delete; ~Assets(); @@ -142,6 +141,16 @@ public: } return &mapIter->second; } + + AssetsLoadInfo& getLoadInfo() const { + return *assetsLoadInfo; + } +private: + util::ObjectsKeeper* vault; + std::vector animations; + + std::unordered_map assets; + std::unique_ptr assetsLoadInfo; }; template diff --git a/src/assets/AssetsLoader.cpp b/src/assets/AssetsLoader.cpp index 207dab3a1..454f2434f 100644 --- a/src/assets/AssetsLoader.cpp +++ b/src/assets/AssetsLoader.cpp @@ -29,7 +29,10 @@ static debug::Logger logger("assets-loader"); AssetsLoader::AssetsLoader( Engine& engine, Assets& assets, const ResPaths& paths ) - : engine(engine), assets(assets), paths(paths) { + : engine(engine), + assets(assets), + paths(paths), + assetsLoadInfo(assets.getLoadInfo()) { addLoader(AssetType::ANIMATION, assetload::animation); addLoader(AssetType::ATLAS, assetload::atlas); addLoader(AssetType::FONT, assetload::font); @@ -53,11 +56,11 @@ void AssetsLoader::add( std::shared_ptr settings, bool overwrite ) { - if (!overwrite && enqueued.find({tag, alias}) != enqueued.end()){ + if (!overwrite && enqueued.find({alias, tag}) != enqueued.end()){ return; } entries.push(aloader_entry {tag, filename, alias, std::move(settings)}); - enqueued.insert({tag, alias}); + enqueued.insert({alias, tag}); } bool AssetsLoader::hasNext() const { @@ -75,7 +78,7 @@ aloader_func AssetsLoader::getLoader(AssetType tag) { } void AssetsLoader::loadNext() { - const aloader_entry& entry = entries.front(); + aloader_entry& entry = entries.front(); logger.info() << "loading " << entry.filename << " as " << entry.alias; std::string error {}; @@ -96,6 +99,8 @@ void AssetsLoader::loadNext() { entries.pop(); throw assetload::error(tag, std::move(filename), std::move(error)); } + assetsLoadInfo.processedEntries[{entry.alias, entry.tag}] = + std::move(entry); entries.pop(); } @@ -411,3 +416,22 @@ std::shared_ptr AssetsLoader::startTask(runnable onDone, int maxWorkers) { } return pool; } + +void AssetsLoader::attachToFile(const io::path& file, AssetFullId assetId) { + assetsLoadInfo.referencedAssets.insert({file, std::move(assetId)}); +} + +int AssetsLoader::addReload(const io::path& path, AssetsLoader& dst) { + int added = 0; + + auto range = assetsLoadInfo.referencedAssets.equal_range(path); + for (auto it = range.first; it != range.second; ++it) { + const auto& recipe = assetsLoadInfo.processedEntries.find(it->second); + if (recipe != assetsLoadInfo.processedEntries.end()) { + dst.entries.push(recipe->second); + added++; + } + } + + return added; +} diff --git a/src/assets/AssetsLoader.hpp b/src/assets/AssetsLoader.hpp index 48676d6ed..86350d2cb 100644 --- a/src/assets/AssetsLoader.hpp +++ b/src/assets/AssetsLoader.hpp @@ -86,6 +86,13 @@ struct aloader_entry { std::shared_ptr config; }; +using AssetFullId = std::pair; + +struct AssetsLoadInfo { + std::map processedEntries; + std::multimap referencedAssets; +}; + class AssetsLoader { public: AssetsLoader(Engine& engine, Assets& assets, const ResPaths& paths); @@ -126,6 +133,9 @@ public: const std::vector& alternatives ); + int addReload(const io::path& path, AssetsLoader& dst); + void attachToFile(const io::path& file, AssetFullId assetId); + Assets& getAssets(); Engine& getEngine(); private: @@ -133,8 +143,10 @@ private: Assets& assets; std::map loaders; std::queue entries; - std::set> enqueued; + std::set enqueued; + const ResPaths& paths; + AssetsLoadInfo& assetsLoadInfo; void processPreload( AssetType tag, const std::string& name, const dv::value& map diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index f406cef45..c14fd8fb2 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -46,13 +46,15 @@ static bool load_animated_texture( ); assetload::postfunc assetload::animation( - AssetsLoader&, + AssetsLoader& loader, const ResPaths& paths, const std::string& filename, const std::string& name, const std::shared_ptr& ) { auto path = paths.find(filename + ".vca"); + loader.attachToFile(path, {name, AssetType::ANIMATION}); + if (io::is_regular_file(path)) { scripting::load_vca_animation(path, name); } @@ -60,20 +62,21 @@ assetload::postfunc assetload::animation( } assetload::postfunc assetload::texture( - AssetsLoader&, + AssetsLoader& loader, const ResPaths& paths, const std::string& filename, const std::string& name, const std::shared_ptr& ) { - auto actualFile = paths.find(filename + ".png"); + auto file = paths.find(filename + ".png"); + loader.attachToFile(file, {name, AssetType::TEXTURE}); try { - std::shared_ptr image(imageio::read(actualFile)); - return [name, image, actualFile](auto& assets) { + std::shared_ptr image(imageio::read(file)); + return [name, image, file](auto& assets) { assets.store(Texture::from(image.get()), name); }; } catch (const std::runtime_error& err) { - logger.error() << actualFile.string() << ": " << err.what(); + logger.error() << file.string() << ": " << err.what(); return [](auto&) {}; } } @@ -339,6 +342,123 @@ static void request_textures(AssetsLoader& loader, const model::Model& model) { } } +static assetload::postfunc load_vec3_model( + const io::path& file, + AssetsLoader& loader, + const std::string& name, + const std::shared_ptr& config +) { + auto bytes = io::read_bytes_buffer(file); + auto modelVEC3 = std::make_shared(vec3::load(file.string(), bytes)); + return [&loader, name, file, config, modelVEC3 = std::move(modelVEC3)]( + Assets& assets + ) { + loader.attachToFile(file, {name, AssetType::MODEL}); + if (config && config->squashed) { + model::Model fullModel; + for (auto& entry : modelVEC3->models) { + auto& vec3model = entry.second; + auto& model = vec3model.model; + model.translate(vec3model.origin); + fullModel.merge(std::move(model)); + } + request_textures(loader, fullModel); + assets.store( + std::make_unique(fullModel), + name + ); + logger.info() << "store model " << util::quote(name); + return; + } + for (auto& [modelName, model] : modelVEC3->models) { + request_textures(loader, model.model); + std::string fullName = name; + if (name != modelName) { + fullName += "." + modelName; + } + assets.store( + std::make_unique(model.model), + fullName + ); + logger.info() << "store model " << util::quote(modelName) + << " as " << util::quote(fullName); + } + }; +} + +static assetload::postfunc load_obj_model( + const io::path& file, + AssetsLoader& loader, + const std::string& name, + const std::shared_ptr& +) { + auto text = io::read_string(file); + try { + auto model = obj::parse(file.string(), text).release(); + return [=, &loader](Assets& assets) { + request_textures(loader, *model); + assets.store(std::unique_ptr(model), name); + }; + } catch (const parsing_error& err) { + std::cerr << err.errorLog() << std::endl; + throw; + } +} + +static assetload::postfunc load_vcm_model( + const io::path& file, + AssetsLoader& loader, + const std::string& name, + const std::shared_ptr& cfg +) { + auto text = io::read_string(file); + try { + auto vcmModel = vcm::parse(file.string(), text, file.extension() == ".xml"); + + assert(vcmModel.parts.size() > 0); + + if (vcmModel.parts.size() == 1 || (cfg && cfg->squashed)) { + auto modelPtr = + std::make_unique(std::move(vcmModel.squash())) + .release(); + return [=, &loader](Assets& assets) { + auto model = std::unique_ptr(modelPtr); + request_textures(loader, *model); + assets.store(std::move(model), name); + logger.info() << "store model " << util::quote(name); + }; + } + auto vcmModelPtr = + std::make_unique(std::move(vcmModel)).release(); + return [=](Assets& assets) { + auto vcmModel = std::unique_ptr(vcmModelPtr); + for (auto& [partName, model] : vcmModel->parts) { + auto fullName = name + "." + partName; + logger.info() + << "store model part " << util::quote(partName) + << " as " << util::quote(fullName); + assets.store( + std::make_unique(std::move(model)), + fullName + ); + } + for (auto& bone : vcmModel->skeleton->getBones()) { + bone->setModel(name + "." + bone->model.name); + } + logger.info() << "store skeleton " << util::quote(name); + assets.store( + std::make_unique( + std::move(*vcmModel->skeleton) + ), + name + ); + }; + } catch (const parsing_error& err) { + std::cerr << err.errorLog() << std::endl; + throw; + } +} + assetload::postfunc assetload::model( AssetsLoader& loader, const ResPaths& paths, @@ -346,59 +466,15 @@ assetload::postfunc assetload::model( const std::string& name, const std::shared_ptr& config ) { - auto cfg = std::dynamic_pointer_cast(config); + auto modelConfig = std::dynamic_pointer_cast(config); auto path = paths.find(file + ".vec3"); if (io::exists(path)) { - auto bytes = io::read_bytes_buffer(path); - auto modelVEC3 = std::make_shared(vec3::load(path.string(), bytes)); - return [&loader, name, cfg, modelVEC3 = std::move(modelVEC3)]( - Assets& assets - ) { - if (cfg && cfg->squashed) { - model::Model fullModel; - for (auto& entry : modelVEC3->models) { - auto& vec3model = entry.second; - auto& model = vec3model.model; - model.translate(vec3model.origin); - fullModel.merge(std::move(model)); - } - request_textures(loader, fullModel); - assets.store( - std::make_unique(fullModel), - name - ); - logger.info() << "store model " << util::quote(name); - return; - } - for (auto& [modelName, model] : modelVEC3->models) { - request_textures(loader, model.model); - std::string fullName = name; - if (name != modelName) { - fullName += "." + modelName; - } - assets.store( - std::make_unique(model.model), - fullName - ); - logger.info() << "store model " << util::quote(modelName) - << " as " << util::quote(fullName); - } - }; + return load_vec3_model(path, loader, name, modelConfig); } path = paths.find(file + ".obj"); if (io::exists(path)) { - auto text = io::read_string(path); - try { - auto model = obj::parse(path.string(), text).release(); - return [=, &loader](Assets& assets) { - request_textures(loader, *model); - assets.store(std::unique_ptr(model), name); - }; - } catch (const parsing_error& err) { - std::cerr << err.errorLog() << std::endl; - throw; - } + return load_obj_model(path, loader, name, modelConfig); } std::array extensions { @@ -418,50 +494,7 @@ assetload::postfunc assetload::model( throw std::runtime_error("could not to find model " + util::quote(file)); } - auto text = io::read_string(path); - try { - auto vcmModel = vcm::parse(path.string(), text, path.extension() == ".xml"); - - assert(vcmModel.parts.size() > 0); - - if (vcmModel.parts.size() == 1 || (cfg && cfg->squashed)) { - auto modelPtr = std::make_unique(std::move(vcmModel.squash())).release(); - return [=, &loader](Assets& assets) { - auto model = std::unique_ptr(modelPtr); - request_textures(loader, *model); - assets.store(std::move(model), name); - logger.info() << "store model " << util::quote(name); - }; - } else { - auto vcmModelPtr = std::make_unique(std::move(vcmModel)).release(); - return [=](Assets& assets) { - auto vcmModel = std::unique_ptr(vcmModelPtr); - for (auto& [partName, model] : vcmModel->parts) { - auto fullName = name + "." + partName; - logger.info() - << "store model part " << util::quote(partName) - << " as " << util::quote(fullName); - assets.store( - std::make_unique(std::move(model)), - fullName - ); - } - for (auto& bone : vcmModel->skeleton->getBones()) { - bone->setModel(name + "." + bone->model.name); - } - logger.info() << "store skeleton " << util::quote(name); - assets.store( - std::make_unique( - std::move(*vcmModel->skeleton) - ), - name - ); - }; - } - } catch (const parsing_error& err) { - std::cerr << err.errorLog() << std::endl; - throw; - } + return load_vcm_model(path, loader, name, modelConfig); } assetload::postfunc assetload::skeleton( From 53237dd10203431c2cdeb3dc9a037e82aae4783d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 30 Aug 2026 23:40:12 +0300 Subject: [PATCH 31/63] remove extra argument --- src/assets/AssetsLoader.cpp | 4 ++-- src/assets/AssetsLoader.hpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/assets/AssetsLoader.cpp b/src/assets/AssetsLoader.cpp index 454f2434f..04bde9a6f 100644 --- a/src/assets/AssetsLoader.cpp +++ b/src/assets/AssetsLoader.cpp @@ -421,14 +421,14 @@ void AssetsLoader::attachToFile(const io::path& file, AssetFullId assetId) { assetsLoadInfo.referencedAssets.insert({file, std::move(assetId)}); } -int AssetsLoader::addReload(const io::path& path, AssetsLoader& dst) { +int AssetsLoader::addReload(const io::path& path) { int added = 0; auto range = assetsLoadInfo.referencedAssets.equal_range(path); for (auto it = range.first; it != range.second; ++it) { const auto& recipe = assetsLoadInfo.processedEntries.find(it->second); if (recipe != assetsLoadInfo.processedEntries.end()) { - dst.entries.push(recipe->second); + entries.push(recipe->second); added++; } } diff --git a/src/assets/AssetsLoader.hpp b/src/assets/AssetsLoader.hpp index 86350d2cb..a61c3b59e 100644 --- a/src/assets/AssetsLoader.hpp +++ b/src/assets/AssetsLoader.hpp @@ -133,7 +133,7 @@ public: const std::vector& alternatives ); - int addReload(const io::path& path, AssetsLoader& dst); + int addReload(const io::path& path); void attachToFile(const io::path& file, AssetFullId assetId); Assets& getAssets(); From 395dc15fb6460d5e50c3e613cb1ea46d3332580c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 30 Aug 2026 23:51:28 +0300 Subject: [PATCH 32/63] fix models load functions --- src/assets/assetload_funcs.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index c14fd8fb2..a8f30739b 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -348,6 +348,7 @@ static assetload::postfunc load_vec3_model( const std::string& name, const std::shared_ptr& config ) { + loader.attachToFile(file, {name, AssetType::MODEL}); auto bytes = io::read_bytes_buffer(file); auto modelVEC3 = std::make_shared(vec3::load(file.string(), bytes)); return [&loader, name, file, config, modelVEC3 = std::move(modelVEC3)]( @@ -392,6 +393,7 @@ static assetload::postfunc load_obj_model( const std::string& name, const std::shared_ptr& ) { + loader.attachToFile(file, {name, AssetType::MODEL}); auto text = io::read_string(file); try { auto model = obj::parse(file.string(), text).release(); @@ -411,6 +413,7 @@ static assetload::postfunc load_vcm_model( const std::string& name, const std::shared_ptr& cfg ) { + loader.attachToFile(file, {name, AssetType::MODEL}); auto text = io::read_string(file); try { auto vcmModel = vcm::parse(file.string(), text, file.extension() == ".xml"); From 22c0f5b9d28318b458f1f0e64611ca3d22136eeb Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 31 Aug 2026 01:18:35 +0300 Subject: [PATCH 33/63] test models hot-reload --- src/animation/rigging.cpp | 4 +++- src/devtools/DebuggingServer.cpp | 20 +++++++++++++++++--- src/engine/Engine.hpp | 4 ++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/animation/rigging.cpp b/src/animation/rigging.cpp index ceaed3365..d5e36a82c 100644 --- a/src/animation/rigging.cpp +++ b/src/animation/rigging.cpp @@ -208,7 +208,9 @@ void SkeletonConfig::render( model, &skeleton.textures ); - } + } else if (!node->model.name.empty()) { + node->model.updateFlag = true; + } } } diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index cb50c87a8..ecb85bead 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -1,9 +1,13 @@ #include "DebuggingServer.hpp" -#include "engine/Engine.hpp" -#include "network/Network.hpp" -#include "debug/Logger.hpp" +#include "assets/AssetsLoader.hpp" #include "coders/json.hpp" +#include "data/dv.hpp" +#include "debug/Logger.hpp" +#include "engine/AssetsManagement.hpp" +#include "engine/Engine.hpp" +#include "io/path.hpp" +#include "network/Network.hpp" using namespace devtools; @@ -260,6 +264,16 @@ bool DebuggingServer::performCommand( } }); return true; + } else if (type == "hot-reload") { + if (!map.has("file")) { + return false; + } + logger.info() << "hot-reload: " << map["file"].asString(); + int count = engine.getAssetsManagement().acquireBackgroundLoader().addReload( + map["file"].asString() + ); + logger.info() << "enqueued to reload: " << count; + return false; } else { logger.error() << "unsupported command '" << type << "'"; } diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index 153acf4df..668ce360b 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -190,5 +190,9 @@ public: return *appScripts; } + AssetsManagement& getAssetsManagement() const { + return *assets; + } + void detachDebugger(); }; From 872b18fbe64905ea268415bd836267ed66bc7be1 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 31 Aug 2026 22:39:40 +0300 Subject: [PATCH 34/63] update debugging_protocol.md --- doc/specs/debugging_protocol.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/specs/debugging_protocol.md b/doc/specs/debugging_protocol.md index a66753a9b..fbf206f28 100644 --- a/doc/specs/debugging_protocol.md +++ b/doc/specs/debugging_protocol.md @@ -1,4 +1,4 @@ -# VC-DBG protocol v1 +# VC-DBG protocol v1.1 ## Notes @@ -116,6 +116,15 @@ Then `value` is: } ``` +### File hot-reload signal + +```json +{ + "type": "hot-reload", + "file": "string" +} +``` + ## Server-to-client ### Response signals From 873359fdd9a24c5560dadd22af59af0978236612 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 31 Aug 2026 22:49:14 +0300 Subject: [PATCH 35/63] remove extra font file extension check --- src/assets/assetload_funcs.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index a8f30739b..ea444fb5a 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -29,7 +29,6 @@ #include #include -#include #include static debug::Logger logger("assetload-funcs"); @@ -129,13 +128,15 @@ assetload::postfunc assetload::shader( } assetload::postfunc assetload::posteffect( - AssetsLoader&, + AssetsLoader& loader, const ResPaths& paths, const std::string& file, const std::string& name, const std::shared_ptr& settings ) { io::path effectFile = paths.find(file + ".glsl"); + loader.attachToFile(effectFile, {name, AssetType::POST_EFFECT}); + std::string effectSource = io::read_string(effectFile); auto& preprocessor = *Shader::preprocessor; @@ -222,7 +223,7 @@ assetload::postfunc assetload::font( ) { auto cfg = std::dynamic_pointer_cast(config); auto ext = fs::path(filename).extension().string(); - if (ext == ".ttf" || ext == ".otf") { + if (!ext.empty()) { logger.info() << "loading vector font " << util::quote(filename); return [=](Assets& assets) { using FontFile = vector_fonts::FontFile; @@ -402,7 +403,8 @@ static assetload::postfunc load_obj_model( assets.store(std::unique_ptr(model), name); }; } catch (const parsing_error& err) { - std::cerr << err.errorLog() << std::endl; + logger.error() << "error on loading model " << name << ": " + << err.errorLog(); throw; } } @@ -457,7 +459,8 @@ static assetload::postfunc load_vcm_model( ); }; } catch (const parsing_error& err) { - std::cerr << err.errorLog() << std::endl; + logger.error() << "error on loading model " << name << ": " + << err.errorLog(); throw; } } @@ -629,8 +632,7 @@ static bool load_animated_texture( read_anim_file(animFile, frameList); } for (const auto& file : paths.listdir(animsDir + "/" + name)) { - if (!frameList.empty() && - !contains(frameList, file.stem())) { + if (!frameList.empty() && !contains(frameList, file.stem())) { continue; } if (!append_atlas(builder, file)) continue; From 39a3c1201f33e257a9061e14152f6102910b9674 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 1 Sep 2026 23:01:47 +0300 Subject: [PATCH 36/63] add animation.action --- res/modules/animation.lua | 23 +++++++++++++++++++++++ res/modules/internal/lifetime_events.lua | 13 +++++++++---- src/logic/scripting/lua/lua_engine.cpp | 2 +- src/logic/scripting/lua/lua_util.hpp | 1 + src/logic/scripting/scripting.cpp | 6 +++--- src/logic/scripting/scripting_hud.cpp | 13 ++++++++----- 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index b7cce0fce..3464273e8 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -299,4 +299,27 @@ function __vc_internals.load_vca_animation(filepath, identifier) this.load_vca(filepath, identifier) end +local running_actions = {} + +function this.action(func) + table.insert(running_actions, coroutine.create(func)) +end + +function __vc_internals.on_animation_frame() + for i=#running_actions,1,-1 do + local co = running_actions[i] + local status, result = coroutine.resume(co) + if not status then + debug.error("error in animation action: "..result) + table.remove(running_actions, i) + elseif coroutine.status(co) == "dead" then + table.remove(running_actions, i) + end + end +end + +function __vc_internals.stop_all_actions() + running_actions = {} +end + return this diff --git a/res/modules/internal/lifetime_events.lua b/res/modules/internal/lifetime_events.lua index e1460c22c..1d60d969d 100644 --- a/res/modules/internal/lifetime_events.lua +++ b/res/modules/internal/lifetime_events.lua @@ -1,5 +1,6 @@ -local __app = __vc_app +local app = __vc_app local __rules = rules +local internals = __vc_internals local function configure_SSAO() -- Temporary using slot to configure built-in SSAO effect @@ -32,7 +33,7 @@ local function configure_SSAO() end events.on("core:setting.graphics.ssao.set", update_ssao_quality) - update_ssao_quality(__app.get_setting("graphics.ssao")) + update_ssao_quality(app.get_setting("graphics.ssao")) end local function __vc_on_hud_open() @@ -165,7 +166,8 @@ file.__close_all_descriptors = nil gui_util.__reset_local = nil stdcomp.reset = nil -local function __vc_on_world_quit() +function internals.on_world_quit() + internals.stop_all_actions() __rules.clear() __gui_util_reset_local() __stdcomp_reset() @@ -215,13 +217,16 @@ function time.post_runnable(runnable) table.insert(__post_runnables, runnable) end +function internals.on_render() + internals.on_animation_frame() +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, __vc_is_post_runnable_context = function() return __vc__is_post_runnable diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index 850b34109..b85608260 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -77,7 +77,7 @@ static void create_libs(State* L, StateType stateType) { createtable(L, 0, 0); pushvalue(L, -1); setglobal(L, "__vc_internals"); - setregistry(L, "internals"); + setregistry(L, lua::INTERNALS_TABLE); if (stateType == StateType::SCRIPT) { getregistry(L, "app"); diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index fd41134a2..b1cd802bd 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -16,6 +16,7 @@ namespace lua { inline std::string CHUNKS_TABLE = "$C"; // precompiled lua chunks inline std::string PACK_ENVS_TABLE = "$P"; inline std::string ENVS_TABLE = "$E"; + inline std::string INTERNALS_TABLE = "$I"; extern std::unordered_map usertypeNames; int userdata_destructor(lua::State* L); diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 661bec651..8f3dc28eb 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -291,7 +291,7 @@ void scripting::on_content_load(Content* content) { lua::getregistry(L, "app"); lua::setglobal(L, "__vc_app"); - lua::getregistry(L, "internals"); + lua::getregistry(L, lua::INTERNALS_TABLE); lua::setglobal(L, "__vc_internals"); try { load_script("post_content.lua", true); @@ -367,7 +367,7 @@ void scripting::on_world_quit() { for (auto& pack : content_control->getAllContentPacks()) { lua::emit_event(L, pack.id + ":.worldquit"); } - if (lua::getglobal(L, "__vc_on_world_quit")) { + if (lua::get_from_registry(L, lua::INTERNALS_TABLE, "on_world_quit", true)) { lua::call_nothrow(L, 0, 0); } scripting::level = nullptr; @@ -849,7 +849,7 @@ void scripting::load_vca_animation( const std::string& identifier ) { auto L = lua::get_main_state(); - if (lua::get_from_registry(L, "internals", "load_vca_animation", true)) { + if (lua::get_from_registry(L, lua::INTERNALS_TABLE, "load_vca_animation", true)) { lua::pushlstring(L, file.string()); lua::pushlstring(L, identifier); if (lua::call(L, 2, 0)) { diff --git a/src/logic/scripting/scripting_hud.cpp b/src/logic/scripting/scripting_hud.cpp index 59b928d21..075896710 100644 --- a/src/logic/scripting/scripting_hud.cpp +++ b/src/logic/scripting/scripting_hud.cpp @@ -63,12 +63,15 @@ void scripting::on_frontend_init( } void scripting::on_frontend_render() { + auto L = lua::get_main_state(); + + if (lua::get_from_registry(L, lua::INTERNALS_TABLE, "on_render", true)) { + lua::call_nothrow(L, 0, 0); + } for (auto& pack : content_control->getAllContentPacks()) { - lua::emit_event( - lua::get_main_state(), - pack.id + ":.hudrender", - [](lua::State* L) { return 0; } - ); + lua::emit_event(L, pack.id + ":.hudrender", [](lua::State* L) { + return 0; + }); } } From 6c568e6706cea0301c336c53aebc9e3d0f037ef6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 1 Sep 2026 23:32:44 +0300 Subject: [PATCH 37/63] cleanup internal functions --- .../internal}/internal_events.lua | 6 ++- res/modules/internal/lifetime_events.lua | 45 ++++++++----------- res/scripts/stdlib.lua | 7 +-- src/engine/Engine.cpp | 2 +- src/logic/scripting/lua/lua_util.hpp | 6 +++ src/logic/scripting/scripting.cpp | 31 +++++-------- src/logic/scripting/scripting.hpp | 2 +- src/logic/scripting/scripting_hud.cpp | 8 +--- 8 files changed, 47 insertions(+), 60 deletions(-) rename res/{scripts => modules/internal}/internal_events.lua (97%) diff --git a/res/scripts/internal_events.lua b/res/modules/internal/internal_events.lua similarity index 97% rename from res/scripts/internal_events.lua rename to res/modules/internal/internal_events.lua index c71df5d61..b62758d4c 100644 --- a/res/scripts/internal_events.lua +++ b/res/modules/internal/internal_events.lua @@ -1,3 +1,5 @@ +local internals = __vc_internals + local updating_blocks = {} local present_queues = {} local REGISTER_BIT = 0x1 @@ -5,7 +7,7 @@ local UPDATING_BIT = 0x2 local PRESENT_BIT = 0x4 local REMOVED_BIT = 0x8 -block.__perform_ticks = function(delta) +function internals.tick_updating_blocks(delta) for id, entry in pairs(updating_blocks) do entry.timer = entry.timer + delta local steps = math.floor(entry.timer / entry.delta * #entry / 3) @@ -63,7 +65,7 @@ end local block_pull_register_events = block.__pull_register_events block.__pull_register_events = nil -block.__process_register_events = function() +function internals.process_block_register_events() local register_events = block_pull_register_events() if not register_events then return diff --git a/res/modules/internal/lifetime_events.lua b/res/modules/internal/lifetime_events.lua index 1d60d969d..ced28eb99 100644 --- a/res/modules/internal/lifetime_events.lua +++ b/res/modules/internal/lifetime_events.lua @@ -36,7 +36,7 @@ local function configure_SSAO() update_ssao_quality(app.get_setting("graphics.ssao")) end -local function __vc_on_hud_open() +function internals.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 @@ -131,7 +131,8 @@ local function ScheduleGroup() end local RULES_FILE = "world:rules.toml" -local function __vc_on_world_open() + +function internals.on_world_open() time.schedules.world = ScheduleGroup() if not file.exists(RULES_FILE) then @@ -143,15 +144,15 @@ local function __vc_on_world_open() end end -local function __vc_on_world_tick(tps) +function internals.on_world_tick(tps) time.schedules.world:tick(1.0 / tps) end -local function __vc_process_before_quit() - block.__process_register_events() +function internals.process_before_quit() + internals.process_block_register_events() end -local function __vc_on_world_save() +function internals.on_world_save() local rule_values = {} for name, rule in pairs(__rules.rules) do rule_values[name] = rule.value @@ -178,8 +179,6 @@ 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 @@ -191,26 +190,30 @@ local function __process_post_runnables() __post_runnables = {} end - __vc_update_coroutines() + internals.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()) + internals.process_block_register_events() + internals.tick_updating_blocks(time.delta()) end end -local __vc__is_post_runnable = false +local is_post_runnable = false -function __vc__process_post_runnables() - __vc__is_post_runnable = true +function internals.process_post_runnables() + 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 + is_post_runnable = false +end + +function internals.is_post_runnable_context() + return is_post_runnable end function time.post_runnable(runnable) @@ -221,14 +224,4 @@ function internals.on_render() internals.on_animation_frame() 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__process_post_runnables = __vc__process_post_runnables, - __vc_is_post_runnable_context = function() - return __vc__is_post_runnable - end -} +return {} diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index d2fdc59ab..069986e8e 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -1,4 +1,5 @@ local __app = __vc_app +local internals = __vc_internals local enable_experimental = __app.get_setting("debug.enable-experimental") ------------------------------------------------ @@ -45,7 +46,7 @@ local function complete_app_lib(app) app.tick = __app_tick local function call_in_app_script_co(func, ...) - if __vc_is_post_runnable_context() then + if internals.post_runnable_context() then func(...) return end @@ -208,7 +209,7 @@ function start_coroutine(chunk, name) __vc_named_coroutines[name] = co end -function __vc_update_coroutines() +function internals.update_coroutines() local dead = {} for name, co in pairs(__vc_named_coroutines) do local success, err = coroutine.resume(co) @@ -368,6 +369,7 @@ core.get_core_token = audio.input.__get_core_token require "core:internal/console" require "core:internal/deprecated" +require "core:internal/internal_events" ------------------------------------------ @@ -397,5 +399,4 @@ __vc_app = nil __vc_internals = nil __vc_lock_internal_modules() __vc_lock_internal_modules = nil -__vc_update_coroutines = nil __VC_SCRIPT_NAME = "" diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index f0e20f922..b3185f371 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -179,7 +179,7 @@ void Engine::initialize(CoreParameters coreParameters) { content = std::make_unique( *project, *paths, input.get(), [this]() { onContentLoad(); } ); - scripting::initialize(this); + scripting::initialize(*this); if (!isHeadless()) { gui->getMenu()->setPageLoader(scripting::create_page_loader()); diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index b1cd802bd..4ce6e2b51 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -636,6 +636,12 @@ namespace lua { int call(lua::State*, int argc, int nresults = -1); int call_nothrow(lua::State*, int argc, int nresults = 1); + inline void call_internal(lua::State* L, const std::string& name) { + if (lua::get_from_registry(L, lua::INTERNALS_TABLE, name, true)) { + lua::call_nothrow(L, 0, 0); + } + } + inline int eval( lua::State* L, int env, diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 8f3dc28eb..de3f61079 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -65,14 +65,13 @@ int scripting::load_script( return lua::execute(lua::get_main_state(), env, src, fileName); } -void scripting::initialize(Engine* engine) { - scripting::engine = engine; - scripting::content_control = &engine->getContentControl(); - lua::initialize(engine->getPaths(), engine->getCoreParameters()); +void scripting::initialize(Engine& engine) { + scripting::engine = &engine; + scripting::content_control = &engine.getContentControl(); + lua::initialize(engine.getPaths(), engine.getCoreParameters()); load_script(io::path("stdlib.lua"), true); load_script(io::path("classes.lua"), true); - load_script(io::path("internal_events.lua"), true); } class LuaCoroutine : public Process { @@ -244,9 +243,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, "__vc__process_post_runnables")) { - lua::call_nothrow(L, 0, 0); - } + lua::call_internal(L, "process_post_runnables"); } template @@ -321,9 +318,7 @@ void scripting::on_world_load(LevelController* controller) { scripting::controller = controller; auto L = lua::get_main_state(); - if (lua::getglobal(L, "__vc_on_world_open")) { - lua::call_nothrow(L, 0, 0); - } + lua::call_internal(L, "on_world_open"); for (auto& pack : content_control->getAllContentPacks()) { lua::emit_event(L, pack.id + ":.worldopen", [](auto L) { @@ -336,7 +331,7 @@ void scripting::on_world_load(LevelController* controller) { void scripting::on_world_tick(int tps) { auto L = lua::get_main_state(); - if (lua::getglobal(L, "__vc_on_world_tick")) { + if (lua::get_from_registry(L, lua::INTERNALS_TABLE, "on_world_tick", true)) { lua::pushinteger(L, tps); lua::call_nothrow(L, 1, 0); } @@ -350,16 +345,12 @@ void scripting::on_world_save() { for (auto& pack : content_control->getAllContentPacks()) { lua::emit_event(L, pack.id + ":.worldsave"); } - if (lua::getglobal(L, "__vc_on_world_save")) { - lua::call_nothrow(L, 0, 0); - } + lua::call_internal(L, "on_world_save"); } void scripting::process_before_quit() { auto L = lua::get_main_state(); - if (lua::getglobal(L, "__vc_process_before_quit")) { - lua::call_nothrow(L, 0, 0); - } + lua::call_internal(L, "process_before_quit"); } void scripting::on_world_quit() { @@ -367,9 +358,7 @@ void scripting::on_world_quit() { for (auto& pack : content_control->getAllContentPacks()) { lua::emit_event(L, pack.id + ":.worldquit"); } - if (lua::get_from_registry(L, lua::INTERNALS_TABLE, "on_world_quit", true)) { - lua::call_nothrow(L, 0, 0); - } + lua::call_internal(L, "on_world_quit"); scripting::level = nullptr; scripting::content = nullptr; scripting::indices = nullptr; diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index e774864ef..a80a6681b 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -48,7 +48,7 @@ namespace scripting { extern std::ostream* output_stream; extern std::ostream* error_stream; - void initialize(Engine* engine); + void initialize(Engine& engine); void on_content_load(Content* content); void on_content_reset(); diff --git a/src/logic/scripting/scripting_hud.cpp b/src/logic/scripting/scripting_hud.cpp index 075896710..dcd310e37 100644 --- a/src/logic/scripting/scripting_hud.cpp +++ b/src/logic/scripting/scripting_hud.cpp @@ -47,9 +47,7 @@ void scripting::on_frontend_init( load_script("hud_classes.lua"); - if (lua::getglobal(L, "__vc_on_hud_open")) { - lua::call_nothrow(L, 0, 0); - } + lua::call_internal(L, "on_hud_open"); for (auto& pack : content_control->getAllContentPacks()) { lua::emit_event( @@ -65,9 +63,7 @@ void scripting::on_frontend_init( void scripting::on_frontend_render() { auto L = lua::get_main_state(); - if (lua::get_from_registry(L, lua::INTERNALS_TABLE, "on_render", true)) { - lua::call_nothrow(L, 0, 0); - } + lua::call_internal(L, "on_render"); for (auto& pack : content_control->getAllContentPacks()) { lua::emit_event(L, pack.id + ":.hudrender", [](lua::State* L) { return 0; From 372214ab73e0664c177e0e780bcde26b8afcaaf0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 1 Sep 2026 23:50:48 +0300 Subject: [PATCH 38/63] fix --- res/scripts/stdlib.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 069986e8e..ff4be05e9 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -46,7 +46,7 @@ local function complete_app_lib(app) app.tick = __app_tick local function call_in_app_script_co(func, ...) - if internals.post_runnable_context() then + if internals.is_post_runnable_context() then func(...) return end From 507ff7ac9351b018cdbfc1239149b2c16ba28f60 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 2 Sep 2026 00:11:45 +0300 Subject: [PATCH 39/63] fix stack overflow error --- src/logic/scripting/lua/lua_util.hpp | 1 + src/logic/scripting/scripting.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index 4ce6e2b51..61b56cef3 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -639,6 +639,7 @@ namespace lua { inline void call_internal(lua::State* L, const std::string& name) { if (lua::get_from_registry(L, lua::INTERNALS_TABLE, name, true)) { lua::call_nothrow(L, 0, 0); + pop(L); } } diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index de3f61079..87c07caf4 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -334,6 +334,7 @@ void scripting::on_world_tick(int tps) { if (lua::get_from_registry(L, lua::INTERNALS_TABLE, "on_world_tick", true)) { lua::pushinteger(L, tps); lua::call_nothrow(L, 1, 0); + lua::pop(L); } for (auto& pack : content_control->getAllContentPacks()) { lua::emit_event(L, pack.id + ":.worldtick"); From da925021cb9cbb0c3efd6998248510e5e48a9dee Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 2 Sep 2026 23:43:36 +0300 Subject: [PATCH 40/63] move vca parsing to a separate module --- res/modules/animation.lua | 131 ++++----------------------- res/modules/internal/formats/vca.lua | 98 ++++++++++++++++++++ res/scripts/stdlib.lua | 1 + 3 files changed, 119 insertions(+), 111 deletions(-) create mode 100644 res/modules/internal/formats/vca.lua diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 3464273e8..19bc562d8 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -1,15 +1,20 @@ -local this = {} +local internals = __vc_internals -local CH_TRANSLATE = 1 -local CH_ROTATE = 2 +local this = { + CH_TRANSLATE = 1, + CH_ROTATE = 2, -local INT_CONST = 1 -local INT_LINEAR = 2 -local INT_BEZIER = 3 + INT_CONST = 1, + INT_LINEAR = 2, + INT_BEZIER = 3, -local TRACE_CODEGEN = false + TRACE_CODEGEN = false, +} -local DEFAULT_FPS = 60 +local INT_CONST = this.INT_CONST +local INT_BEZIER = this.INT_BEZIER +local CH_TRANSLATE = this.CH_TRANSLATE +local CH_ROTATE = this.CH_ROTATE local function bezier(a, b, c, d, u) local s = 1 - u @@ -151,90 +156,6 @@ local function codegen_track(raw_track, lines, memoised, keysets) return code end -local action_to_channel = { - move = CH_TRANSLATE, - rotate = CH_ROTATE -} - -local curve_to_interp = { - const = INT_CONST, - linear = INT_LINEAR, - bezier = INT_BEZIER, -} - -local function parse_configure(raw_track, node) - if node.fps then - raw_track.fps = node.fps - end - if node.frames then - raw_track.duration = node.frames / node.fps - elseif node.duration then - raw_track.duration = node.duration - end -end - -local function parse_curve(line, node) - line.interp = curve_to_interp[node.curve] - line.keys = {} - for j, key_node in ipairs(node) do - local keyframe = { - frame = tonumber(key_node.frame), - value = tonumber(key_node.value), - } - if line.interp == INT_BEZIER then - keyframe.lx = tonumber(key_node.lx) - keyframe.ly = tonumber(key_node.ly) - keyframe.rx = tonumber(key_node.rx) - keyframe.ry = tonumber(key_node.ry) - end - table.insert(line.keys, keyframe) - end -end - -local function parse_track(root) - local raw_track = { - duration = math.huge, - fps = DEFAULT_FPS, - linesets = {}, - } - local linesets = raw_track.linesets - for i, node in ipairs(root) do - if type(node) == "string" then - goto continue - end - local tag = node['#'] - if tag == "configure" then - parse_configure(raw_track, node) - goto continue - end - - local bone = node.bone - - local lineset = linesets[bone] - if not lineset then - lineset = {lines = {}} - linesets[bone] = lineset - end - - local line = { - axis = ("xyz"):find(node.by), - channel = action_to_channel[tag] - } - if node.func then - line.expression = node.func - elseif node.curve then - parse_curve(line, node) - else - error("not implemented") - end - - table.insert(lineset.lines, line) - ::continue:: - end - return raw_track -end - - function this.compile_track(raw_track, track_name) local code = "" local memoised = {} @@ -261,7 +182,7 @@ function this.compile_track(raw_track, track_name) local src = "return function(rig, t, m)\n local dst = DST\n" .. code .. "\nend" - if TRACE_CODEGEN then + if this.TRACE_CODEGEN then debug.log("[".. string.escape(track_name or "nil").." codegen trace]:\n"..src) end @@ -277,26 +198,14 @@ function this.compile_track(raw_track, track_name) } end -local cached_tracks = {} +local loaded_tracks = {} -function this.load_vca(filepath, identifier) - local track = cached_tracks[filepath] - if track then - return track - end - local source = file.read(filepath) - local raw_track = parse_track(xml.parse_vcd(source, "track")) - track = this.compile_track(raw_track, filepath) - cached_tracks[identifier] = track - return track +function internals.store_animation(name, track) + loaded_tracks[name] = track end function this.get_track(identifier) - return cached_tracks[identifier] -end - -function __vc_internals.load_vca_animation(filepath, identifier) - this.load_vca(filepath, identifier) + return loaded_tracks[identifier] end local running_actions = {} @@ -305,7 +214,7 @@ function this.action(func) table.insert(running_actions, coroutine.create(func)) end -function __vc_internals.on_animation_frame() +function internals.on_animation_frame() for i=#running_actions,1,-1 do local co = running_actions[i] local status, result = coroutine.resume(co) @@ -318,7 +227,7 @@ function __vc_internals.on_animation_frame() end end -function __vc_internals.stop_all_actions() +function internals.stop_all_actions() running_actions = {} end diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua new file mode 100644 index 000000000..613a6e410 --- /dev/null +++ b/res/modules/internal/formats/vca.lua @@ -0,0 +1,98 @@ +local internals = __vc_internals + +local DEFAULT_FPS = 60 +local INT_BEZIER = animation.INT_BEZIER + +local action_to_channel = { + move = animation.CH_TRANSLATE, + rotate = animation.CH_ROTATE +} + +local curve_to_interp = { + const = animation.INT_CONST, + linear = animation.INT_LINEAR, + bezier = animation.INT_BEZIER, +} + +local function parse_configure(raw_track, node) + if node.fps then + raw_track.fps = node.fps + end + if node.frames then + raw_track.duration = node.frames / node.fps + elseif node.duration then + raw_track.duration = node.duration + end +end + +local function parse_curve(line, node) + line.interp = curve_to_interp[node.curve] + line.keys = {} + for j, key_node in ipairs(node) do + local keyframe = { + frame = tonumber(key_node.frame), + value = tonumber(key_node.value), + } + if line.interp == INT_BEZIER then + keyframe.lx = tonumber(key_node.lx) + keyframe.ly = tonumber(key_node.ly) + keyframe.rx = tonumber(key_node.rx) + keyframe.ry = tonumber(key_node.ry) + end + table.insert(line.keys, keyframe) + end +end + +local function parse_track(root) + local raw_track = { + duration = math.huge, + fps = DEFAULT_FPS, + linesets = {}, + } + local linesets = raw_track.linesets + for i, node in ipairs(root) do + if type(node) == "string" then + goto continue + end + local tag = node['#'] + if tag == "configure" then + parse_configure(raw_track, node) + goto continue + end + + local bone = node.bone + + local lineset = linesets[bone] + if not lineset then + lineset = {lines = {}} + linesets[bone] = lineset + end + + local line = { + axis = ("xyz"):find(node.by), + channel = action_to_channel[tag] + } + if node.func then + line.expression = node.func + elseif node.curve then + parse_curve(line, node) + else + error("not implemented") + end + + table.insert(lineset.lines, line) + ::continue:: + end + return raw_track +end + +local function load_vca(filepath) + local source = file.read(filepath) + local raw_track = parse_track(xml.parse_vcd(source, "track")) + return animation.compile_track(raw_track, filepath) +end + +function internals.load_vca_animation(filepath, identifier) + local track = load_vca(filepath) + internals.store_animation(identifier, track) +end diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index ff4be05e9..dee3bf579 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -334,6 +334,7 @@ world.raycast = entities.__world_raycast entities.__world_raycast = nil animation = require "core:animation" +require "core:internal/formats/vca" __vc_scripts_registry = require "core:internal/scripts_registry" From 4370eedc0a4324de15916f1a7890295133aa75c3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 3 Sep 2026 20:28:08 +0300 Subject: [PATCH 41/63] fix rig:set_matrix for nil index --- src/logic/scripting/lua/libs/lib__skeleton.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/logic/scripting/lua/libs/lib__skeleton.cpp b/src/logic/scripting/lua/libs/lib__skeleton.cpp index 2453b121f..a310abe87 100644 --- a/src/logic/scripting/lua/libs/lib__skeleton.cpp +++ b/src/logic/scripting/lua/libs/lib__skeleton.cpp @@ -77,6 +77,9 @@ static int l_get_matrix(lua::State* L) { static int l_set_matrix(lua::State* L) { if (auto skeleton = get_skeleton(L)) { + if (lua::isnoneornil(L, 2)) { + return 0; + } auto index = index_range_check(*skeleton, lua::tointeger(L, 2)); skeleton->pose.matrices[index] = lua::tomat4(L, 3); } From bbdf31fc197ba191dc7025162a6e18993fcd5c67 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 3 Sep 2026 20:46:59 +0300 Subject: [PATCH 42/63] add scale to vca --- res/modules/animation.lua | 17 +++++++++++++---- res/modules/internal/formats/vca.lua | 11 ++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 19bc562d8..302e23993 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -3,6 +3,7 @@ local internals = __vc_internals local this = { CH_TRANSLATE = 1, CH_ROTATE = 2, + CH_SCALE = 3, INT_CONST = 1, INT_LINEAR = 2, @@ -13,8 +14,6 @@ local this = { local INT_CONST = this.INT_CONST local INT_BEZIER = this.INT_BEZIER -local CH_TRANSLATE = this.CH_TRANSLATE -local CH_ROTATE = this.CH_ROTATE local function bezier(a, b, c, d, u) local s = 1 - u @@ -119,6 +118,7 @@ local function codegen_track(raw_track, lines, memoised, keysets) local code = "" local translation = {false, false, false} local rotation = {false, false, false} + local scale = {false, false, false} for i, line in ipairs(lines) do if line.expression then code = code .. "\n local l" .. i .. " = (" .. @@ -130,10 +130,12 @@ local function codegen_track(raw_track, lines, memoised, keysets) i, i, raw_track.fps, line.interp) end - if line.channel == CH_TRANSLATE then + if line.channel == this.CH_TRANSLATE then translation[line.axis] = i - elseif line.channel == CH_ROTATE then + elseif line.channel == this.CH_ROTATE then rotation[line.axis] = i + elseif line.channel == this.CH_SCALE then + scale[line.axis] = i end end @@ -153,6 +155,13 @@ local function codegen_track(raw_track, lines, memoised, keysets) end end + if scale[1] or scale[2] or scale[3] then + code = code .. "\n mat4.scale(dst, {" .. + (scale[1] and ("l" .. scale[1]) or '1').. ", " .. + (scale[2] and ("l" .. scale[2]) or '1').. ", " .. + (scale[3] and ("l" .. scale[3]) or '1').. "}, dst)" + end + return code end diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 613a6e410..48e3b47ad 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -5,7 +5,8 @@ local INT_BEZIER = animation.INT_BEZIER local action_to_channel = { move = animation.CH_TRANSLATE, - rotate = animation.CH_ROTATE + rotate = animation.CH_ROTATE, + scale = animation.CH_SCALE, } local curve_to_interp = { @@ -60,12 +61,11 @@ local function parse_track(root) goto continue end - local bone = node.bone - - local lineset = linesets[bone] + local target_name = node.bone or "" + local lineset = linesets[target_name] if not lineset then lineset = {lines = {}} - linesets[bone] = lineset + linesets[target_name] = lineset end local line = { @@ -83,6 +83,7 @@ local function parse_track(root) table.insert(lineset.lines, line) ::continue:: end + debug.print(raw_track) return raw_track end From 8422349e45d76bd3d53b03c1d7dc1b5df793ba9b Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 3 Sep 2026 21:44:14 +0300 Subject: [PATCH 43/63] fix loaded tracks cache does not reset on assets reset --- res/modules/animation.lua | 11 +++++++- res/modules/internal/lifetime_events.lua | 2 +- src/engine/AssetsManagement.cpp | 32 +++++++++++++++--------- src/logic/scripting/scripting.cpp | 8 ++++++ src/logic/scripting/scripting.hpp | 3 +++ 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 302e23993..9bbc37343 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -208,6 +208,7 @@ function this.compile_track(raw_track, track_name) end local loaded_tracks = {} +local backup_tracks = {} function internals.store_animation(name, track) loaded_tracks[name] = track @@ -236,8 +237,16 @@ function internals.on_animation_frame() end end -function internals.stop_all_actions() +function internals.stop_animation_actions() running_actions = {} end +function internals.backup_and_clear_animation() + loaded_tracks, backup_tracks = backup_tracks, {} +end + +function internals.restore_animation_backup() + loaded_tracks, backup_tracks = backup_tracks, {} +end + return this diff --git a/res/modules/internal/lifetime_events.lua b/res/modules/internal/lifetime_events.lua index ced28eb99..d1750e4cb 100644 --- a/res/modules/internal/lifetime_events.lua +++ b/res/modules/internal/lifetime_events.lua @@ -168,7 +168,7 @@ gui_util.__reset_local = nil stdcomp.reset = nil function internals.on_world_quit() - internals.stop_all_actions() + internals.stop_animation_actions() __rules.clear() __gui_util_reset_local() __stdcomp_reset() diff --git a/src/engine/AssetsManagement.cpp b/src/engine/AssetsManagement.cpp index a71382835..2233b2d44 100644 --- a/src/engine/AssetsManagement.cpp +++ b/src/engine/AssetsManagement.cpp @@ -9,6 +9,7 @@ #include "graphics/core/Shader.hpp" #include "graphics/render/ModelsGenerator.hpp" #include "graphics/ui/GUI.hpp" +#include "logic/scripting/scripting.hpp" static debug::Logger logger("assets-management"); @@ -49,6 +50,8 @@ AssetsLoader& AssetsManagement::acquireBackgroundLoader() { void AssetsManagement::loadAssets(Content* content) { finishBackgroundLoader(); + scripting::on_assets_loading(); + const auto& paths = engine.getPaths(); logger.info() << "loading assets"; Shader::preprocessor->setPaths(&paths.resPaths); @@ -59,19 +62,24 @@ void AssetsManagement::loadAssets(Content* content) { AssetsLoader loader(engine, *newAssets, paths.resPaths); loader.addDefaults(content); - // no need - // correct log messages order is more useful - // todo: before setting to true, check if GLSLExtension thread safe - bool threading = false; // look at three upper lines - if (threading) { - auto task = loader.startTask( - [=]() {}, 0 - ); - task->waitForEnd(); - } else { - while (loader.hasNext()) { - loader.loadNext(); + try { + // no need + // correct log messages order is more useful + // todo: before setting to true, check if GLSLExtension thread safe + bool threading = false; // look at three upper lines + if (threading) { + auto task = loader.startTask( + [=]() {}, 0 + ); + task->waitForEnd(); + } else { + while (loader.hasNext()) { + loader.loadNext(); + } } + } catch (...) { + scripting::revert_assets_loading(); + throw; } assets = std::move(newAssets); if (content) { diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 87c07caf4..071380a63 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -260,6 +260,14 @@ static int push_properties_tables( return 1; } +void scripting::on_assets_loading() { + lua::call_internal(lua::get_main_state(), "backup_and_clear_animation"); +} + +void scripting::revert_assets_loading() { + lua::call_internal(lua::get_main_state(), "restore_animation_backup"); +} + void scripting::on_content_load(Content* content) { scripting::content = content; scripting::indices = content->getIndices(); diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index a80a6681b..da04d7bab 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -50,6 +50,9 @@ namespace scripting { void initialize(Engine& engine); + void on_assets_loading(); + void revert_assets_loading(); + void on_content_load(Content* content); void on_content_reset(); From 6d6d11d6c65cfe7f2e20d2c063de8a0e3864c163 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 5 Sep 2026 17:12:32 +0300 Subject: [PATCH 44/63] add cameras support & add animation.play --- res/modules/animation.lua | 123 +++++++++++++++++++++++---- res/modules/internal/formats/vca.lua | 10 ++- 2 files changed, 114 insertions(+), 19 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 9bbc37343..15bdd1245 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -4,12 +4,13 @@ local this = { CH_TRANSLATE = 1, CH_ROTATE = 2, CH_SCALE = 3, + CH_ZOOM = 4, INT_CONST = 1, INT_LINEAR = 2, INT_BEZIER = 3, - TRACE_CODEGEN = false, + TRACE_CODEGEN = true, } local INT_CONST = this.INT_CONST @@ -111,37 +112,64 @@ local env = { end return left.value * (1.0 - t) + right.value * t end, + set_matrix = function(target, matrix) + local info = mat4.decompose(matrix) + if info then + local pos = info.translation + local rot = info.rotation + local scale = info.scale + if target.set_pos then + target:set_pos(pos) + end + if target.set_rot then + target:set_rot(rot) + end + if target.set_scale then + target:set_scale(scale) + end + end + end } table.extend(env, math) -local function codegen_track(raw_track, lines, memoised, keysets) +local function codegen_track(raw_track, lines, memoised, keysets, transforms) local code = "" + local hastsf = false local translation = {false, false, false} local rotation = {false, false, false} local scale = {false, false, false} for i, line in ipairs(lines) do if line.expression then - code = code .. "\n local l" .. i .. " = (" .. + code = code .. "\n local l" .. i .. " = (" .. process_expression(line.expression, memoised) .. ")" elseif line.keys then keysets[i] = line.keys code = code .. string.format( - "\n local l%d = value_at(keysets[%d], t * %s, %s)", + "\n local l%d = value_at(keysets[%d], t * %s, %s)", i, i, raw_track.fps, line.interp) end if line.channel == this.CH_TRANSLATE then translation[line.axis] = i + hastsf = true elseif line.channel == this.CH_ROTATE then rotation[line.axis] = i + hastsf = true elseif line.channel == this.CH_SCALE then scale[line.axis] = i + hastsf = true + elseif line.channel == this.CH_ZOOM then + code = code .. "\n zoom = l" .. i end end - code = code .. "\n mat4.idt(dst)" + if not hastsf or not transforms then + return code + end + + code = code .. "\n mat4.idt(dst)" if translation[1] or translation[2] or translation[3] then - code = code .. "\n mat4.translate(dst, {" .. + code = code .. "\n mat4.translate(dst, {" .. (translation[1] and ("l" .. translation[1]) or '0').. ", " .. (translation[2] and ("l" .. translation[2]) or '0').. ", " .. (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" @@ -150,13 +178,13 @@ local function codegen_track(raw_track, lines, memoised, keysets) local axis_names = {"X", "Y", "Z"} for axis, var in ipairs(rotation) do if var then - code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. + code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. ",l" .. var .. ", dst)" end end if scale[1] or scale[2] or scale[3] then - code = code .. "\n mat4.scale(dst, {" .. + code = code .. "\n mat4.scale(dst, {" .. (scale[1] and ("l" .. scale[1]) or '1').. ", " .. (scale[2] and ("l" .. scale[2]) or '1').. ", " .. (scale[3] and ("l" .. scale[3]) or '1').. "}, dst)" @@ -165,18 +193,59 @@ local function codegen_track(raw_track, lines, memoised, keysets) return code end +local function codegen_rig_target(raw_track, memoised, keysets) + local code = "\n if target.set_matrix and target.index then\n" + code = code .. " local dst = DST\n" + for bone, lineset in pairs(raw_track.linesets) do + if lineset.target_type ~= "bone" then + goto continue + end + local lineset_code = codegen_track( + raw_track, lineset.lines, memoised, keysets, true) + + code = code .. "\n do" .. lineset_code .. "\n end\n" .. + " target:set_matrix(target:index(" .. string.escape(bone) .. "), dst)\n" + ::continue:: + end + return code .. " end" +end + +local function codegen_object_target(raw_track, memoised, keysets) + local code = "\n if target.set_pos then\n" + code = code .. " local dst = DST\n" + local lineset = raw_track.linesets[""] + if not lineset then + return "" + end + local lineset_code = codegen_track( + raw_track, lineset.lines, memoised, keysets, true) + code = code .. "\n do" .. lineset_code .. "\n end\n" + .. " set_matrix(target, dst)\n" + return code .. " end" +end + +local function codegen_camera_target(raw_track, memoised, keysets) + local code = "\n if target.set_zoom then\n" + code = code .. " local zoom = 1.0\n" + local lineset = raw_track.linesets[""] + if not lineset then + return "" + end + local lineset_code = codegen_track( + raw_track, lineset.lines, memoised, keysets, false) + code = code .. "\n do" .. lineset_code .. "\n end\n" + .. " target:set_zoom(zoom)\n" + return code .. " end" +end + function this.compile_track(raw_track, track_name) local code = "" local memoised = {} local keysets = {} - for bone, lineset in pairs(raw_track.linesets) do - local lineset_code = codegen_track( - raw_track, lineset.lines, memoised, keysets) - - code = code .. "\n do" .. lineset_code .. "\n end\n" .. - " rig:set_matrix(rig:index(" .. string.escape(bone) .. "), dst)\n" - end + code = code .. codegen_rig_target(raw_track, memoised, keysets) + code = code .. codegen_object_target(raw_track, memoised, keysets) + code = code .. codegen_camera_target(raw_track, memoised, keysets) local memoised_code = "" for name, expression in pairs(memoised) do @@ -188,7 +257,7 @@ function this.compile_track(raw_track, track_name) code = memoised_code .. "\n" .. code end - local src = "return function(rig, t, m)\n local dst = DST\n" + local src = "return function(target, t, m)\n" .. code .. "\nend" if this.TRACE_CODEGEN then @@ -219,13 +288,18 @@ function this.get_track(identifier) end local running_actions = {} +local playing_tracks = {} function this.action(func) table.insert(running_actions, coroutine.create(func)) end +function this.play(name, target) + table.insert(playing_tracks, {name=name, target=target, timer=0.0}) +end + function internals.on_animation_frame() - for i=#running_actions,1,-1 do + for i=1,#running_actions do local co = running_actions[i] local status, result = coroutine.resume(co) if not status then @@ -235,6 +309,21 @@ function internals.on_animation_frame() table.remove(running_actions, i) end end + for i=1,#playing_tracks do + local track_info = playing_tracks[i] + local track = loaded_tracks[track_info.name] + if not track then + debug.error("animation track not found: "..track_info.name) + table.remove(playing_tracks, i) + else + track_info.timer = track_info.timer + time.delta() + if track_info.timer > track.duration then + table.remove(playing_tracks, i) + else + track.func(track_info.target, track_info.timer) + end + end + end end function internals.stop_animation_actions() diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 48e3b47ad..ecc3a195b 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -7,6 +7,7 @@ local action_to_channel = { move = animation.CH_TRANSLATE, rotate = animation.CH_ROTATE, scale = animation.CH_SCALE, + zoom = animation.CH_ZOOM, } local curve_to_interp = { @@ -61,15 +62,19 @@ local function parse_track(root) goto continue end + local target_type = nil + if node.bone then + target_type = "bone" + end local target_name = node.bone or "" local lineset = linesets[target_name] if not lineset then - lineset = {lines = {}} + lineset = {lines = {}, target_type = target_type} linesets[target_name] = lineset end local line = { - axis = ("xyz"):find(node.by), + axis = node.by and ("xyz"):find(node.by) or "", channel = action_to_channel[tag] } if node.func then @@ -90,6 +95,7 @@ end local function load_vca(filepath) local source = file.read(filepath) local raw_track = parse_track(xml.parse_vcd(source, "track")) + debug.print(raw_track) return animation.compile_track(raw_track, filepath) end From 32876aa9e461afb687ddb1f94a8672bd81543018 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 5 Sep 2026 19:19:13 +0300 Subject: [PATCH 45/63] fix maths_inline mat4.idt(dst) --- res/modules/internal/maths_inline.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/res/modules/internal/maths_inline.lua b/res/modules/internal/maths_inline.lua index e02fa96fe..4a6671cdf 100644 --- a/res/modules/internal/maths_inline.lua +++ b/res/modules/internal/maths_inline.lua @@ -249,6 +249,7 @@ function mat4.idt(dst) for i=1,16 do dst[i] = (i % 5 == 1) and 1 or 0 end + return dst end return { 1, 0, 0, 0, From 7f15cd897c47b56ae28a10a6805ab83ef7acf3f7 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 5 Sep 2026 20:18:17 +0300 Subject: [PATCH 46/63] fix key frames based animation --- res/modules/animation.lua | 40 ++++++++++++++++++---------- res/modules/internal/formats/vca.lua | 6 ++++- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 15bdd1245..92c136eeb 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -106,6 +106,9 @@ local env = { return left.value end right = keys[right] + if left == nil then + return right.value + end local t = (frame - left.frame) / (right.frame - left.frame) if interp == INT_BEZIER then return bezier_interpolation(left, right, t) @@ -128,13 +131,15 @@ local env = { target:set_scale(scale) end end - end + end, + dump = debug.print } table.extend(env, math) -local function codegen_track(raw_track, lines, memoised, keysets, transforms) +local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) + local lines = lineset.lines local code = "" - local hastsf = false + local has_tsf = false local translation = {false, false, false} local rotation = {false, false, false} local scale = {false, false, false} @@ -143,27 +148,33 @@ local function codegen_track(raw_track, lines, memoised, keysets, transforms) code = code .. "\n local l" .. i .. " = (" .. process_expression(line.expression, memoised) .. ")" elseif line.keys then - keysets[i] = line.keys + debug.print(raw_track) + local target_keysets = keysets[lineset.target_name] + if not target_keysets then + target_keysets = {} + keysets[lineset.target_name] = target_keysets + end + target_keysets[i] = line.keys code = code .. string.format( - "\n local l%d = value_at(keysets[%d], t * %s, %s)", - i, i, raw_track.fps, line.interp) + "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, line.interp) end if line.channel == this.CH_TRANSLATE then translation[line.axis] = i - hastsf = true + has_tsf = true elseif line.channel == this.CH_ROTATE then rotation[line.axis] = i - hastsf = true + has_tsf = true elseif line.channel == this.CH_SCALE then scale[line.axis] = i - hastsf = true + has_tsf = true elseif line.channel == this.CH_ZOOM then code = code .. "\n zoom = l" .. i end end - if not hastsf or not transforms then + if not has_tsf or not use_tsf then return code end @@ -201,7 +212,7 @@ local function codegen_rig_target(raw_track, memoised, keysets) goto continue end local lineset_code = codegen_track( - raw_track, lineset.lines, memoised, keysets, true) + raw_track, lineset, memoised, keysets, true) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " target:set_matrix(target:index(" .. string.escape(bone) .. "), dst)\n" @@ -218,7 +229,7 @@ local function codegen_object_target(raw_track, memoised, keysets) return "" end local lineset_code = codegen_track( - raw_track, lineset.lines, memoised, keysets, true) + raw_track, lineset, memoised, keysets, true) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " set_matrix(target, dst)\n" return code .. " end" @@ -232,7 +243,7 @@ local function codegen_camera_target(raw_track, memoised, keysets) return "" end local lineset_code = codegen_track( - raw_track, lineset.lines, memoised, keysets, false) + raw_track, lineset, memoised, keysets, false) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " target:set_zoom(zoom)\n" return code .. " end" @@ -309,6 +320,7 @@ function internals.on_animation_frame() table.remove(running_actions, i) end end + local delta = time.delta() for i=1,#playing_tracks do local track_info = playing_tracks[i] local track = loaded_tracks[track_info.name] @@ -316,7 +328,7 @@ function internals.on_animation_frame() debug.error("animation track not found: "..track_info.name) table.remove(playing_tracks, i) else - track_info.timer = track_info.timer + time.delta() + track_info.timer = track_info.timer + delta if track_info.timer > track.duration then table.remove(playing_tracks, i) else diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index ecc3a195b..cb7b7390d 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -69,7 +69,11 @@ local function parse_track(root) local target_name = node.bone or "" local lineset = linesets[target_name] if not lineset then - lineset = {lines = {}, target_type = target_type} + lineset = { + lines = {}, + target_type = target_type, + target_name = target_name + } linesets[target_name] = lineset end From 2104bc76701d6d71656df68de8d0ac05ab1f249e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 5 Sep 2026 23:51:56 +0300 Subject: [PATCH 47/63] fix vca duration calculation --- res/modules/internal/formats/vca.lua | 2 +- res/scripts/components/animator.lua | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index cb7b7390d..a44a89826 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -21,7 +21,7 @@ local function parse_configure(raw_track, node) raw_track.fps = node.fps end if node.frames then - raw_track.duration = node.frames / node.fps + raw_track.duration = node.frames / (node.fps or DEFAULT_FPS) elseif node.duration then raw_track.duration = node.duration end diff --git a/res/scripts/components/animator.lua b/res/scripts/components/animator.lua index 810ea7648..5bd3ef4fa 100644 --- a/res/scripts/components/animator.lua +++ b/res/scripts/components/animator.lua @@ -1,3 +1,5 @@ +local ALMOST_HUGE = 1e9 + local rig = entity.skeleton local states = {} @@ -50,7 +52,11 @@ local function update(delta) state.track = state.track or animation.get_track(state.track_id) if state.track and intensity > 0.0 then - state.track.func(rig, state.timer, intensity * state.max_intencity) + state.track.func( + rig, + state.timer % math.min(ALMOST_HUGE, state.track.duration), + intensity * state.max_intencity + ) end end end From eeff17e143224cd90e743cd14aba6bebc9306864 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 6 Sep 2026 00:05:03 +0300 Subject: [PATCH 48/63] add rotation-order parameter to vca configure --- res/modules/animation.lua | 8 ++++++-- res/modules/internal/formats/vca.lua | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 92c136eeb..258b87d70 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -187,10 +187,14 @@ local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) end local axis_names = {"X", "Y", "Z"} - for axis, var in ipairs(rotation) do + local axis_indices = {X=1, Y=2, Z=3} + local rotation_order = raw_track.rotation_order + for i=1,3 do + local axis = axis_indices[rotation_order[i]] + local var = rotation[axis] if var then code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. - ",l" .. var .. ", dst)" + ", l" .. var .. ", dst)" end end diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index a44a89826..1cc48f0e3 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -25,6 +25,7 @@ local function parse_configure(raw_track, node) elseif node.duration then raw_track.duration = node.duration end + raw_track.rotation_order = string.upper(node["rotation-order"]) or "XYZ" end local function parse_curve(line, node) From 3b1ab36ac1171aae512589b265f15f63f6c97390 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 6 Sep 2026 00:45:34 +0300 Subject: [PATCH 49/63] remove rotation-order (use original directives order) --- res/modules/animation.lua | 13 ++++++++----- res/modules/internal/formats/vca.lua | 1 - 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 258b87d70..2f5af4ca4 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -187,14 +187,17 @@ local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) end local axis_names = {"X", "Y", "Z"} - local axis_indices = {X=1, Y=2, Z=3} - local rotation_order = raw_track.rotation_order - for i=1,3 do - local axis = axis_indices[rotation_order[i]] + local axes = {1, 2, 3} + + table.sort(axes, function(a, b) + return rotation[a] < rotation[b] + end) + + for _, axis in ipairs(axes) do local var = rotation[axis] if var then code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. - ", l" .. var .. ", dst)" + ", l" .. var .. ", dst)" end end diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 1cc48f0e3..a44a89826 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -25,7 +25,6 @@ local function parse_configure(raw_track, node) elseif node.duration then raw_track.duration = node.duration end - raw_track.rotation_order = string.upper(node["rotation-order"]) or "XYZ" end local function parse_curve(line, node) From 49e4b1f51919a3554c0b17b450de0ae86fa45c0f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 6 Sep 2026 00:53:44 +0300 Subject: [PATCH 50/63] revert "remove rotation-order (use original directives order)" Keeping VCA directives unordered is seems more preferred This reverts commit 3b1ab36ac1171aae512589b265f15f63f6c97390. --- res/modules/animation.lua | 13 +++++-------- res/modules/internal/formats/vca.lua | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 2f5af4ca4..258b87d70 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -187,17 +187,14 @@ local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) end local axis_names = {"X", "Y", "Z"} - local axes = {1, 2, 3} - - table.sort(axes, function(a, b) - return rotation[a] < rotation[b] - end) - - for _, axis in ipairs(axes) do + local axis_indices = {X=1, Y=2, Z=3} + local rotation_order = raw_track.rotation_order + for i=1,3 do + local axis = axis_indices[rotation_order[i]] local var = rotation[axis] if var then code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. - ", l" .. var .. ", dst)" + ", l" .. var .. ", dst)" end end diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index a44a89826..1cc48f0e3 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -25,6 +25,7 @@ local function parse_configure(raw_track, node) elseif node.duration then raw_track.duration = node.duration end + raw_track.rotation_order = string.upper(node["rotation-order"]) or "XYZ" end local function parse_curve(line, node) From 9193f8db4d79bd5965884258cbaad4c4ce5cf49e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 6 Sep 2026 01:38:08 +0300 Subject: [PATCH 51/63] add identifier-start-chars, identifier-part-chars to syntax highlighting settings file --- src/coders/syntax_parser.cpp | 33 ++++++++++++++++++++++++++++----- src/coders/syntax_parser.hpp | 2 ++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/coders/syntax_parser.cpp b/src/coders/syntax_parser.cpp index b0196ec2d..318d92b03 100644 --- a/src/coders/syntax_parser.cpp +++ b/src/coders/syntax_parser.cpp @@ -37,16 +37,22 @@ void Syntax::deserialize(const dv::value& src) { std::string multilineCommentEnd; std::string multilineStringStart; std::string multilineStringEnd; + std::string identifierStartChars; + std::string identifierPartChars; src.at("line-comment").get(lineComment); src.at("multiline-comment-start").get(multilineCommentStart); src.at("multiline-comment-end").get(multilineCommentEnd); src.at("multiline-string-start").get(multilineStringStart); src.at("multiline-string-end").get(multilineStringEnd); + src.at("identifier-start-chars").get(identifierStartChars); + src.at("identifier-part-chars").get(identifierPartChars); this->lineComment = util::str2wstr_utf8(lineComment); this->multilineCommentStart = util::str2wstr_utf8(multilineCommentStart); this->multilineCommentEnd = util::str2wstr_utf8(multilineCommentEnd); this->multilineStringStart = util::str2wstr_utf8(multilineStringStart); this->multilineStringEnd = util::str2wstr_utf8(multilineStringEnd); + this->identifierStarts = util::str2wstr_utf8(identifierStartChars); + this->identifierParts = util::str2wstr_utf8(identifierPartChars); if (src.has("extensions")) { const auto& extsList = src["extensions"]; @@ -77,6 +83,8 @@ inline bool is_lua_operator_start(int c) { || c == '.'; } +#include + class Tokenizer : BasicParser { const Syntax& syntax; std::vector tokens; @@ -87,13 +95,28 @@ public: : BasicParser(file, source), syntax(syntax) { } - std::wstring parseLuaName() { - char c = peek(); - if (!is_identifier_start(c)) { + + bool isIdentifierStart(wchar_t c) { + if (!syntax.identifierStarts.empty()) { + return syntax.identifierStarts.find(c) != std::wstring::npos; + } + return is_common_identifier_start(c); + } + + bool isIdentifierPart(wchar_t c) { + if (!syntax.identifierParts.empty()) { + return syntax.identifierParts.find(c) != std::wstring::npos; + } + return is_common_identifier_part(c); + } + + std::wstring parseIdentifier() { + wchar_t c = peek(); + if (!isIdentifierStart(c)) { throw error("identifier expected"); } int start = pos; - while (hasNext() && is_minimal_identifier_part(source[pos])) { + while (hasNext() && isIdentifierPart(source[pos])) { pos++; } return std::wstring(source.substr(start, pos - start)); @@ -167,7 +190,7 @@ public: wchar_t c = peek(); auto start = currentLocation(); if (is_common_identifier_start(c)) { - auto name = parseLuaName(); + auto name = parseIdentifier(); TokenTag tag = (syntax.keywords.find(name) == syntax.keywords.end() ? TokenTag::NAME diff --git a/src/coders/syntax_parser.hpp b/src/coders/syntax_parser.hpp index c7bd254cc..eeaf9c7a0 100644 --- a/src/coders/syntax_parser.hpp +++ b/src/coders/syntax_parser.hpp @@ -17,6 +17,8 @@ namespace devtools { std::wstring multilineCommentEnd; std::wstring multilineStringStart; std::wstring multilineStringEnd; + std::wstring identifierStarts; + std::wstring identifierParts; dv::value serialize() const override; void deserialize(const dv::value& src) override; From 6f6b26327d1b0b043019bcacee7c5dff21790228 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 6 Sep 2026 01:38:19 +0300 Subject: [PATCH 52/63] update vca.toml --- res/devtools/syntax/vca.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/devtools/syntax/vca.toml b/res/devtools/syntax/vca.toml index 369630a09..b87411998 100644 --- a/res/devtools/syntax/vca.toml +++ b/res/devtools/syntax/vca.toml @@ -1,6 +1,7 @@ language = "VCA" extensions = ["vca"] line-comment = "#" +identifier-part-chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" keywords = [ - "bone", "by", "func", "curve", "fps", "duration", "frames" + "bone", "by", "func", "curve", "fps", "duration", "frames", "rotation-order" ] From 21c8f5caa3aa86b483e34cf8f67194448c173acc Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 6 Sep 2026 16:55:46 +0300 Subject: [PATCH 53/63] cleanup expressions environment --- res/modules/animation.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 258b87d70..e1d21acbd 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -134,7 +134,14 @@ local env = { end, dump = debug.print } -table.extend(env, math) + +local math_funcs = { + "sqrt", "min", "max", "deg", "rad", "log", "log10", "floor", "ceil", "sin", + "tan", "noise", "noise2", "sign", "round", "exp", "pi", "e" +} +for _, name in ipairs(math_funcs) do + env[name] = math[name] +end local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) local lines = lineset.lines From 9826aedee37d583a2b98415ff582ad79f1313fdc Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 6 Sep 2026 16:59:47 +0300 Subject: [PATCH 54/63] add missing constant --- res/modules/animation.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index e1d21acbd..d7da501f9 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -92,6 +92,7 @@ end local env = { mat4 = mat4, + e = math.exp(1), X = {1, 0, 0}, Y = {0, 1, 0}, Z = {0, 0, 1}, From b180a29bb5eed8f452dec2f7b38508bd0b589e1e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 6 Sep 2026 17:01:09 +0300 Subject: [PATCH 55/63] add vca docs --- doc/ru/main-page.md | 1 + doc/ru/vca.md | 105 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 doc/ru/vca.md diff --git a/doc/ru/main-page.md b/doc/ru/main-page.md index 772f327fd..6f264ea87 100644 --- a/doc/ru/main-page.md +++ b/doc/ru/main-page.md @@ -21,4 +21,5 @@ - [Скриптинг](scripting.md) - [Стили текста](text-styles.md) - [Формат моделей VCM](vcm.md) +- [Формат анимации VCA](vca.md) - [Частицы](particles.md) diff --git a/doc/ru/vca.md b/doc/ru/vca.md new file mode 100644 index 000000000..0ad664e73 --- /dev/null +++ b/doc/ru/vca.md @@ -0,0 +1,105 @@ +# VCA + +Текстовый формат анимации, использующий синтаксис [VCM](vcm.md). + +VCA файл состоит из набора директив, порядок следования которых не влияет на порядок применение трансформаций. + +Директивы применяются к переданной цели, которая может быть скелетом сущности и камерой. +При использовании скелета, в директиве указывается имя кости через атрибут `bone`: + +```vcd +@move bone имя_кости ... +``` + +Директивы отвечают за отдельные скалярные значения, такие как x, y, z и т.д., указываемые через атрибут `by`: + +```vcd +@move bone hand by y ... +``` + +На данный момент доступны два вида описания кривых, дающих значение `f(t) = x` где t - время: +- [кривые с ключевыми кадрами (curve)](#кривые-с-ключевыми-кадрами) +- [кривые через выражения (func)](#) + +## Директивы анимации + +- `@move` - смещение объекта/кости +- `@rotate` - поворот объекта/кости +- `@scale` - масштабирование объекта/кости (множитель) +- `@zoom` - масштаб камеры (множитель) + +## Мета-информация + +Информация о анимации в файле настраивается через директиву `configure`: +- `fps` - частота кадров, являющаяся делителем при рассчёте длительности +- `frames` - число кадров анимации, являющееся делимым при рассчёте длительности +- `duration` - явно указанная длительность в секундах (требуется указание fps при наличии ключевых кадров) +- `rotation-order` - указывает порядок применения углов Эйлера при вращении (так как порядок директив не учитывается форматом). Примеры: XYZ, ZYX, YZX. + +## Кривые с ключевыми кадрами + +Требуется через атрибут `curve` указать тип кривой: +- `const` - значения не интерполируются, сменяясь в момент достижения ключевого кадра +- `linear` - ломанная линия, используется линейная интерполяция +- `bezier` - ключевые кадры кривой Безье с явно указанными левой и правой касательными (lx, ly, rx, ry) + +Ключевые кадры описываются в последующем `{...}` блоке в виде упорядоченных `@key` директив: + +```vcd +@rotate by z curve linear { + @key ... + @key ... +} +``` + +Ключевой кадр должен содержать номер кадра `frame` и значенин `value`. Для `bezier` кадров также описываются касательные. + +Пример для `curve bezier`: +```vcd +@key frame 138 value 53.00411 lx 127.067 ly 53.00411 rx 138.001 ry 53.00411 +``` + +## Кривые через выражения + +Через атрибут описывается функция `f(t) = x` где t - время в секундах. + +Пример: `func (sin(t * 2.5) + pi)`. + +Операции доступные в выражениях: + +- `a + b` - сложение +- `a * b` - умножение +- `a / b` - деление +- `a % b` - деление по модулю +- `a ^ b` - возведение в степень + +Константы доступные в выражениях: + +- `pi` число π +- `e` число Эйлера + +Функции доступные в выражениях: + +- `sin(x)` - синус от x +- `cos(x)` - косинус от x +- `tan(x)` - тангенс от x +- `noise(x, octaves)` - быстрый одномерный шум от x с указанным числом октав +- `noise2d(x, y, octaves)` - двумерный шум от x,y с указанным числом октав +- `sign(x)` - целое число, указывающее знак x (-1/0/1) +- `rand(low, high)` - псевдослучайное дробное число в диапазоне от low до high +- `round(x, places)` - округляет значение x до указанного количества знаков после запятой places (опционально). +- `floor(x)` - округляет значение x до ближайшего меньшего целого +- `ceil(x)` - округляет значение x до ближайшего большего целого +- `exp(x)` - $e^{x}$ +- `min(x, ...)` - выбирает наименьшее значение из переданных аргументов +- `max(x, ...)` - выбирает наибольшее значение из переданных аргументов +- `sqrt(x)` - квадратный корень от x +- `log(x)` - $ln({x})$ +- `log10(x)` - $log_{10}({x})$ +- `deg(x)` - конвертирует радианы в градусы +- `rad(x)` - конвертирует градусы в радианы + + +>[!WARNING] +> Использование незадокументированных функций может иметь иметь любые незадокументированные последствия. + From 41713e89a4f2a8d83d7dc62f8d25b7e23653c8ca Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 7 Sep 2026 17:11:09 +0300 Subject: [PATCH 56/63] fix missing textures load request for vcm --- src/assets/assetload_funcs.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index ea444fb5a..7b231fecf 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -435,13 +435,14 @@ static assetload::postfunc load_vcm_model( } auto vcmModelPtr = std::make_unique(std::move(vcmModel)).release(); - return [=](Assets& assets) { + return [=, &loader](Assets& assets) { auto vcmModel = std::unique_ptr(vcmModelPtr); for (auto& [partName, model] : vcmModel->parts) { auto fullName = name + "." + partName; logger.info() << "store model part " << util::quote(partName) << " as " << util::quote(fullName); + request_textures(loader, model); assets.store( std::make_unique(std::move(model)), fullName From 86d2e6a6148eb000d269add0eded9ca79fd14727 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 8 Sep 2026 19:38:25 +0300 Subject: [PATCH 57/63] add assets.parse_animation --- res/modules/internal/formats/vca.lua | 7 +++---- src/assets/assetload_funcs.cpp | 2 +- src/logic/scripting/lua/libs/libassets.cpp | 16 ++++++++++++++++ src/logic/scripting/scripting.cpp | 8 +++++++- src/logic/scripting/scripting.hpp | 1 + 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 1cc48f0e3..ec63772b4 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -97,14 +97,13 @@ local function parse_track(root) return raw_track end -local function load_vca(filepath) - local source = file.read(filepath) +local function load_vca(source, filepath) local raw_track = parse_track(xml.parse_vcd(source, "track")) debug.print(raw_track) return animation.compile_track(raw_track, filepath) end -function internals.load_vca_animation(filepath, identifier) - local track = load_vca(filepath) +function internals.load_vca_animation(filepath, source, identifier) + local track = load_vca(source or file.read(filepath), filepath) internals.store_animation(identifier, track) end diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index 7b231fecf..f6080cd1c 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -55,7 +55,7 @@ assetload::postfunc assetload::animation( loader.attachToFile(path, {name, AssetType::ANIMATION}); if (io::is_regular_file(path)) { - scripting::load_vca_animation(path, name); + scripting::load_vca_animation(path, nullptr, name); } return [](auto&) {}; } diff --git a/src/logic/scripting/lua/libs/libassets.cpp b/src/logic/scripting/lua/libs/libassets.cpp index a9691038e..9a61ce9a8 100644 --- a/src/logic/scripting/lua/libs/libassets.cpp +++ b/src/logic/scripting/lua/libs/libassets.cpp @@ -11,6 +11,7 @@ #include "graphics/commons/Model.hpp" #include "graphics/core/Atlas.hpp" #include "graphics/core/Texture.hpp" +#include "io/io.hpp" #include "util/Buffer.hpp" using namespace scripting; @@ -66,6 +67,20 @@ static int l_load_texture(lua::State* L) { return 0; } +static int l_parse_animation(lua::State* L) { + auto format = lua::require_lstring(L, 1); + auto string = lua::require_lstring(L, 2); + std::string name = lua::require_string(L, 3); + + if (format != "vca") { + throw std::runtime_error( + "unknown format " + util::quote(std::string(format)) + ); + } + scripting::load_vca_animation(name, &string, name); + return 0; +} + static int l_parse_model(lua::State* L) { auto& assets = engine->requireAssets(); @@ -161,6 +176,7 @@ static int l_to_canvas(lua::State* L) { const luaL_Reg assetslib[] = { {"request_texture", lua::wrap}, {"load_texture", lua::wrap}, + {"parse_animation", lua::wrap}, {"parse_model", lua::wrap}, {"to_canvas", lua::wrap}, {nullptr, nullptr} diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 071380a63..efaf32866 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -844,13 +844,19 @@ void scripting::load_layout_script( void scripting::load_vca_animation( const io::path& file, + const std::string_view* content, const std::string& identifier ) { auto L = lua::get_main_state(); if (lua::get_from_registry(L, lua::INTERNALS_TABLE, "load_vca_animation", true)) { lua::pushlstring(L, file.string()); + if (content) { + lua::pushlstring(L, *content); + } else { + lua::pushnil(L); + } lua::pushlstring(L, identifier); - if (lua::call(L, 2, 0)) { + if (lua::call(L, 3, 0)) { lua::pop(L); } lua::pop(L); diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index da04d7bab..116226aff 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -257,6 +257,7 @@ namespace scripting { void load_vca_animation( const io::path& file, + const std::string_view* content, const std::string& identifier ); From b98bfcc135554f625676e50745cc7defc0e05fba Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 8 Sep 2026 19:39:03 +0300 Subject: [PATCH 58/63] cleanup --- res/modules/animation.lua | 3 +-- res/modules/internal/formats/vca.lua | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index d7da501f9..7ca08175e 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -10,7 +10,7 @@ local this = { INT_LINEAR = 2, INT_BEZIER = 3, - TRACE_CODEGEN = true, + TRACE_CODEGEN = false, } local INT_CONST = this.INT_CONST @@ -156,7 +156,6 @@ local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) code = code .. "\n local l" .. i .. " = (" .. process_expression(line.expression, memoised) .. ")" elseif line.keys then - debug.print(raw_track) local target_keysets = keysets[lineset.target_name] if not target_keysets then target_keysets = {} diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index ec63772b4..0faa89404 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -93,13 +93,11 @@ local function parse_track(root) table.insert(lineset.lines, line) ::continue:: end - debug.print(raw_track) return raw_track end local function load_vca(source, filepath) local raw_track = parse_track(xml.parse_vcd(source, "track")) - debug.print(raw_track) return animation.compile_track(raw_track, filepath) end From 8c37d0860cbde54d5ff4e09b1fcaa790a4bcdb47 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 8 Sep 2026 19:42:09 +0300 Subject: [PATCH 59/63] update doc/*/scripting/builtins/libassets.md --- doc/en/scripting/builtins/libassets.md | 10 ++++++++++ doc/ru/scripting/builtins/libassets.md | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/doc/en/scripting/builtins/libassets.md b/doc/en/scripting/builtins/libassets.md index 8f51dd762..45ef138e9 100644 --- a/doc/en/scripting/builtins/libassets.md +++ b/doc/en/scripting/builtins/libassets.md @@ -37,6 +37,16 @@ assets.parse_model( skeleton_name: string ) +-- Parses and loads animation +assets.parse_animation( + -- Animation file format (vca) + format: string, + -- Animation file content + content: string, + -- Animation name after loading + name: string +) + -- Creates a Canvas from a loaded texture. assets.to_canvas( -- The name of the loaded texture. diff --git a/doc/ru/scripting/builtins/libassets.md b/doc/ru/scripting/builtins/libassets.md index c7f5b8620..1bf0d301f 100644 --- a/doc/ru/scripting/builtins/libassets.md +++ b/doc/ru/scripting/builtins/libassets.md @@ -38,6 +38,16 @@ assets.parse_model( skeleton_name: string ) +-- Парсит и загружает анимацию +assets.parse_animation( + -- Формат файла анимации (vca) + format: string, + -- Содержимое файла анимации + content: string, + -- Имя анимации после загрузки + name: string +) + -- Создаёт холст (Canvas) из загруженной текстуры assets.to_canvas( -- Имя загруженной текстуры. From 6b07f062d7da977206a7ea37f9c19dba243f88c5 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 8 Sep 2026 20:00:23 +0300 Subject: [PATCH 60/63] add some temporary solution (don't look) --- res/modules/animation.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 7ca08175e..a54a090b1 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -57,8 +57,22 @@ local patterns = { {name="sint", pattern="sin(t)"}, {name="sint2", pattern="sin(t * 2)"}, } +local exclude_patters = { + "end", + (string.pattern_safe("'")), + (string.pattern_safe('"')), + (string.pattern_safe("--")), + (string.pattern_safe("..")), +} +-- TODO: replace with actual expression -> lua translator local function process_expression(src, memoised) + for i, pattern in ipairs(exclude_patters) do + if src:find(pattern) then + debug.print(exclude_patters) + error("invalid syntax "..string.escape(src)) + end + end for i, pattern in ipairs(patterns) do local pattern_safe = string.pattern_safe(pattern.pattern) if src:find(pattern_safe) then From 3a188cd151f92c1d1260db07548be2628c4456f0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 8 Sep 2026 20:48:32 +0300 Subject: [PATCH 61/63] fix doc/ru/vca.md --- doc/ru/vca.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ru/vca.md b/doc/ru/vca.md index 0ad664e73..719474c0a 100644 --- a/doc/ru/vca.md +++ b/doc/ru/vca.md @@ -19,7 +19,7 @@ VCA файл состоит из набора директив, порядок На данный момент доступны два вида описания кривых, дающих значение `f(t) = x` где t - время: - [кривые с ключевыми кадрами (curve)](#кривые-с-ключевыми-кадрами) -- [кривые через выражения (func)](#) +- [кривые через выражения (func)](#кривые-через-выражения) ## Директивы анимации From bd9f3d86cac457ad11244b728d83ac8ba9912336 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 8 Sep 2026 21:08:28 +0300 Subject: [PATCH 62/63] add initial animation docs (test) --- doc/ru/animation.md | 34 +++++++++++++++++++++++ doc/ru/main-page.md | 1 + doc/ru/scripting.md | 1 + doc/ru/scripting/builtins/libanimation.md | 21 ++++++++++++++ doc/ru/vca.md | 2 ++ 5 files changed, 59 insertions(+) create mode 100644 doc/ru/animation.md create mode 100644 doc/ru/scripting/builtins/libanimation.md diff --git a/doc/ru/animation.md b/doc/ru/animation.md new file mode 100644 index 000000000..45e69a0e3 --- /dev/null +++ b/doc/ru/animation.md @@ -0,0 +1,34 @@ +# Анимация + +Для того, чтобы загрузить дорожку анимации для скелета сущности ([см. риггинг](rigging.md)) или камеры ([см. камеры](scripting/builtins/libcameras.md)) её, как любой, незагружаемый по-умолчанию, следует указать в [preload.json пака](assets-preload.md) в секции animation. + +На данный момент, движком поддерживается один формат описания анимации - [VCA](vca.md). + +Общий API для работы с анимацией, как и другими продолжительными событиями, существует библиотека animation, доступная в виде глобальной переменной. + +Самый простой способ анимировать что-то: + +```lua +animation.play("имя_анимации", цель) +``` + +Где цель может быть камерой: + +```lua +local camera = cameras.get(cameras.index("пак:имя_камеры")) +animation.play("имя_анимации", camera) +``` + +Или скелет сущности: + +```lua +animation.play("имя_анимации", entity.skeleton) +``` + +Но для скелетов, подразумевающих переключение между состояниями (те же мобы), лучше использовать [компонент](scripting/ecs.md), реализующий простую машину состояний для анимации - [core:animator](#компонент-coreanimator) + +Более подробно функциональность библиотеки animation описана [в соответствующем разделе](scripting/builtins/libanimation.md) + +## Компонент core:animator + +Компонент находится в работе. Зайдите позже. diff --git a/doc/ru/main-page.md b/doc/ru/main-page.md index 6f264ea87..916202151 100644 --- a/doc/ru/main-page.md +++ b/doc/ru/main-page.md @@ -6,6 +6,7 @@ ## Разделы - [XML разметка интерфейса](xml-ui-layouts.md) +- [Анимация](animation.md) - [Аудио](audio.md) - [Движок генерации мира](world-generator.md) - [Консоль](console.md) diff --git a/doc/ru/scripting.md b/doc/ru/scripting.md index 1e6d73098..1959c4649 100644 --- a/doc/ru/scripting.md +++ b/doc/ru/scripting.md @@ -9,6 +9,7 @@ - [Свойства и методы UI элементов](scripting/ui.md) - [Сущности и компоненты](scripting/ecs.md) - [Библиотеки](#) + - [animation](scripting/builtins/libanimation.md) - [app](scripting/builtins/libapp.md) - [assets](scripting/builtins/libassets.md) - [base64](scripting/builtins/libbase64.md) diff --git a/doc/ru/scripting/builtins/libanimation.md b/doc/ru/scripting/builtins/libanimation.md new file mode 100644 index 000000000..25048c151 --- /dev/null +++ b/doc/ru/scripting/builtins/libanimation.md @@ -0,0 +1,21 @@ +# Библиотека *animation* + +Библиотека для работы с анимацией и решения некоторых сопутствующих задач. + +```lua +-- Воспроизводит анимацию на указанном объекте +animation.play( + -- Имя дорожки анимации + name: string, + -- Цель, что будет анимирована + target: Skeleton | Camera +) +``` + +```lua +-- Запускает корутину, обновляющуюся вне паузы и завершающуюся вместе с выходом из мира. +animation.action( + -- Целевая асинхронная функция + func: function() +) +``` diff --git a/doc/ru/vca.md b/doc/ru/vca.md index 719474c0a..eb1f77b07 100644 --- a/doc/ru/vca.md +++ b/doc/ru/vca.md @@ -36,6 +36,8 @@ VCA файл состоит из набора директив, порядок - `duration` - явно указанная длительность в секундах (требуется указание fps при наличии ключевых кадров) - `rotation-order` - указывает порядок применения углов Эйлера при вращении (так как порядок директив не учитывается форматом). Примеры: XYZ, ZYX, YZX. +Мета-информация применяется глобально. Поведение не зависит от её положения в файле, но рекомендуется располагать её в начале. + ## Кривые с ключевыми кадрами Требуется через атрибут `curve` указать тип кривой: From 69bbe402e471c978fc794e43e3ea020f62a7c27d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 8 Sep 2026 21:10:47 +0300 Subject: [PATCH 63/63] update doc/*/assets-preload.md --- doc/en/assets-preload.md | 8 +++++--- doc/ru/assets-preload.md | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/en/assets-preload.md b/doc/en/assets-preload.md index f66f451c8..3d2cd6f34 100644 --- a/doc/en/assets-preload.md +++ b/doc/en/assets-preload.md @@ -3,11 +3,13 @@ `preload.json` in content-pack folder is used for specifying additional assets should be loaded, like sounds. The file contains following categories available: +- animation - fonts -- shaders -- textures -- sounds - models +- shaders +- skeletons +- sounds +- textures > [!NOTE] > Sound loading with all variations following template: diff --git a/doc/ru/assets-preload.md b/doc/ru/assets-preload.md index b3920e288..5b0c3b4ec 100644 --- a/doc/ru/assets-preload.md +++ b/doc/ru/assets-preload.md @@ -3,11 +3,13 @@ Для загрузки ассетов, не загружаемых автоматически, такие как звуки, дополнительные текстуры, используется файл `preload.json`, создающийся в папке контент-пака. Ассеты в файле разделяются на категории: +- animation - анимация - fonts - шрифты -- shaders - шейдеры -- textures - текстуры -- sounds - звуки - models - модели +- shaders - шейдеры +- skeletons - скелеты +- sounds - звуки +- textures - текстуры > [!NOTE] > При загрузке звука подгружаются все его вариации, по шаблону: