92045ec6df
still non playable cartridges Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
835 lines
30 KiB
C++
835 lines
30 KiB
C++
#include "lua/wherigo.h"
|
||
#include "app.h"
|
||
#include "lua/game_engine.h"
|
||
#include "lua/geo_utils.h"
|
||
#include "lua/media_manager.h"
|
||
#include "lua/zobject.h"
|
||
#include "lua/ztimer.h"
|
||
#include "ui/wherigo_dialog.h"
|
||
|
||
extern "C" {
|
||
#include <lauxlib.h>
|
||
#include <lua.h>
|
||
}
|
||
|
||
#include <wx/choicdlg.h>
|
||
#include <wx/file.h>
|
||
#include <wx/filename.h>
|
||
#include <wx/log.h>
|
||
#include <wx/process.h>
|
||
#include <wx/textdlg.h>
|
||
#include <wx/utils.h>
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
namespace wherigo {
|
||
|
||
// ── Distance / Bearing metatables ─────────────────────────────────────────────
|
||
|
||
// __call on Distance: distance("m") → number
|
||
static int distance_call(lua_State *L) {
|
||
luaL_checktype(L, 1, LUA_TTABLE);
|
||
const char *units = luaL_optstring(L, 2, "meters");
|
||
lua_getfield(L, 1, "value");
|
||
const double meters = lua_tonumber(L, -1);
|
||
lua_pop(L, 1);
|
||
lua_pushnumber(L, metersToUnit(meters, units));
|
||
return 1;
|
||
}
|
||
|
||
// GetValue closure: captured Distance table as upvalue 1
|
||
static int distance_getvalue_upval(lua_State *L) {
|
||
const char *units = luaL_optstring(L, 1, "meters");
|
||
lua_pushvalue(L, lua_upvalueindex(1)); // Distance table
|
||
lua_getfield(L, -1, "value");
|
||
const double meters = lua_tonumber(L, -1);
|
||
lua_pop(L, 2);
|
||
lua_pushnumber(L, metersToUnit(meters, units));
|
||
return 1;
|
||
}
|
||
|
||
// __call on Bearing: bearing() → number (degrees)
|
||
static int bearing_call(lua_State *L) {
|
||
luaL_checktype(L, 1, LUA_TTABLE);
|
||
lua_getfield(L, 1, "value");
|
||
const double val = lua_tonumber(L, -1);
|
||
lua_pop(L, 1);
|
||
lua_pushnumber(L, std::fmod(val + 360.0, 360.0));
|
||
return 1;
|
||
}
|
||
|
||
static void setupMetatables(lua_State *L) {
|
||
luaL_newmetatable(L, "Wherigo.Distance");
|
||
lua_pushcfunction(L, distance_call);
|
||
lua_setfield(L, -2, "__call");
|
||
lua_pop(L, 1);
|
||
|
||
luaL_newmetatable(L, "Wherigo.Bearing");
|
||
lua_pushcfunction(L, bearing_call);
|
||
lua_setfield(L, -2, "__call");
|
||
lua_pop(L, 1);
|
||
}
|
||
|
||
// ── App-level helpers ─────────────────────────────────────────────────────────
|
||
|
||
// Returns the loaded cartridge's name, or "Wherigo" as fallback.
|
||
static wxString cartridgeTitle() {
|
||
if (auto *app = dynamic_cast<cApp*>(wxTheApp);
|
||
app && app->isCartridgeLoaded())
|
||
return wxString::FromUTF8(app->getCartridge()->cartridgeName());
|
||
return "Wherigo";
|
||
}
|
||
|
||
// ── Completion code helper ────────────────────────────────────────────────────
|
||
|
||
std::string getCompletionCode(lua_State *L) {
|
||
lua_getglobal(L, "Player");
|
||
if (!lua_istable(L, -1)) { lua_pop(L, 1); return ""; }
|
||
lua_getfield(L, -1, "CompletionCode");
|
||
std::string code;
|
||
if (lua_isstring(L, -1)) {
|
||
code = lua_tostring(L, -1);
|
||
if (code.length() > 15) code = code.substr(0, 15);
|
||
}
|
||
lua_pop(L, 2);
|
||
return code;
|
||
}
|
||
|
||
// ── Wherigo API functions ─────────────────────────────────────────────────────
|
||
|
||
static int wherigo_RequestSync(lua_State *L) {
|
||
auto completionCode = getCompletionCode(L);
|
||
if (completionCode.empty()) {
|
||
if (auto *app = dynamic_cast<cApp*>(wxTheApp);
|
||
app && app->isCartridgeLoaded())
|
||
completionCode = app->getCartridge()->completionCode().substr(0, 15);
|
||
}
|
||
const wxString msg = wxString::Format(
|
||
"Completion Code:\n\n%s\n\nEnter this code on wherigo.com to verify your completion.",
|
||
completionCode.c_str());
|
||
wherigo::WherigoMessageDialog dlg(nullptr, msg, cartridgeTitle() + " – Abschluss", {"OK"});
|
||
dlg.ShowModal();
|
||
return 0;
|
||
}
|
||
|
||
static int wherigo_ZonePoint(lua_State *L) {
|
||
const lua_Number lat = luaL_checknumber(L, 1);
|
||
const lua_Number lng = luaL_checknumber(L, 2);
|
||
const lua_Number alt = luaL_optnumber(L, 3, 0.0);
|
||
lua_newtable(L);
|
||
lua_pushnumber(L, lat); lua_setfield(L, -2, "latitude");
|
||
lua_pushnumber(L, lng); lua_setfield(L, -2, "longitude");
|
||
lua_pushnumber(L, alt); lua_setfield(L, -2, "altitude");
|
||
lua_pushstring(L, "ZonePoint"); lua_setfield(L, -2, "_classname");
|
||
return 1;
|
||
}
|
||
|
||
static int wherigo_Distance(lua_State *L) {
|
||
const lua_Number value = luaL_checknumber(L, 1);
|
||
const char *unit = luaL_optstring(L, 2, "meters");
|
||
const double meters = distanceToMeters(value, unit);
|
||
|
||
lua_newtable(L);
|
||
lua_pushnumber(L, meters); lua_setfield(L, -2, "value");
|
||
lua_pushstring(L, "Distance"); lua_setfield(L, -2, "_classname");
|
||
|
||
// GetValue closure: captures this Distance table as upvalue
|
||
lua_pushvalue(L, -1);
|
||
lua_pushcclosure(L, distance_getvalue_upval, 1);
|
||
lua_setfield(L, -2, "GetValue");
|
||
|
||
luaL_getmetatable(L, "Wherigo.Distance");
|
||
lua_setmetatable(L, -2);
|
||
return 1;
|
||
}
|
||
|
||
static int wherigo_Bearing(lua_State *L) {
|
||
const double deg = luaL_optnumber(L, 1, 0.0);
|
||
pushBearing(L, deg);
|
||
return 1;
|
||
}
|
||
|
||
// If arg1 is a plain properties table (no _classname), use it as the object base
|
||
// (mirrors the reference: "if type(cartridge)=='table' and not cartridge._classname then self=cartridge").
|
||
// Otherwise create a fresh table.
|
||
static void pushObjectBase(lua_State *L) {
|
||
if (lua_istable(L, 1)) {
|
||
lua_getfield(L, 1, "ClassName"); // our objects use "ClassName" (not "_classname")
|
||
const bool isPlainProps = lua_isnil(L, -1);
|
||
lua_pop(L, 1);
|
||
if (isPlainProps) { lua_pushvalue(L, 1); return; }
|
||
}
|
||
lua_newtable(L);
|
||
}
|
||
|
||
// Set field to value only if currently nil/unset — preserves props from caller's table
|
||
#define SET_DEFAULT_BOOL(key, val) do { \
|
||
lua_getfield(L, -1, key); \
|
||
if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushboolean(L, val); lua_setfield(L, -2, key); } \
|
||
else { lua_pop(L, 1); } } while(0)
|
||
|
||
#define SET_DEFAULT_STR(key, val) do { \
|
||
lua_getfield(L, -1, key); \
|
||
if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushstring(L, val); lua_setfield(L, -2, key); } \
|
||
else { lua_pop(L, 1); } } while(0)
|
||
|
||
#define SET_DEFAULT_NUM(key, val) do { \
|
||
lua_getfield(L, -1, key); \
|
||
if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, val); lua_setfield(L, -2, key); } \
|
||
else { lua_pop(L, 1); } } while(0)
|
||
|
||
#define SET_DEFAULT_TABLE(key) do { \
|
||
lua_getfield(L, -1, key); \
|
||
if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_newtable(L); lua_setfield(L, -2, key); } \
|
||
else { lua_pop(L, 1); } } while(0)
|
||
|
||
static int wherigo_Zone(lua_State *L) {
|
||
pushObjectBase(L);
|
||
lua_pushstring(L, "Zone"); lua_setfield(L, -2, "ClassName");
|
||
SET_DEFAULT_BOOL("Active", 0);
|
||
SET_DEFAULT_BOOL("Visible", 1);
|
||
SET_DEFAULT_TABLE("Points");
|
||
// State machine fields – always reset to initial state
|
||
lua_pushstring(L, "NotInRange"); lua_setfield(L, -2, "State");
|
||
lua_pushstring(L, "NotInRange"); lua_setfield(L, -2, "_state");
|
||
lua_pushboolean(L, 0); lua_setfield(L, -2, "_inside");
|
||
SET_DEFAULT_STR("ShowObjects", "OnEnter");
|
||
lua_getfield(L, -1, "ProximityRange");
|
||
if (lua_isnil(L, -1)) { lua_pop(L, 1); pushDistance(L, 60.0); lua_setfield(L, -2, "ProximityRange"); }
|
||
else { lua_pop(L, 1); }
|
||
lua_getfield(L, -1, "DistanceRange");
|
||
if (lua_isnil(L, -1)) { lua_pop(L, 1); pushDistance(L, -0.3048); lua_setfield(L, -2, "DistanceRange"); }
|
||
else { lua_pop(L, 1); }
|
||
pushDistance(L, 0.0); lua_setfield(L, -2, "CurrentDistance");
|
||
pushBearing(L, 0.0); lua_setfield(L, -2, "CurrentBearing");
|
||
lua_pushcfunction(L, zobject_Contains); lua_setfield(L, -2, "Contains");
|
||
lua_pushcfunction(L, zobject_MoveTo); lua_setfield(L, -2, "MoveTo");
|
||
return 1;
|
||
}
|
||
|
||
static int wherigo_ZCharacter(lua_State *L) {
|
||
pushObjectBase(L);
|
||
lua_pushstring(L, "ZCharacter"); lua_setfield(L, -2, "ClassName");
|
||
SET_DEFAULT_BOOL("Active", 0);
|
||
SET_DEFAULT_BOOL("Visible", 1);
|
||
lua_pushcfunction(L, zobject_MoveTo); lua_setfield(L, -2, "MoveTo");
|
||
lua_pushcfunction(L, zobject_Contains); lua_setfield(L, -2, "Contains");
|
||
return 1;
|
||
}
|
||
|
||
static int wherigo_ZItem(lua_State *L) {
|
||
pushObjectBase(L);
|
||
lua_pushstring(L, "ZItem"); lua_setfield(L, -2, "ClassName");
|
||
SET_DEFAULT_BOOL("Active", 0);
|
||
SET_DEFAULT_BOOL("Visible", 1);
|
||
lua_pushcfunction(L, zobject_MoveTo); lua_setfield(L, -2, "MoveTo");
|
||
lua_pushcfunction(L, zobject_Contains); lua_setfield(L, -2, "Contains");
|
||
return 1;
|
||
}
|
||
|
||
static int wherigo_ZTask(lua_State *L) {
|
||
pushObjectBase(L);
|
||
lua_pushstring(L, "ZTask"); lua_setfield(L, -2, "ClassName");
|
||
SET_DEFAULT_BOOL("Active", 0);
|
||
SET_DEFAULT_BOOL("Complete", 0);
|
||
return 1;
|
||
}
|
||
|
||
static int wherigo_ZTimer(lua_State *L) {
|
||
pushObjectBase(L);
|
||
lua_pushstring(L, "ZTimer"); lua_setfield(L, -2, "ClassName");
|
||
SET_DEFAULT_STR("Type", "Countdown");
|
||
SET_DEFAULT_NUM("Duration", 0);
|
||
lua_pushboolean(L, 0); lua_setfield(L, -2, "Running");
|
||
lua_pushcfunction(L, ztimer_Start); lua_setfield(L, -2, "Start");
|
||
lua_pushcfunction(L, ztimer_Stop); lua_setfield(L, -2, "Stop");
|
||
lua_pushcfunction(L, ztimer_Reset); lua_setfield(L, -2, "Reset");
|
||
return 1;
|
||
}
|
||
|
||
static int wherigo_ZInput(lua_State *L) {
|
||
pushObjectBase(L);
|
||
lua_pushstring(L, "ZInput"); lua_setfield(L, -2, "ClassName");
|
||
SET_DEFAULT_STR("InputType", "Text");
|
||
SET_DEFAULT_TABLE("Choices");
|
||
return 1;
|
||
}
|
||
|
||
static int s_mediaCounter = 0;
|
||
|
||
static int wherigo_ZMedia(lua_State *L) {
|
||
lua_newtable(L);
|
||
lua_pushstring(L, "ZMedia"); lua_setfield(L, -2, "ClassName");
|
||
s_mediaCounter++;
|
||
lua_pushinteger(L, s_mediaCounter); lua_setfield(L, -2, "MediaIndex");
|
||
return 1;
|
||
}
|
||
|
||
void resetMediaCounter() { s_mediaCounter = 0; }
|
||
|
||
static int wherigo_ZCartridge(lua_State *L) {
|
||
lua_newtable(L);
|
||
lua_pushstring(L, "ZCartridge"); lua_setfield(L, -2, "ClassName");
|
||
lua_newtable(L); lua_setfield(L, -2, "AllZObjects");
|
||
lua_pushcfunction(L, wherigo_RequestSync); lua_setfield(L, -2, "RequestSync");
|
||
return 1;
|
||
}
|
||
|
||
static int wherigo_ZCommand(lua_State *L) {
|
||
lua_newtable(L);
|
||
lua_pushstring(L, "ZCommand"); lua_setfield(L, -2, "ClassName");
|
||
lua_pushboolean(L, 1); lua_setfield(L, -2, "Enabled");
|
||
return 1;
|
||
}
|
||
|
||
static int wherigo_Player(lua_State *L) {
|
||
lua_newtable(L);
|
||
lua_pushstring(L, "Player"); lua_setfield(L, -2, "ClassName");
|
||
lua_pushstring(L, "Player"); lua_setfield(L, -2, "Name");
|
||
lua_newtable(L); lua_setfield(L, -2, "Inventory");
|
||
lua_newtable(L); lua_setfield(L, -2, "InsideOfZones");
|
||
|
||
// ObjectLocation: ZonePoint(0,0,0)
|
||
lua_newtable(L);
|
||
lua_pushnumber(L, 0.0); lua_setfield(L, -2, "latitude");
|
||
lua_pushnumber(L, 0.0); lua_setfield(L, -2, "longitude");
|
||
lua_pushnumber(L, 0.0); lua_setfield(L, -2, "altitude");
|
||
lua_setfield(L, -2, "ObjectLocation");
|
||
|
||
pushDistance(L, 5.0); lua_setfield(L, -2, "PositionAccuracy");
|
||
return 1;
|
||
}
|
||
|
||
// ── UI functions ──────────────────────────────────────────────────────────────
|
||
|
||
static int wherigo_MessageBox(lua_State *L) {
|
||
if (!lua_istable(L, 1)) return 0;
|
||
|
||
lua_getfield(L, 1, "Text");
|
||
const char *text = lua_isstring(L, -1) ? lua_tostring(L, -1) : "";
|
||
lua_pop(L, 1);
|
||
|
||
// Media: ZMedia table → Name field (used for image display)
|
||
std::string mediaName;
|
||
lua_getfield(L, 1, "Media");
|
||
if (lua_istable(L, -1)) {
|
||
lua_getfield(L, -1, "Name");
|
||
if (lua_isstring(L, -1)) mediaName = lua_tostring(L, -1);
|
||
lua_pop(L, 1);
|
||
}
|
||
lua_pop(L, 1);
|
||
|
||
std::vector<wxString> buttons;
|
||
lua_getfield(L, 1, "Buttons");
|
||
if (lua_istable(L, -1)) {
|
||
const int n = lua_objlen(L, -1);
|
||
for (int i = 1; i <= n; i++) {
|
||
lua_rawgeti(L, -1, i);
|
||
if (lua_isstring(L, -1))
|
||
buttons.push_back(wxString::FromUTF8(lua_tostring(L, -1)));
|
||
lua_pop(L, 1);
|
||
}
|
||
}
|
||
lua_pop(L, 1);
|
||
|
||
lua_getfield(L, 1, "Callback");
|
||
const bool hasCallback = lua_isfunction(L, -1);
|
||
int callbackRef = LUA_NOREF;
|
||
if (hasCallback)
|
||
callbackRef = luaL_ref(L, LUA_REGISTRYINDEX);
|
||
else
|
||
lua_pop(L, 1);
|
||
|
||
wxLogDebug("MessageBox: %s", text);
|
||
WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(text), cartridgeTitle(), buttons,
|
||
wxString::FromUTF8(mediaName));
|
||
dlg.ShowModal();
|
||
const int selected = dlg.getSelectedButton();
|
||
|
||
if (callbackRef != LUA_NOREF) {
|
||
lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef);
|
||
if (!buttons.empty() && selected >= 0 && selected < static_cast<int>(buttons.size()))
|
||
lua_pushstring(L, buttons[selected].ToUTF8().data());
|
||
else if (buttons.empty() && selected >= 0)
|
||
lua_pushstring(L, ""); // OK clicked → "" keeps chain alive
|
||
else
|
||
lua_pushnil(L); // window closed with X → nil breaks the chain
|
||
if (lua_pcall(L, 1, 0, 0) != 0) {
|
||
wxLogError("MessageBox callback error: %s", lua_tostring(L, -1));
|
||
lua_pop(L, 1);
|
||
}
|
||
luaL_unref(L, LUA_REGISTRYINDEX, callbackRef);
|
||
}
|
||
|
||
GameEngine::getInstance().notifyStateChanged();
|
||
return 0;
|
||
}
|
||
|
||
static int wherigo_Dialog(lua_State *L) {
|
||
if (!lua_istable(L, 1)) return 0;
|
||
|
||
std::vector<DialogEntry> entries;
|
||
const int n = lua_objlen(L, 1);
|
||
for (int i = 1; i <= n; i++) {
|
||
lua_rawgeti(L, 1, i);
|
||
if (lua_istable(L, -1)) {
|
||
DialogEntry entry;
|
||
lua_getfield(L, -1, "Text");
|
||
entry.text = lua_isstring(L, -1) ? lua_tostring(L, -1) : "";
|
||
lua_pop(L, 1);
|
||
|
||
lua_getfield(L, -1, "Media");
|
||
if (lua_istable(L, -1)) {
|
||
lua_getfield(L, -1, "Name");
|
||
entry.mediaName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "";
|
||
lua_pop(L, 1);
|
||
}
|
||
lua_pop(L, 1);
|
||
|
||
lua_getfield(L, -1, "Buttons");
|
||
if (lua_istable(L, -1)) {
|
||
const int bn = lua_objlen(L, -1);
|
||
for (int j = 1; j <= bn; j++) {
|
||
lua_rawgeti(L, -1, j);
|
||
if (lua_isstring(L, -1)) entry.buttons.push_back(lua_tostring(L, -1));
|
||
lua_pop(L, 1);
|
||
}
|
||
}
|
||
lua_pop(L, 1);
|
||
entries.push_back(entry);
|
||
}
|
||
lua_pop(L, 1);
|
||
}
|
||
|
||
for (const auto &e : entries)
|
||
wxLogDebug("Dialog: %s", e.text.c_str());
|
||
|
||
WherigoDialogRunner::getInstance().showDialog(entries);
|
||
return 0;
|
||
}
|
||
|
||
static int wherigo_GetInput(lua_State *L) {
|
||
if (!lua_istable(L, 1)) return 0;
|
||
|
||
lua_getfield(L, 1, "InputType");
|
||
const std::string inputType = lua_isstring(L,-1) ? lua_tostring(L,-1) : "Text";
|
||
lua_pop(L, 1);
|
||
|
||
lua_getfield(L, 1, "Text");
|
||
const wxString prompt = lua_isstring(L,-1)
|
||
? wxString::FromUTF8(lua_tostring(L,-1)) : wxString("Eingabe:");
|
||
lua_pop(L, 1);
|
||
|
||
lua_getfield(L, 1, "Name");
|
||
const wxString inputName = lua_isstring(L,-1)
|
||
? wxString::FromUTF8(lua_tostring(L,-1)) : wxString("(unnamed)");
|
||
lua_pop(L, 1);
|
||
|
||
// Debug: log question text
|
||
wxLogDebug("GetInput[%s] Frage: %s", inputType, prompt);
|
||
|
||
std::string response;
|
||
|
||
if (inputType == "MultipleChoice") {
|
||
wxArrayString choices;
|
||
lua_getfield(L, 1, "Choices");
|
||
if (lua_istable(L, -1)) {
|
||
const int n = lua_objlen(L, -1);
|
||
for (int i = 1; i <= n; i++) {
|
||
lua_rawgeti(L, -1, i);
|
||
if (lua_isstring(L, -1)) {
|
||
wxString c = wxString::FromUTF8(lua_tostring(L,-1));
|
||
choices.Add(c);
|
||
wxLogDebug("GetInput Option: %s", c);
|
||
}
|
||
lua_pop(L, 1);
|
||
}
|
||
}
|
||
lua_pop(L, 1);
|
||
if (!choices.IsEmpty()) {
|
||
wxSingleChoiceDialog dlg(nullptr, prompt, cartridgeTitle(), choices);
|
||
if (dlg.ShowModal() == wxID_OK)
|
||
response = dlg.GetStringSelection().ToUTF8().data();
|
||
}
|
||
} else {
|
||
wxLogDebug("GetInput Freitext (Name: %s)", inputName);
|
||
wxTextEntryDialog dlg(nullptr, prompt, cartridgeTitle(), "");
|
||
if (dlg.ShowModal() == wxID_OK)
|
||
response = dlg.GetValue().ToUTF8().data();
|
||
}
|
||
|
||
lua_getfield(L, 1, "OnGetInput");
|
||
if (lua_isfunction(L, -1)) {
|
||
lua_pushvalue(L, 1);
|
||
lua_pushstring(L, response.c_str());
|
||
if (lua_pcall(L, 2, 0, 0) != 0) {
|
||
wxLogError("GetInput OnGetInput error: %s", lua_tostring(L,-1));
|
||
lua_pop(L, 1);
|
||
}
|
||
} else {
|
||
lua_pop(L, 1);
|
||
}
|
||
|
||
// OnGetInput may have changed item visibility → rebuild inventory
|
||
GameEngine::getInstance().rebuildPlayerInventory();
|
||
GameEngine::getInstance().notifyStateChanged();
|
||
return 0;
|
||
}
|
||
|
||
// ── Audio playback ────────────────────────────────────────────────────────────
|
||
|
||
static long s_audioPid = 0;
|
||
static wxString s_audioTemp;
|
||
|
||
static void stopAudio() {
|
||
if (s_audioPid > 0) {
|
||
wxProcess::Kill(s_audioPid, wxSIGKILL, wxKILL_CHILDREN);
|
||
s_audioPid = 0;
|
||
}
|
||
if (!s_audioTemp.empty()) {
|
||
wxRemoveFile(s_audioTemp);
|
||
s_audioTemp.clear();
|
||
}
|
||
}
|
||
|
||
static void playAudio(const std::vector<uint8_t>& data, const std::string& ext) {
|
||
stopAudio();
|
||
|
||
static int s_counter = 0;
|
||
s_audioTemp = wxFileName::GetTempDir() +
|
||
wxFileName::GetPathSeparator() +
|
||
wxString::Format("wherigo_%d.%s", s_counter++,
|
||
wxString::FromUTF8(ext).Lower());
|
||
|
||
{
|
||
wxFile f(s_audioTemp, wxFile::write);
|
||
if (!f.IsOpened()) {
|
||
wxLogWarning("PlayAudio: cannot write temp file %s", s_audioTemp);
|
||
s_audioTemp.clear();
|
||
return;
|
||
}
|
||
f.Write(data.data(), data.size());
|
||
}
|
||
|
||
#if defined(__APPLE__)
|
||
const wxString cmd = wxString::Format("afplay \"%s\"", s_audioTemp);
|
||
#elif defined(__linux__)
|
||
const wxString cmd = wxString::Format(
|
||
"mpg123 -q \"%s\" 2>/dev/null || "
|
||
"ffplay -nodisp -autoexit -loglevel quiet \"%s\"",
|
||
s_audioTemp, s_audioTemp);
|
||
#else
|
||
const wxString cmd = wxString::Format(
|
||
"powershell -c \"Add-Type -AssemblyName presentationCore;"
|
||
"$mp=[Windows.Media.Playback.MediaPlayer]::new();"
|
||
"$mp.Source=[Windows.Media.Core.MediaSource]::CreateFromUri('%s');"
|
||
"$mp.Play();Start-Sleep -s 300\"",
|
||
s_audioTemp);
|
||
#endif
|
||
|
||
wxLogDebug("PlayAudio: launching %s", cmd);
|
||
s_audioPid = wxExecute(cmd, wxEXEC_ASYNC);
|
||
if (s_audioPid <= 0) {
|
||
wxLogWarning("PlayAudio: failed to start player");
|
||
wxRemoveFile(s_audioTemp);
|
||
s_audioTemp.clear();
|
||
s_audioPid = 0;
|
||
}
|
||
}
|
||
|
||
static int wherigo_PlayAudio(lua_State *L) {
|
||
if (!lua_istable(L, 1)) return 0;
|
||
|
||
lua_getfield(L, 1, "Name");
|
||
wxLogDebug("PlayAudio: %s", lua_isstring(L,-1) ? lua_tostring(L,-1) : "(unnamed)");
|
||
lua_pop(L, 1);
|
||
|
||
// MediaIndex assigned by wherigo_ZMedia at construction time
|
||
lua_getfield(L, 1, "MediaIndex");
|
||
const int mediaIndex = lua_isnumber(L,-1) ? lua_tointeger(L,-1) : -1;
|
||
lua_pop(L, 1);
|
||
if (mediaIndex <= 0) return 0;
|
||
|
||
// File extension from Resources[1].Type (set by cartridge code)
|
||
std::string ext = "mp3";
|
||
lua_getfield(L, 1, "Resources");
|
||
if (lua_istable(L, -1)) {
|
||
lua_rawgeti(L, -1, 1);
|
||
if (lua_istable(L, -1)) {
|
||
lua_getfield(L, -1, "Type");
|
||
if (lua_isstring(L, -1)) ext = lua_tostring(L, -1);
|
||
lua_pop(L, 1);
|
||
}
|
||
lua_pop(L, 1);
|
||
}
|
||
lua_pop(L, 1);
|
||
|
||
const auto data = MediaManager::getInstance().getMediaByIndex(mediaIndex);
|
||
if (data.empty()) {
|
||
wxLogWarning("PlayAudio: no data for MediaIndex %d", mediaIndex);
|
||
return 0;
|
||
}
|
||
|
||
playAudio(data, ext);
|
||
return 0;
|
||
}
|
||
|
||
static int wherigo_ShowScreen(lua_State *L) {
|
||
const int screen = luaL_optinteger(L, 1, 0);
|
||
wxLogDebug("ShowScreen: %d", screen);
|
||
|
||
if (screen == 5 && lua_gettop(L) >= 2 && lua_istable(L, 2)) {
|
||
lua_getfield(L, 2, "Name");
|
||
const std::string name = lua_isstring(L,-1) ? lua_tostring(L,-1) : "Details";
|
||
lua_pop(L, 1);
|
||
lua_getfield(L, 2, "Description");
|
||
const std::string desc = lua_isstring(L,-1) ? lua_tostring(L,-1) : "";
|
||
lua_pop(L, 1);
|
||
|
||
std::string mediaName;
|
||
lua_getfield(L, 2, "Media");
|
||
if (lua_istable(L, -1)) {
|
||
lua_getfield(L, -1, "Name");
|
||
if (lua_isstring(L, -1)) mediaName = lua_tostring(L, -1);
|
||
lua_pop(L, 1);
|
||
}
|
||
lua_pop(L, 1);
|
||
if (mediaName.empty()) {
|
||
lua_getfield(L, 2, "Icon");
|
||
if (lua_istable(L, -1)) {
|
||
lua_getfield(L, -1, "Name");
|
||
if (lua_isstring(L, -1)) mediaName = lua_tostring(L, -1);
|
||
lua_pop(L, 1);
|
||
}
|
||
lua_pop(L, 1);
|
||
}
|
||
WherigoMessageDialog dlg(nullptr,
|
||
wxString::FromUTF8(desc), wxString::FromUTF8(name),
|
||
{"OK"}, wxString::FromUTF8(mediaName));
|
||
dlg.ShowModal();
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int wherigo_ShowStatusText(lua_State *L) {
|
||
const char *text = luaL_checkstring(L, 1);
|
||
wxLogDebug("ShowStatusText: %s", text);
|
||
return 0;
|
||
}
|
||
|
||
static int wherigo_LogMessage(lua_State *L) {
|
||
const char *msg = luaL_checkstring(L, 1);
|
||
wxLogDebug("Wherigo.LogMessage: %s", msg);
|
||
return 0;
|
||
}
|
||
|
||
static int wherigo_Command(lua_State *L) {
|
||
const char *text = luaL_checkstring(L, 1);
|
||
wxLogDebug("Wherigo.Command: %s", text);
|
||
if (!strcmp(text, "SaveClose")) {
|
||
wxLogMessage("Wherigo.Command: SaveClose – exiting");
|
||
wxTheApp->ExitMainLoop();
|
||
} else if (!strcmp(text, "StopSound")) {
|
||
stopAudio();
|
||
} else if (!strcmp(text, "Alert")) {
|
||
wxBell();
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
// ── String utilities ──────────────────────────────────────────────────────────
|
||
|
||
static int wherigo_NoCaseEquals(lua_State *L) {
|
||
const char *s1 = luaL_checkstring(L, 1);
|
||
const char *s2 = luaL_checkstring(L, 2);
|
||
std::string a(s1), b(s2);
|
||
std::transform(a.begin(), a.end(), a.begin(), ::tolower);
|
||
std::transform(b.begin(), b.end(), b.begin(), ::tolower);
|
||
lua_pushboolean(L, a == b);
|
||
return 1;
|
||
}
|
||
|
||
// ── Geo functions ─────────────────────────────────────────────────────────────
|
||
|
||
static int wherigo_IsPointInZone(lua_State *L) {
|
||
if (!lua_istable(L, 1) || !lua_istable(L, 2)) {
|
||
lua_pushboolean(L, 0);
|
||
return 1;
|
||
}
|
||
lua_getfield(L, 1, "latitude"); const double lat = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 1, "longitude"); const double lon = lua_tonumber(L,-1); lua_pop(L,1);
|
||
const auto pts = readZonePoints(L, 2);
|
||
lua_pushboolean(L, pointInPolygon(lat, lon, pts) ? 1 : 0);
|
||
return 1;
|
||
}
|
||
|
||
static int wherigo_VectorToPoint(lua_State *L) {
|
||
if (!lua_istable(L, 1) || !lua_istable(L, 2)) {
|
||
pushDistance(L, 0.0); pushBearing(L, 0.0); return 2;
|
||
}
|
||
lua_getfield(L, 1, "latitude"); const double lat1 = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 1, "longitude"); const double lon1 = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 2, "latitude"); const double lat2 = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 2, "longitude"); const double lon2 = lua_tonumber(L,-1); lua_pop(L,1);
|
||
const auto [dist, brg] = haversine(lat1, lon1, lat2, lon2);
|
||
pushDistance(L, dist);
|
||
pushBearing(L, brg);
|
||
return 2;
|
||
}
|
||
|
||
static int wherigo_VectorToSegment(lua_State *L) {
|
||
if (!lua_istable(L, 1) || !lua_istable(L, 2) || !lua_istable(L, 3)) {
|
||
pushDistance(L, 0.0); pushBearing(L, 0.0); return 2;
|
||
}
|
||
lua_getfield(L, 1, "latitude"); const double plat = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 1, "longitude"); const double plon = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 2, "latitude"); const double lat1 = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 2, "longitude"); const double lon1 = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 3, "latitude"); const double lat2 = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 3, "longitude"); const double lon2 = lua_tonumber(L,-1); lua_pop(L,1);
|
||
double nLat, nLon;
|
||
const double dist = distToSegment(plat, plon, lat1, lon1, lat2, lon2, &nLat, &nLon);
|
||
const double brg = haversine(plat, plon, nLat, nLon).second;
|
||
pushDistance(L, dist);
|
||
pushBearing(L, brg);
|
||
return 2;
|
||
}
|
||
|
||
static int wherigo_VectorToZone(lua_State *L) {
|
||
if (!lua_istable(L, 1) || !lua_istable(L, 2)) {
|
||
pushDistance(L, 0.0); pushBearing(L, 0.0); return 2;
|
||
}
|
||
lua_getfield(L, 1, "latitude"); const double plat = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 1, "longitude"); const double plon = lua_tonumber(L,-1); lua_pop(L,1);
|
||
const auto pts = readZonePoints(L, 2);
|
||
if (pointInPolygon(plat, plon, pts)) {
|
||
pushDistance(L, 0.0); pushBearing(L, 0.0); return 2;
|
||
}
|
||
const auto [dist, brg] = nearestZoneEdge(plat, plon, pts);
|
||
pushDistance(L, dist);
|
||
pushBearing(L, brg);
|
||
return 2;
|
||
}
|
||
|
||
static int wherigo_TranslatePoint(lua_State *L) {
|
||
if (!lua_istable(L, 1)) { lua_pushnil(L); return 1; }
|
||
|
||
lua_getfield(L, 1, "latitude"); const double lat = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 1, "longitude"); const double lon = lua_tonumber(L,-1); lua_pop(L,1);
|
||
lua_getfield(L, 1, "altitude");
|
||
const double alt = lua_isnumber(L,-1) ? lua_tonumber(L,-1) : 0.0;
|
||
lua_pop(L, 1);
|
||
|
||
double distMeters = 0.0;
|
||
if (lua_istable(L, 2)) {
|
||
lua_getfield(L, 2, "value"); distMeters = lua_tonumber(L,-1); lua_pop(L,1);
|
||
} else if (lua_isnumber(L, 2)) {
|
||
distMeters = lua_tonumber(L, 2);
|
||
}
|
||
|
||
double bearingDeg = 0.0;
|
||
if (lua_istable(L, 3)) {
|
||
lua_getfield(L, 3, "value"); bearingDeg = lua_tonumber(L,-1); lua_pop(L,1);
|
||
} else if (lua_isnumber(L, 3)) {
|
||
bearingDeg = lua_tonumber(L, 3);
|
||
}
|
||
|
||
const auto [newLat, newLon] = translatePoint(lat, lon, distMeters, bearingDeg);
|
||
lua_newtable(L);
|
||
lua_pushnumber(L, newLat); lua_setfield(L, -2, "latitude");
|
||
lua_pushnumber(L, newLon); lua_setfield(L, -2, "longitude");
|
||
lua_pushnumber(L, alt); lua_setfield(L, -2, "altitude");
|
||
lua_pushstring(L, "ZonePoint"); lua_setfield(L, -2, "_classname");
|
||
return 1;
|
||
}
|
||
|
||
// ── Function table & module init ──────────────────────────────────────────────
|
||
|
||
static const luaL_Reg wherigo_funcs[] = {
|
||
{"ZonePoint", wherigo_ZonePoint},
|
||
{"Distance", wherigo_Distance},
|
||
{"Bearing", wherigo_Bearing},
|
||
{"Zone", wherigo_Zone},
|
||
{"ZCharacter", wherigo_ZCharacter},
|
||
{"ZItem", wherigo_ZItem},
|
||
{"ZTask", wherigo_ZTask},
|
||
{"ZTimer", wherigo_ZTimer},
|
||
{"ZInput", wherigo_ZInput},
|
||
{"ZMedia", wherigo_ZMedia},
|
||
{"ZCartridge", wherigo_ZCartridge},
|
||
{"ZCommand", wherigo_ZCommand},
|
||
{"MessageBox", wherigo_MessageBox},
|
||
{"Dialog", wherigo_Dialog},
|
||
{"GetInput", wherigo_GetInput},
|
||
{"PlayAudio", wherigo_PlayAudio},
|
||
{"ShowScreen", wherigo_ShowScreen},
|
||
{"ShowStatusText", wherigo_ShowStatusText},
|
||
{"LogMessage", wherigo_LogMessage},
|
||
{"Command", wherigo_Command},
|
||
{"NoCaseEquals", wherigo_NoCaseEquals},
|
||
{"IsPointInZone", wherigo_IsPointInZone},
|
||
{"TranslatePoint", wherigo_TranslatePoint},
|
||
{"VectorToZone", wherigo_VectorToZone},
|
||
{"VectorToPoint", wherigo_VectorToPoint},
|
||
{"VectorToSegment",wherigo_VectorToSegment},
|
||
{"RequestSync", wherigo_RequestSync},
|
||
{nullptr, nullptr}
|
||
};
|
||
|
||
int luaopen_Wherigo(lua_State *L) {
|
||
// Register metatables before any object constructors are used
|
||
setupMetatables(L);
|
||
|
||
luaL_register(L, "Wherigo", wherigo_funcs);
|
||
|
||
// Player object
|
||
// One Player table, referenced by both Wherigo.Player and the global Player
|
||
wherigo_Player(L); // push Player table
|
||
lua_pushvalue(L, -1); // duplicate
|
||
lua_setfield(L, -3, "Player"); // Wherigo.Player = table (pops duplicate)
|
||
lua_setglobal(L, "Player"); // Player (global) = same table (pops original)
|
||
|
||
// Screen constants (numbers matching original Groundspeak spec)
|
||
lua_pushnumber(L, 0); lua_setfield(L, -2, "MAINSCREEN");
|
||
lua_pushnumber(L, 1); lua_setfield(L, -2, "LOCATIONSCREEN");
|
||
lua_pushnumber(L, 2); lua_setfield(L, -2, "ITEMSCREEN");
|
||
lua_pushnumber(L, 3); lua_setfield(L, -2, "INVENTORYSCREEN");
|
||
lua_pushnumber(L, 4); lua_setfield(L, -2, "TASKSCREEN");
|
||
lua_pushnumber(L, 5); lua_setfield(L, -2, "DETAILSCREEN");
|
||
|
||
// Log level constants
|
||
lua_pushnumber(L, 150); lua_setfield(L, -2, "LOGDEBUG");
|
||
lua_pushnumber(L, 151); lua_setfield(L, -2, "LOGCARTRIDGE");
|
||
lua_pushnumber(L, 152); lua_setfield(L, -2, "LOGINFO");
|
||
lua_pushnumber(L, 153); lua_setfield(L, -2, "LOGWARNING");
|
||
lua_pushnumber(L, 154); lua_setfield(L, -2, "LOGERROR");
|
||
|
||
// Class name constants
|
||
lua_pushstring(L, "Zone"); lua_setfield(L, -2, "CLASS_ZONE");
|
||
lua_pushstring(L, "ZMedia"); lua_setfield(L, -2, "CLASS_ZMEDIA");
|
||
lua_pushstring(L, "ZCartridge"); lua_setfield(L, -2, "CLASS_ZCARTRIDGE");
|
||
lua_pushstring(L, "ZCharacter"); lua_setfield(L, -2, "CLASS_ZCHARACTER");
|
||
lua_pushstring(L, "ZCommand"); lua_setfield(L, -2, "CLASS_ZCOMMAND");
|
||
lua_pushstring(L, "ZInput"); lua_setfield(L, -2, "CLASS_ZINPUT");
|
||
lua_pushstring(L, "ZTask"); lua_setfield(L, -2, "CLASS_ZTASK");
|
||
lua_pushstring(L, "ZItem"); lua_setfield(L, -2, "CLASS_ZITEM");
|
||
lua_pushstring(L, "ZTimer"); lua_setfield(L, -2, "CLASS_ZTIMER");
|
||
lua_pushstring(L, "Distance"); lua_setfield(L, -2, "CLASS_DISTANCE");
|
||
lua_pushstring(L, "Bearing"); lua_setfield(L, -2, "CLASS_BEARING");
|
||
lua_pushstring(L, "ZonePoint"); lua_setfield(L, -2, "CLASS_ZONEPOINT");
|
||
|
||
// INVALID_ZONEPOINT
|
||
lua_pushnil(L); lua_setfield(L, -2, "INVALID_ZONEPOINT");
|
||
|
||
// Register globals expected by Wherigo Builder output
|
||
lua_pushcfunction(L, wherigo_ZonePoint); lua_setglobal(L, "ZonePoint");
|
||
lua_pushcfunction(L, wherigo_Distance); lua_setglobal(L, "Distance");
|
||
|
||
return 1;
|
||
}
|
||
|
||
} // namespace wherigo
|