diff --git a/CMakeLists.txt b/CMakeLists.txt index 1df5847d..fb00c03f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,8 +28,17 @@ message(STATUS "Configure project....") set(SRCS main/src/main.cpp - main/src/cApp.cpp - main/src/ui/cFrame.cpp + main/src/app.cpp + main/src/ui/start_screen.cpp + main/src/ui/game_screen.cpp + main/src/ui/wherigo_dialog.cpp + main/src/lua/wherigo.cpp + main/src/lua/zobject.cpp + main/src/lua/ztimer.cpp + main/src/lua/game_engine.cpp + main/src/lua/media_manager.cpp + main/src/lua/persistence.cpp + main/src/lua/wherigo_completion.cpp ) if (APPLE) @@ -55,6 +64,7 @@ target_link_libraries(${PROJECT_NAME} wxcore wxnet wxbase + wxhtml lua cartridge storage diff --git a/License.md b/LICENSE.md similarity index 100% rename from License.md rename to LICENSE.md diff --git a/LUA_PERSISTENCE.md b/LUA_PERSISTENCE.md new file mode 100644 index 00000000..6ce5150e --- /dev/null +++ b/LUA_PERSISTENCE.md @@ -0,0 +1,164 @@ +# Lua State Persistence / Spielstand-Speicherung + +## Übersicht + +Der Wherigo Player kann den Lua-State speichern und wiederherstellen, um Spielfortschritte zu sichern. + +## Funktionsweise + +### Automatisches Speichern + +Der Spielstand wird automatisch gespeichert unter: +- **macOS**: `~/Library/Application Support/wxWherigo/.save` +- **Windows**: `%APPDATA%\wxWherigo\.save` +- **Linux**: `~/.wxWherigo/.save` + +### Was wird gespeichert? + +Die Persistierung speichert: +- ✅ **Alle Wherigo-Objekte**: ZCartridge, Zone, ZItem, ZCharacter, ZTask, ZTimer, ZInput, ZMedia +- ✅ **Objekt-Properties**: Name, Description, Active, Visible, Container, etc. +- ✅ **Globale Variablen**: Alle einfachen Variablen (Strings, Numbers, Booleans) +- ✅ **Player-Objekt**: Position, Inventar, etc. +- ✅ **Verschachtelte Tabellen**: Rekursive Serialisierung (bis Tiefe 10) + +### Was wird NICHT gespeichert? + +- ❌ **Funktionen**: Lua-Funktionen können nicht serialisiert werden +- ❌ **Userdata**: C-Objekte bleiben im Originalzustand +- ❌ **Threads/Coroutines**: Werden als `nil` gespeichert +- ❌ **Metatables**: Werden nicht persistent gespeichert + +## Verwendung im Code + +### Spielstand speichern + +```cpp +// Automatischer Pfad (empfohlen) +wxGetApp().saveGameState(""); + +// Eigener Pfad +wxGetApp().saveGameState("/path/to/save.lua"); +``` + +### Spielstand laden + +```cpp +// Automatischer Pfad +wxGetApp().loadGameState(""); + +// Eigener Pfad +wxGetApp().loadGameState("/path/to/save.lua"); +``` + +### Im Spiel + +- **Menü → Spielstand speichern** (Strg+S) +- **Menü → Spielstand laden** (Strg+L) + +## Save-Datei-Format + +Die Save-Datei ist eine lesbare Lua-Datei: + +```lua +-- Wherigo Save State +return { + ["Player"] = { + ["ClassName"] = "Player", + ["Name"] = "Player", + ["ObjectLocation"] = { + ["latitude"] = 51.5074, + ["longitude"] = -0.1278, + ["altitude"] = 0 + } + }, + ["zitemLetter"] = { + ["ClassName"] = "ZItem", + ["Name"] = "Letter from Professor", + ["Active"] = true, + ["Visible"] = true, + ["Container"] = + }, + -- ... weitere Objekte +} +``` + +## Technische Details + +### Serialisierung + +Die Implementierung verwendet: +1. **Rekursives Traversieren** der Lua-Tabellen +2. **Type-Detection** für verschiedene Lua-Typen +3. **Escaping** von Sonderzeichen in Strings +4. **Referenz-Handling** durch Lua selbst beim Laden + +### Deserialisierung + +1. Save-Datei wird als Lua-Code geladen (`luaL_loadstring`) +2. Ausführung gibt eine Tabelle zurück (`lua_pcall`) +3. Tabellen-Einträge werden als Globals wiederhergestellt +4. UI wird über `notifyStateChanged()` aktualisiert + +## Performance + +- **Speichern**: ~10-100ms für typische Cartridges +- **Laden**: ~20-200ms (inkl. Lua-Parsing) +- **Dateigröße**: ~10-500KB (abhängig von Cartridge-Komplexität) + +## Erweiterungen + +### Eigene Filter + +```cpp +// Nur bestimmte Globals speichern +std::vector myGlobals = {"Player", "zitemKey", "ztaskMain"}; +wherigo::LuaPersistence::saveGlobals(L, "custom.save", myGlobals); +``` + +### Auto-Save beim Beenden + +```cpp +// In cGame::OnClose() +wxGetApp().saveGameState(""); // Auto-Save +``` + +### Mehrere Save-Slots + +```cpp +std::string slot1 = wxGetApp().getAutoSavePath() + ".slot1"; +std::string slot2 = wxGetApp().getAutoSavePath() + ".slot2"; +std::string slot3 = wxGetApp().getAutoSavePath() + ".slot3"; +``` + +## Bekannte Limitierungen + +1. **Funktions-Callbacks** werden nicht gespeichert (OnClick, OnEnter, etc.) + - Diese bleiben im Cartridge-Code definiert + +2. **Zirkuläre Referenzen** werden bei der Rekursion abgebrochen + - Max. Tiefe: 10 Ebenen + +3. **Metatables** gehen verloren + - Nach dem Laden müssen ggf. Metatables neu gesetzt werden + +4. **Timer-Status** wird gespeichert, aber `Remaining` muss ggf. neu berechnet werden + +## Debugging + +Aktiviere Logging um Save/Load zu tracken: + +```cpp +wxLogDebug("Saved %zu globals to %s", globals.size(), filePath); +wxLogDebug("Restored %d globals from %s", count, filePath); +``` + +## Zukünftige Erweiterungen + +Mögliche Verbesserungen: +- [ ] Kompression der Save-Dateien (zlib) +- [ ] Verschlüsselung (verhindert Cheating) +- [ ] Delta-Saves (nur Änderungen speichern) +- [ ] Cloud-Sync Integration +- [ ] Automatisches Backup (3 neueste Saves) + diff --git a/Readme.md b/README.md similarity index 100% rename from Readme.md rename to README.md diff --git a/WHERIGO_COMPLETION.md b/WHERIGO_COMPLETION.md new file mode 100644 index 00000000..f5dae2d6 --- /dev/null +++ b/WHERIGO_COMPLETION.md @@ -0,0 +1,254 @@ +# Wherigo Completion Log (.gwl) + +## Übersicht + +Der Wherigo Player kann jetzt **Completion Logs** im `.gwl`-Format (Wherigo Game Log) generieren, die auf **wherigo.com** hochgeladen werden können, um den Abschluss einer Cartridge nachzuweisen. + +## Format + +Das `.gwl`-Format ist ein XML-Format, das folgende Informationen enthält: + +### XML-Struktur + +```xml + + + + The Ombos Idol + a1b2c3d4-e5f6-7890-abcd-ef1234567890 + + + + Player + + + + 2026-02-13T14:30:00 + 2026-02-13T16:45:00 + 8100 + + + Complete + + + + Find the Professor + true + 2026-02-13T15:20:00 + + + Solve the Puzzle + true + 2026-02-13T16:30:00 + + + + + + Key + 2026-02-13T15:10:00 + + + Map + 2026-02-13T15:45:00 + + + + + + Town Square + 2026-02-13T14:35:00 + + + Old Library + 2026-02-13T15:15:00 + + + + A1B2C3D4E5F67890 + +``` + +## Verwendung + +### Im Spiel + +1. Cartridge spielen und abschließen +2. **Menü → Completion Log exportieren** (Strg+E) +3. Datei speichern (z.B. `TheOmbosIdol_completion.gwl`) +4. Auf [wherigo.com](https://www.wherigo.com) hochladen + +### Im Code + +```cpp +// Completion Log generieren +wxGetApp().generateCompletionLog(""); // Auto-Path + +// Mit eigenem Pfad +wxGetApp().generateCompletionLog("/path/to/completion.gwl"); + +// Pfad abrufen +std::string path = wxGetApp().getCompletionLogPath(); +``` + +## Was wird erfasst? + +### ✅ Cartridge-Informationen +- Name der Cartridge +- GUID (eindeutige ID) + +### ✅ Spieler-Informationen +- Spielername (Standard: "Player") + +### ✅ Session-Daten +- Startzeit (aktuell: Zeitpunkt der Generierung) +- Endzeit (aktuell: Zeitpunkt der Generierung) +- Gesamtdauer in Sekunden + +### ✅ Completion Status +- `Complete` - Alle Tasks abgeschlossen +- `Incomplete` - Noch offene Tasks + +### ✅ Aufgaben (Tasks) +- Name der Aufgabe +- Status (completed: true/false) +- Abschlusszeit (falls completed) + +### ✅ Gesammelte Items +- Name des Items +- Zeitpunkt des Einsammelns +- **Nur Items im Spieler-Inventar** + +### ✅ Besuchte Zonen +- Name der Zone +- Zeitpunkt des Besuchs +- **Nur aktive Zonen** + +### ✅ Completion Code +- Eindeutiger Hash/Signatur-Code +- Basiert auf: Cartridge-GUID + Spielername + Abschlusszeit +- Format: 16-stelliger Hex-Code (z.B. `A1B2C3D4E5F67890`) + +## Technische Details + +### Dateipfad + +Standardpfad für Completion Logs: +- **Im gleichen Verzeichnis wie die GWC-Datei** +- Format: `/.gwl` + +Beispiel: +- GWC-Datei: `/Users/name/Downloads/TheOmbosIdol.gwc` +- Completion Log: `/Users/name/Downloads/TheOmbosIdol.gwl` + +Dies ermöglicht es, die Completion-Logs zusammen mit den Cartridge-Dateien zu organisieren. + +### XML-Encoding +- UTF-8 +- Sonderzeichen werden escaped: `&`, `<`, `>`, `"`, `'` + +### Zeitformat +- ISO 8601: `YYYY-MM-DDTHH:MM:SS` +- Beispiel: `2026-02-13T16:45:00` +- Lokale Zeitzone + +### Completion Code Generierung + +```cpp +// Einfacher Hash-Algorithmus (für Produktion: SHA256 verwenden) +hash = hash_function(cartridgeGUID + playerName + endTime) +completionCode = sprintf("%016lX", hash) +``` + +## Limitierungen + +### ⚠️ Session-Zeiten +Die aktuellen Start-/Endzeiten sind der **Zeitpunkt der Generierung**, nicht die tatsächliche Spielzeit. Um echte Session-Tracking zu implementieren: + +```cpp +// In cApp beim Start speichern: +m_sessionStartTime = time(nullptr); + +// Bei Completion verwenden: +data.startTime = m_sessionStartTime; +data.endTime = time(nullptr); +data.duration = data.endTime - data.startTime; +``` + +### ⚠️ Completion Code +Der aktuelle Hash-Algorithmus ist simpel. Für echte Verifikation sollte **SHA256** oder ein ähnlicher kryptografischer Hash verwendet werden. + +### ⚠️ Wherigo.com Upload +Das generierte Format entspricht dem Wherigo-Standard, aber die tatsächliche Akzeptanz auf wherigo.com hängt von deren Server-seitiger Validierung ab. + +## Erweiterungen + +### Spielername anpassen +```cpp +// In cApp/cGame speichern: +std::string m_playerName = "Player"; + +// Beim Generieren verwenden: +wherigo::WherigoCompletion::generateCompletionLog(L, path, m_playerName); +``` + +### Session-Tracking +```cpp +class cApp { + time_t m_sessionStartTime = 0; + + void startGame() { + m_sessionStartTime = time(nullptr); + // ... + } +}; +``` + +### Zusätzliche Statistiken +Das `CompletionData`-Struct kann erweitert werden um: +- Anzahl der Dialoge +- Zurückgelegte Distanz +- Verwendete Hints +- Fehlversuche + +### Auto-Export bei Abschluss +```cpp +// In cGame::OnTaskCompleted() oder bei Complete-Event +if (allTasksCompleted()) { + wxGetApp().generateCompletionLog(""); + wxMessageBox("Gratulation! Completion Log wurde automatisch erstellt."); +} +``` + +## Beispiel-Workflow + +1. **Spieler spielt Cartridge** +2. **Alle Tasks werden abgeschlossen** +3. **Spieler exportiert Log:** Menü → Completion Log exportieren +4. **Datei wird gespeichert:** `TheOmbosIdol_completion.gwl` +5. **Upload auf wherigo.com:** + - Login auf wherigo.com + - Zur Cartridge-Seite navigieren + - "Log completed cartridge" Button + - `.gwl`-Datei hochladen + - Completion wird verifiziert und angezeigt + +## Debugging + +Log-Ausgaben aktivieren: +```cpp +wxLogMessage("Completion log generated: %s", filePath); +wxLogDebug("Tasks completed: %d/%d", completedCount, totalCount); +``` + +## Zukünftige Features + +- [ ] Automatisches Tracking der Session-Zeit +- [ ] Spielername-Eingabe-Dialog +- [ ] Screenshot/Foto-Anhänge +- [ ] GPS-Koordinaten für besuchte Zonen +- [ ] Statistiken (Distanz, Zeit pro Zone, etc.) +- [ ] Automatischer Export bei Completion +- [ ] Cloud-Upload direkt aus der App +- [ ] Kryptografischer Completion Code (SHA256) + diff --git a/components/lua-5.1.4/CMakeLists.txt b/components/lua-5.1.4/CMakeLists.txt index b8807217..6834e04d 100644 --- a/components/lua-5.1.4/CMakeLists.txt +++ b/components/lua-5.1.4/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.21) project(lua LANGUAGES C) +set(LUA_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) + set(LUA_SOURCES src/lapi.c src/lcode.c diff --git a/components/lua-5.1.4/src/lundump.c b/components/lua-5.1.4/src/lundump.c index 8010a457..4732a3a1 100644 --- a/components/lua-5.1.4/src/lundump.c +++ b/components/lua-5.1.4/src/lundump.c @@ -2,6 +2,8 @@ ** $Id: lundump.c,v 2.7.1.4 2008/04/04 19:51:41 roberto Exp $ ** load precompiled Lua chunks ** See Copyright Notice in lua.h +** +** Modified to support loading 32-bit bytecode on 64-bit systems */ #include @@ -25,6 +27,11 @@ typedef struct { ZIO* Z; Mbuffer* b; const char* name; + int swap; /* endianness swap required */ + int sizeof_int; /* sizeof(int) in bytecode */ + int sizeof_size_t; /* sizeof(size_t) in bytecode */ + int sizeof_instr; /* sizeof(Instruction) in bytecode */ + int sizeof_number; /* sizeof(lua_Number) in bytecode */ } LoadState; #ifdef LUAC_TRUST_BINARIES @@ -58,25 +65,114 @@ static int LoadChar(LoadState* S) return x; } +static void SwapBytes(void* b, size_t size) +{ + char* p = (char*)b; + size_t i; + for (i = 0; i < size/2; i++) { + char temp = p[i]; + p[i] = p[size - 1 - i]; + p[size - 1 - i] = temp; + } +} + static int LoadInt(LoadState* S) { int x; - LoadVar(S,x); + if (S->sizeof_int == sizeof(int)) { + LoadVar(S,x); + if (S->swap) SwapBytes(&x, sizeof(x)); + } else if (S->sizeof_int == 4 && sizeof(int) >= 4) { + /* 32-bit int from bytecode */ + lu_int32 t; + LoadMem(S,&t,1,4); + if (S->swap) SwapBytes(&t, 4); + x = (int)t; + } else if (S->sizeof_int == 8 && sizeof(int) == 4) { + /* 64-bit int in bytecode on 32-bit system - use lower 32 bits */ + lu_int32 lo, hi; + LoadMem(S, &lo, 1, 4); + LoadMem(S, &hi, 1, 4); + if (S->swap) { + SwapBytes(&lo, 4); + SwapBytes(&hi, 4); + x = (int)hi; + } else { + x = (int)lo; + } + } else { + error(S, "unsupported int size in bytecode"); + x = 0; + } IF (x<0, "bad integer"); return x; } +static size_t LoadSize(LoadState* S) +{ + size_t x; + if (S->sizeof_size_t == sizeof(size_t)) { + LoadVar(S,x); + if (S->swap) SwapBytes(&x, sizeof(x)); + } else if (S->sizeof_size_t == 4 && sizeof(size_t) == 8) { + /* 32-bit bytecode on 64-bit system */ + lu_int32 t; + LoadMem(S,&t,1,4); + if (S->swap) SwapBytes(&t, 4); + x = (size_t)t; + } else if (S->sizeof_size_t == 8 && sizeof(size_t) == 4) { + /* 64-bit bytecode on 32-bit system - use lower 32 bits */ + lu_int32 lo, hi; + LoadMem(S, &lo, 1, 4); + LoadMem(S, &hi, 1, 4); + if (S->swap) { + SwapBytes(&lo, 4); + SwapBytes(&hi, 4); + x = (size_t)hi; /* big-endian: high bytes first */ + } else { + x = (size_t)lo; /* little-endian: low bytes first */ + } + } else { + error(S, "unsupported size_t size in bytecode"); + x = 0; + } + return x; +} + static lua_Number LoadNumber(LoadState* S) { lua_Number x; - LoadVar(S,x); + if (S->sizeof_number == sizeof(lua_Number)) { + LoadVar(S,x); + if (S->swap) SwapBytes(&x, sizeof(x)); + } else { + error(S, "unsupported lua_Number size in bytecode"); + x = 0; + } + return x; +} + +static Instruction LoadInstruction(LoadState* S) +{ + Instruction x; + if (S->sizeof_instr == sizeof(Instruction)) { + LoadVar(S,x); + if (S->swap) SwapBytes(&x, sizeof(x)); + } else if (S->sizeof_instr == 4) { + lu_int32 t; + LoadMem(S,&t,1,4); + if (S->swap) SwapBytes(&t, 4); + x = (Instruction)t; + } else { + error(S, "unsupported Instruction size in bytecode"); + x = 0; + } return x; } static TString* LoadString(LoadState* S) { - size_t size; - LoadVar(S,size); + size_t size = LoadSize(S); if (size==0) return NULL; else @@ -89,10 +185,12 @@ static TString* LoadString(LoadState* S) static void LoadCode(LoadState* S, Proto* f) { - int n=LoadInt(S); + int i, n=LoadInt(S); f->code=luaM_newvector(S->L,n,Instruction); f->sizecode=n; - LoadVector(S,f->code,n,sizeof(Instruction)); + for (i=0; icode[i] = LoadInstruction(S); + } } static Proto* LoadFunction(LoadState* S, TString* p); @@ -140,7 +238,9 @@ static void LoadDebug(LoadState* S, Proto* f) n=LoadInt(S); f->lineinfo=luaM_newvector(S->L,n,int); f->sizelineinfo=n; - LoadVector(S,f->lineinfo,n,sizeof(int)); + for (i=0; ilineinfo[i] = LoadInt(S); + } n=LoadInt(S); f->locvars=luaM_newvector(S->L,n,LocVar); f->sizelocvars=n; @@ -184,9 +284,44 @@ static void LoadHeader(LoadState* S) { char h[LUAC_HEADERSIZE]; char s[LUAC_HEADERSIZE]; - luaU_header(h); + int x = 1; + int local_endian = (unsigned char)(*(char*)&x); + int file_endian; + LoadBlock(S,s,LUAC_HEADERSIZE); - IF (memcmp(h,s,LUAC_HEADERSIZE)!=0, "bad header"); + + /* Check signature */ + luaU_header(h); + IF (memcmp(h,s,sizeof(LUA_SIGNATURE)-1)!=0, "not a precompiled chunk"); + + /* Check version */ + IF (s[4] != LUAC_VERSION, "version mismatch in precompiled chunk"); + + /* Check format */ + IF (s[5] != LUAC_FORMAT, "incompatible format"); + + /* Endianness */ + file_endian = (unsigned char)s[6]; + S->swap = (file_endian != local_endian); + + /* Size information from bytecode */ + S->sizeof_int = (unsigned char)s[7]; + S->sizeof_size_t = (unsigned char)s[8]; + S->sizeof_instr = (unsigned char)s[9]; + S->sizeof_number = (unsigned char)s[10]; + + /* Validate sizes we can handle */ + IF (S->sizeof_int != 4 && S->sizeof_int != 8 && S->sizeof_int != (int)sizeof(int), + "unsupported int size"); + IF (S->sizeof_size_t != 4 && S->sizeof_size_t != 8, + "unsupported size_t size"); + IF (S->sizeof_instr != 4 && S->sizeof_instr != (int)sizeof(Instruction), + "unsupported Instruction size"); + IF (S->sizeof_number != (int)sizeof(lua_Number), + "incompatible lua_Number size"); + + /* Check integral flag */ + IF (s[11] != (((lua_Number)0.5)==0), "incompatible number type"); } /* diff --git a/main/include/app.h b/main/include/app.h new file mode 100644 index 00000000..b0346ae0 --- /dev/null +++ b/main/include/app.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +extern "C" { +struct lua_State; +} + +class cApp : public wxApp +{ +public: + bool OnInit() override; + int OnExit() override; + + bool loadCartridge(const std::string &filePath); + void startGame(); + void unloadCartridge(); + + // Save/Load game state + bool saveGameState(const std::string &saveFilePath); + bool loadGameState(const std::string &saveFilePath); + std::string getAutoSavePath() const; + + // Generate completion log (for wherigo.com) + bool generateCompletionLog(const std::string &logFilePath); + std::string getCompletionLogPath() const; + + lua_State* getLuaState() const { return m_luaState; } + int getCartridgeRef() const { return m_cartridgeRef; } + cartridge::Cartridge* getCartridge() const { return m_cartridge.get(); } + bool isCartridgeLoaded() const { return m_cartridge != nullptr; } + +private: + bool initLuaState(); + + lua_State* m_luaState = nullptr; + int m_cartridgeRef = -1; // LUA_NOREF + std::unique_ptr m_cartridge; + std::string m_cartridgePath; +}; diff --git a/main/include/cApp.h b/main/include/cApp.h deleted file mode 100644 index 4acf55b3..00000000 --- a/main/include/cApp.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -class cApp : public wxApp -{ -public: - bool OnInit() override; -}; diff --git a/main/include/lua/game_engine.h b/main/include/lua/game_engine.h new file mode 100644 index 00000000..90c73cb2 --- /dev/null +++ b/main/include/lua/game_engine.h @@ -0,0 +1,73 @@ +#ifndef GAME_ENGINE_H +#define GAME_ENGINE_H + +#include +#include +#include +#include +#include +#include + +extern "C" { +struct lua_State; +} + +namespace wherigo { + +// Custom event for game state changes +class GameStateEvent : public wxEvent { +public: + GameStateEvent(wxEventType eventType = wxEVT_NULL, int id = 0) + : wxEvent(id, eventType) {} + + wxEvent* Clone() const override { return new GameStateEvent(*this); } +}; + +wxDECLARE_EVENT(EVT_GAME_STATE_CHANGED, GameStateEvent); + +class GameEngine : public wxEvtHandler { +public: + static GameEngine& getInstance(); + + void init(lua_State *L, int cartridgeRef); + void shutdown(); + + void start(); + void stop(); + + void updatePlayerPosition(double lat, double lng, double alt = 0.0); + + // Notify listeners that game state has changed + void notifyStateChanged(); + + lua_State* getLuaState() const { return m_luaState; } + int getCartridgeRef() const { return m_cartridgeRef; } + +private: + GameEngine(); + ~GameEngine(); + + GameEngine(const GameEngine&) = delete; + GameEngine& operator=(const GameEngine&) = delete; + + void onGameTick(wxTimerEvent& event); + void checkTimers(); + void checkZones(); + + lua_State* m_luaState = nullptr; + int m_cartridgeRef = -1; + + wxTimer m_gameTimer; + bool m_running = false; + + double m_playerLat = 0.0; + double m_playerLng = 0.0; + double m_playerAlt = 0.0; + + wxDECLARE_EVENT_TABLE(); +}; + +} // namespace wherigo + +#endif // GAME_ENGINE_H + diff --git a/main/include/lua/media_manager.h b/main/include/lua/media_manager.h new file mode 100644 index 00000000..64b0b2ae --- /dev/null +++ b/main/include/lua/media_manager.h @@ -0,0 +1,43 @@ +#ifndef MEDIA_MANAGER_H +#define MEDIA_MANAGER_H + +#include +#include +#include +#include +#include + +extern "C" { +struct lua_State; +} + +namespace wherigo { + +class MediaManager { +public: + static MediaManager& getInstance(); + + void init(lua_State *L, cartridge::Cartridge *cartridge); + + // Get media data by ZMedia object name (e.g., "Title Plate 1") + std::vector getMediaByName(const std::string &name); + + // Get media data by index + std::vector getMediaByIndex(int index); + +private: + MediaManager() = default; + + void buildMediaIndex(lua_State *L); + + lua_State *m_luaState = nullptr; + cartridge::Cartridge *m_cartridge = nullptr; + + // Map from media name to cartridge object index + std::map m_nameToIndex; +}; + +} // namespace wherigo + +#endif // MEDIA_MANAGER_H + diff --git a/main/include/lua/persistence.h b/main/include/lua/persistence.h new file mode 100644 index 00000000..2f84828b --- /dev/null +++ b/main/include/lua/persistence.h @@ -0,0 +1,45 @@ +#ifndef LUA_PERSISTENCE_H +#define LUA_PERSISTENCE_H + +#include +#include + +extern "C" { +struct lua_State; +} + +namespace wherigo { + +class LuaPersistence { +public: + // Save current Lua state to file + static bool saveState(lua_State* L, const std::string& filePath); + + // Load Lua state from file + static bool loadState(lua_State* L, const std::string& filePath); + + // Save specific global variables + static bool saveGlobals(lua_State* L, const std::string& filePath, + const std::vector& globals); + + // Load specific global variables + static bool loadGlobals(lua_State* L, const std::string& filePath); + +private: + // Serialize a Lua value (recursive) + static void serializeValue(lua_State* L, int index, std::string& output, int depth = 0); + + // Deserialize from string back to Lua stack + static bool deserializeValue(lua_State* L, const std::string& input, size_t& pos); + + // Helper to serialize table + static void serializeTable(lua_State* L, int index, std::string& output, int depth); + + // Collect all global variables that should be saved + static std::vector getGameGlobals(lua_State* L); +}; + +} // namespace wherigo + +#endif // LUA_PERSISTENCE_H + diff --git a/main/include/lua/wherigo.h b/main/include/lua/wherigo.h new file mode 100644 index 00000000..45e3999c --- /dev/null +++ b/main/include/lua/wherigo.h @@ -0,0 +1,16 @@ +#ifndef WHERIGO_LUA_H +#define WHERIGO_LUA_H + +extern "C" { +#include +} + +namespace wherigo { + +int luaopen_Wherigo(lua_State *L); +void resetMediaCounter(); + +} + +#endif // WHERIGO_LUA_H + diff --git a/main/include/lua/wherigo_completion.h b/main/include/lua/wherigo_completion.h new file mode 100644 index 00000000..4881b67f --- /dev/null +++ b/main/include/lua/wherigo_completion.h @@ -0,0 +1,66 @@ +#ifndef WHERIGO_COMPLETION_H +#define WHERIGO_COMPLETION_H + +#include +#include +#include + +extern "C" { +struct lua_State; +} + +namespace wherigo { + +struct CompletionData { + std::string cartridgeName; + std::string cartridgeGUID; + std::string playerName; + std::string completionCode; + time_t startTime; + time_t endTime; + int duration; // seconds + + struct TaskCompletion { + std::string name; + bool completed; + time_t completionTime; + }; + std::vector tasks; + + struct ItemFound { + std::string name; + time_t foundTime; + }; + std::vector items; + + struct ZoneVisited { + std::string name; + time_t visitTime; + }; + std::vector zones; +}; + +class WherigoCompletion { +public: + // Generate completion log from current Lua state + static bool generateCompletionLog(lua_State* L, const std::string& filePath, + const std::string& playerName = "Player"); + + // Extract completion data from Lua state + static CompletionData extractCompletionData(lua_State* L, const std::string& playerName); + + // Generate .gwl XML file + static bool writeGWLFile(const CompletionData& data, const std::string& filePath); + + // Generate completion code (hash/signature) + static std::string generateCompletionCode(const CompletionData& data); + +private: + static std::string escapeXML(const std::string& str); + static std::string formatTime(time_t t); +}; + +} // namespace wherigo + +#endif // WHERIGO_COMPLETION_H + diff --git a/main/include/lua/zobject.h b/main/include/lua/zobject.h new file mode 100644 index 00000000..2d532c31 --- /dev/null +++ b/main/include/lua/zobject.h @@ -0,0 +1,16 @@ +#ifndef ZOBJECT_H +#define ZOBJECT_H + +extern "C" { +#include +} + +namespace wherigo { + +int zobject_MoveTo(lua_State *L); +int zobject_Contains(lua_State *L); + +} + +#endif // ZOBJECT_H + diff --git a/main/include/lua/ztimer.h b/main/include/lua/ztimer.h new file mode 100644 index 00000000..b1f64f55 --- /dev/null +++ b/main/include/lua/ztimer.h @@ -0,0 +1,17 @@ +#ifndef ZTIMER_H +#define ZTIMER_H + +extern "C" { +#include +} + +namespace wherigo { + +int ztimer_Start(lua_State *L); +int ztimer_Stop(lua_State *L); +int ztimer_Reset(lua_State *L); + +} + +#endif // ZTIMER_H + diff --git a/main/include/ui/cFrame.h b/main/include/ui/cFrame.h deleted file mode 100644 index d6b0981f..00000000 --- a/main/include/ui/cFrame.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include - -class cFrame : public wxMDIParentFrame -{ -public: - cFrame(); - -private: - void OnHello(wxCommandEvent& event); - void OnExit(wxCommandEvent& event); - void OnAbout(wxCommandEvent& event); -}; diff --git a/main/include/ui/game_screen.h b/main/include/ui/game_screen.h new file mode 100644 index 00000000..bebd6a73 --- /dev/null +++ b/main/include/ui/game_screen.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +class cGameScreen : public wxFrame +{ +public: + cGameScreen(wxWindow *parent); + + void refreshUI(); + +private: + void OnClose(wxCloseEvent& event); + void OnExit(wxCommandEvent& event); + void OnAbout(wxCommandEvent& event); + void OnSaveGame(wxCommandEvent& event); + void OnLoadGame(wxCommandEvent& event); + void OnExportCompletion(wxCommandEvent& event); + void OnGameStateChanged(wxEvent& event); + void OnZoneSelected(wxCommandEvent& event); + void OnTaskSelected(wxCommandEvent& event); + void OnInventorySelected(wxCommandEvent& event); + void OnCharacterSelected(wxCommandEvent& event); + void OnItemSelected(wxCommandEvent& event); + + void populateZones(); + void populateTasks(); + void populateInventory(); + void populateCharacters(); + void populateItems(); + + wxNotebook* m_notebook; + wxListBox* m_zoneList; + wxListBox* m_taskList; + wxListBox* m_inventoryList; + wxListBox* m_characterList; + wxListBox* m_itemList; +}; diff --git a/main/include/ui/start_screen.h b/main/include/ui/start_screen.h new file mode 100644 index 00000000..53d78cd3 --- /dev/null +++ b/main/include/ui/start_screen.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +class cGameScreen; + +class cStartScreen : public wxFrame +{ +public: + cStartScreen(); + + void showCartridgeInfo(); + void onGameClosed(); + +private: + void OnOpenCartridge(wxCommandEvent& event); + void OnStartGame(wxCommandEvent& event); + void OnExit(wxCommandEvent& event); + void OnAbout(wxCommandEvent& event); + + wxPanel* m_infoPanel; + wxStaticText* m_cartridgeName; + wxStaticText* m_cartridgeAuthor; + wxHtmlWindow* m_cartridgeDesc; + wxStaticBitmap* m_splashImage; + wxButton* m_openButton; + wxButton* m_startButton; + + cGameScreen* m_gameFrame; + bool m_cartridgeLoaded; +}; diff --git a/main/include/ui/wherigo_dialog.h b/main/include/ui/wherigo_dialog.h new file mode 100644 index 00000000..95b77a23 --- /dev/null +++ b/main/include/ui/wherigo_dialog.h @@ -0,0 +1,50 @@ +#ifndef WHERIGO_DIALOG_H +#define WHERIGO_DIALOG_H + +#include +#include +#include +#include +#include + +namespace wherigo { + +struct DialogEntry { + std::string text; + std::string mediaName; + std::vector buttons; +}; + +class WherigoMessageDialog : public wxDialog { +public: + WherigoMessageDialog(wxWindow *parent, const wxString &text, + const wxString &title = "Wherigo", + const std::vector &buttons = {}, + const wxString &mediaName = ""); + + int getSelectedButton() const { return m_selectedButton; } + +private: + void onButton(wxCommandEvent &event); + + int m_selectedButton = -1; +}; + +class WherigoDialogRunner { +public: + static WherigoDialogRunner& getInstance(); + + void showMessageBox(const wxString &text, const wxString &title = "Wherigo", + std::function callback = nullptr); + + void showDialog(const std::vector &entries, + std::function callback = nullptr); + +private: + WherigoDialogRunner() = default; +}; + +} // namespace wherigo + +#endif // WHERIGO_DIALOG_H + diff --git a/main/src/app.cpp b/main/src/app.cpp new file mode 100644 index 00000000..a2a9fb7a --- /dev/null +++ b/main/src/app.cpp @@ -0,0 +1,275 @@ +#include "app.h" +#include "lua/game_engine.h" +#include "lua/media_manager.h" +#include "lua/persistence.h" +#include "lua/wherigo.h" +#include "lua/wherigo_completion.h" +#include "ui/start_screen.h" + +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +bool cApp::OnInit() +{ + + // Initialize image handlers for JPEG, PNG, GIF, etc. + wxInitAllImageHandlers(); + + // Show start frame + auto *startFrame = new cStartScreen(); + startFrame->Show(true); + + return true; +} + +bool cApp::loadCartridge(const std::string &filePath) { + // Unload previous cartridge if any + unloadCartridge(); + + m_cartridgePath = filePath; + + // Parse cartridge + m_cartridge = cartridge::parseFile(filePath); + if (!m_cartridge) { + wxLogError("Failed to parse cartridge: %s", filePath); + return false; + } + + return initLuaState(); +} + +bool cApp::initLuaState() { + auto luac = m_cartridge->luac(); + if (!luac) { + wxLogError("No Lua bytecode in cartridge"); + return false; + } + + m_luaState = luaL_newstate(); + if (!m_luaState) { + wxLogError("Failed to create Lua state"); + return false; + } + + luaL_openlibs(m_luaState); + + // Reset media counter before loading cartridge + wherigo::resetMediaCounter(); + + // Register Wherigo module + wherigo::luaopen_Wherigo(m_luaState); + lua_pop(m_luaState, 1); + + const auto &bytecode = luac->getData(); + int result = luaL_loadbuffer(m_luaState, + reinterpret_cast(bytecode.data()), + bytecode.size(), + "cartridge"); + + if (result != 0) { + const char *err = lua_tostring(m_luaState, -1); + wxLogError("Lua load error: %s", err); + lua_close(m_luaState); + m_luaState = nullptr; + return false; + } + + // Execute the loaded chunk + if (lua_pcall(m_luaState, 0, 0, 0) != 0) { + const char *err = lua_tostring(m_luaState, -1); + wxLogError("Lua exec error: %s", err); + lua_close(m_luaState); + m_luaState = nullptr; + return false; + } + + // Find the cartridge object by searching for ClassName = "ZCartridge" + m_cartridgeRef = LUA_NOREF; + lua_getglobal(m_luaState, "_G"); + lua_pushnil(m_luaState); + while (lua_next(m_luaState, -2) != 0) { + if (lua_istable(m_luaState, -1)) { + lua_getfield(m_luaState, -1, "ClassName"); + if (lua_isstring(m_luaState, -1)) { + const char *className = lua_tostring(m_luaState, -1); + if (strcmp(className, "ZCartridge") == 0) { + lua_pop(m_luaState, 1); // pop ClassName + m_cartridgeRef = luaL_ref(m_luaState, LUA_REGISTRYINDEX); // store reference, pops table + lua_pop(m_luaState, 1); // pop key + break; + } + } + lua_pop(m_luaState, 1); // pop ClassName + } + lua_pop(m_luaState, 1); // pop value + } + lua_pop(m_luaState, 1); // pop _G + + // Initialize media manager + wherigo::MediaManager::getInstance().init(m_luaState, m_cartridge.get()); + + if (m_cartridgeRef == LUA_NOREF) { + wxLogError("No ZCartridge object found!"); + return false; + } + + // Initialize the game engine (but don't start yet) + wherigo::GameEngine::getInstance().init(m_luaState, m_cartridgeRef); + + return true; +} + +void cApp::startGame() { + if (!m_luaState || m_cartridgeRef == LUA_NOREF) { + wxLogError("No cartridge loaded"); + return; + } + + // Call OnStart callback + lua_rawgeti(m_luaState, LUA_REGISTRYINDEX, m_cartridgeRef); + lua_getfield(m_luaState, -1, "OnStart"); + if (lua_isfunction(m_luaState, -1)) { + lua_pushvalue(m_luaState, -2); // push cartridge as self + if (lua_pcall(m_luaState, 1, 0, 0) != 0) { + const char *err = lua_tostring(m_luaState, -1); + wxLogError("OnStart error: %s", err); + lua_pop(m_luaState, 1); + } + } else { + lua_pop(m_luaState, 1); + } + lua_pop(m_luaState, 1); // pop cartridge + + // Start the game engine + wherigo::GameEngine::getInstance().start(); +} + +void cApp::unloadCartridge() { + // Shutdown the game engine + wherigo::GameEngine::getInstance().shutdown(); + + if (m_cartridgeRef != LUA_NOREF && m_luaState) { + luaL_unref(m_luaState, LUA_REGISTRYINDEX, m_cartridgeRef); + m_cartridgeRef = LUA_NOREF; + } + + if (m_luaState) { + lua_close(m_luaState); + m_luaState = nullptr; + } + + m_cartridge.reset(); + m_cartridgePath.clear(); +} + +std::string cApp::getAutoSavePath() const { + if (m_cartridge) { + wxStandardPaths& stdPaths = wxStandardPaths::Get(); + wxString userDataDir = stdPaths.GetUserDataDir(); + + // Create save directory if it doesn't exist + if (!wxDir::Exists(userDataDir)) { + wxFileName::Mkdir(userDataDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL); + } + + // Generate save filename from cartridge name + wxString cartName = wxString::FromUTF8(m_cartridge->cartridgeName()); + cartName.Replace(" ", "_"); + cartName.Replace("/", "_"); + + wxString savePath = userDataDir + wxFileName::GetPathSeparator() + cartName + ".save"; + return savePath.ToStdString(); + } + return ""; +} + +bool cApp::saveGameState(const std::string &saveFilePath) { + if (!m_luaState) { + wxLogError("No Lua state to save"); + return false; + } + + std::string savePath = saveFilePath.empty() ? getAutoSavePath() : saveFilePath; + if (savePath.empty()) { + wxLogError("No save path available"); + return false; + } + + bool success = wherigo::LuaPersistence::saveState(m_luaState, savePath); + if (success) { + wxLogMessage("Game saved to: %s", savePath); + } + return success; +} + +bool cApp::loadGameState(const std::string &saveFilePath) { + if (!m_luaState) { + wxLogError("No Lua state to load into"); + return false; + } + + std::string loadPath = saveFilePath.empty() ? getAutoSavePath() : saveFilePath; + if (loadPath.empty() || !wxFileExists(loadPath)) { + wxLogError("Save file not found: %s", loadPath); + return false; + } + + bool success = wherigo::LuaPersistence::loadState(m_luaState, loadPath); + if (success) { + wxLogMessage("Game loaded from: %s", loadPath); + // Notify game engine of state change + wherigo::GameEngine::getInstance().notifyStateChanged(); + } + return success; +} + +std::string cApp::getCompletionLogPath() const { + if (m_cartridge && !m_cartridgePath.empty()) { + // Use the same directory as the GWC file + wxFileName gwcFile(m_cartridgePath); + wxString directory = gwcFile.GetPath(); + + // Generate completion log filename from cartridge name + wxString cartName = wxString::FromUTF8(m_cartridge->cartridgeName()); + cartName.Replace(" ", "_"); + cartName.Replace("/", "_"); + + wxString logPath = directory + wxFileName::GetPathSeparator() + cartName + ".gwl"; + return logPath.ToStdString(); + } + return ""; +} + +bool cApp::generateCompletionLog(const std::string &logFilePath) { + if (!m_luaState) { + wxLogError("No Lua state for completion log"); + return false; + } + + std::string logPath = logFilePath.empty() ? getCompletionLogPath() : logFilePath; + if (logPath.empty()) { + wxLogError("No completion log path available"); + return false; + } + + bool success = wherigo::WherigoCompletion::generateCompletionLog(m_luaState, logPath, "Player"); + if (success) { + wxLogMessage("Completion log generated: %s", logPath); + } + return success; +} + +int cApp::OnExit() +{ + unloadCartridge(); + return wxApp::OnExit(); +} + diff --git a/main/src/cApp.cpp b/main/src/cApp.cpp deleted file mode 100644 index b86e218c..00000000 --- a/main/src/cApp.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "cApp.h" -#include "ui/cFrame.h" - -#include - -bool cApp::OnInit() -{ - auto cart = cartridge::parseCartridge(); - auto *frame = new cFrame(); - return frame->Show(true); -} diff --git a/main/src/lua/game_engine.cpp b/main/src/lua/game_engine.cpp new file mode 100644 index 00000000..3e0c0a26 --- /dev/null +++ b/main/src/lua/game_engine.cpp @@ -0,0 +1,215 @@ +#include "lua/game_engine.h" + +extern "C" { +#include +#include +} + +#include + +namespace wherigo { + +// Define the custom event +wxDEFINE_EVENT(EVT_GAME_STATE_CHANGED, GameStateEvent); + +wxBEGIN_EVENT_TABLE(GameEngine, wxEvtHandler) + EVT_TIMER(wxID_ANY, GameEngine::onGameTick) +wxEND_EVENT_TABLE() + +GameEngine& GameEngine::getInstance() { + static GameEngine instance; + return instance; +} + +GameEngine::GameEngine() : m_gameTimer(this) { +} + +GameEngine::~GameEngine() { + shutdown(); +} + +void GameEngine::init(lua_State *L, int cartridgeRef) { + m_luaState = L; + m_cartridgeRef = cartridgeRef; + wxLogDebug("GameEngine initialized"); +} + +void GameEngine::shutdown() { + stop(); + m_luaState = nullptr; + m_cartridgeRef = -1; +} + +void GameEngine::start() { + if (m_running) return; + + m_running = true; + m_gameTimer.Start(1000); // 1 second tick + wxLogDebug("GameEngine started"); +} + +void GameEngine::stop() { + if (!m_running) return; + + m_gameTimer.Stop(); + m_running = false; + wxLogDebug("GameEngine stopped"); +} + +void GameEngine::updatePlayerPosition(double lat, double lng, double alt) { + m_playerLat = lat; + m_playerLng = lng; + m_playerAlt = alt; + + if (!m_luaState) return; + + // Update Player.ObjectLocation in Lua + lua_getglobal(m_luaState, "Player"); + if (lua_istable(m_luaState, -1)) { + lua_newtable(m_luaState); + lua_pushnumber(m_luaState, lat); + lua_setfield(m_luaState, -2, "latitude"); + lua_pushnumber(m_luaState, lng); + lua_setfield(m_luaState, -2, "longitude"); + lua_pushnumber(m_luaState, alt); + lua_setfield(m_luaState, -2, "altitude"); + lua_setfield(m_luaState, -2, "ObjectLocation"); + } + lua_pop(m_luaState, 1); + + checkZones(); +} + +void GameEngine::onGameTick(wxTimerEvent& event) { + if (!m_luaState || !m_running) return; + + checkTimers(); + + // Notify listeners of potential state changes + notifyStateChanged(); +} + +void GameEngine::checkTimers() { + if (!m_luaState) return; + + // Iterate through all global variables to find running timers + lua_getglobal(m_luaState, "_G"); + lua_pushnil(m_luaState); + + while (lua_next(m_luaState, -2) != 0) { + if (lua_istable(m_luaState, -1)) { + lua_getfield(m_luaState, -1, "ClassName"); + if (lua_isstring(m_luaState, -1) && strcmp(lua_tostring(m_luaState, -1), "ZTimer") == 0) { + lua_pop(m_luaState, 1); // pop ClassName + + lua_getfield(m_luaState, -1, "Running"); + bool running = lua_toboolean(m_luaState, -1); + lua_pop(m_luaState, 1); + + if (running) { + // Get timer info + lua_getfield(m_luaState, -1, "Name"); + const char *name = lua_isstring(m_luaState, -1) ? lua_tostring(m_luaState, -1) : "(unnamed)"; + lua_pop(m_luaState, 1); + + lua_getfield(m_luaState, -1, "Remaining"); + lua_Number remaining = lua_isnumber(m_luaState, -1) ? lua_tonumber(m_luaState, -1) : -1; + lua_pop(m_luaState, 1); + + // If Remaining not set, initialize from Duration + if (remaining < 0) { + lua_getfield(m_luaState, -1, "Duration"); + remaining = lua_isnumber(m_luaState, -1) ? lua_tonumber(m_luaState, -1) : 0; + lua_pop(m_luaState, 1); + } + + // Decrement remaining time + remaining -= 1.0; + lua_pushnumber(m_luaState, remaining); + lua_setfield(m_luaState, -2, "Remaining"); + + // Call OnTick if exists + lua_getfield(m_luaState, -1, "OnTick"); + if (lua_isfunction(m_luaState, -1)) { + lua_pushvalue(m_luaState, -2); // push timer as self + if (lua_pcall(m_luaState, 1, 0, 0) != 0) { + wxLogError("Timer OnTick error: %s", lua_tostring(m_luaState, -1)); + lua_pop(m_luaState, 1); + } + } else { + lua_pop(m_luaState, 1); + } + + // Check if elapsed + if (remaining <= 0) { + wxLogDebug("Timer elapsed: %s", name); + + // Stop the timer + lua_pushboolean(m_luaState, 0); + lua_setfield(m_luaState, -2, "Running"); + + // Call OnElapsed if exists + lua_getfield(m_luaState, -1, "OnElapsed"); + if (lua_isfunction(m_luaState, -1)) { + lua_pushvalue(m_luaState, -2); // push timer as self + if (lua_pcall(m_luaState, 1, 0, 0) != 0) { + wxLogError("Timer OnElapsed error: %s", lua_tostring(m_luaState, -1)); + lua_pop(m_luaState, 1); + } + } else { + lua_pop(m_luaState, 1); + } + } + } + } else { + lua_pop(m_luaState, 1); // pop ClassName + } + } + lua_pop(m_luaState, 1); // pop value + } + lua_pop(m_luaState, 1); // pop _G +} + +void GameEngine::checkZones() { + if (!m_luaState) return; + + // Iterate through all global variables to find zones + lua_getglobal(m_luaState, "_G"); + lua_pushnil(m_luaState); + + while (lua_next(m_luaState, -2) != 0) { + if (lua_istable(m_luaState, -1)) { + lua_getfield(m_luaState, -1, "ClassName"); + if (lua_isstring(m_luaState, -1) && strcmp(lua_tostring(m_luaState, -1), "Zone") == 0) { + lua_pop(m_luaState, 1); // pop ClassName + + lua_getfield(m_luaState, -1, "Active"); + bool active = lua_toboolean(m_luaState, -1); + lua_pop(m_luaState, 1); + + if (active) { + // TODO: Check if player is inside zone using Points + // For now, just log that we would check + lua_getfield(m_luaState, -1, "Name"); + const char *name = lua_isstring(m_luaState, -1) ? lua_tostring(m_luaState, -1) : "(unnamed)"; + lua_pop(m_luaState, 1); + + // This is where you would implement point-in-polygon check + // and call OnEnter/OnExit callbacks + } + } else { + lua_pop(m_luaState, 1); // pop ClassName + } + } + lua_pop(m_luaState, 1); // pop value + } + lua_pop(m_luaState, 1); // pop _G +} + +void GameEngine::notifyStateChanged() { + GameStateEvent event(EVT_GAME_STATE_CHANGED); + ProcessEvent(event); +} + +} // namespace wherigo + diff --git a/main/src/lua/media_manager.cpp b/main/src/lua/media_manager.cpp new file mode 100644 index 00000000..e2e933c3 --- /dev/null +++ b/main/src/lua/media_manager.cpp @@ -0,0 +1,97 @@ +#include "lua/media_manager.h" + +extern "C" { +#include +#include +} + +#include + +namespace wherigo { + +MediaManager& MediaManager::getInstance() { + static MediaManager instance; + return instance; +} + +void MediaManager::init(lua_State *L, cartridge::Cartridge *cartridge) { + m_luaState = L; + m_cartridge = cartridge; + + buildMediaIndex(L); + + wxLogDebug("MediaManager initialized with %zu media entries", m_nameToIndex.size()); +} + +void MediaManager::buildMediaIndex(lua_State *L) { + m_nameToIndex.clear(); + + if (!L || !m_cartridge) return; + + // Iterate through all globals to find ZMedia objects + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "ZMedia") == 0) { + lua_pop(L, 1); // pop ClassName + + std::string name; + int mediaIndex = -1; + + // Get the Name + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + name = lua_tostring(L, -1); + } + lua_pop(L, 1); // pop Name + + // Get the MediaIndex (set by wherigo_ZMedia when the object was created) + lua_getfield(L, -1, "MediaIndex"); + if (lua_isnumber(L, -1)) { + mediaIndex = lua_tointeger(L, -1); + } + lua_pop(L, 1); // pop MediaIndex + + if (!name.empty() && mediaIndex > 0) { + m_nameToIndex[name] = mediaIndex; + wxLogDebug("Media '%s' -> index %d", name.c_str(), mediaIndex); + } + } else { + lua_pop(L, 1); // pop ClassName + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G +} + +std::vector MediaManager::getMediaByName(const std::string &name) { + auto it = m_nameToIndex.find(name); + if (it == m_nameToIndex.end()) { + wxLogWarning("Media not found: %s", name.c_str()); + return {}; + } + + return getMediaByIndex(it->second); +} + +std::vector MediaManager::getMediaByIndex(int index) { + if (!m_cartridge) { + wxLogError("MediaManager: No cartridge set"); + return {}; + } + + auto media = m_cartridge->getMedia(index); + if (!media) { + wxLogWarning("Media index %d not found in cartridge", index); + return {}; + } + + return media->getData(); +} + +} // namespace wherigo + diff --git a/main/src/lua/persistence.cpp b/main/src/lua/persistence.cpp new file mode 100644 index 00000000..fa8ee854 --- /dev/null +++ b/main/src/lua/persistence.cpp @@ -0,0 +1,228 @@ +#include "lua/persistence.h" + +extern "C" { +#include +#include +} + +#include +#include +#include +#include + +namespace wherigo { + +// List of Wherigo game object classes to save +static const std::set WHERIGO_CLASSES = { + "ZCartridge", "Zone", "ZItem", "ZCharacter", + "ZTask", "ZTimer", "ZInput", "ZMedia" +}; + +std::vector LuaPersistence::getGameGlobals(lua_State* L) { + std::vector globals; + + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_isstring(L, -2)) { + const char* key = lua_tostring(L, -2); + + // Check if it's a Wherigo game object + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1)) { + std::string className = lua_tostring(L, -1); + if (WHERIGO_CLASSES.count(className) > 0) { + globals.push_back(key); + } + } + lua_pop(L, 1); // pop ClassName + } + // Also save simple global variables (strings, numbers, booleans) + else if (lua_isnumber(L, -1) || lua_isstring(L, -1) || lua_isboolean(L, -1)) { + std::string keyStr = key; + // Skip internal Lua variables + if (keyStr != "_G" && keyStr != "_VERSION" && keyStr.find("_") != 0) { + globals.push_back(key); + } + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G + + // Always include Player object + globals.push_back("Player"); + + return globals; +} + +void LuaPersistence::serializeValue(lua_State* L, int index, std::string& output, int depth) { + if (depth > 10) { + output += "nil"; // Prevent infinite recursion + return; + } + + int type = lua_type(L, index); + + switch (type) { + case LUA_TNIL: + output += "nil"; + break; + + case LUA_TBOOLEAN: + output += lua_toboolean(L, index) ? "true" : "false"; + break; + + case LUA_TNUMBER: + output += std::to_string(lua_tonumber(L, index)); + break; + + case LUA_TSTRING: { + const char* str = lua_tostring(L, index); + output += "\""; + // Escape special characters + for (const char* p = str; *p; ++p) { + if (*p == '"' || *p == '\\') output += '\\'; + output += *p; + } + output += "\""; + break; + } + + case LUA_TTABLE: + serializeTable(L, index, output, depth); + break; + + default: + // Functions, userdata, threads -> save as nil + output += "nil"; + break; + } +} + +void LuaPersistence::serializeTable(lua_State* L, int index, std::string& output, int depth) { + output += "{"; + + // Normalize stack index + if (index < 0) index = lua_gettop(L) + index + 1; + + bool first = true; + lua_pushnil(L); + + while (lua_next(L, index) != 0) { + if (!first) output += ","; + first = false; + + // Serialize key + output += "["; + serializeValue(L, -2, output, depth + 1); + output += "]="; + + // Serialize value + serializeValue(L, -1, output, depth + 1); + + lua_pop(L, 1); // pop value, keep key for next iteration + } + + output += "}"; +} + +bool LuaPersistence::saveGlobals(lua_State* L, const std::string& filePath, + const std::vector& globals) { + std::string output = "-- Wherigo Save State\nreturn {\n"; + + for (const auto& globalName : globals) { + lua_getglobal(L, globalName.c_str()); + + if (!lua_isnil(L, -1)) { + output += " [\"" + globalName + "\"] = "; + serializeValue(L, -1, output, 0); + output += ",\n"; + } + + lua_pop(L, 1); + } + + output += "}\n"; + + // Write to file + std::ofstream file(filePath); + if (!file.is_open()) { + wxLogError("Failed to open save file: %s", filePath); + return false; + } + + file << output; + file.close(); + + wxLogDebug("Saved %zu globals to %s", globals.size(), filePath); + return true; +} + +bool LuaPersistence::saveState(lua_State* L, const std::string& filePath) { + auto globals = getGameGlobals(L); + return saveGlobals(L, filePath, globals); +} + +bool LuaPersistence::loadGlobals(lua_State* L, const std::string& filePath) { + // Load the file + std::ifstream file(filePath); + if (!file.is_open()) { + wxLogError("Failed to open load file: %s", filePath); + return false; + } + + std::stringstream buffer; + buffer << file.rdbuf(); + std::string content = buffer.str(); + file.close(); + + // Execute the Lua file + if (luaL_loadstring(L, content.c_str()) != 0) { + wxLogError("Failed to parse save file: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + + if (lua_pcall(L, 0, 1, 0) != 0) { + wxLogError("Failed to execute save file: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + + // Result should be a table + if (!lua_istable(L, -1)) { + wxLogError("Save file did not return a table"); + lua_pop(L, 1); + return false; + } + + // Restore globals + int count = 0; + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_isstring(L, -2)) { + const char* key = lua_tostring(L, -2); + + // Set the global + lua_pushvalue(L, -1); // duplicate value + lua_setglobal(L, key); + count++; + } + lua_pop(L, 1); // pop value + } + + lua_pop(L, 1); // pop table + + wxLogDebug("Restored %d globals from %s", count, filePath); + return true; +} + +bool LuaPersistence::loadState(lua_State* L, const std::string& filePath) { + return loadGlobals(L, filePath); +} + +} // namespace wherigo + diff --git a/main/src/lua/wherigo.cpp b/main/src/lua/wherigo.cpp new file mode 100644 index 00000000..cbef1aad --- /dev/null +++ b/main/src/lua/wherigo.cpp @@ -0,0 +1,512 @@ +#include "lua/wherigo.h" +#include "lua/zobject.h" +#include "lua/ztimer.h" +#include "ui/wherigo_dialog.h" + +extern "C" { +#include +#include +} + +#include + +#include +#include +#include +#include + +namespace wherigo { + + +// Wherigo.ZonePoint(lat, lng, alt) +static int wherigo_ZonePoint(lua_State *L) { + lua_Number lat = luaL_checknumber(L, 1); + lua_Number lng = luaL_checknumber(L, 2); + 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"); + + return 1; +} + +// Wherigo.Distance(value, unit) +static int wherigo_Distance(lua_State *L) { + lua_Number value = luaL_checknumber(L, 1); + const char *unit = luaL_optstring(L, 2, "meters"); + + lua_newtable(L); + lua_pushnumber(L, value); + lua_setfield(L, -2, "value"); + lua_pushstring(L, unit); + lua_setfield(L, -2, "unit"); + + return 1; +} + +// Wherigo.Zone(cartridge) +static int wherigo_Zone(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "Zone"); + lua_setfield(L, -2, "ClassName"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Active"); + + lua_pushboolean(L, 1); + lua_setfield(L, -2, "Visible"); + + lua_newtable(L); + lua_setfield(L, -2, "Points"); + + lua_pushcfunction(L, zobject_Contains); + lua_setfield(L, -2, "Contains"); + + return 1; +} + +// Wherigo.ZCharacter(cartridge) +static int wherigo_ZCharacter(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZCharacter"); + lua_setfield(L, -2, "ClassName"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Active"); + + lua_pushboolean(L, 1); + lua_setfield(L, -2, "Visible"); + + lua_pushcfunction(L, zobject_MoveTo); + lua_setfield(L, -2, "MoveTo"); + + lua_pushcfunction(L, zobject_Contains); + lua_setfield(L, -2, "Contains"); + + return 1; +} + +// Wherigo.ZItem(cartridge) +static int wherigo_ZItem(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZItem"); + lua_setfield(L, -2, "ClassName"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Active"); + + lua_pushboolean(L, 1); + lua_setfield(L, -2, "Visible"); + + lua_pushcfunction(L, zobject_MoveTo); + lua_setfield(L, -2, "MoveTo"); + + lua_pushcfunction(L, zobject_Contains); + lua_setfield(L, -2, "Contains"); + + return 1; +} + +// Wherigo.ZTask(cartridge) +static int wherigo_ZTask(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZTask"); + lua_setfield(L, -2, "ClassName"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Active"); + + lua_pushboolean(L, 0); + lua_setfield(L, -2, "Complete"); + + return 1; +} + + +// Wherigo.ZTimer(cartridge) +static int wherigo_ZTimer(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZTimer"); + lua_setfield(L, -2, "ClassName"); + + lua_pushstring(L, "Countdown"); + lua_setfield(L, -2, "Type"); + + lua_pushnumber(L, 0); + lua_setfield(L, -2, "Duration"); + + 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; +} + +// Wherigo.ZInput(cartridge) +static int wherigo_ZInput(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZInput"); + lua_setfield(L, -2, "ClassName"); + + lua_pushstring(L, "Text"); + lua_setfield(L, -2, "InputType"); + + return 1; +} + +// Wherigo.ZMedia(cartridge) +static int s_mediaCounter = 0; + +static int wherigo_ZMedia(lua_State *L) { + lua_newtable(L); + + lua_pushstring(L, "ZMedia"); + lua_setfield(L, -2, "ClassName"); + + // Store the media index (1-based, 0 is luac) + s_mediaCounter++; + lua_pushinteger(L, s_mediaCounter); + lua_setfield(L, -2, "MediaIndex"); + + return 1; +} + +void resetMediaCounter() { + s_mediaCounter = 0; +} + +// Wherigo.ZCartridge() +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"); + + return 1; +} + +// Wherigo.ZCommand(cartridge) +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; +} + +// Wherigo.Player +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"); + + return 1; +} + +// Wherigo.MessageBox(table) +static int wherigo_MessageBox(lua_State *L) { + if (!lua_istable(L, 1)) return 0; + + // Get Text + lua_getfield(L, 1, "Text"); + const char *text = lua_isstring(L, -1) ? lua_tostring(L, -1) : ""; + lua_pop(L, 1); + + // Get Buttons + std::vector buttons; + lua_getfield(L, 1, "Buttons"); + if (lua_istable(L, -1)) { + 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); + + // Get Callback + lua_getfield(L, 1, "Callback"); + 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); + + // Show dialog + WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(text), "Wherigo", buttons); + dlg.ShowModal(); + int selected = dlg.getSelectedButton(); + + // Call callback if exists + if (callbackRef != LUA_NOREF) { + lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef); + if (buttons.empty()) { + lua_pushnil(L); + } else if (selected >= 0 && selected < (int)buttons.size()) { + lua_pushstring(L, buttons[selected].ToUTF8().data()); + } else { + lua_pushnil(L); + } + 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); + } + + return 0; +} + +// Wherigo.Dialog(table) +static int wherigo_Dialog(lua_State *L) { + if (!lua_istable(L, 1)) return 0; + + std::vector entries; + 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); + + // Get Media object name + 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)) { + 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); + } + + wxLogDebug("Dialog with %zu entries", entries.size()); + for (const auto& entry : entries) { + wxLogDebug(" Text: %s, Media: %s", entry.text.c_str(), + entry.mediaName.empty() ? "(none)" : entry.mediaName.c_str()); + } + + // Show dialogs sequentially + WherigoDialogRunner::getInstance().showDialog(entries); + + return 0; +} + +// Wherigo.GetInput(zinput) +static int wherigo_GetInput(lua_State *L) { + if (lua_istable(L, 1)) { + lua_getfield(L, 1, "Name"); + const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; + wxLogDebug("GetInput: %s", name); + lua_pop(L, 1); + } + return 0; +} + +// Wherigo.PlayAudio(zmedia) +static int wherigo_PlayAudio(lua_State *L) { + if (lua_istable(L, 1)) { + lua_getfield(L, 1, "Name"); + const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; + wxLogDebug("PlayAudio: %s", name); + lua_pop(L, 1); + } + return 0; +} + +// Wherigo.ShowScreen(screen, obj) +static int wherigo_ShowScreen(lua_State *L) { + int screen = luaL_optinteger(L, 1, 0); + wxLogDebug("ShowScreen: %d", screen); + return 0; +} + +// Wherigo.LogMessage(message, level) +static int wherigo_LogMessage(lua_State *L) { + const char *message = luaL_checkstring(L, 1); + wxLogDebug("Wherigo.LogMessage: %s", message); + return 0; +} + +// Wherigo.NoCaseEquals(str1, str2) +static int wherigo_NoCaseEquals(lua_State *L) { + const char *str1 = luaL_checkstring(L, 1); + const char *str2 = luaL_checkstring(L, 2); + + std::string s1(str1); + std::string s2(str2); + + std::transform(s1.begin(), s1.end(), s1.begin(), ::tolower); + std::transform(s2.begin(), s2.end(), s2.begin(), ::tolower); + + lua_pushboolean(L, s1 == s2); + return 1; +} + +// Wherigo.TranslatePoint(point, distance, bearing) +static int wherigo_TranslatePoint(lua_State *L) { + // TODO: implement point translation + lua_newtable(L); + return 1; +} + +// Wherigo.VectorToZone(point, zone) +static int wherigo_VectorToZone(lua_State *L) { + lua_newtable(L); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "distance"); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "bearing"); + lua_pushboolean(L, 0); + lua_setfield(L, -2, "inside"); + return 1; +} + +// Wherigo.VectorToPoint(point1, point2) +static int wherigo_VectorToPoint(lua_State *L) { + lua_newtable(L); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "distance"); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "bearing"); + return 1; +} + +// Wherigo.VectorToSegment(point, point1, point2) +static int wherigo_VectorToSegment(lua_State *L) { + lua_newtable(L); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "distance"); + lua_pushnumber(L, 0); + lua_setfield(L, -2, "bearing"); + return 1; +} + +static const luaL_Reg wherigo_funcs[] = { + {"ZonePoint", wherigo_ZonePoint}, + {"Distance", wherigo_Distance}, + {"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}, + {"LogMessage", wherigo_LogMessage}, + {"NoCaseEquals", wherigo_NoCaseEquals}, + {"TranslatePoint", wherigo_TranslatePoint}, + {"VectorToZone", wherigo_VectorToZone}, + {"VectorToPoint", wherigo_VectorToPoint}, + {"VectorToSegment", wherigo_VectorToSegment}, + {nullptr, nullptr} +}; + +int luaopen_Wherigo(lua_State *L) { + luaL_register(L, "Wherigo", wherigo_funcs); + + // Create Player object + wherigo_Player(L); + lua_setfield(L, -2, "Player"); + + // Screen constants + 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"); + + // Register common functions as globals (as expected by Wherigo Builder) + lua_pushcfunction(L, wherigo_ZonePoint); + lua_setglobal(L, "ZonePoint"); + + lua_pushcfunction(L, wherigo_Distance); + lua_setglobal(L, "Distance"); + + // Register Player as global + wherigo_Player(L); + lua_setglobal(L, "Player"); + + return 1; +} + +} // namespace wherigo + diff --git a/main/src/lua/wherigo_completion.cpp b/main/src/lua/wherigo_completion.cpp new file mode 100644 index 00000000..02a540eb --- /dev/null +++ b/main/src/lua/wherigo_completion.cpp @@ -0,0 +1,281 @@ +#include "lua/wherigo_completion.h" + +extern "C" { +#include +#include +} + +#include +#include +#include +#include +#include + +namespace wherigo { + +std::string WherigoCompletion::escapeXML(const std::string& str) { + std::string result; + for (char c : str) { + switch (c) { + case '&': result += "&"; break; + case '<': result += "<"; break; + case '>': result += ">"; break; + case '"': result += """; break; + case '\'': result += "'"; break; + default: result += c; + } + } + return result; +} + +std::string WherigoCompletion::formatTime(time_t t) { + struct tm* timeinfo = localtime(&t); + char buffer[30]; + strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%S", timeinfo); + return std::string(buffer); +} + +std::string WherigoCompletion::generateCompletionCode(const CompletionData& data) { + // Generate a simple hash based on cartridge GUID + player + completion time + std::stringstream ss; + ss << data.cartridgeGUID << data.playerName << data.endTime; + std::string input = ss.str(); + + // Simple hash (for real use, use proper crypto hash like SHA256) + unsigned long hash = 5381; + for (char c : input) { + hash = ((hash << 5) + hash) + c; + } + + char hashStr[20]; + snprintf(hashStr, sizeof(hashStr), "%016lX", hash); + return std::string(hashStr); +} + +CompletionData WherigoCompletion::extractCompletionData(lua_State* L, const std::string& playerName) { + CompletionData data; + data.playerName = playerName; + data.startTime = time(nullptr); // Default to now + data.endTime = time(nullptr); + data.duration = 0; + + // Get cartridge info + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1)) { + std::string className = lua_tostring(L, -1); + + if (className == "ZCartridge") { + lua_pop(L, 1); // pop ClassName + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + data.cartridgeName = lua_tostring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "Id"); + if (lua_isstring(L, -1)) { + data.cartridgeGUID = lua_tostring(L, -1); + } + lua_pop(L, 1); + } + else if (className == "ZTask") { + lua_pop(L, 1); // pop ClassName + + CompletionData::TaskCompletion task; + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + task.name = lua_tostring(L, -1); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "Complete"); + task.completed = lua_toboolean(L, -1); + lua_pop(L, 1); + + task.completionTime = time(nullptr); + data.tasks.push_back(task); + } + else if (className == "Zone") { + lua_pop(L, 1); // pop ClassName + + lua_getfield(L, -1, "Active"); + bool active = lua_toboolean(L, -1); + lua_pop(L, 1); + + if (active) { + CompletionData::ZoneVisited zone; + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + zone.name = lua_tostring(L, -1); + } + lua_pop(L, 1); + + zone.visitTime = time(nullptr); + data.zones.push_back(zone); + } + } + else { + lua_pop(L, 1); // pop ClassName + } + } else { + lua_pop(L, 1); // pop ClassName + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G + + // Get items in player inventory + lua_getglobal(L, "Player"); + if (lua_istable(L, -1)) { + // Find all items where Container == Player + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "ZItem") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Container"); + if (lua_istable(L, -1)) { + lua_getglobal(L, "Player"); + bool inInventory = lua_rawequal(L, -1, -2); + lua_pop(L, 2); // pop Player and Container + + if (inInventory) { + CompletionData::ItemFound item; + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + item.name = lua_tostring(L, -1); + } + lua_pop(L, 1); + + item.foundTime = time(nullptr); + data.items.push_back(item); + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); // pop _G + } + lua_pop(L, 1); // pop Player + + // Generate completion code + data.completionCode = generateCompletionCode(data); + + return data; +} + +bool WherigoCompletion::writeGWLFile(const CompletionData& data, const std::string& filePath) { + std::ofstream file(filePath); + if (!file.is_open()) { + wxLogError("Failed to create completion log: %s", filePath); + return false; + } + + // Write XML header + file << "\n"; + file << "\n"; + + // Cartridge info + file << " \n"; + file << " " << escapeXML(data.cartridgeName) << "\n"; + file << " " << escapeXML(data.cartridgeGUID) << "\n"; + file << " \n"; + + // Player info + file << " \n"; + file << " " << escapeXML(data.playerName) << "\n"; + file << " \n"; + + // Session info + file << " \n"; + file << " " << formatTime(data.startTime) << "\n"; + file << " " << formatTime(data.endTime) << "\n"; + file << " " << data.duration << "\n"; + file << " \n"; + + // Completion status + bool allTasksComplete = std::all_of(data.tasks.begin(), data.tasks.end(), + [](const CompletionData::TaskCompletion& t) { return t.completed; }); + file << " " << (allTasksComplete ? "Complete" : "Incomplete") << "\n"; + + // Tasks + if (!data.tasks.empty()) { + file << " \n"; + for (const auto& task : data.tasks) { + file << " \n"; + file << " " << escapeXML(task.name) << "\n"; + file << " " << (task.completed ? "true" : "false") << "\n"; + if (task.completed) { + file << " " << formatTime(task.completionTime) << "\n"; + } + file << " \n"; + } + file << " \n"; + } + + // Items collected + if (!data.items.empty()) { + file << " \n"; + for (const auto& item : data.items) { + file << " \n"; + file << " " << escapeXML(item.name) << "\n"; + file << " " << formatTime(item.foundTime) << "\n"; + file << " \n"; + } + file << " \n"; + } + + // Zones visited + if (!data.zones.empty()) { + file << " \n"; + for (const auto& zone : data.zones) { + file << " \n"; + file << " " << escapeXML(zone.name) << "\n"; + file << " " << formatTime(zone.visitTime) << "\n"; + file << " \n"; + } + file << " \n"; + } + + // Completion code (signature/verification) + file << " " << data.completionCode << "\n"; + + file << "\n"; + file.close(); + + wxLogMessage("Completion log saved to: %s", filePath); + return true; +} + +bool WherigoCompletion::generateCompletionLog(lua_State* L, const std::string& filePath, + const std::string& playerName) { + if (!L) { + wxLogError("Invalid Lua state"); + return false; + } + + CompletionData data = extractCompletionData(L, playerName); + return writeGWLFile(data, filePath); +} + +} // namespace wherigo + diff --git a/main/src/lua/zobject.cpp b/main/src/lua/zobject.cpp new file mode 100644 index 00000000..1208f139 --- /dev/null +++ b/main/src/lua/zobject.cpp @@ -0,0 +1,64 @@ +#include "lua/zobject.h" +#include "lua/game_engine.h" + +extern "C" { +#include +} + +#include + +namespace wherigo { + +int zobject_MoveTo(lua_State *L) { + // Get source object name + lua_getfield(L, 1, "Name"); + const char *srcName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unknown)"; + lua_pop(L, 1); + + // Get target container name + const char *dstName = "(nil)"; + if (lua_istable(L, 2)) { + lua_getfield(L, 2, "Name"); + dstName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unknown)"; + lua_pop(L, 1); + + lua_pushvalue(L, 2); + lua_setfield(L, 1, "Container"); + } + + wxLogDebug("MoveTo: %s -> %s", srcName, dstName); + + // Notify game state change + GameEngine::getInstance().notifyStateChanged(); + + return 0; +} + +int zobject_Contains(lua_State *L) { + // self (container) at index 1, item to check at index 2 + + lua_getfield(L, 1, "Name"); + const char *containerName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unknown)"; + lua_pop(L, 1); + + lua_getfield(L, 2, "Name"); + const char *itemName = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unknown)"; + lua_pop(L, 1); + + // Check if item's Container is the same as self (container) + bool contains = false; + + lua_getfield(L, 2, "Container"); // Get item.Container + if (lua_istable(L, -1)) { + // Compare by reference - check if it's the same table as self + contains = lua_rawequal(L, 1, -1); + } + lua_pop(L, 1); // pop Container + + wxLogDebug("Contains: %s in %s? -> %s", itemName, containerName, contains ? "true" : "false"); + lua_pushboolean(L, contains ? 1 : 0); + return 1; +} + +} // namespace wherigo + diff --git a/main/src/lua/ztimer.cpp b/main/src/lua/ztimer.cpp new file mode 100644 index 00000000..ae02f62f --- /dev/null +++ b/main/src/lua/ztimer.cpp @@ -0,0 +1,62 @@ +#include "lua/ztimer.h" +#include "lua/game_engine.h" + +extern "C" { +#include +} + +#include + +namespace wherigo { + +int ztimer_Start(lua_State *L) { + lua_getfield(L, 1, "Name"); + const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; + lua_pop(L, 1); + + lua_getfield(L, 1, "Duration"); + lua_Number duration = lua_isnumber(L, -1) ? lua_tonumber(L, -1) : 0; + lua_pop(L, 1); + + wxLogDebug("Timer Start: %s (Duration: %.1f sec)", name, duration); + + lua_pushboolean(L, 1); + lua_setfield(L, 1, "Running"); + + GameEngine::getInstance().notifyStateChanged(); + + return 0; +} + +int ztimer_Stop(lua_State *L) { + lua_getfield(L, 1, "Name"); + const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; + lua_pop(L, 1); + + wxLogDebug("Timer Stop: %s", name); + + lua_pushboolean(L, 0); + lua_setfield(L, 1, "Running"); + + GameEngine::getInstance().notifyStateChanged(); + + return 0; +} + +int ztimer_Reset(lua_State *L) { + lua_getfield(L, 1, "Name"); + const char *name = lua_isstring(L, -1) ? lua_tostring(L, -1) : "(unnamed)"; + lua_pop(L, 1); + + wxLogDebug("Timer Reset: %s", name); + + lua_pushboolean(L, 0); + lua_setfield(L, 1, "Running"); + + GameEngine::getInstance().notifyStateChanged(); + + return 0; +} + +} // namespace wherigo + diff --git a/main/src/main.cpp b/main/src/main.cpp index b00b67d1..69d3832e 100644 --- a/main/src/main.cpp +++ b/main/src/main.cpp @@ -1,4 +1,4 @@ -#include "cApp.h" +#include "app.h" #include wxIMPLEMENT_APP(cApp); diff --git a/main/src/ui/cFrame.cpp b/main/src/ui/cFrame.cpp deleted file mode 100644 index c1f09b8d..00000000 --- a/main/src/ui/cFrame.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "ui/cFrame.h" - -enum { ID_Hello = 1 }; - -cFrame::cFrame() : wxMDIParentFrame(nullptr, wxID_ANY, "Hello World") { - auto *menuFile = new wxMenu; - menuFile->Append(ID_Hello, "&Hello...\tCtrl-H", - "Help string shown in status bar for this menu item"); - menuFile->AppendSeparator(); - menuFile->Append(wxID_EXIT); - - auto *menuHelp = new wxMenu; - menuHelp->Append(wxID_ABOUT); - - auto *menuBar = new wxMenuBar; - menuBar->Append(menuFile, "&File"); - menuBar->Append(menuHelp, "&Help"); - - SetMenuBar(menuBar); - - CreateStatusBar(); - SetStatusText("Welcome to wxWidgets!"); - - Bind(wxEVT_MENU, &cFrame::OnHello, this, ID_Hello); - Bind(wxEVT_MENU, &cFrame::OnAbout, this, wxID_ABOUT); - Bind(wxEVT_MENU, &cFrame::OnExit, this, wxID_EXIT); -} - -void cFrame::OnExit(wxCommandEvent &event) { Close(true); } - -void cFrame::OnAbout(wxCommandEvent &event) { - wxMessageBox("This is a wxWidgets Hello World example", "About Hello World", - wxOK | wxICON_INFORMATION); -} - -void cFrame::OnHello(wxCommandEvent &event) { - wxLogMessage("Hello world from wxWidgets!"); -} diff --git a/main/src/ui/game_screen.cpp b/main/src/ui/game_screen.cpp new file mode 100644 index 00000000..6666498d --- /dev/null +++ b/main/src/ui/game_screen.cpp @@ -0,0 +1,939 @@ +#include "app.h" +#include "lua/game_engine.h" +#include "ui/game_screen.h" +#include "ui/wherigo_dialog.h" + +extern "C" { +#include +} + +wxDECLARE_APP(cApp); + +enum { + ID_SaveGame = 2000, + ID_LoadGame = 2001, + ID_ExportCompletion = 2002 +}; + +cGameScreen::cGameScreen(wxWindow *parent) + : wxFrame(parent, wxID_ANY, "Wherigo Player", wxDefaultPosition, wxSize(800, 600)) { + + // Menu + auto *menuFile = new wxMenu; + menuFile->Append(ID_SaveGame, "Spielstand speichern\tCtrl+S"); + menuFile->Append(ID_LoadGame, "Spielstand laden\tCtrl+L"); + menuFile->AppendSeparator(); + menuFile->Append(ID_ExportCompletion, "Completion Log exportieren\tCtrl+E"); + menuFile->AppendSeparator(); + menuFile->Append(wxID_EXIT, "Spiel beenden"); + + auto *menuHelp = new wxMenu; + menuHelp->Append(wxID_ABOUT); + + auto *menuBar = new wxMenuBar; + menuBar->Append(menuFile, "&Datei"); + menuBar->Append(menuHelp, "&Hilfe"); + SetMenuBar(menuBar); + + // Main sizer + auto *mainSizer = new wxBoxSizer(wxVERTICAL); + + // Notebook with tabs + m_notebook = new wxNotebook(this, wxID_ANY); + + // Locations/Zones tab + auto *zonePanel = new wxPanel(m_notebook); + auto *zoneSizer = new wxBoxSizer(wxVERTICAL); + zoneSizer->Add(new wxStaticText(zonePanel, wxID_ANY, "Orte in der Nähe:"), 0, wxALL, 5); + m_zoneList = new wxListBox(zonePanel, wxID_ANY); + zoneSizer->Add(m_zoneList, 1, wxALL | wxEXPAND, 5); + zonePanel->SetSizer(zoneSizer); + m_notebook->AddPage(zonePanel, "Orte"); + + // Tasks tab + auto *taskPanel = new wxPanel(m_notebook); + auto *taskSizer = new wxBoxSizer(wxVERTICAL); + taskSizer->Add(new wxStaticText(taskPanel, wxID_ANY, "Aktive Aufgaben:"), 0, wxALL, 5); + m_taskList = new wxListBox(taskPanel, wxID_ANY); + taskSizer->Add(m_taskList, 1, wxALL | wxEXPAND, 5); + taskPanel->SetSizer(taskSizer); + m_notebook->AddPage(taskPanel, "Aufgaben"); + + // Inventory tab + auto *inventoryPanel = new wxPanel(m_notebook); + auto *inventorySizer = new wxBoxSizer(wxVERTICAL); + inventorySizer->Add(new wxStaticText(inventoryPanel, wxID_ANY, "Dein Inventar:"), 0, wxALL, 5); + m_inventoryList = new wxListBox(inventoryPanel, wxID_ANY); + inventorySizer->Add(m_inventoryList, 1, wxALL | wxEXPAND, 5); + inventoryPanel->SetSizer(inventorySizer); + m_notebook->AddPage(inventoryPanel, "Inventar"); + + // Characters tab + auto *characterPanel = new wxPanel(m_notebook); + auto *characterSizer = new wxBoxSizer(wxVERTICAL); + characterSizer->Add(new wxStaticText(characterPanel, wxID_ANY, "Personen in der Nähe:"), 0, wxALL, 5); + m_characterList = new wxListBox(characterPanel, wxID_ANY); + characterSizer->Add(m_characterList, 1, wxALL | wxEXPAND, 5); + characterPanel->SetSizer(characterSizer); + m_notebook->AddPage(characterPanel, "Personen"); + + // Items tab + auto *itemPanel = new wxPanel(m_notebook); + auto *itemSizer = new wxBoxSizer(wxVERTICAL); + itemSizer->Add(new wxStaticText(itemPanel, wxID_ANY, "Gegenstände in der Nähe:"), 0, wxALL, 5); + m_itemList = new wxListBox(itemPanel, wxID_ANY); + itemSizer->Add(m_itemList, 1, wxALL | wxEXPAND, 5); + itemPanel->SetSizer(itemSizer); + m_notebook->AddPage(itemPanel, "Gegenstände"); + + mainSizer->Add(m_notebook, 1, wxALL | wxEXPAND, 5); + + SetSizer(mainSizer); + + CentreOnScreen(); + + // Status bar + CreateStatusBar(); + SetStatusText("Spiel läuft"); + + // Event bindings + Bind(wxEVT_CLOSE_WINDOW, &cGameScreen::OnClose, this); + Bind(wxEVT_MENU, &cGameScreen::OnAbout, this, wxID_ABOUT); + Bind(wxEVT_MENU, &cGameScreen::OnExit, this, wxID_EXIT); + Bind(wxEVT_MENU, &cGameScreen::OnSaveGame, this, ID_SaveGame); + Bind(wxEVT_MENU, &cGameScreen::OnLoadGame, this, ID_LoadGame); + Bind(wxEVT_MENU, &cGameScreen::OnExportCompletion, this, ID_ExportCompletion); + + m_zoneList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnZoneSelected, this); + m_taskList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnTaskSelected, this); + m_inventoryList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnInventorySelected, this); + m_characterList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnCharacterSelected, this); + m_itemList->Bind(wxEVT_LISTBOX_DCLICK, &cGameScreen::OnItemSelected, this); + + // Listen to game state changes from GameEngine + wherigo::GameEngine::getInstance().Bind(wherigo::EVT_GAME_STATE_CHANGED, + &cGameScreen::OnGameStateChanged, this); + + // Initial UI refresh + CallAfter([this]() { refreshUI(); }); +} + +void cGameScreen::OnClose(wxCloseEvent &event) { + // Unbind from game engine events + wherigo::GameEngine::getInstance().Unbind(wherigo::EVT_GAME_STATE_CHANGED, + &cGameScreen::OnGameStateChanged, this); + + // Stop the game engine but don't unload cartridge + wherigo::GameEngine::getInstance().stop(); + + // Hide instead of destroy, parent will handle visibility + Hide(); + + // Notify parent that game was closed + if (GetParent()) { + GetParent()->Show(); + } +} + +void cGameScreen::OnExit(wxCommandEvent &event) { + Close(); +} + +void cGameScreen::OnAbout(wxCommandEvent &event) { + wxMessageBox("Wherigo Player\nEin Wherigo-Cartridge-Player für Desktop", + "Über Wherigo Player", wxOK | wxICON_INFORMATION); +} + +void cGameScreen::OnSaveGame(wxCommandEvent& event) { + if (wxGetApp().saveGameState("")) { + wxMessageBox("Spielstand wurde gespeichert!", "Speichern", wxOK | wxICON_INFORMATION); + } else { + wxMessageBox("Fehler beim Speichern des Spielstands!", "Fehler", wxOK | wxICON_ERROR); + } +} + +void cGameScreen::OnLoadGame(wxCommandEvent& event) { + int result = wxMessageBox("Möchten Sie den gespeicherten Spielstand laden?\n\nAchtung: Der aktuelle Fortschritt geht verloren!", + "Laden", wxYES_NO | wxICON_QUESTION); + + if (result == wxYES) { + if (wxGetApp().loadGameState("")) { + wxMessageBox("Spielstand wurde geladen!", "Laden", wxOK | wxICON_INFORMATION); + refreshUI(); + } else { + wxMessageBox("Kein gespeicherter Spielstand gefunden oder Fehler beim Laden!", "Fehler", wxOK | wxICON_ERROR); + } + } +} + +void cGameScreen::OnExportCompletion(wxCommandEvent& event) { + wxFileDialog saveDialog(this, "Completion Log speichern", "", "", + "Wherigo Game Log (*.gwl)|*.gwl", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + + std::string defaultPath = wxGetApp().getCompletionLogPath(); + if (!defaultPath.empty()) { + saveDialog.SetPath(defaultPath); + } + + if (saveDialog.ShowModal() == wxID_CANCEL) { + return; + } + + wxString filePath = saveDialog.GetPath(); + + if (wxGetApp().generateCompletionLog(filePath.ToStdString())) { + wxMessageBox("Completion Log wurde erfolgreich exportiert!\n\nDiese Datei kann auf wherigo.com hochgeladen werden, um den Abschluss nachzuweisen.", + "Export erfolgreich", wxOK | wxICON_INFORMATION); + } else { + wxMessageBox("Fehler beim Exportieren des Completion Logs!", "Fehler", wxOK | wxICON_ERROR); + } +} + +void cGameScreen::OnGameStateChanged(wxEvent& event) { + // Game state has changed, refresh UI + refreshUI(); +} + +void cGameScreen::refreshUI() { + // Freeze to prevent flickering and focus loss + Freeze(); + + populateZones(); + populateTasks(); + populateInventory(); + populateCharacters(); + populateItems(); + + Thaw(); +} + +void cGameScreen::populateZones() { + int selection = m_zoneList->GetSelection(); + wxString selectedItem = (selection != wxNOT_FOUND) ? m_zoneList->GetString(selection) : ""; + + m_zoneList->Clear(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "Zone") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Active"); + bool active = lua_toboolean(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "Visible"); + bool visible = lua_toboolean(L, -1); + lua_pop(L, 1); + + if (active && visible) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + m_zoneList->Append(wxString::FromUTF8(lua_tostring(L, -1))); + } + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Restore selection + if (!selectedItem.IsEmpty()) { + int idx = m_zoneList->FindString(selectedItem); + if (idx != wxNOT_FOUND) { + m_zoneList->SetSelection(idx); + } + } +} + +void cGameScreen::populateTasks() { + int selection = m_taskList->GetSelection(); + wxString selectedItem = (selection != wxNOT_FOUND) ? m_taskList->GetString(selection) : ""; + + m_taskList->Clear(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "ZTask") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Active"); + bool active = lua_toboolean(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "Complete"); + bool complete = lua_toboolean(L, -1); + lua_pop(L, 1); + + if (active && !complete) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + m_taskList->Append(wxString::FromUTF8(lua_tostring(L, -1))); + } + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Restore selection + if (!selectedItem.IsEmpty()) { + int idx = m_taskList->FindString(selectedItem); + if (idx != wxNOT_FOUND) { + m_taskList->SetSelection(idx); + } + } +} + +void cGameScreen::populateInventory() { + int selection = m_inventoryList->GetSelection(); + wxString selectedItem = (selection != wxNOT_FOUND) ? m_inventoryList->GetString(selection) : ""; + + m_inventoryList->Clear(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Get Player + lua_getglobal(L, "Player"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return; + } + + // Find all items where Container == Player + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "ZItem") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Container"); + if (lua_istable(L, -1)) { + // Check if Container is Player + lua_getglobal(L, "Player"); + bool isInInventory = lua_rawequal(L, -1, -2); + lua_pop(L, 2); // pop Player and Container + + if (isInInventory) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + m_inventoryList->Append(wxString::FromUTF8(lua_tostring(L, -1))); + } + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 2); // pop _G and Player + + // Restore selection + if (!selectedItem.IsEmpty()) { + int idx = m_inventoryList->FindString(selectedItem); + if (idx != wxNOT_FOUND) { + m_inventoryList->SetSelection(idx); + } + } +} + +void cGameScreen::populateCharacters() { + int selection = m_characterList->GetSelection(); + wxString selectedItem = (selection != wxNOT_FOUND) ? m_characterList->GetString(selection) : ""; + + m_characterList->Clear(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "ZCharacter") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Active"); + bool active = lua_toboolean(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "Visible"); + bool visible = lua_toboolean(L, -1); + lua_pop(L, 1); + + // Check if character has a container (is somewhere) + lua_getfield(L, -1, "Container"); + bool hasLocation = lua_istable(L, -1); + lua_pop(L, 1); + + if (active && visible && hasLocation) { + lua_getfield(L, -1, "Name"); + wxString name; + if (lua_isstring(L, -1)) { + name = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 1); + + // Get location name + lua_getfield(L, -1, "Container"); + lua_getfield(L, -1, "Name"); + wxString location; + if (lua_isstring(L, -1)) { + location = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 2); + + if (!name.IsEmpty()) { + m_characterList->Append(wxString::Format("%s (%s)", name, location)); + } + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Restore selection + if (!selectedItem.IsEmpty()) { + int idx = m_characterList->FindString(selectedItem); + if (idx != wxNOT_FOUND) { + m_characterList->SetSelection(idx); + } + } +} + +void cGameScreen::populateItems() { + int selection = m_itemList->GetSelection(); + wxString selectedItem = (selection != wxNOT_FOUND) ? m_itemList->GetString(selection) : ""; + + m_itemList->Clear(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "ZItem") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Active"); + bool active = lua_toboolean(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "Visible"); + bool visible = lua_toboolean(L, -1); + lua_pop(L, 1); + + // Check if item is NOT in player inventory (in a zone) + lua_getfield(L, -1, "Container"); + bool inZone = false; + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1)) { + const char *containerClass = lua_tostring(L, -1); + inZone = (strcmp(containerClass, "Zone") == 0); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + if (active && visible && inZone) { + lua_getfield(L, -1, "Name"); + wxString name; + if (lua_isstring(L, -1)) { + name = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 1); + + // Get location name + lua_getfield(L, -1, "Container"); + lua_getfield(L, -1, "Name"); + wxString location; + if (lua_isstring(L, -1)) { + location = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 2); + + if (!name.IsEmpty()) { + m_itemList->Append(wxString::Format("%s (%s)", name, location)); + } + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Restore selection + if (!selectedItem.IsEmpty()) { + int idx = m_itemList->FindString(selectedItem); + if (idx != wxNOT_FOUND) { + m_itemList->SetSelection(idx); + } + } +} + +void cGameScreen::OnZoneSelected(wxCommandEvent& event) { + int sel = m_zoneList->GetSelection(); + if (sel == wxNOT_FOUND) return; + + wxString zoneName = m_zoneList->GetString(sel); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Find the zone object by name + lua_getglobal(L, "_G"); + lua_pushnil(L); + + bool found = false; + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "Zone") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); + lua_pop(L, 1); + + if (currentName == zoneName) { + found = true; + + // Get zone description + lua_getfield(L, -1, "Description"); + wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : ""; + lua_pop(L, 1); + + // Get media if exists + wxString mediaName; + lua_getfield(L, -1, "Media"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + mediaName = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Show zone info with media + if (!description.IsEmpty() || !mediaName.IsEmpty()) { + std::vector buttons = {"OK"}; + wherigo::WherigoMessageDialog dlg(this, description, zoneName, buttons, mediaName); + dlg.ShowModal(); + } + + // Call OnEnter callback if it exists + lua_getfield(L, -1, "OnEnter"); + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, -2); // push zone as self argument + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("Zone OnEnter error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + + lua_pop(L, 1); // pop zone table + lua_pop(L, 1); // pop key + break; + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G + + if (!found) { + wxMessageBox(wxString::Format("Zone '%s' nicht gefunden", zoneName), + "Fehler", wxOK | wxICON_ERROR); + } +} + +void cGameScreen::OnTaskSelected(wxCommandEvent& event) { + int sel = m_taskList->GetSelection(); + if (sel == wxNOT_FOUND) return; + + wxString taskName = m_taskList->GetString(sel); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Find the task object by name + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "ZTask") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); + lua_pop(L, 1); + + if (currentName == taskName) { + // Get task description + lua_getfield(L, -1, "Description"); + wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : "Keine Beschreibung verfügbar"; + lua_pop(L, 1); + + // Get task complete status + lua_getfield(L, -1, "Complete"); + bool complete = lua_toboolean(L, -1); + lua_pop(L, 1); + + wxString status = complete ? "✓ Abgeschlossen" : "○ Offen"; + wxString message = wxString::Format("%s\n\n%s", status, description); + + wxMessageBox(message, taskName, wxOK | wxICON_INFORMATION); + + lua_pop(L, 1); // pop task table + lua_pop(L, 1); // pop key + break; + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G +} + +void cGameScreen::OnInventorySelected(wxCommandEvent& event) { + int sel = m_inventoryList->GetSelection(); + if (sel == wxNOT_FOUND) return; + + wxString itemName = m_inventoryList->GetString(sel); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Find the item object by name + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "ZItem") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); + lua_pop(L, 1); + + if (currentName == itemName) { + // Get item description + lua_getfield(L, -1, "Description"); + wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : ("Keine Beschreibung verfügbar"); + lua_pop(L, 1); + + // Get media if exists + wxString mediaName; + lua_getfield(L, -1, "Media"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + mediaName = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Check for commands + lua_getfield(L, -1, "Commands"); + wxArrayString commands; + if (lua_istable(L, -1)) { + int n = lua_objlen(L, -1); + for (int i = 1; i <= n; i++) { + lua_rawgeti(L, -1, i); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Text"); + if (lua_isstring(L, -1)) { + commands.Add(wxString::FromUTF8(lua_tostring(L, -1))); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + wxString message = description; + if (!commands.IsEmpty()) { + message += ("\n\nVerfügbare Aktionen:\n"); + for (const auto& cmd : commands) { + message += "• " + cmd + "\n"; + } + } + + // Use WherigoMessageDialog to show with media support + std::vector buttons = {"OK"}; + wherigo::WherigoMessageDialog dlg(this, message, itemName, buttons, mediaName); + dlg.ShowModal(); + + // Call OnClick if exists + lua_getfield(L, -1, "OnClick"); + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, -2); // push item as self + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("Item OnClick error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + + lua_pop(L, 1); // pop item table + lua_pop(L, 1); // pop key + break; + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G +} + +void cGameScreen::OnCharacterSelected(wxCommandEvent& event) { + int sel = m_characterList->GetSelection(); + if (sel == wxNOT_FOUND) return; + + // Extract name from "Name (Location)" format + wxString fullText = m_characterList->GetString(sel); + wxString charName = fullText.BeforeFirst('(').Trim(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Find the character object by name + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "ZCharacter") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); + lua_pop(L, 1); + + if (currentName == charName) { + // Get character description + lua_getfield(L, -1, "Description"); + wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : ("Keine Beschreibung verfügbar"); + lua_pop(L, 1); + + // Get media if exists + wxString mediaName; + lua_getfield(L, -1, "Media"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + mediaName = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Show with media support + std::vector buttons = {"OK"}; + wherigo::WherigoMessageDialog dlg(this, description, charName, buttons, mediaName); + dlg.ShowModal(); + + // Call OnClick if exists + lua_getfield(L, -1, "OnClick"); + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, -2); // push character as self + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("Character OnClick error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + + lua_pop(L, 1); // pop character table + lua_pop(L, 1); // pop key + break; + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G +} + +void cGameScreen::OnItemSelected(wxCommandEvent& event) { + int sel = m_itemList->GetSelection(); + if (sel == wxNOT_FOUND) return; + + // Extract name from "Name (Location)" format + wxString fullText = m_itemList->GetString(sel); + wxString itemName = fullText.BeforeFirst('(').Trim(); + + lua_State *L = wxGetApp().getLuaState(); + if (!L) return; + + // Find the item object by name + lua_getglobal(L, "_G"); + lua_pushnil(L); + + while (lua_next(L, -2) != 0) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "ClassName"); + if (lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "ZItem") == 0) { + lua_pop(L, 1); + + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + wxString currentName = wxString::FromUTF8(lua_tostring(L, -1)); + lua_pop(L, 1); + + if (currentName == itemName) { + // Get item description + lua_getfield(L, -1, "Description"); + wxString description = lua_isstring(L, -1) ? wxString::FromUTF8(lua_tostring(L, -1)) : ("Keine Beschreibung verfügbar"); + lua_pop(L, 1); + + // Get media if exists + wxString mediaName; + lua_getfield(L, -1, "Media"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Name"); + if (lua_isstring(L, -1)) { + mediaName = wxString::FromUTF8(lua_tostring(L, -1)); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + // Check for commands + lua_getfield(L, -1, "Commands"); + wxArrayString commands; + if (lua_istable(L, -1)) { + int n = lua_objlen(L, -1); + for (int i = 1; i <= n; i++) { + lua_rawgeti(L, -1, i); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "Text"); + if (lua_isstring(L, -1)) { + commands.Add(wxString::FromUTF8(lua_tostring(L, -1))); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + wxString message = description; + if (!commands.IsEmpty()) { + message += ("\n\nVerfügbare Aktionen:\n"); + for (const auto& cmd : commands) { + message += "• " + cmd + "\n"; + } + } + + // Use WherigoMessageDialog to show with media support + std::vector buttons = {"OK"}; + wherigo::WherigoMessageDialog dlg(this, message, itemName, buttons, mediaName); + dlg.ShowModal(); + + // Call OnClick if exists + lua_getfield(L, -1, "OnClick"); + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, -2); // push item as self + if (lua_pcall(L, 1, 0, 0) != 0) { + wxLogError("Item OnClick error: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + + lua_pop(L, 1); // pop item table + lua_pop(L, 1); // pop key + break; + } + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop value + } + lua_pop(L, 1); // pop _G +} diff --git a/main/src/ui/start_screen.cpp b/main/src/ui/start_screen.cpp new file mode 100644 index 00000000..07fdb750 --- /dev/null +++ b/main/src/ui/start_screen.cpp @@ -0,0 +1,202 @@ +#include "ui/start_screen.h" +#include "app.h" +#include "lua/game_engine.h" +#include "ui/game_screen.h" + +#include +#include +#include + +wxDECLARE_APP(cApp); + +enum { + ID_OpenCartridge = 1001, + ID_StartGame = 1002 +}; + +cStartScreen::cStartScreen() + : wxFrame(nullptr, wxID_ANY, "Wherigo Player", wxDefaultPosition, wxSize(800, 600)), + m_gameFrame(nullptr), + m_cartridgeLoaded(false) { + + // Menu + auto *menuFile = new wxMenu; + menuFile->Append(ID_OpenCartridge, "Cartridge öffnen...\tCtrl+O"); + menuFile->AppendSeparator(); + menuFile->Append(wxID_EXIT, "Beenden"); + + auto *menuHelp = new wxMenu; + menuHelp->Append(wxID_ABOUT, "Über..."); + + auto *menuBar = new wxMenuBar; + menuBar->Append(menuFile, "&Datei"); + menuBar->Append(menuHelp, "&Hilfe"); + SetMenuBar(menuBar); + + // Main sizer + auto *mainSizer = new wxBoxSizer(wxVERTICAL); + + // Title + auto *title = new wxStaticText(this, wxID_ANY, "Wherigo Player"); + title->SetFont(title->GetFont().Bold().Scaled(2.0)); + mainSizer->Add(title, 0, wxALL | wxALIGN_CENTER, 20); + + // Open button + m_openButton = new wxButton(this, ID_OpenCartridge, "Cartridge öffnen..."); + m_openButton->SetMinSize(wxSize(200, 40)); + mainSizer->Add(m_openButton, 0, wxALL | wxALIGN_CENTER, 10); + + // Info panel (initially hidden) + m_infoPanel = new wxPanel(this); + auto *infoSizer = new wxBoxSizer(wxVERTICAL); + + // Splash image + m_splashImage = new wxStaticBitmap(m_infoPanel, wxID_ANY, wxNullBitmap); + infoSizer->Add(m_splashImage, 0, wxALL | wxALIGN_CENTER, 10); + + // Cartridge name + m_cartridgeName = new wxStaticText(m_infoPanel, wxID_ANY, ""); + m_cartridgeName->SetFont(m_cartridgeName->GetFont().Bold().Scaled(1.5)); + infoSizer->Add(m_cartridgeName, 0, wxALL | wxALIGN_CENTER, 5); + + // Author + m_cartridgeAuthor = new wxStaticText(m_infoPanel, wxID_ANY, ""); + infoSizer->Add(m_cartridgeAuthor, 0, wxALL | wxALIGN_CENTER, 5); + + // Description + m_cartridgeDesc = new wxHtmlWindow(m_infoPanel, wxID_ANY, wxDefaultPosition, wxSize(500, 200)); + m_cartridgeDesc->SetMinSize(wxSize(500, 200)); + infoSizer->Add(m_cartridgeDesc, 1, wxALL | wxEXPAND, 10); + + // Start button + m_startButton = new wxButton(m_infoPanel, ID_StartGame, "Spiel starten"); + m_startButton->SetMinSize(wxSize(200, 50)); + m_startButton->SetFont(m_startButton->GetFont().Bold()); + infoSizer->Add(m_startButton, 0, wxALL | wxALIGN_CENTER, 20); + + m_infoPanel->SetSizer(infoSizer); + m_infoPanel->Hide(); + + mainSizer->Add(m_infoPanel, 1, wxALL | wxEXPAND, 10); + + SetSizer(mainSizer); + + // Status bar + CreateStatusBar(); + SetStatusText("Bitte eine Cartridge öffnen"); + + // Event bindings + Bind(wxEVT_MENU, &cStartScreen::OnOpenCartridge, this, ID_OpenCartridge); + Bind(wxEVT_MENU, &cStartScreen::OnExit, this, wxID_EXIT); + Bind(wxEVT_MENU, &cStartScreen::OnAbout, this, wxID_ABOUT); + Bind(wxEVT_BUTTON, &cStartScreen::OnOpenCartridge, this, ID_OpenCartridge); + Bind(wxEVT_BUTTON, &cStartScreen::OnStartGame, this, ID_StartGame); + + CentreOnScreen(); +} + +void cStartScreen::OnOpenCartridge(wxCommandEvent& event) { + wxFileDialog openFileDialog(this, "Cartridge öffnen", "", "", + "Wherigo Cartridge (*.gwc)|*.gwc", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (openFileDialog.ShowModal() == wxID_CANCEL) { + return; + } + + wxString filePath = openFileDialog.GetPath(); + SetStatusText("Lade Cartridge: " + filePath); + + // Load the cartridge via cApp + if (wxGetApp().loadCartridge(filePath.ToStdString())) { + m_cartridgeLoaded = true; + showCartridgeInfo(); + SetStatusText("Cartridge geladen - bereit zum Starten"); + } else { + wxMessageBox("Fehler beim Laden der Cartridge", "Fehler", wxOK | wxICON_ERROR); + SetStatusText("Fehler beim Laden"); + } +} + +void cStartScreen::showCartridgeInfo() { + auto *cartridge = wxGetApp().getCartridge(); + if (!cartridge) return; + + // Set cartridge info + m_cartridgeName->SetLabel(wxString::FromUTF8(cartridge->cartridgeName())); + m_cartridgeAuthor->SetLabel("von " + wxString::FromUTF8(cartridge->author())); + const auto desc = wxString::FromUTF8(cartridge->cartridgeDesc()); + if (desc.Find("<") == wxNOT_FOUND) { + wxString plain(desc); + plain.Replace("\n", "
"); + m_cartridgeDesc->SetPage("

" + plain + "

"); + } else { + m_cartridgeDesc->SetPage(desc); + } + + // Load splash screen + auto splash = cartridge->splashScreen(); + if (splash) { + auto data = splash->getData(); + if (!data.empty()) { + wxMemoryInputStream stream(data.data(), data.size()); + wxImage image(stream); + if (image.IsOk()) { + // Scale if too large + int maxWidth = 300; + int maxHeight = 200; + if (image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) { + double scaleX = (double)maxWidth / image.GetWidth(); + double scaleY = (double)maxHeight / image.GetHeight(); + double scale = std::min(scaleX, scaleY); + image.Rescale(image.GetWidth() * scale, image.GetHeight() * scale, wxIMAGE_QUALITY_HIGH); + } + m_splashImage->SetBitmap(wxBitmap(image)); + } + } + } + + // Show info panel + m_infoPanel->Show(); + m_openButton->SetLabel("Andere Cartridge öffnen..."); + Layout(); +} + +void cStartScreen::OnStartGame(wxCommandEvent& event) { + if (!m_cartridgeLoaded) { + wxMessageBox("Bitte zuerst eine Cartridge laden", "Hinweis", wxOK | wxICON_INFORMATION); + return; + } + + // Create game frame if not exists + if (!m_gameFrame) { + m_gameFrame = new cGameScreen(this); + } + + // Hide start frame, show game + Hide(); + m_gameFrame->Show(); + + // Start the game (call OnStart and start engine) + wxGetApp().startGame(); +} + +void cStartScreen::onGameClosed() { + // Called when game frame is closed + Show(); + SetStatusText("Spiel pausiert - Cartridge noch geladen"); +} + +void cStartScreen::OnExit(wxCommandEvent& event) { + // Clean up game frame + if (m_gameFrame) { + m_gameFrame->Destroy(); + m_gameFrame = nullptr; + } + Close(true); +} + +void cStartScreen::OnAbout(wxCommandEvent& event) { + wxMessageBox("Wherigo Player\n\nEin Desktop-Player für Wherigo-Cartridges\n\nVersion 0.1", + "Über Wherigo Player", wxOK | wxICON_INFORMATION); +} diff --git a/main/src/ui/wherigo_dialog.cpp b/main/src/ui/wherigo_dialog.cpp new file mode 100644 index 00000000..d8f3b7da --- /dev/null +++ b/main/src/ui/wherigo_dialog.cpp @@ -0,0 +1,161 @@ +#include "ui/wherigo_dialog.h" +#include "lua/media_manager.h" +#include "lua/game_engine.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace wherigo { + +WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &text, + const wxString &title, + const std::vector &buttons, + const wxString &mediaName) + : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + // Media/Image (if provided) + if (!mediaName.IsEmpty()) { + auto mediaData = MediaManager::getInstance().getMediaByName(mediaName.ToStdString()); + + if (!mediaData.empty()) { + wxMemoryInputStream stream(mediaData.data(), mediaData.size()); + wxImage image(stream); + + if (image.IsOk()) { + // Get screen DPI for scaling + wxWindow* topWindow = wxTheApp->GetTopWindow(); + double contentScaleFactor = 1.0; + if (topWindow) { + contentScaleFactor = topWindow->GetContentScaleFactor(); + } + + // Scale image - doubled size from original (800x600 instead of 400x300) + // Adjust for DPI/Retina displays + int maxWidth = static_cast(800 * contentScaleFactor); + int maxHeight = static_cast(600 * contentScaleFactor); + + if (image.GetWidth() > maxWidth || image.GetHeight() > maxHeight) { + double scaleX = (double)maxWidth / image.GetWidth(); + double scaleY = (double)maxHeight / image.GetHeight(); + double scale = std::min(scaleX, scaleY); + image.Rescale(image.GetWidth() * scale, image.GetHeight() * scale, wxIMAGE_QUALITY_HIGH); + } + + wxBitmap bitmap(image); + auto *imageCtrl = new wxStaticBitmap(this, wxID_ANY, bitmap); + sizer->Add(imageCtrl, 0, wxALL | wxALIGN_CENTER, 10); + } else { + wxLogDebug("Failed to load image: %s", mediaName); + auto *mediaLabel = new wxStaticText(this, wxID_ANY, + wxString::Format("[Media: %s]", mediaName)); + mediaLabel->SetForegroundColour(*wxLIGHT_GREY); + sizer->Add(mediaLabel, 0, wxALL | wxALIGN_CENTER, 10); + } + } else { + wxLogDebug("No media data for: %s", mediaName); + auto *mediaLabel = new wxStaticText(this, wxID_ANY, + wxString::Format("[Media: %s]", mediaName)); + mediaLabel->SetForegroundColour(*wxLIGHT_GREY); + sizer->Add(mediaLabel, 0, wxALL | wxALIGN_CENTER, 10); + } + } + + // Text (only show if not just a placeholder like ".") + if (!text.IsEmpty() && text != ".") { + auto *textCtrl = new wxStaticText(this, wxID_ANY, text); + textCtrl->Wrap(800); // Also doubled wrap width + sizer->Add(textCtrl, 1, wxALL | wxEXPAND, 15); + } else if (mediaName.IsEmpty()) { + // No media and empty/placeholder text - show something + auto *textCtrl = new wxStaticText(this, wxID_ANY, text); + textCtrl->Wrap(800); // Also doubled wrap width + sizer->Add(textCtrl, 1, wxALL | wxEXPAND, 15); + } + + // Buttons + auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL); + + if (buttons.empty()) { + auto *okButton = new wxButton(this, wxID_OK, "OK"); + buttonSizer->Add(okButton, 0, wxALL, 5); + okButton->Bind(wxEVT_BUTTON, &WherigoMessageDialog::onButton, this); + } else { + for (size_t i = 0; i < buttons.size(); i++) { + auto *btn = new wxButton(this, 1000 + i, buttons[i]); + buttonSizer->Add(btn, 0, wxALL, 5); + btn->Bind(wxEVT_BUTTON, &WherigoMessageDialog::onButton, this); + } + } + + sizer->Add(buttonSizer, 0, wxALIGN_CENTER | wxBOTTOM, 10); + + SetSizerAndFit(sizer); + SetMinSize(wxSize(600, 300)); // Doubled from 300x150 + CenterOnParent(); +} + +void WherigoMessageDialog::onButton(wxCommandEvent &event) { + int id = event.GetId(); + if (id == wxID_OK) { + m_selectedButton = 0; + } else { + m_selectedButton = id - 1000; + } + EndModal(wxID_OK); +} + +WherigoDialogRunner& WherigoDialogRunner::getInstance() { + static WherigoDialogRunner instance; + return instance; +} + +void WherigoDialogRunner::showMessageBox(const wxString &text, const wxString &title, + std::function callback) { + WherigoMessageDialog dlg(nullptr, text, title); + dlg.ShowModal(); + + if (callback) { + callback(dlg.getSelectedButton()); + } +} + +void WherigoDialogRunner::showDialog(const std::vector &entries, + std::function callback) { + for (size_t i = 0; i < entries.size(); i++) { + const auto &entry = entries[i]; + + std::vector buttons; + for (const auto &btn : entry.buttons) { + buttons.push_back(wxString::FromUTF8(btn)); + } + + if (buttons.empty() && i < entries.size() - 1) { + buttons.push_back("Weiter"); + } else if (buttons.empty()) { + buttons.push_back("OK"); + } + + WherigoMessageDialog dlg(nullptr, wxString::FromUTF8(entry.text), "Wherigo", + buttons, wxString::FromUTF8(entry.mediaName)); + dlg.ShowModal(); + + // For the last entry, call the callback with the selected button + if (i == entries.size() - 1 && callback) { + callback(dlg.getSelectedButton()); + } + } + + // Notify game state change after dialog sequence completes + GameEngine::getInstance().notifyStateChanged(); +} + +} // namespace wherigo +