#include "ScriptEngine.h" #include "Game.h" #include #include #include #include #include "Location.h" #include "items/ItemRegistry.h" #include "utils/Utils.h" #define SOL_ALL_SAFETIES_ON 1 #include namespace FRG { namespace { // 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 while scripts continue to pass function references. 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; } Character* findNpc(Location* location, const std::string& id) { for (auto& npc : location->npcs) if (npc && npc->entityId() == id) return npc.get(); return nullptr; } InteractiveObject* findInteractiveObject( Location* location, const std::string& id) { for (auto& object : location->interactiveObjects) if (object.entityId() == id) return &object; return nullptr; } } // anonymous namespace 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; sol::protected_function locationInitCallback; sol::protected_function locationEnterCallback; sol::protected_function locationExitCallback; sol::protected_function darklandsEnterCallback; sol::protected_function darklandsExitCallback; sol::protected_function triggerNightEnterCallback; sol::protected_function callTaxiCallback; sol::protected_function chatOpenCallbacks[3]; }; ScriptEngine::ScriptEngine() = default; ScriptEngine::~ScriptEngine() = default; void ScriptEngine::init(Location* loc, AudioPlayerAsync* audioPlayer, 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); // --- Переопределение функции print --- lua.set_function("print", [](sol::this_state ts, sol::variadic_args va) { sol::state_view lua_view(ts); sol::function tostring = lua_view["tostring"]; auto& log = logger(); log << "[script] "; bool first = true; for (auto arg : va) { if (!first) log << "\t"; // Разделяем аргументы табуляцией, как в стандартном Lua print std::string str = tostring(arg); log << str; first = false; } log << std::endl; }); // ------------------------------------- auto api = lua.create_named_table("game_api"); // npc_walk_to(npc_id, 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", [loc, this_impl = impl.get()](const std::string& npcId, float x, float y, float z, sol::object on_arrived) { Character* npc = findNpc(loc, npcId); if (!npc) { std::cerr << "[script] npc_walk_to: unknown NPC id '" << npcId << "'\n"; return; } std::string cbName; if (on_arrived.is()) { sol::protected_function fn = on_arrived.as(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); if (cbName.empty()) std::cerr << "[script] npc_walk_to: on_arrived must be a global Lua function\n"; } loc->gameServer().submit(SetCharacterHomePositionCommand{ loc->locationId(), npc->entityId(), Eigen::Vector3f(x, 0.f, z)}); loc->setCharacterTarget( *npc, Eigen::Vector3f(x, y, z), std::move(cbName)); }); api.set_function("npc_rotate_to", [loc](const std::string& npcId, float angle) { Character* npc = findNpc(loc, npcId); if (!npc) { std::cerr << "[script] npc_rotate_to: unknown NPC id '" << npcId << "'\n"; return; } const float rad = angle * static_cast(M_PI) / 180.f; logger() << "[script] npc_rotate_to: " << npcId << " angle " << angle << " degrees (" << rad << " radians)" << std::endl; loc->gameServer().submit(SetCharacterFacingAngleCommand{ loc->locationId(), npc->entityId(), rad, false}); }); api.set_function("player_rotate_to", [loc](float angle) { const float rad = angle * static_cast(M_PI) / 180.f; logger() << "[script] player_rotate_to: angle " << angle << " degrees (" << rad << " radians)" << std::endl; loc->gameServer().submit(SetCharacterFacingAngleCommand{ loc->locationId(), loc->player->entityId(), rad, false}); }); api.set_function("player_stop_and_rotate_to_npc", [loc](const std::string& npcId) { Character* npc = findNpc(loc, npcId); if (!npc) { std::cerr << "[script] player_stop_and_rotate_to_npc: unknown NPC id '" << npcId << "'\n"; return; } loc->faceCharacter(*loc->player, npc->entityId()); }); api.set_function("npc_stop_and_rotate_to_player", [loc](const std::string& npcId) { Character* npc = findNpc(loc, npcId); if (!npc) { std::cerr << "[script] npc_stop_and_rotate_to_player: unknown NPC id '" << npcId << "'\n"; return; } loc->faceCharacter(*npc, "player"); }); api.set_function("player_walk_to", [loc, this_impl = impl.get()](float x, float y, float z, sol::object on_arrived) { std::string cbName; if (on_arrived.is()) { sol::protected_function fn = on_arrived.as(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); if (cbName.empty()) std::cerr << "[script] player_walk_to: on_arrived must be a global Lua function\n"; } loc->gameServer().submit(SetCharacterHomePositionCommand{ loc->locationId(), loc->player->entityId(), Eigen::Vector3f(x, 0.f, z)}); loc->setCharacterTarget( *loc->player, Eigen::Vector3f(x, y, z), std::move(cbName)); }); api.set_function("has_item", [loc](const std::string& itemId) { const Item* item = ItemRegistry::instance().findById(itemId); if (item) { if (loc->hasInventoryItem(itemId)) { logger() << "[script] has_item: " << item->name << " returns true" << std::endl; return true; } else { logger() << "[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", [loc](const std::string& itemId) { const Item* item = ItemRegistry::instance().findById(itemId); if (item) { loc->addInventoryItem(item->id); logger() << "[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", [loc](const std::string& id) { logger() << "[script] remove_item: " << id << std::endl; loc->removeInventoryItem(id); }); api.set_function("deactivate_interactive_object", [loc](const std::string& objectId) { InteractiveObject* object = findInteractiveObject(loc, objectId); if (object) { object->state.isActive = false; logger() << "[script] deactivate_interactive_object: " << objectId << std::endl; return; } std::cerr << "[script] deactivate_interactive_object: unknown object id: " << objectId << std::endl; }); api.set_function("activate_interactive_object", [loc](const std::string& objectId) { InteractiveObject* object = findInteractiveObject(loc, objectId); if (object) { object->state.isActive = true; logger() << "[script] activate_interactive_object: " << objectId << std::endl; return; } std::cerr << "[script] activate_interactive_object: unknown object id: " << objectId << std::endl; }); api.set_function("set_object_rotation", [loc](const std::string& objectId, float value) { InteractiveObject* object = findInteractiveObject(loc, objectId); if (object) { object->state.rotationY = value * static_cast(M_PI) / 180.f; logger() << "[script] set_object_rotation: " << objectId << " " << value << std::endl; return; } std::cerr << "[script] set_object_rotation: unknown object id: " << objectId << std::endl; }); api.set_function("set_object_alpha", [loc](const std::string& objectId, float value) { InteractiveObject* object = findInteractiveObject(loc, objectId); if (object) { object->state.alpha = value; logger() << "[script] set_object_alpha: " << objectId << " " << value << std::endl; return; } std::cerr << "[script] set_object_alpha: unknown object id: " << objectId << std::endl; }); // get_inventory_count() api.set_function("get_inventory_count", [loc]() { return loc->inventoryItemCount(); }); // has_item(item_id) api.set_function("has_item", [loc](const std::string& id) { return loc->hasInventoryItem(id); }); api.set_function("start_dialogue", [loc](const std::string& dialogueId) { if (!loc->requestDialogueStart(dialogueId)) { std::cerr << "[script] start_dialogue failed for id: " << dialogueId << "\n"; } }); // start_cutscene(cutscene_id) api.set_function("start_cutscene", [loc](const std::string& cutsceneId) { if (!loc->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", [loc](const std::string& flag, int value) { loc->setDialogueFlag(flag, value); }); api.set_function("get_dialogue_flag", [loc](const std::string& flag) { return loc->getDialogueFlag(flag); }); api.set_function("set_navigation_area_available", [loc](const std::string& areaName, bool available) { if (!loc->setNavigationAreaAvailable(areaName, available)) { std::cerr << "[script] set_navigation_area_available: area not found: " << areaName << "\n"; } }); api.set_function("switch_navigation", [loc](int index) { if (!loc->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", [loc, this_impl = impl.get()](const std::string& zoneId, sol::object onEnter, sol::object onExit) { const bool exists = std::any_of( loc->triggerZones.begin(), loc->triggerZones.end(), [&zoneId](const TriggerZone& zone) { return zone.id == zoneId; }); if (!exists) { std::cerr << "[script] set_trigger_zone_callbacks: unknown zone id '" << zoneId << "'\n"; return; } 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", [loc]() { if (loc->requestDarklandsTransition) loc->requestDarklandsTransition(); }); api.set_function("set_day", [loc]() { loc->requestNightDayTransition(false, false); }); api.set_function("set_night", [loc]() { loc->requestNightDayTransition(true, false); logger() << "Set night called" << std::endl; }); api.set_function("set_dawn", [loc]() { loc->requestNightDayTransition(true, true); logger() << "Set dawn called" << std::endl; }); api.set_function("is_night", [loc]() { return loc->isNight(); }); api.set_function("is_dawn", [loc]() { return loc->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", [loc]() { if (loc->requestAdvanceDarklandsHud) loc->requestAdvanceDarklandsHud(); }); // set_location_callbacks(on_init, on_enter, on_exit) // on_init() is called only when this location has no retained state. // 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 onInit, sol::object onEnter, sol::object onExit) { if (onInit.is()) this_impl->locationInitCallback = onInit.as(); if (onEnter.is()) this_impl->locationEnterCallback = onEnter.as(); if (onExit.is()) this_impl->locationExitCallback = onExit.as(); }); // is_darklands() → bool api.set_function("is_darklands", [loc]() { return loc->isDarklands(); }); api.set_function("setFloatValue", [loc](const std::string& key, float value) { loc->setGlobalFloat(key, value); }); api.set_function("getFloatValue", [loc](const std::string& key) -> float { return loc->globalFloat(key); }); api.set_function("get_player_x", [loc]() -> float { auto state = loc->player ? loc->player->stateSnapshot() : nullptr; return state ? state->position.x() : 0.0f; }); api.set_function("get_player_z", [loc]() -> float { auto state = loc->player ? loc->player->stateSnapshot() : nullptr; return state ? state->position.z() : 0.0f; }); api.set_function("refill_current_player_hp", [loc]() { if (loc->player) { loc->refillCharacterHp(*loc->player); loc->resetCharacterAfterDeath(*loc->player); } }); // get_player_hp() → float (0 if no player) api.set_function("get_player_hp", [loc]() -> float { return loc->player ? loc->player->getHp() : 0.0f; }); // Global integer store — shared across all location scripts. // setIntValue(key, value) / getIntValue(key) → int (0 if not set) api.set_function("setIntValue", [loc](const std::string& key, int value) { loc->setGlobalInt(key, value); }); api.set_function("getIntValue", [loc](const std::string& key) -> int { return loc->globalInt(key); }); // 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", [loc]() { if (loc->requestClosePhone) loc->requestClosePhone(); }); api.set_function("return_to_main_menu", [loc]() { if (loc->requestReturnToMainMenu) loc->requestReturnToMainMenu(); }); // set_chat_unread(chat_index, unread [, preview_msg]) // chat_index: 0=first contact, 1=second, 2=third // unread: true to mark as unread, false to mark as read // preview_msg: optional string to show in the chat list preview (trimmed to 22 chars) // When unread=false and preview_msg is omitted, preview auto-updates from chat history. api.set_function("set_chat_unread", [loc](int chatIndex, bool unread, sol::object previewMsg) { if (!loc->requestSetChatUnread) { std::cerr << "[script] set_chat_unread: callback not wired\n"; return; } std::string msg = previewMsg.is() ? previewMsg.as() : ""; //Some translations: //See start_uni_interior.lua for more details //CONST_MSG_EMPTY = "(пусто)" //CONST_MSG_WHERE = "Бекзат, ты где? Я жду..." if (msg == "(пусто)") { if (g_currentLanguage == Language::English) { msg = "(empty)"; } } else if (msg == "Бекзат, ты где? Я жду...") { if (g_currentLanguage == Language::English) { msg = "Bekzat, where are you? ..."; } } loc->requestSetChatUnread(chatIndex, unread, msg); }); 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_call_taxi_callback", [this_impl = impl.get()](sol::object onTaxi) { if (onTaxi.is()) this_impl->callTaxiCallback = onTaxi.as(); }); api.set_function("set_trigger_zone_enabled", [loc](const std::string& zoneId, bool value) { for (auto& zone : loc->triggerZones) { if (zone.id != zoneId) continue; zone.enabled = value; return; } std::cerr << "[script] set_trigger_zone_enabled: unknown zone id '" << zoneId << "'\n"; }); // set_teleport_active(zone_id, active) // Swaps the particle effect texture to match the new state. api.set_function("set_teleport_active", [loc](const std::string& zoneId, bool value) { for (auto& zone : loc->teleportZones) { if (zone.id != zoneId) continue; zone.setActive(value); return; } std::cerr << "[script] set_teleport_active: unknown zone id '" << zoneId << "'\n"; }); // is_teleport_active(zone_id) → bool api.set_function("is_teleport_active", [loc](const std::string& zoneId) -> bool { for (const auto& zone : loc->teleportZones) if (zone.id == zoneId) return zone.active; std::cerr << "[script] is_teleport_active: unknown zone id '" << zoneId << "'\n"; return false; }); // npc_set_hp(npc_id, value) api.set_function("npc_set_hp", [loc](const std::string& npcId, float value) { Character* npc = findNpc(loc, npcId); if (!npc) { std::cerr << "[script] npc_set_hp: unknown NPC id '" << npcId << "'\n"; return; } loc->setCharacterHp(*npc, value); }); // npc_set_position(npc_id, x, y, z) api.set_function("npc_set_position", [loc](const std::string& npcId, float x, float y, float z) { Character* npc = findNpc(loc, npcId); if (!npc) { std::cerr << "[script] npc_set_position: unknown NPC id '" << npcId << "'\n"; return; } Eigen::Vector3f pos(x, y, z); loc->gameServer().submit(SetCharacterPositionCommand{ loc->locationId(), npc->entityId(), pos, true}); loc->setCharacterTarget(*npc, pos); }); // npc_set_rotation(npc_id, angle) api.set_function("npc_set_rotation", [loc](const std::string& npcId, float angle) { Character* npc = findNpc(loc, npcId); if (!npc) { std::cerr << "[script] npc_set_rotation: unknown NPC id '" << npcId << "'\n"; return; } const float rad = angle * static_cast(M_PI) / 180.f; loc->gameServer().submit(SetCharacterFacingAngleCommand{ loc->locationId(), npc->entityId(), rad, true}); }); // set_npc_enabled(npc_id, enabled) api.set_function("set_npc_enabled", [loc](const std::string& npcId, bool value) { Character* npc = findNpc(loc, npcId); if (!npc) { std::cerr << "[script] set_npc_enabled: unknown NPC id '" << npcId << "'\n"; return; } loc->gameServer().submit(SetCharacterEnabledCommand{ loc->locationId(), npc->entityId(), value}); }); api.set_function("player_set_position", [loc](float x, float y, float z) { Eigen::Vector3f pos(x, y, z); loc->gameServer().submit(SetCharacterPositionCommand{ loc->locationId(), loc->player->entityId(), pos, true}); loc->setCharacterTarget(*loc->player, pos); }); api.set_function("player_set_rotation", [loc](float angle) { const float rad = angle * static_cast(M_PI) / 180.f; loc->gameServer().submit(SetCharacterFacingAngleCommand{ loc->locationId(), loc->player->entityId(), rad, true}); }); // move_object(name, x, y, z, duration_sec [, on_complete]) api.set_function("move_object", [loc, this_impl = impl.get()](const std::string& objectId, float x, float y, float z, float durationSec, sol::object onComplete) { InteractiveObject* object = findInteractiveObject(loc, objectId); if (!object) { std::cerr << "[script] move_object: unknown object id '" << objectId << "'\n"; return; } if (object->state.isAnimating) { std::cerr << "[script] move_object: '" << objectId << "' is already animating\n"; return; } std::string cbName; if (onComplete.is()) { sol::protected_function fn = onComplete.as(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); if (cbName.empty()) std::cerr << "[script] move_object: on_complete must be a global Lua function\n"; } object->state.moveTo( Eigen::Vector3f(x, y, z), durationSec, std::move(cbName)); }); // rotate_object(name, angle_deg, duration_sec [, on_complete]) api.set_function("rotate_object", [loc, this_impl = impl.get()](const std::string& objectId, float angleDeg, float durationSec, sol::object onComplete) { InteractiveObject* object = findInteractiveObject(loc, objectId); if (!object) { std::cerr << "[script] rotate_object: unknown object id '" << objectId << "'\n"; return; } if (object->state.isAnimating) { std::cerr << "[script] rotate_object: '" << objectId << "' is already animating\n"; return; } std::string cbName; if (onComplete.is()) { sol::protected_function fn = onComplete.as(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); if (cbName.empty()) std::cerr << "[script] rotate_object: on_complete must be a global Lua function\n"; } const float angleRad = angleDeg * static_cast(M_PI) / 180.f; object->state.rotateTo( object->state.rotationY + angleRad, durationSec, std::move(cbName)); }); // fade_object(name, target_alpha, duration_sec [, on_complete]) api.set_function("fade_object", [loc, this_impl = impl.get()](const std::string& objectId, float targetAlpha, float durationSec, sol::object onComplete) { InteractiveObject* object = findInteractiveObject(loc, objectId); if (!object) { std::cerr << "[script] fade_object: unknown object id '" << objectId << "'\n"; return; } if (object->state.isAnimating) { std::cerr << "[script] fade_object: '" << objectId << "' is already animating\n"; return; } std::string cbName; if (onComplete.is()) { sol::protected_function fn = onComplete.as(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); if (cbName.empty()) std::cerr << "[script] fade_object: on_complete must be a global Lua function\n"; } object->state.fadeTo(targetAlpha, durationSec, std::move(cbName)); }); // scale_object(name, target_scale, duration_sec [, on_complete]) api.set_function("scale_object", [loc, this_impl = impl.get()](const std::string& objectId, float targetScale, float durationSec, sol::object onComplete) { InteractiveObject* object = findInteractiveObject(loc, objectId); if (!object) { std::cerr << "[script] scale_object: unknown object id '" << objectId << "'\n"; return; } if (object->state.isAnimating) { std::cerr << "[script] scale_object: '" << objectId << "' is already animating\n"; return; } std::string cbName; if (onComplete.is()) { sol::protected_function fn = onComplete.as(); cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn); if (cbName.empty()) std::cerr << "[script] scale_object: on_complete must be a global Lua function\n"; } object->state.scaleTo(targetScale, durationSec, std::move(cbName)); }); api.set_function("resetPlayerAfterDeath", [loc]() { if (loc->player) loc->resetCharacterAfterDeath(*loc->player); }); // player_stop() — cancels the player's current walk target and stops them immediately. api.set_function("player_stop", [loc]() { if (loc->player) loc->stopCharacter(*loc->player); }); // npc_stop(npc_id) api.set_function("npc_stop", [loc](const std::string& npcId) { Character* npc = findNpc(loc, npcId); if (!npc) { std::cerr << "[script] npc_stop: unknown NPC id '" << npcId << "'\n"; return; } loc->stopCharacter(*npc); }); // set_npc_bump_callbacks(npc_id, 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", [loc, this_impl = impl.get()](const std::string& npcId, sol::object onPlayerBumpsNpc, sol::object onNpcBumpsPlayer) { if (!findNpc(loc, npcId)) { std::cerr << "[script] set_npc_bump_callbacks: unknown NPC id '" << npcId << "'\n"; return; } if (onPlayerBumpsNpc.is()) this_impl->npcBumpedByPlayerCallbacks[npcId] = onPlayerBumpsNpc.as(); if (onNpcBumpsPlayer.is()) this_impl->npcBumpsPlayerCallbacks[npcId] = onNpcBumpsPlayer.as(); }); // quest_unlock(quest_id) api.set_function("quest_unlock", [loc](const std::string& questId) -> bool { return loc->unlockQuest(questId); }); // quest_complete(quest_id) api.set_function("quest_complete", [loc](const std::string& questId) -> bool { return loc->completeQuest(questId); }); // quest_fail(quest_id) api.set_function("quest_fail", [loc](const std::string& questId) -> bool { return loc->failQuest(questId); }); // quest_set_objective_completed(quest_id, objective_id [, completed]) api.set_function("quest_set_objective_completed", [loc](const std::string& questId, const std::string& objId, sol::object completed) -> bool { const bool val = completed.is() ? completed.as() : true; return loc->setQuestObjectiveCompleted(questId, objId, val); }); // quest_set_objective_visible(quest_id, objective_id [, visible]) api.set_function("quest_set_objective_visible", [loc](const std::string& questId, const std::string& objId, sol::object visible) -> bool { const bool val = visible.is() ? visible.as() : true; return loc->setQuestObjectiveVisible(questId, objId, val); }); // quest_set_active_objective(quest_id, objective_index) api.set_function("quest_set_active_objective", [loc](const std::string& questId, int index) -> bool { return loc->setQuestActiveObjective(questId, index); }); api.set_function("call_tutorial_taxi_required", [loc]() -> void { if (loc->onPlayerTaxiRequired) { loc->onPlayerTaxiRequired(); loc->onPlayerTaxiRequired = nullptr; } else { std::cerr << "[script] call_tutorial_taxi_required: function is empty\n"; } }); api.set_function("play_sound", [audioPlayer](const std::string& soundPath) -> void { if (audioPlayer) { audioPlayer->playSoundAsync(soundPath); } }); //lua.script_file(scriptPath); #ifdef __ANDROID__ // Читаем файл в строку через SDL_RWops std::string scriptContent = readTextFile(scriptPath); if (scriptContent.empty()) { std::cerr << "[script] Failed to read script file or file is empty: " << scriptPath << "\n"; } else { // Загружаем (компилируем) строку скрипта. Вторым аргументом передаем имя для логов auto loadResult = lua.load(scriptContent, scriptPath); if (!loadResult.valid()) { sol::error err = loadResult; std::cerr << "[script] Lua compile error: " << err.what() << "\n"; } else { // Если компиляция успешна — вызываем функцию для выполнения кода скрипта auto runResult = loadResult(); if (!runResult.valid()) { sol::error err = runResult; std::cerr << "[script] Lua runtime error: " << err.what() << "\n"; } } } #else // На остальных платформах оставляем стандартную загрузку из файла auto result = lua.script_file(scriptPath, sol::script_pass_on_error); if (!result.valid()) { sol::error err = result; std::cerr << "[script] Lua load error: " << err.what() << "\n"; } #endif } void ScriptEngine::callNpcInteractCallback(const std::string& npcId) { 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(npcId); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] on_npc_interact error: " << err.what() << "\n"; } else { logger() << "[SCRIPT] on_npc_interact called with id " << npcId << 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::callFunction(const std::string& functionName) { if (!impl) { throw std::runtime_error("[SCRIPT] Engine not initialized!"); } if (functionName.empty()) { throw std::runtime_error("[SCRIPT] Function name is empty!"); } sol::state& lua = impl->lua; logger() << "[SCRIPT] Looking for function: " << functionName << std::endl; sol::protected_function function = lua[functionName]; if (!function.valid()) { throw std::runtime_error("[SCRIPT] Lua function not found: " + functionName); } logger() << "[SCRIPT] Found function! Calling: " << functionName << std::endl; auto result = function(); if (!result.valid()) { sol::error err = result; throw std::runtime_error("[SCRIPT] Error executing " + functionName + ": " + std::string(err.what())); } logger() << "[SCRIPT] Function executed successfully!" << std::endl; } void ScriptEngine::callActivateFunction(const std::string& functionName) { callFunction(functionName); } 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()) { logger() << "[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 { logger() << "[SCRIPT] Callback executed successfully!" << std::endl; } } else { logger() << "[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::callLocationInitCallback() { if (!impl || !impl->locationInitCallback.valid()) return; auto result = impl->locationInitCallback(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] location init callback error: " << err.what() << "\n"; } } 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::callCallTaxiCallback() { if (!impl || !impl->callTaxiCallback.valid()) return; auto result = impl->callTaxiCallback(); if (!result.valid()) { sol::error err = result; std::cerr << "[SCRIPT] call_taxi 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(const std::string& npcId) { if (!impl) return; auto it = impl->npcBumpedByPlayerCallbacks.find(npcId); 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 " << npcId << ": " << 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(const std::string& npcId) { if (!impl) return; auto it = impl->npcBumpsPlayerCallbacks.find(npcId); 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 " << npcId << ": " << err.what() << "\n"; } } void ScriptEngine::saveScriptGlobals(nlohmann::json& out) const { if (!impl) return; lua_State* L = impl->lua.lua_state(); // Iterate the global table (_G). lua_next pops the key and pushes key+value. lua_pushglobaltable(L); // stack: [_G] lua_pushnil(L); // stack: [_G, nil] (first key) while (lua_next(L, -2) != 0) { // stack: [_G, key, value] if (lua_type(L, -2) == LUA_TSTRING) { const char* key = lua_tostring(L, -2); // Skip Lua built-in globals and the game API table. const std::string k(key); if (k == "_G" || k == "_VERSION" || k == "game_api") { lua_pop(L, 1); // pop value, keep key for next iteration continue; } const int t = lua_type(L, -1); if (t == LUA_TNUMBER) { if (lua_isinteger(L, -1)) { out[k] = static_cast(lua_tointeger(L, -1)); } else { out[k] = lua_tonumber(L, -1); } } else if (t == LUA_TSTRING) { out[k] = lua_tostring(L, -1); } else if (t == LUA_TBOOLEAN) { out[k] = (lua_toboolean(L, -1) != 0); } // Skip functions, tables, userdata — recreated by re-running the script. } lua_pop(L, 1); // pop value, keep key for next iteration } lua_pop(L, 1); // pop _G } void ScriptEngine::loadScriptGlobals(const nlohmann::json& in) { if (!impl) return; lua_State* L = impl->lua.lua_state(); for (auto it = in.begin(); it != in.end(); ++it) { const std::string& key = it.key(); const auto& val = it.value(); if (val.is_number_integer()) { lua_pushinteger(L, static_cast(val.get())); } else if (val.is_number_float()) { lua_pushnumber(L, static_cast(val.get())); } else if (val.is_string()) { const std::string s = val.get(); lua_pushstring(L, s.c_str()); } else if (val.is_boolean()) { lua_pushboolean(L, val.get() ? 1 : 0); } else { continue; } lua_setglobal(L, key.c_str()); } } } // namespace FRG