#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 #include } #include #include #include #include #include #include #include #include #include #include #include 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(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(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 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(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 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& 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