Files
shadow-over-bishkek001/src/ScriptEngine.cpp
T

1152 lines
49 KiB
C++

#include "ScriptEngine.h"
#include "Game.h"
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <unordered_map>
#include "Location.h"
#include "items/ItemRegistry.h"
#include "utils/Utils.h"
#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
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<std::string, sol::protected_function> triggerEnterCallbacks;
std::unordered_map<std::string, sol::protected_function> triggerExitCallbacks;
std::unordered_map<std::string, sol::protected_function> cutsceneCompleteCallbacks;
std::unordered_map<std::string, sol::protected_function> npcBumpedByPlayerCallbacks;
std::unordered_map<std::string, sol::protected_function> 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<Impl>();
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>()) {
sol::protected_function fn = on_arrived.as<sol::protected_function>();
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<float>(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<float>(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>()) {
sol::protected_function fn = on_arrived.as<sol::protected_function>();
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<float>(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<sol::protected_function>())
this_impl->cutsceneCompleteCallbacks[cutsceneId] = onComplete.as<sol::protected_function>();
});
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<sol::protected_function>())
this_impl->triggerEnterCallbacks[zoneId] = onEnter.as<sol::protected_function>();
if (onExit.is<sol::protected_function>())
this_impl->triggerExitCallbacks[zoneId] = onExit.as<sol::protected_function>();
});
// 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<sol::protected_function>())
this_impl->locationInitCallback = onInit.as<sol::protected_function>();
if (onEnter.is<sol::protected_function>())
this_impl->locationEnterCallback = onEnter.as<sol::protected_function>();
if (onExit.is<sol::protected_function>())
this_impl->locationExitCallback = onExit.as<sol::protected_function>();
});
// 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<sol::protected_function>())
this_impl->darklandsEnterCallback = onEnter.as<sol::protected_function>();
if (onExit.is<sol::protected_function>())
this_impl->darklandsExitCallback = onExit.as<sol::protected_function>();
});
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<std::string>()
? previewMsg.as<std::string>() : "";
//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<sol::protected_function>()) this_impl->chatOpenCallbacks[0] = cb0.as<sol::protected_function>();
if (cb1.is<sol::protected_function>()) this_impl->chatOpenCallbacks[1] = cb1.as<sol::protected_function>();
if (cb2.is<sol::protected_function>()) this_impl->chatOpenCallbacks[2] = cb2.as<sol::protected_function>();
});
api.set_function("set_enter_night_callback",
[this_impl = impl.get()](sol::object onEnter) {
if (onEnter.is<sol::protected_function>())
this_impl->triggerNightEnterCallback = onEnter.as<sol::protected_function>();
});
api.set_function("set_call_taxi_callback",
[this_impl = impl.get()](sol::object onTaxi) {
if (onTaxi.is<sol::protected_function>())
this_impl->callTaxiCallback = onTaxi.as<sol::protected_function>();
});
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<float>(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<float>(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>()) {
sol::protected_function fn = onComplete.as<sol::protected_function>();
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>()) {
sol::protected_function fn = onComplete.as<sol::protected_function>();
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<float>(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>()) {
sol::protected_function fn = onComplete.as<sol::protected_function>();
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>()) {
sol::protected_function fn = onComplete.as<sol::protected_function>();
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<sol::protected_function>())
this_impl->npcBumpedByPlayerCallbacks[npcId] = onPlayerBumpsNpc.as<sol::protected_function>();
if (onNpcBumpsPlayer.is<sol::protected_function>())
this_impl->npcBumpsPlayerCallbacks[npcId] = onNpcBumpsPlayer.as<sol::protected_function>();
});
// 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<bool>() ? completed.as<bool>() : 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<bool>() ? visible.as<bool>() : 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<int64_t>(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<lua_Integer>(val.get<int64_t>()));
} else if (val.is_number_float()) {
lua_pushnumber(L, static_cast<lua_Number>(val.get<double>()));
} else if (val.is_string()) {
const std::string s = val.get<std::string>();
lua_pushstring(L, s.c_str());
} else if (val.is_boolean()) {
lua_pushboolean(L, val.get<bool>() ? 1 : 0);
} else {
continue;
}
lua_setglobal(L, key.c_str());
}
}
} // namespace FRG