#include "ScriptEngine.h" #include "Game.h" #include #include #include #include "Location.h" #include "items/ItemRegistry.h" #define SOL_ALL_SAFETIES_ON 1 #include namespace ZL { struct ScriptEngine::Impl { sol::state lua; std::unordered_map triggerEnterCallbacks; std::unordered_map triggerExitCallbacks; std::unordered_map cutsceneCompleteCallbacks; std::unordered_map npcBumpedByPlayerCallbacks; std::unordered_map npcBumpsPlayerCallbacks; std::unordered_map* globalInts = nullptr; std::unordered_map* globalFloats = nullptr; Quest::QuestJournal* questJournal = nullptr; sol::protected_function locationEnterCallback; sol::protected_function locationExitCallback; sol::protected_function darklandsEnterCallback; sol::protected_function darklandsExitCallback; sol::protected_function triggerNightEnterCallback; sol::protected_function chatOpenCallbacks[3]; }; ScriptEngine::ScriptEngine() = default; ScriptEngine::~ScriptEngine() = default; void ScriptEngine::init(Location* game, Inventory* inventory, const std::string& scriptPath) { impl = std::make_unique(); sol::state& lua = impl->lua; lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string, sol::lib::table); auto api = lua.create_named_table("game_api"); // npc_walk_to(index, x, y, z [, on_arrived]) // 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", [game](int index, float x, float y, float z, sol::object on_arrived) { auto& npcs = game->npcs; if (index < 0 || index >= static_cast(npcs.size())) { std::cerr << "[script] npc_walk_to: index " << index << " out of range (0.." << npcs.size() - 1 << ")\n"; return; } std::function cb; 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"; } }; } npcs[index]->homePosition = Eigen::Vector3f(x, 0.f, z); npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb)); }); api.set_function("player_walk_to", [game](float x, float y, float z, sol::object on_arrived) { std::function cb; 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"; } }; } game->player->homePosition = Eigen::Vector3f(x, 0.f, z); game->player->setTarget(Eigen::Vector3f(x, y, z), std::move(cb)); }); api.set_function("has_item", [inventory](const std::string& itemId) { const Item* item = ItemRegistry::instance().findById(itemId); if (item) { if (inventory->hasItem(itemId)) { std::cout << "[script] has_item: " << item->name << " returns true" << std::endl; return true; } else { std::cout << "[script] has_item: " << item->name << " returns false" << std::endl; return false; } } else { std::cerr << "[script] has_item: item '" << itemId << "' not found in ItemRegistry\n"; return false; } }); // pickup_item(item_id) api.set_function("pickup_item", [inventory](const std::string& itemId) { const Item* item = ItemRegistry::instance().findById(itemId); if (item) { inventory->addItem(*item); std::cout << "[script] pickup_item: " << item->name << std::endl; } else { std::cerr << "[script] pickup_item: item '" << itemId << "' not found in ItemRegistry\n"; } }); // remove_item(item_id) api.set_function("remove_item", [game, inventory](const std::string& id) { std::cout << "[script] remove_item: " << id << std::endl; inventory->removeItem(id); }); // deactivate_interactive_object(object_name) api.set_function("deactivate_interactive_object", [game](const std::string& objectName) { for (auto& intObj : game->interactiveObjects) { if (intObj.loadedObject.name == objectName) { intObj.isActive = false; std::cout << "[script] deactivate_interactive_object: " << objectName << std::endl; return; } } std::cerr << "[script] deactivate_interactive_object: not found: " << objectName << std::endl; }); // activate_interactive_object(object_name) api.set_function("activate_interactive_object", [game](const std::string& objectName) { for (auto& intObj : game->interactiveObjects) { if (intObj.loadedObject.name == objectName) { intObj.isActive = true; std::cout << "[script] activate_interactive_object: " << objectName << std::endl; return; } } std::cerr << "[script] activate_interactive_object: not found: " << objectName << std::endl; }); api.set_function("set_object_rotation", [game](const std::string& objectName, float value) { for (auto& intObj : game->interactiveObjects) { if (intObj.loadedObject.name == objectName) { intObj.rotationY = value * static_cast(M_PI) / 180.f; std::cout << "[script] set_object_rotation: " << objectName << " " << value << std::endl; return; } } std::cerr << "[script] set_object_rotation: not found: " << objectName << std::endl; }); api.set_function("set_object_alpha", [game](const std::string& objectName, float value) { for (auto& intObj : game->interactiveObjects) { if (intObj.loadedObject.name == objectName) { intObj.alpha = value; std::cout << "[script] set_object_alpha: " <getCount(); }); // has_item(item_id) api.set_function("has_item", [inventory](const std::string& id) { return inventory->hasItem(id); }); api.set_function("start_dialogue", [game](const std::string& dialogueId) { if (!game->requestDialogueStart(dialogueId)) { std::cerr << "[script] start_dialogue failed for id: " << dialogueId << "\n"; } }); // start_cutscene(cutscene_id) api.set_function("start_cutscene", [game](const std::string& cutsceneId) { if (!game->requestCutsceneStart(cutsceneId)) std::cerr << "[script] start_cutscene failed for id: " << cutsceneId << "\n"; }); // set_cutscene_callback(cutscene_id, on_complete) // on_complete() is called with no arguments when the named cutscene finishes. api.set_function("set_cutscene_callback", [this_impl = impl.get()](const std::string& cutsceneId, sol::object onComplete) { if (onComplete.is()) this_impl->cutsceneCompleteCallbacks[cutsceneId] = onComplete.as(); }); api.set_function("set_dialogue_flag", [game](const std::string& flag, int value) { game->setDialogueFlag(flag, value); }); api.set_function("get_dialogue_flag", [game](const std::string& flag) { return game->getDialogueFlag(flag); }); api.set_function("set_navigation_area_available", [game](const std::string& areaName, bool available) { if (!game->setNavigationAreaAvailable(areaName, available)) { std::cerr << "[script] set_navigation_area_available: area not found: " << areaName << "\n"; } }); api.set_function("switch_navigation", [game](int index) { if (!game->switchNavigation(index)) { std::cerr << "[script] switch_navigation: index " << index << " out of range\n"; } }); // set_trigger_zone_callbacks(zone_id, on_enter, on_exit) // on_enter and on_exit are optional Lua functions (pass nil to omit). // Called when the player enters or exits the named trigger zone. api.set_function("set_trigger_zone_callbacks", [this_impl = impl.get()](const std::string& zoneId, sol::object onEnter, sol::object onExit) { if (onEnter.is()) this_impl->triggerEnterCallbacks[zoneId] = onEnter.as(); if (onExit.is()) this_impl->triggerExitCallbacks[zoneId] = onExit.as(); }); // start_darklands_transition() // Triggers the white-flash transition to toggle darklands mode. // Does nothing if a transition is already in progress. api.set_function("start_darklands_transition", [game]() { if (game->requestDarklandsTransition) game->requestDarklandsTransition(); }); api.set_function("set_day", [game]() { game->requestNightDayTransition(false, false); }); api.set_function("set_night", [game]() { game->requestNightDayTransition(true, false); std::cout << "Set night called" << std::endl; }); api.set_function("set_dawn", [game]() { game->requestNightDayTransition(true, true); std::cout << "Set dawn called" << std::endl; }); api.set_function("is_night", [game]() { return game->isNight; }); api.set_function("is_dawn", [game]() { return game->isDawn; }); // advance_darklands_hud() // Advances the uni_interior darklands HUD from step12 to step13. // Call when the player enters the ghost trigger zone in darklands. api.set_function("advance_darklands_hud", [game]() { if (game->requestAdvanceDarklandsHud) game->requestAdvanceDarklandsHud(); }); // set_location_callbacks(on_enter, on_exit) // on_enter() called once when the player arrives at this location. // on_exit() called once just before the player leaves this location. api.set_function("set_location_callbacks", [this_impl = impl.get()](sol::object onEnter, sol::object onExit) { if (onEnter.is()) this_impl->locationEnterCallback = onEnter.as(); if (onExit.is()) this_impl->locationExitCallback = onExit.as(); }); // is_darklands() → bool api.set_function("is_darklands", [game]() { return game->isDarklands; }); api.set_function("setFloatValue", [this_impl = impl.get()](const std::string& key, float value) { if (this_impl->globalFloats) (*this_impl->globalFloats)[key] = value; }); api.set_function("getFloatValue", [this_impl = impl.get()](const std::string& key) -> float { if (!this_impl->globalFloats) return 0.0f; auto it = this_impl->globalFloats->find(key); return it != this_impl->globalFloats->end() ? it->second : 0.0f; }); api.set_function("get_player_x", [game]() -> float { return game->player ? game->player->position.x() : 0.0f; }); api.set_function("get_player_z", [game]() -> float { return game->player ? game->player->position.z() : 0.0f; }); // set_player_hp(value) — sets the player's current HP directly. api.set_function("set_player_hp", [game](float value) { if (game->player) { game->player->hp = value; if (game->player->currentState == AnimationState::ACTION_TO_DEATH || game->player->currentState == AnimationState::DEATH_IDLE) { game->player->currentState = AnimationState::STAND; game->player->resetAnim = true; } } }); // get_player_hp() → float (0 if no player) api.set_function("get_player_hp", [game]() -> float { return game->player ? game->player->hp : 0.0f; }); // Global integer store — shared across all location scripts. // setIntValue(key, value) / getIntValue(key) → int (0 if not set) api.set_function("setIntValue", [this_impl = impl.get()](const std::string& key, int value) { if (this_impl->globalInts) (*this_impl->globalInts)[key] = value; }); api.set_function("getIntValue", [this_impl = impl.get()](const std::string& key) -> int { if (!this_impl->globalInts) return 0; auto it = this_impl->globalInts->find(key); return it != this_impl->globalInts->end() ? it->second : 0; }); // set_darklands_callbacks(on_enter, on_exit) // on_enter() called when the player switches into Darklands mode. // on_exit() called when the player switches back to normal mode. // Pass nil to omit either callback. api.set_function("set_darklands_callbacks", [this_impl = impl.get()](sol::object onEnter, sol::object onExit) { if (onEnter.is()) this_impl->darklandsEnterCallback = onEnter.as(); if (onExit.is()) this_impl->darklandsExitCallback = onExit.as(); }); api.set_function("close_phone", [game]() { if (game->requestClosePhone) game->requestClosePhone(); }); api.set_function("set_chat_callbacks", [this_impl = impl.get()](sol::object cb0, sol::object cb1, sol::object cb2) { if (cb0.is()) this_impl->chatOpenCallbacks[0] = cb0.as(); if (cb1.is()) this_impl->chatOpenCallbacks[1] = cb1.as(); if (cb2.is()) this_impl->chatOpenCallbacks[2] = cb2.as(); }); api.set_function("set_enter_night_callback", [this_impl = impl.get()](sol::object onEnter) { if (onEnter.is()) this_impl->triggerNightEnterCallback = onEnter.as(); }); api.set_function("set_trigger_zone_enabled", [game](int index, bool value) { auto& triggerZones = game->triggerZones; if (index < 0 || index >= static_cast(triggerZones.size())) { std::cerr << "[script] set_trigger_zone_enabled: index " << index << " out of range\n"; return; } triggerZones[index].enabled = value; }); // npc_set_hp(index, value) — sets an NPC's current HP directly. api.set_function("npc_set_hp", [game](int index, float value) { auto& npcs = game->npcs; if (index < 0 || index >= static_cast(npcs.size())) { std::cerr << "[script] npc_set_hp: index " << index << " out of range\n"; return; } npcs[index]->hp = value; }); // npc_set_position(index, x, y, z) — teleports an NPC instantly, no walking. api.set_function("npc_set_position", [game](int index, float x, float y, float z) { auto& npcs = game->npcs; if (index < 0 || index >= static_cast(npcs.size())) { std::cerr << "[script] npc_set_position: index " << index << " out of range\n"; return; } Eigen::Vector3f pos(x, y, z); npcs[index]->position = pos; npcs[index]->homePosition = pos; npcs[index]->setTarget(pos); }); // npc_set_rotation(index, angle) — sets NPC facing angle around Y axis (degrees). api.set_function("npc_set_rotation", [game](int index, float angle) { auto& npcs = game->npcs; if (index < 0 || index >= static_cast(npcs.size())) { std::cerr << "[script] npc_set_rotation: index " << index << " out of range\n"; return; } const float rad = angle * static_cast(M_PI) / 180.f; npcs[index]->facingAngle = rad; npcs[index]->targetFacingAngle = rad; }); // set_npc_enabled(index, enabled) api.set_function("set_npc_enabled", [game](int index, bool value) { auto& npcs = game->npcs; if (index < 0 || index >= static_cast(npcs.size())) { std::cerr << "[script] set_npc_enabled: index " << index << " out of range\n"; return; } npcs[index]->enabled = value; npcs[index]->attack = 0; npcs[index]->attack_cooldown = 0; npcs[index]->currentState = AnimationState::STAND; if (npcs[index]->canAttack) { npcs[index]->attackTarget = game->player.get(); } npcs[index]->battle_state = 0; }); // move_object(name, x, y, z, duration_sec [, on_complete]) api.set_function("move_object", [game](const std::string& name, float x, float y, float z, float durationSec, sol::object onComplete) { for (auto& intObj : game->interactiveObjects) { if (intObj.loadedObject.name != name) continue; if (intObj.isAnimating) { std::cerr << "[script] move_object: '" << name << "' is already animating\n"; return; } std::function cb; 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"; } }; } intObj.moveTo(Eigen::Vector3f(x, y, z), durationSec, std::move(cb)); return; } std::cerr << "[script] move_object: object '" << name << "' not found\n"; }); // rotate_object(name, angle_deg, duration_sec [, on_complete]) api.set_function("rotate_object", [game](const std::string& name, float angleDeg, float durationSec, sol::object onComplete) { for (auto& intObj : game->interactiveObjects) { if (intObj.loadedObject.name != name) continue; if (intObj.isAnimating) { std::cerr << "[script] rotate_object: '" << name << "' is already animating\n"; return; } std::function cb; 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"; } }; } const float angleRad = angleDeg * static_cast(M_PI) / 180.f; intObj.rotateTo(intObj.rotationY + angleRad, durationSec, std::move(cb)); return; } std::cerr << "[script] rotate_object: object '" << name << "' not found\n"; }); // fade_object(name, target_alpha, duration_sec [, on_complete]) api.set_function("fade_object", [game](const std::string& name, float targetAlpha, float durationSec, sol::object onComplete) { for (auto& intObj : game->interactiveObjects) { if (intObj.loadedObject.name != name) continue; if (intObj.isAnimating) { std::cerr << "[script] fade_object: '" << name << "' is already animating\n"; return; } std::function cb; 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"; } }; } intObj.fadeTo(targetAlpha, durationSec, std::move(cb)); return; } std::cerr << "[script] fade_object: object '" << name << "' not found\n"; }); // scale_object(name, target_scale, duration_sec [, on_complete]) api.set_function("scale_object", [game](const std::string& name, float targetScale, float durationSec, sol::object onComplete) { for (auto& intObj : game->interactiveObjects) { if (intObj.loadedObject.name != name) continue; if (intObj.isAnimating) { std::cerr << "[script] scale_object: '" << name << "' is already animating\n"; return; } std::function cb; 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"; } }; } intObj.scaleTo(targetScale, durationSec, std::move(cb)); return; } std::cerr << "[script] scale_object: object '" << name << "' not found\n"; }); api.set_function("resetPlayerAfterDeath", [game]() { if (game->player) game->player->resetPlayerAfterDeath(); }); // player_stop() — cancels the player's current walk target and stops them immediately. api.set_function("player_stop", [game]() { if (game->player) game->player->stopInPlace(); }); // npc_stop(index) — cancels an NPC's current walk target and stops them immediately. api.set_function("npc_stop", [game](int index) { auto& npcs = game->npcs; if (index < 0 || index >= static_cast(npcs.size())) { std::cerr << "[script] npc_stop: index " << index << " out of range (0.." << npcs.size() - 1 << ")\n"; return; } npcs[index]->stopInPlace(); }); // set_npc_bump_callbacks(npc_index, on_player_bumps_npc, on_npc_bumps_player) // Both callbacks are optional (pass nil to omit). // on_player_bumps_npc() — player was moving and walked into the NPC. // on_npc_bumps_player() — the NPC was moving and walked into the standing player. // Each callback fires at most once every 5 seconds per NPC (cooldown managed by Location). api.set_function("set_npc_bump_callbacks", [this_impl = impl.get()](int index, sol::object onPlayerBumpsNpc, sol::object onNpcBumpsPlayer) { if (onPlayerBumpsNpc.is()) this_impl->npcBumpedByPlayerCallbacks[index] = onPlayerBumpsNpc.as(); if (onNpcBumpsPlayer.is()) this_impl->npcBumpsPlayerCallbacks[index] = onNpcBumpsPlayer.as(); }); // quest_unlock(quest_id) api.set_function("quest_unlock", [this_impl = impl.get()](const std::string& questId) -> bool { if (!this_impl->questJournal) { std::cerr << "[script] quest_unlock: QuestJournal not set\n"; return false; } return this_impl->questJournal->unlockQuest(questId); }); // quest_complete(quest_id) api.set_function("quest_complete", [this_impl = impl.get()](const std::string& questId) -> bool { if (!this_impl->questJournal) { std::cerr << "[script] quest_complete: QuestJournal not set\n"; return false; } return this_impl->questJournal->completeQuest(questId); }); // quest_fail(quest_id) api.set_function("quest_fail", [this_impl = impl.get()](const std::string& questId) -> bool { if (!this_impl->questJournal) { std::cerr << "[script] quest_fail: QuestJournal not set\n"; return false; } return this_impl->questJournal->failQuest(questId); }); // quest_set_objective_completed(quest_id, objective_id [, completed]) api.set_function("quest_set_objective_completed", [this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object completed) -> bool { if (!this_impl->questJournal) { std::cerr << "[script] quest_set_objective_completed: QuestJournal not set\n"; return false; } const bool val = completed.is() ? completed.as() : true; return this_impl->questJournal->setObjectiveCompleted(questId, objId, val); }); // quest_set_objective_visible(quest_id, objective_id [, visible]) api.set_function("quest_set_objective_visible", [this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object visible) -> bool { if (!this_impl->questJournal) { std::cerr << "[script] quest_set_objective_visible: QuestJournal not set\n"; return false; } const bool val = visible.is() ? visible.as() : true; return this_impl->questJournal->setObjectiveVisible(questId, objId, val); }); // quest_set_active_objective(quest_id, objective_index) api.set_function("quest_set_active_objective", [this_impl = impl.get()](const std::string& questId, int index) -> bool { if (!this_impl->questJournal) { std::cerr << "[script] quest_set_active_objective: QuestJournal not set\n"; return false; } return this_impl->questJournal->setActiveObjective(questId, index); }); lua.script_file(scriptPath); } void ScriptEngine::callNpcInteractCallback(int npcIndex) { if (!impl) { std::cerr << "[SCRIPT] Engine not initialized!" << std::endl; return; } sol::state& lua = impl->lua; sol::function fn = lua["on_npc_interact"]; if (fn.valid()) { auto result = fn(npcIndex); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] on_npc_interact error: " << err.what() << "\n"; } else { std::cout << "[SCRIPT] on_npc_interact called with index " << npcIndex << std::endl; } } else { std::cerr << "[SCRIPT] Lua function 'on_npc_interact' not found!" << std::endl; } } void ScriptEngine::runScript(const std::string& path) { auto result = impl->lua.safe_script_file(path, sol::script_pass_on_error); if (!result.valid()) { sol::error err = result; std::cerr << "[script] Error in " << path << ": " << err.what() << "\n"; } } void ScriptEngine::callActivateFunction(const std::string& functionName) { if (!impl) { throw std::runtime_error("[SCRIPT] Engine not initialized!"); } if (functionName.empty()) { throw std::runtime_error("[SCRIPT] Activate function name is empty!"); } sol::state& lua = impl->lua; std::cout << "[SCRIPT] Looking for activate function: " << functionName << std::endl; sol::function activateFunc = lua[functionName]; if (!activateFunc.valid()) { throw std::runtime_error("[SCRIPT] Lua function not found: " + functionName); } std::cout << "[SCRIPT] Found function! Calling: " << functionName << std::endl; auto result = activateFunc(); if (!result.valid()) { sol::error err = result; throw std::runtime_error("[SCRIPT] Error executing " + functionName + ": " + std::string(err.what())); } std::cout << "[SCRIPT] Function executed successfully!" << std::endl; } void ScriptEngine::callItemPickupCallback(const std::string& objectName) { if (!impl) { std::cerr << "[SCRIPT] impl is null!" << std::endl; return; } sol::state& lua = impl->lua; // Try to find custom activate function first sol::function activateFunc = lua["on_item_pickup"]; if (activateFunc.valid()) { std::cout << "[SCRIPT] Callback found! Calling with argument: " << objectName << std::endl; auto result = activateFunc(objectName); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] on_item_pickup callback error: " << err.what() << "\n"; } else { std::cout << "[SCRIPT] Callback executed successfully!" << std::endl; } } else { std::cout << "[SCRIPT] Fallback: on_item_pickup not found" << std::endl; } } void ScriptEngine::callTriggerEnterCallback(const std::string& zoneId) { if (!impl) return; auto it = impl->triggerEnterCallbacks.find(zoneId); if (it == impl->triggerEnterCallbacks.end()) return; auto result = it->second(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] trigger enter callback error for '" << zoneId << "': " << err.what() << "\n"; } } void ScriptEngine::callTriggerExitCallback(const std::string& zoneId) { if (!impl) return; auto it = impl->triggerExitCallbacks.find(zoneId); if (it == impl->triggerExitCallbacks.end()) return; auto result = it->second(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] trigger exit callback error for '" << zoneId << "': " << err.what() << "\n"; } } void ScriptEngine::setGlobalStore(std::unordered_map* store) { if (impl) impl->globalInts = store; } void ScriptEngine::setGlobalFloatStore(std::unordered_map* store) { if (impl) impl->globalFloats = store; } void ScriptEngine::setQuestJournal(Quest::QuestJournal* journal) { if (impl) impl->questJournal = journal; } void ScriptEngine::callLocationEnterCallback() { if (!impl || !impl->locationEnterCallback.valid()) return; auto result = impl->locationEnterCallback(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] location enter callback error: " << err.what() << "\n"; } } void ScriptEngine::callLocationExitCallback() { if (!impl || !impl->locationExitCallback.valid()) return; auto result = impl->locationExitCallback(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] location exit callback error: " << err.what() << "\n"; } } void ScriptEngine::callDarklandsEnterCallback() { if (!impl || !impl->darklandsEnterCallback.valid()) return; auto result = impl->darklandsEnterCallback(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] darklands enter callback error: " << err.what() << "\n"; } } void ScriptEngine::callTriggerNightEnterCallback() { if (!impl || !impl->triggerNightEnterCallback.valid()) return; auto result = impl->triggerNightEnterCallback(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] trigger night enter callback error: " << err.what() << "\n"; } } void ScriptEngine::callDarklandsExitCallback() { if (!impl || !impl->darklandsExitCallback.valid()) return; auto result = impl->darklandsExitCallback(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] darklands exit callback error: " << err.what() << "\n"; } } void ScriptEngine::callCutsceneCompleteCallback(const std::string& cutsceneId) { if (!impl) return; auto it = impl->cutsceneCompleteCallbacks.find(cutsceneId); if (it == impl->cutsceneCompleteCallbacks.end()) return; auto result = it->second(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] cutscene complete callback error for '" << cutsceneId << "': " << err.what() << "\n"; } } void ScriptEngine::callNpcBumpedByPlayerCallback(int npcIndex) { if (!impl) return; auto it = impl->npcBumpedByPlayerCallbacks.find(npcIndex); if (it == impl->npcBumpedByPlayerCallbacks.end()) return; auto result = it->second(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] on_player_bumps_npc error for NPC " << npcIndex << ": " << err.what() << "\n"; } } void ScriptEngine::callChatOpenCallback(int chatIndex) { if (!impl) return; if (chatIndex < 0 || chatIndex > 2) return; auto& fn = impl->chatOpenCallbacks[chatIndex]; if (!fn.valid()) return; auto result = fn(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] chat open callback error for chat " << chatIndex << ": " << err.what() << "\n"; } } void ScriptEngine::callNpcBumpsPlayerCallback(int npcIndex) { if (!impl) return; auto it = impl->npcBumpsPlayerCallbacks.find(npcIndex); if (it == impl->npcBumpsPlayerCallbacks.end()) return; auto result = it->second(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] on_npc_bumps_player error for NPC " << npcIndex << ": " << err.what() << "\n"; } } } // namespace ZL