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/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/en/scripting/ecs.md b/doc/en/scripting/ecs.md index c54165bd4..d9194ea93 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/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/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/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] > При загрузке звука подгружаются все его вариации, по шаблону: diff --git a/doc/ru/main-page.md b/doc/ru/main-page.md index 772f327fd..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) @@ -21,4 +22,5 @@ - [Скриптинг](scripting.md) - [Стили текста](text-styles.md) - [Формат моделей VCM](vcm.md) +- [Формат анимации VCA](vca.md) - [Частицы](particles.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/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( -- Имя загруженной текстуры. diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index be99774d5..fa6186d41 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 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 diff --git a/doc/ru/vca.md b/doc/ru/vca.md new file mode 100644 index 000000000..eb1f77b07 --- /dev/null +++ b/doc/ru/vca.md @@ -0,0 +1,107 @@ +# 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] +> Использование незадокументированных функций может иметь иметь любые незадокументированные последствия. + 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 diff --git a/res/devtools/syntax/vca.toml b/res/devtools/syntax/vca.toml new file mode 100644 index 000000000..b87411998 --- /dev/null +++ b/res/devtools/syntax/vca.toml @@ -0,0 +1,7 @@ +language = "VCA" +extensions = ["vca"] +line-comment = "#" +identifier-part-chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" +keywords = [ + "bone", "by", "func", "curve", "fps", "duration", "frames", "rotation-order" +] 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 diff --git a/res/modules/animation.lua b/res/modules/animation.lua new file mode 100644 index 000000000..a54a090b1 --- /dev/null +++ b/res/modules/animation.lua @@ -0,0 +1,378 @@ +local internals = __vc_internals + +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, +} + +local INT_CONST = this.INT_CONST +local INT_BEZIER = this.INT_BEZIER + +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 + +local function 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 + +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 + 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, + e = math.exp(1), + 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] + 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) + 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, + dump = debug.print +} + +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 + local code = "" + local has_tsf = 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 .. " = (" .. + process_expression(line.expression, memoised) .. ")" + elseif line.keys then + 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['%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 + has_tsf = true + elseif line.channel == this.CH_ROTATE then + rotation[line.axis] = i + has_tsf = true + elseif line.channel == this.CH_SCALE then + scale[line.axis] = i + has_tsf = true + elseif line.channel == this.CH_ZOOM then + code = code .. "\n zoom = l" .. i + end + end + + if not has_tsf or not use_tsf 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, {" .. + (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"} + 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)" + 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 + +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, 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, 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, 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 = {} + + 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 + memoised_code = memoised_code .. "\n local " .. name .. " = " + .. expression + end + + if #memoised_code > 0 then + code = memoised_code .. "\n" .. code + end + + local src = "return function(target, t, m)\n" + .. code .. "\nend" + + if this.TRACE_CODEGEN then + debug.log("[".. + string.escape(track_name or "nil").." codegen trace]:\n"..src) + end + + local generator, err = load( + src, "", "bt", table.extend({keysets = keysets}, env)) + if not generator then + error(err) + end + return { + duration = raw_track.duration, + func = generator(), + } +end + +local loaded_tracks = {} +local backup_tracks = {} + +function internals.store_animation(name, track) + loaded_tracks[name] = track +end + +function this.get_track(identifier) + return loaded_tracks[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=1,#running_actions 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 + local delta = time.delta() + 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 + 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() + 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/formats/vca.lua b/res/modules/internal/formats/vca.lua new file mode 100644 index 000000000..0faa89404 --- /dev/null +++ b/res/modules/internal/formats/vca.lua @@ -0,0 +1,107 @@ +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, + scale = animation.CH_SCALE, + zoom = animation.CH_ZOOM, +} + +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 or DEFAULT_FPS) + 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) + 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 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 = {}, + target_type = target_type, + target_name = target_name + } + linesets[target_name] = lineset + end + + local line = { + axis = node.by and ("xyz"):find(node.by) or "", + 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(source, 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, source, identifier) + local track = load_vca(source or file.read(filepath), filepath) + internals.store_animation(identifier, track) +end 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 e1460c22c..d1750e4cb 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,10 +33,10 @@ 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() +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 @@ -130,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 @@ -142,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 @@ -165,7 +167,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_animation_actions() __rules.clear() __gui_util_reset_local() __stdcomp_reset() @@ -176,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 @@ -189,41 +190,38 @@ 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) table.insert(__post_runnables, runnable) end -return { - __vc_on_hud_open = __vc_on_hud_open, - __vc_on_world_open = __vc_on_world_open, - __vc_on_world_tick = __vc_on_world_tick, - __vc_process_before_quit = __vc_process_before_quit, - __vc_on_world_save = __vc_on_world_save, - __vc_on_world_quit = __vc_on_world_quit, - __vc__process_post_runnables = __vc__process_post_runnables, - __vc_is_post_runnable_context = function() - return __vc__is_post_runnable - end -} +function internals.on_render() + internals.on_animation_frame() +end + +return {} diff --git a/res/modules/internal/maths_inline.lua b/res/modules/internal/maths_inline.lua index 36c0aecfe..4a6671cdf 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 ======================= -- -- =================================================== -- @@ -238,3 +243,93 @@ 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 + return dst + end + return { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 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, y, z = axis[1], axis[2], 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 {} + + 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[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[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[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 diff --git a/res/modules/internal/stdcomp.lua b/res/modules/internal/stdcomp.lua index ac11692ca..4a0ace852 100644 --- a/res/modules/internal/stdcomp.lua +++ b/res/modules/internal/stdcomp.lua @@ -50,8 +50,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, @@ -86,7 +87,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 @@ -112,6 +113,7 @@ return { entity.rigidbody = new_Rigidbody(eid) entity.skeleton = new_Skeleton(eid) entity.components = {} + entity.ordered_components = {} entities[eid] = entity; return entity end, @@ -122,6 +124,7 @@ return { local entity = entities[eid] if entity then entity.components = nil + entity.ordered_components = nil entities[eid] = nil; end end, @@ -130,10 +133,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 = rawget(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 @@ -144,7 +147,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 = rawget(component, "on_physics_update") if not component.__disabled and callback then local result, err = pcall(callback, delta) @@ -157,10 +160,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 = rawget(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/res/scripts/components/animator.lua b/res/scripts/components/animator.lua new file mode 100644 index 000000000..5bd3ef4fa --- /dev/null +++ b/res/scripts/components/animator.lua @@ -0,0 +1,66 @@ +local ALMOST_HUGE = 1e9 + +local rig = entity.skeleton + +local states = {} + +function create_state(name, track_id, max_intencity) + states[name] = { + track = nil, + track_id = track_id, + timer = 0.0, + fade_time = 0.0, + fade_timer = 0.0, + fade_in = false, + max_intencity = max_intencity, + } +end + +local current_state = nil + +function set_state(name, fade_time) + local state = states[name] + if state == current_state then + return + end + 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 +end + +local function update(delta) + for _, state in pairs(states) do + state.timer = state.timer + delta + + 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 + + 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 % math.min(ALMOST_HUGE, state.track.duration), + intensity * state.max_intencity + ) + end + end +end + +function on_render() + update(time.delta()) +end diff --git a/res/scripts/components/mob.lua b/res/scripts/components/mob.lua index 9f2dc53b7..9bddc4bcd 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,33 +112,30 @@ 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 @@ -147,7 +144,7 @@ function set_flight(flag) body:set_gravity_scale(flight and 0.0 or props.gravity_scale) 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() @@ -156,7 +153,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( diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 56d86bf8f..dee3bf579 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.is_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) @@ -332,6 +333,9 @@ end 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" file.open = require "core:internal/stream_providers/file" @@ -366,6 +370,7 @@ core.get_core_token = audio.input.__get_core_token require "core:internal/console" require "core:internal/deprecated" +require "core:internal/internal_events" ------------------------------------------ @@ -395,5 +400,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/objects/rigging.cpp b/src/animation/rigging.cpp similarity index 86% rename from src/objects/rigging.cpp rename to src/animation/rigging.cpp index f47f7c733..d5e36a82c 100644 --- a/src/objects/rigging.cpp +++ b/src/animation/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,24 +29,27 @@ 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) : 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; } } @@ -194,12 +193,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], @@ -207,7 +208,9 @@ void SkeletonConfig::render( model, &skeleton.textures ); - } + } else if (!node->model.name.empty()) { + node->model.updateFlag = true; + } } } diff --git a/src/objects/rigging.hpp b/src/animation/rigging.hpp similarity index 98% rename from src/objects/rigging.hpp rename to src/animation/rigging.hpp index 9e7fc75f6..82066e132 100644 --- a/src/objects/rigging.hpp +++ b/src/animation/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); }; 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 b9faa877f..5858e17c5 100644 --- a/src/assets/Assets.hpp +++ b/src/assets/Assets.hpp @@ -1,5 +1,10 @@ #pragma once +#include "util/stringutil.hpp" +#include "util/ObjectsKeeper.hpp" +#include "graphics/core/TextureAnimation.hpp" +#include "io/fwd.hpp" + #include #include #include @@ -11,13 +16,12 @@ #include #include -#include "util/stringutil.hpp" -#include "util/ObjectsKeeper.hpp" -#include "graphics/core/TextureAnimation.hpp" - class Assets; +struct AssetsLoadInfo; +class AssetsLoader; enum class AssetType { + ANIMATION, TEXTURE, SHADER, FONT, @@ -31,12 +35,10 @@ enum class AssetType { namespace assetload { /// @brief final work to do in the main thread - using postfunc = std::function; - - using setupfunc = std::function; + using postfunc = std::function; template - void assets_setup(const Assets*); + void assets_setup(const Assets&); class error : public std::runtime_error { AssetType type; @@ -62,16 +64,13 @@ namespace assetload { return reason; } }; + } class Assets { - util::ObjectsKeeper* vault; - std::vector animations; - - using assets_map = std::unordered_map>; - std::unordered_map assets; - std::vector setupFuncs; public: + using assets_map = std::unordered_map>; + Assets(util::ObjectsKeeper* vault); Assets(const Assets&) = delete; ~Assets(); @@ -143,20 +142,20 @@ public: return &mapIter->second; } - void setup() { - for (auto& setupFunc : setupFuncs) { - setupFunc(this); - } + AssetsLoadInfo& getLoadInfo() const { + return *assetsLoadInfo; } +private: + util::ObjectsKeeper* vault; + std::vector animations; - void addSetupFunc(assetload::setupfunc setupfunc) { - setupFuncs.push_back(setupfunc); - } + std::unordered_map assets; + std::unique_ptr assetsLoadInfo; }; 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 581e6e109..04bde9a6f 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" @@ -26,17 +26,23 @@ namespace fs = std::filesystem; static debug::Logger logger("assets-loader"); -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); +AssetsLoader::AssetsLoader( + Engine& engine, Assets& assets, const ResPaths& 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); 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) { @@ -50,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 { @@ -72,15 +78,15 @@ 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 {}; 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) { @@ -93,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(); } @@ -117,34 +125,28 @@ 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::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 ""; } @@ -160,7 +162,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; @@ -221,14 +224,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 } @@ -263,65 +267,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 + ); } } } @@ -361,17 +373,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 +395,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)); @@ -404,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) { + 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()) { + entries.push(recipe->second); + added++; + } + } + + return added; +} diff --git a/src/assets/AssetsLoader.hpp b/src/assets/AssetsLoader.hpp index d919d1f08..a61c3b59e 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; @@ -81,24 +86,18 @@ struct aloader_entry { std::shared_ptr config; }; +using AssetFullId = std::pair; + +struct AssetsLoadInfo { + std::map processedEntries; + std::multimap referencedAssets; +}; + 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; + void addLoader(AssetType tag, aloader_func func); /// @brief Enqueue asset load @@ -125,9 +124,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, @@ -135,6 +133,25 @@ public: const std::vector& alternatives ); + int addReload(const io::path& path); + void attachToFile(const io::path& file, AssetFullId assetId); + Assets& getAssets(); Engine& getEngine(); +private: + Engine& engine; + Assets& assets; + std::map loaders; + std::queue entries; + std::set enqueued; + + const ResPaths& paths; + AssetsLoadInfo& assetsLoadInfo; + + 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/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index 74a298933..f6080cd1c 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,26 +15,28 @@ #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 "logic/scripting/scripting.hpp" +#include "io/io.hpp" #include "util/stringutil.hpp" -#include "Assets.hpp" -#include "AssetsLoader.hpp" + +#include +#include +#include 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, @@ -44,22 +44,39 @@ static bool load_animation( Atlas* dstAtlas ); -assetload::postfunc assetload::texture( - AssetsLoader*, +assetload::postfunc assetload::animation( + AssetsLoader& loader, const ResPaths& paths, const std::string& filename, const std::string& name, const std::shared_ptr& ) { - auto actualFile = paths.find(filename + ".png"); + auto path = paths.find(filename + ".vca"); + loader.attachToFile(path, {name, AssetType::ANIMATION}); + + if (io::is_regular_file(path)) { + scripting::load_vca_animation(path, nullptr, name); + } + return [](auto&) {}; +} + +assetload::postfunc assetload::texture( + AssetsLoader& loader, + const ResPaths& paths, + const std::string& filename, + const std::string& name, + const std::shared_ptr& +) { + 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) { - assets->store(Texture::from(image.get()), name); + 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(); - return [](auto) {}; + logger.error() << file.string() << ": " << err.what(); + return [](auto&) {}; } } @@ -86,7 +103,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 +116,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,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; @@ -131,7 +150,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 +159,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 +179,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 +190,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 +205,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, @@ -204,17 +223,17 @@ 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) { + 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 +249,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 +284,7 @@ assetload::postfunc assetload::layout( }; } assetload::postfunc assetload::sound( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& file, const std::string& name, @@ -307,81 +326,162 @@ 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 ); } } } +static assetload::postfunc load_vec3_model( + const io::path& file, + AssetsLoader& loader, + 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)]( + 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& +) { + loader.attachToFile(file, {name, AssetType::MODEL}); + 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) { + logger.error() << "error on loading model " << name << ": " + << err.errorLog(); + throw; + } +} + +static assetload::postfunc load_vcm_model( + const io::path& file, + AssetsLoader& loader, + 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"); + + 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 [=, &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 + ); + } + 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) { + logger.error() << "error on loading model " << name << ": " + << err.errorLog(); + throw; + } +} + assetload::postfunc assetload::model( - AssetsLoader* loader, + AssetsLoader& loader, const ResPaths& paths, const std::string& file, 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 [=](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 { @@ -401,60 +501,17 @@ 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 [=](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( - 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 +522,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 +609,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, @@ -576,8 +633,7 @@ static bool load_animation( 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; @@ -591,10 +647,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..9ccc601ab 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; @@ -13,57 +13,64 @@ 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*, + 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 +78,7 @@ namespace assetload { ); postfunc skeleton( - AssetsLoader*, + AssetsLoader&, const ResPaths& paths, const std::string& file, const std::string& name, 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; 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/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/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/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; +} 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/AssetsManagement.cpp b/src/engine/AssetsManagement.cpp index 13df11220..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,35 +50,41 @@ 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); - 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 - // 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(new_assets); + assets = std::move(newAssets); if (content) { ModelsGenerator::prepare(*content, *assets); } - assets->setup(); engine.getGUI().onAssetsLoad(assets.get()); } 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/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(); }; diff --git a/src/graphics/render/HandsRenderer.cpp b/src/graphics/render/HandsRenderer.cpp index 17c38d76d..3fc21342a 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 e3dab25df..f3b793c0a 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 "BlockWrapsRenderer.hpp" #include "ChunksRenderer.hpp" #include "CloudsRenderer.hpp" @@ -42,7 +43,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 c214ad1d0..a310abe87 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" @@ -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; @@ -66,19 +66,34 @@ 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; } 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); } 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 +184,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/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/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index 4434136ba..2d3eeecea 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 @@ -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/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..61b56cef3 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); @@ -635,6 +636,13 @@ 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); + pop(L); + } + } + inline int eval( lua::State* L, int env, diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 15da3697e..efaf32866 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 @@ -263,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(); @@ -291,13 +296,15 @@ 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); } catch (const std::exception&) { lua::pushnil(L); lua::setglobal(L, "__vc_app"); + lua::pushnil(L); + lua::setglobal(L, "__vc_internals"); throw; } lua::pushnil(L); @@ -319,9 +326,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) { @@ -334,9 +339,10 @@ 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); + lua::pop(L); } for (auto& pack : content_control->getAllContentPacks()) { lua::emit_event(L, pack.id + ":.worldtick"); @@ -348,16 +354,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() { @@ -365,9 +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")) { - lua::call_nothrow(L, 0, 0); - } + lua::call_internal(L, "on_world_quit"); scripting::level = nullptr; scripting::content = nullptr; scripting::indices = nullptr; @@ -842,6 +842,27 @@ void scripting::load_layout_script( lua::pop(L); } +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, 3, 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..116226aff 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -48,7 +48,10 @@ namespace scripting { extern std::ostream* output_stream; extern std::ostream* error_stream; - void initialize(Engine* engine); + void initialize(Engine& engine); + + void on_assets_loading(); + void revert_assets_loading(); void on_content_load(Content* content); void on_content_reset(); @@ -251,6 +254,12 @@ namespace scripting { const std::string& fileName, UiDocScript& script ); + + void load_vca_animation( + const io::path& file, + const std::string_view* content, + const std::string& identifier + ); /// @brief Finalize lua state. Using scripting after will lead to Lua panic void close(); diff --git a/src/logic/scripting/scripting_entities.cpp b/src/logic/scripting/scripting_entities.cpp index 2de177562..ba19c4243 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 diff --git a/src/logic/scripting/scripting_hud.cpp b/src/logic/scripting/scripting_hud.cpp index 59b928d21..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( @@ -63,12 +61,13 @@ void scripting::on_frontend_init( } void scripting::on_frontend_render() { + auto L = lua::get_main_state(); + + lua::call_internal(L, "on_render"); 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; + }); } } diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index 3bb19ccba..1158c68a0 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 @@ -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) { diff --git a/src/objects/Entity.cpp b/src/objects/Entity.cpp index 46664e24e..697c6618f 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 44ea8859f..1defcedc9 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; 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_; }