From 07f45ad1e1c071e4c72916d464f25c3ae29ea193 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Fri, 10 Jul 2026 14:09:31 +0300 Subject: [PATCH] Working on stuff --- src/Character.cpp | 10 +- src/Character.h | 4 +- src/CharacterState.cpp | 4 + src/CharacterState.h | 5 + src/Game.cpp | 1 + src/Location.cpp | 18 ++++ src/ScriptEngine.cpp | 158 +++++++++++++++++++---------- src/ScriptEngine.h | 9 ++ src/items/InteractiveObject.cpp | 23 +++-- src/items/InteractiveObject.h | 10 +- src/items/InteractiveObjectState.h | 4 + 11 files changed, 176 insertions(+), 70 deletions(-) diff --git a/src/Character.cpp b/src/Character.cpp index 52d4d51..86dc39f 100644 --- a/src/Character.cpp +++ b/src/Character.cpp @@ -109,7 +109,7 @@ std::unique_ptr Character::createFromState(CharacterState st, Rendere return c; } -void Character::setTarget(const Eigen::Vector3f& target, std::function onArrived) { +void Character::setTarget(const Eigen::Vector3f& target, std::function onArrived, std::string callbackName) { Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z()); const bool sameRequestedTarget = @@ -123,8 +123,9 @@ void Character::setTarget(const Eigen::Vector3f& target, std::function o return; } - state.requestedWalkTarget = normalizedTarget; - state.onArrivedCallback = std::move(onArrived); + state.requestedWalkTarget = normalizedTarget; + state.onArrivedCallback = std::move(onArrived); + state.onArrivedCallbackName = std::move(callbackName); if (pathPlanner) { state.pathWaypoints = pathPlanner(state.position, normalizedTarget); @@ -140,6 +141,7 @@ void Character::setTarget(const Eigen::Vector3f& target, std::function o state.walkTarget = Eigen::Vector3f(state.position.x(), 0.f, state.position.z()); state.onArrivedCallback = nullptr; + state.onArrivedCallbackName.clear(); return; } @@ -178,6 +180,7 @@ void Character::forceReplan() { state.walkTarget = Eigen::Vector3f(state.position.x(), 0.f, state.position.z()); state.onArrivedCallback = nullptr; + state.onArrivedCallbackName.clear(); } void Character::setTexture(std::shared_ptr texture) { @@ -294,6 +297,7 @@ void Character::update(int64_t deltaMs, const CharacterResolver& resolver) { if (!hasNextWaypoint && state.onArrivedCallback) { auto cb = std::move(state.onArrivedCallback); state.onArrivedCallback = nullptr; + state.onArrivedCallbackName.clear(); cb(); } diff --git a/src/Character.h b/src/Character.h index 20544ad..2d74429 100644 --- a/src/Character.h +++ b/src/Character.h @@ -52,7 +52,9 @@ public: void setPathPlanner(PathPlanner planner); // ---- Navigation (path data lives in state; pathPlanner lives here) ---- - void setTarget(const Eigen::Vector3f& target, std::function onArrived = nullptr); + // callbackName, if given, is the global Lua function name backing onArrived — + // stored in state.onArrivedCallbackName so it survives save/load (see ScriptEngine::rehydrateNamedCallback). + void setTarget(const Eigen::Vector3f& target, std::function onArrived = nullptr, std::string callbackName = ""); void forceReplan(); void stopInPlace() { state.stopInPlace(); } diff --git a/src/CharacterState.cpp b/src/CharacterState.cpp index d3c8de3..5490854 100644 --- a/src/CharacterState.cpp +++ b/src/CharacterState.cpp @@ -21,6 +21,7 @@ void CharacterState::stopInPlace() { pathWaypoints.clear(); currentWaypointIndex = 0; onArrivedCallback = nullptr; + onArrivedCallbackName.clear(); } void CharacterState::setHp(float newHp) { @@ -52,6 +53,8 @@ void CharacterState::save(nlohmann::json& out) const out["pathWaypoints"] = std::move(wpArr); out["currentWaypointIndex"] = currentWaypointIndex; out["homeDriftCheckTimer"] = homeDriftCheckTimer; + if (!onArrivedCallbackName.empty()) + out["onArrivedCallbackName"] = onArrivedCallbackName; } void CharacterState::load(const nlohmann::json& in) @@ -84,6 +87,7 @@ void CharacterState::load(const nlohmann::json& in) } currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex); homeDriftCheckTimer = in.value("homeDriftCheckTimer", homeDriftCheckTimer); + onArrivedCallbackName = in.value("onArrivedCallbackName", std::string()); } } // namespace ZL diff --git a/src/CharacterState.h b/src/CharacterState.h index 39ee497..3663f38 100644 --- a/src/CharacterState.h +++ b/src/CharacterState.h @@ -116,6 +116,11 @@ public: std::function onDeathAnimComplete; std::function onHpChanged; + // Global Lua function name backing onArrivedCallback, if it was sourced from a + // script (see ScriptEngine::rehydrateNamedCallback). Serialized so the callback + // can be re-looked-up by name after a save/load; empty if none or non-Lua. + std::string onArrivedCallbackName; + // --- Asset / creation info (used to reload visuals from a saved state) --- CharacterCreationInfo creationInfo; diff --git a/src/Game.cpp b/src/Game.cpp index c2688f0..5254010 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -20,6 +20,7 @@ #include #endif + #include "GameConstants.h" namespace ZL diff --git a/src/Location.cpp b/src/Location.cpp index 4f1fbf6..4faca7e 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -1995,6 +1995,24 @@ namespace ZL if (in.contains("scriptGlobals")) { scriptEngine.loadScriptGlobals(in["scriptGlobals"]); } + + // Re-wire in-flight, one-shot Lua callbacks (walk-arrival / animation-complete) + // that were serialized by name. Must run after scriptEngine has already run the + // location's script (guaranteed: Location::setup() always runs before load()) + // and after the states above are restored. + if (player && !player->state.onArrivedCallbackName.empty()) { + scriptEngine.rehydrateNamedCallback(player->state.onArrivedCallbackName, player->state.onArrivedCallback); + } + for (auto& npc : npcs) { + if (npc && !npc->state.onArrivedCallbackName.empty()) { + scriptEngine.rehydrateNamedCallback(npc->state.onArrivedCallbackName, npc->state.onArrivedCallback); + } + } + for (auto& intObj : interactiveObjects) { + if (intObj.state.isAnimating && intObj.state.animTask && !intObj.state.animTask->onCompleteName.empty()) { + scriptEngine.rehydrateNamedCallback(intObj.state.animTask->onCompleteName, intObj.state.animTask->onComplete); + } + } } } // namespace ZL diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp index 7bad805..4f675b5 100644 --- a/src/ScriptEngine.cpp +++ b/src/ScriptEngine.cpp @@ -11,6 +11,54 @@ namespace ZL { + namespace { + + // Wraps a sol::protected_function into a std::function, invoking it and + // logging any Lua-side error to stderr. Shared by every binding that stores a + // callback for later, one-shot invocation (npc_walk_to, move_object, etc.) and by + // rehydrateNamedCallback, which re-wraps a callback looked up by name after load. + std::function wrapLuaCallback(sol::protected_function fn) { + return [fn]() mutable { + auto result = fn(); + if (!result.valid()) { + sol::error err = result; + std::cerr << "[script] callback error: " << err.what() << "\n"; + } + }; + } + + // Reverse-looks-up the global name a Lua function is registered under, by + // comparing raw function pointers via lua_topointer. Returns "" if fn isn't + // (or is no longer) reachable as a plain global — e.g. an anonymous function, + // or one stored only in a local/upvalue. This lets callback-accepting bindings + // remember a callback "by name" for save/load without changing the Lua-facing + // API (scripts still just pass a function reference). + std::string findGlobalFunctionName(lua_State* L, const sol::protected_function& fn) { + if (!fn.valid()) return {}; + + fn.push(L); + const void* target = lua_topointer(L, -1); + lua_pop(L, 1); + if (!target) return {}; + + std::string found; + lua_pushglobaltable(L); // stack: [_G] + lua_pushnil(L); // stack: [_G, nil] + while (lua_next(L, -2) != 0) { // stack: [_G, key, value] + if (lua_type(L, -2) == LUA_TSTRING && lua_isfunction(L, -1) && + lua_topointer(L, -1) == target) { + found = lua_tostring(L, -2); + lua_pop(L, 2); // pop value, key + break; + } + lua_pop(L, 1); // pop value, keep key for next iteration + } + lua_pop(L, 1); // pop _G + return found; + } + + } // anonymous namespace + struct ScriptEngine::Impl { sol::state lua; std::unordered_map triggerEnterCallbacks; @@ -45,7 +93,7 @@ namespace ZL { // on_arrived is an optional Lua function called when the NPC reaches the target. // It can call npc_walk_to again (or anything else) to chain behaviour. api.set_function("npc_walk_to", - [loc](int index, float x, float y, float z, sol::object on_arrived) { + [loc, this_impl = impl.get()](int index, float x, float y, float z, sol::object on_arrived) { auto& npcs = loc->npcs; if (index < 0 || index >= static_cast(npcs.size())) { std::cerr << "[script] npc_walk_to: index " << index @@ -53,18 +101,16 @@ namespace ZL { return; } std::function cb; + std::string cbName; if (on_arrived.is()) { sol::protected_function fn = on_arrived.as(); - cb = [fn]() mutable { - auto result = fn(); - if (!result.valid()) { - sol::error err = result; - std::cerr << "[script] on_arrived error: " << err.what() << "\n"; - } - }; + cb = wrapLuaCallback(fn); + cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); + if (cbName.empty()) + std::cerr << "[script] npc_walk_to: on_arrived callback has no global name, will not survive save/load\n"; } npcs[index]->state.homePosition = Eigen::Vector3f(x, 0.f, z); - npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb)); + npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb), std::move(cbName)); }); @@ -83,21 +129,19 @@ namespace ZL { }); api.set_function("player_walk_to", - [loc](float x, float y, float z, sol::object on_arrived) { + [loc, this_impl = impl.get()](float x, float y, float z, sol::object on_arrived) { std::function cb; + std::string cbName; if (on_arrived.is()) { sol::protected_function fn = on_arrived.as(); - cb = [fn]() mutable { - auto result = fn(); - if (!result.valid()) { - sol::error err = result; - std::cerr << "[script] on_arrived error: " << err.what() << "\n"; - } - }; + cb = wrapLuaCallback(fn); + cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); + if (cbName.empty()) + std::cerr << "[script] player_walk_to: on_arrived callback has no global name, will not survive save/load\n"; } loc->player->state.homePosition = Eigen::Vector3f(x, 0.f, z); - loc->player->setTarget(Eigen::Vector3f(x, y, z), std::move(cb)); + loc->player->setTarget(Eigen::Vector3f(x, y, z), std::move(cb), std::move(cbName)); }); @@ -556,7 +600,7 @@ namespace ZL { // move_object(name, x, y, z, duration_sec [, on_complete]) api.set_function("move_object", - [loc](const std::string& name, float x, float y, float z, + [loc, this_impl = impl.get()](const std::string& name, float x, float y, float z, float durationSec, sol::object onComplete) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name != name) continue; @@ -565,17 +609,15 @@ namespace ZL { return; } std::function cb; + std::string cbName; if (onComplete.is()) { sol::protected_function fn = onComplete.as(); - cb = [fn]() mutable { - auto res = fn(); - if (!res.valid()) { - sol::error err = res; - std::cerr << "[script] move_object on_complete error: " << err.what() << "\n"; - } - }; + cb = wrapLuaCallback(fn); + cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); + if (cbName.empty()) + std::cerr << "[script] move_object: on_complete callback has no global name, will not survive save/load\n"; } - intObj.moveTo(Eigen::Vector3f(x, y, z), durationSec, std::move(cb)); + intObj.moveTo(Eigen::Vector3f(x, y, z), durationSec, std::move(cb), std::move(cbName)); return; } std::cerr << "[script] move_object: object '" << name << "' not found\n"; @@ -583,7 +625,7 @@ namespace ZL { // rotate_object(name, angle_deg, duration_sec [, on_complete]) api.set_function("rotate_object", - [loc](const std::string& name, float angleDeg, + [loc, this_impl = impl.get()](const std::string& name, float angleDeg, float durationSec, sol::object onComplete) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name != name) continue; @@ -592,18 +634,16 @@ namespace ZL { return; } std::function cb; + std::string cbName; if (onComplete.is()) { sol::protected_function fn = onComplete.as(); - cb = [fn]() mutable { - auto res = fn(); - if (!res.valid()) { - sol::error err = res; - std::cerr << "[script] rotate_object on_complete error: " << err.what() << "\n"; - } - }; + cb = wrapLuaCallback(fn); + cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); + if (cbName.empty()) + std::cerr << "[script] rotate_object: on_complete callback has no global name, will not survive save/load\n"; } const float angleRad = angleDeg * static_cast(M_PI) / 180.f; - intObj.rotateTo(intObj.state.rotationY + angleRad, durationSec, std::move(cb)); + intObj.rotateTo(intObj.state.rotationY + angleRad, durationSec, std::move(cb), std::move(cbName)); return; } std::cerr << "[script] rotate_object: object '" << name << "' not found\n"; @@ -611,7 +651,7 @@ namespace ZL { // fade_object(name, target_alpha, duration_sec [, on_complete]) api.set_function("fade_object", - [loc](const std::string& name, float targetAlpha, + [loc, this_impl = impl.get()](const std::string& name, float targetAlpha, float durationSec, sol::object onComplete) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name != name) continue; @@ -620,17 +660,15 @@ namespace ZL { return; } std::function cb; + std::string cbName; if (onComplete.is()) { sol::protected_function fn = onComplete.as(); - cb = [fn]() mutable { - auto res = fn(); - if (!res.valid()) { - sol::error err = res; - std::cerr << "[script] fade_object on_complete error: " << err.what() << "\n"; - } - }; + cb = wrapLuaCallback(fn); + cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); + if (cbName.empty()) + std::cerr << "[script] fade_object: on_complete callback has no global name, will not survive save/load\n"; } - intObj.fadeTo(targetAlpha, durationSec, std::move(cb)); + intObj.fadeTo(targetAlpha, durationSec, std::move(cb), std::move(cbName)); return; } std::cerr << "[script] fade_object: object '" << name << "' not found\n"; @@ -638,7 +676,7 @@ namespace ZL { // scale_object(name, target_scale, duration_sec [, on_complete]) api.set_function("scale_object", - [loc](const std::string& name, float targetScale, + [loc, this_impl = impl.get()](const std::string& name, float targetScale, float durationSec, sol::object onComplete) { for (auto& intObj : loc->interactiveObjects) { if (intObj.loadedObject.name != name) continue; @@ -647,17 +685,15 @@ namespace ZL { return; } std::function cb; + std::string cbName; if (onComplete.is()) { sol::protected_function fn = onComplete.as(); - cb = [fn]() mutable { - auto res = fn(); - if (!res.valid()) { - sol::error err = res; - std::cerr << "[script] scale_object on_complete error: " << err.what() << "\n"; - } - }; + cb = wrapLuaCallback(fn); + cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); + if (cbName.empty()) + std::cerr << "[script] scale_object: on_complete callback has no global name, will not survive save/load\n"; } - intObj.scaleTo(targetScale, durationSec, std::move(cb)); + intObj.scaleTo(targetScale, durationSec, std::move(cb), std::move(cbName)); return; } std::cerr << "[script] scale_object: object '" << name << "' not found\n"; @@ -1065,4 +1101,18 @@ namespace ZL { } } + void ScriptEngine::rehydrateNamedCallback(const std::string& name, std::function& outCallback) const + { + outCallback = nullptr; + if (!impl || name.empty()) return; + + sol::protected_function fn = impl->lua[name]; + if (!fn.valid()) { + std::cerr << "[SCRIPT] rehydrateNamedCallback: global function '" << name + << "' not found (script changed since save?)\n"; + return; + } + outCallback = wrapLuaCallback(fn); + } + } // namespace ZL diff --git a/src/ScriptEngine.h b/src/ScriptEngine.h index 4cbfe2c..d5100dd 100644 --- a/src/ScriptEngine.h +++ b/src/ScriptEngine.h @@ -2,6 +2,7 @@ #include #include #include +#include #include "quest/QuestJournal.h" #include "external/nlohmann/json.hpp" #include "ISaveable.h" @@ -62,6 +63,14 @@ public: // has already run and registered all callbacks. void loadScriptGlobals(const nlohmann::json& in); + // Re-wires an in-flight, one-shot callback (NPC/player walk-arrival, object + // animation-complete) that was serialized by name rather than by value. + // Looks up `name` as a global Lua function and assigns a wrapped invoker to + // outCallback; leaves outCallback empty and logs an error if not found. + // Call after init() (the script must already have run) and after the owning + // state (CharacterState / InteractiveObjectState) has been loaded. + void rehydrateNamedCallback(const std::string& name, std::function& outCallback) const; + void save(nlohmann::json& out) const override { saveScriptGlobals(out); } void load(const nlohmann::json& in) override { loadScriptGlobals(in); } diff --git a/src/items/InteractiveObject.cpp b/src/items/InteractiveObject.cpp index b2c1851..fc09cba 100644 --- a/src/items/InteractiveObject.cpp +++ b/src/items/InteractiveObject.cpp @@ -71,7 +71,7 @@ namespace ZL { } - void InteractiveObject::moveTo(const Eigen::Vector3f& target, float durationSec, std::function onComplete) { + void InteractiveObject::moveTo(const Eigen::Vector3f& target, float durationSec, std::function onComplete, std::string callbackName) { if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Move; @@ -83,12 +83,13 @@ namespace ZL { task.targetScale = state.scale; task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; - task.onComplete = std::move(onComplete); + task.onComplete = std::move(onComplete); + task.onCompleteName = std::move(callbackName); state.animTask = std::move(task); state.isAnimating = true; } - void InteractiveObject::rotateTo(float targetRotY, float durationSec, std::function onComplete) { + void InteractiveObject::rotateTo(float targetRotY, float durationSec, std::function onComplete, std::string callbackName) { if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Rotate; @@ -100,12 +101,13 @@ namespace ZL { task.targetScale = state.scale; task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; - task.onComplete = std::move(onComplete); + task.onComplete = std::move(onComplete); + task.onCompleteName = std::move(callbackName); state.animTask = std::move(task); state.isAnimating = true; } - void InteractiveObject::scaleTo(float targetScale, float durationSec, std::function onComplete) { + void InteractiveObject::scaleTo(float targetScale, float durationSec, std::function onComplete, std::string callbackName) { if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Scale; @@ -117,12 +119,13 @@ namespace ZL { task.targetScale = targetScale; task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; - task.onComplete = std::move(onComplete); + task.onComplete = std::move(onComplete); + task.onCompleteName = std::move(callbackName); state.animTask = std::move(task); state.isAnimating = true; } - void InteractiveObject::fadeTo(float targetAlpha, float durationSec, std::function onComplete) { + void InteractiveObject::fadeTo(float targetAlpha, float durationSec, std::function onComplete, std::string callbackName) { if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Fade; @@ -134,7 +137,8 @@ namespace ZL { task.targetScale = targetAlpha; // reuse targetScale to hold target alpha task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; - task.onComplete = std::move(onComplete); + task.onComplete = std::move(onComplete); + task.onCompleteName = std::move(callbackName); state.animTask = std::move(task); state.isAnimating = true; } @@ -256,6 +260,8 @@ namespace ZL { at["targetScale"] = animTask->targetScale; at["durationMs"] = animTask->durationMs; at["elapsedMs"] = animTask->elapsedMs; + if (!animTask->onCompleteName.empty()) + at["onCompleteName"] = animTask->onCompleteName; out["animTask"] = std::move(at); } } @@ -284,6 +290,7 @@ namespace ZL { task.targetScale = at.value("targetScale", 1.f); task.durationMs = at.value("durationMs", 1000.f); task.elapsedMs = at.value("elapsedMs", 0.f); + task.onCompleteName = at.value("onCompleteName", std::string()); // onComplete is non-serializable; re-wired at runtime by Lua script callbacks. animTask = std::move(task); } diff --git a/src/items/InteractiveObject.h b/src/items/InteractiveObject.h index b94a02c..c9b6e09 100644 --- a/src/items/InteractiveObject.h +++ b/src/items/InteractiveObject.h @@ -31,10 +31,12 @@ namespace ZL { Renderer& renderer, const std::string& zipPath = ""); - void moveTo(const Eigen::Vector3f& target, float durationSec, std::function onComplete = {}); - void rotateTo(float targetRotY, float durationSec, std::function onComplete = {}); - void scaleTo(float targetScale, float durationSec, std::function onComplete = {}); - void fadeTo(float targetAlpha, float durationSec, std::function onComplete = {}); + // callbackName, if given, is the global Lua function name backing onComplete — + // stored in the AnimTask so it survives save/load (see ScriptEngine::rehydrateNamedCallback). + void moveTo(const Eigen::Vector3f& target, float durationSec, std::function onComplete = {}, std::string callbackName = ""); + void rotateTo(float targetRotY, float durationSec, std::function onComplete = {}, std::string callbackName = ""); + void scaleTo(float targetScale, float durationSec, std::function onComplete = {}, std::string callbackName = ""); + void fadeTo(float targetAlpha, float durationSec, std::function onComplete = {}, std::string callbackName = ""); void update(int64_t deltaMs); void draw(Renderer& renderer) const; void drawDarklands(Renderer& renderer) const; diff --git a/src/items/InteractiveObjectState.h b/src/items/InteractiveObjectState.h index 52cdccd..172f049 100644 --- a/src/items/InteractiveObjectState.h +++ b/src/items/InteractiveObjectState.h @@ -22,6 +22,10 @@ struct AnimTask { float durationMs = 1000.f; float elapsedMs = 0.f; std::function onComplete; // non-serializable; re-wired at runtime by Lua callbacks + // Global Lua function name backing onComplete, if it was sourced from a script + // (see ScriptEngine::rehydrateNamedCallback). Serialized so the callback can be + // re-looked-up by name after a save/load; empty if none or non-Lua. + std::string onCompleteName; }; // Paths and baked mesh transforms needed to recreate a LoadedGameObject from disk.