Files
wx_wherigo/main/src/lua/wherigo.cpp
T
mars3142 92045ec6df some tweaks
still non playable cartridges

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-06-02 23:21:56 +02:00

835 lines
30 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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