starting playing the wherigo

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
This commit is contained in:
2026-02-13 02:41:12 +01:00
parent 50267e47dc
commit f9c45ca81f
34 changed files with 4055 additions and 84 deletions

View File

@@ -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

164
LUA_PERSISTENCE.md Normal file
View File

@@ -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/<CartridgeName>.save`
- **Windows**: `%APPDATA%\wxWherigo\<CartridgeName>.save`
- **Linux**: `~/.wxWherigo/<CartridgeName>.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"] = <reference to Player>
},
-- ... 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<std::string> 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)

254
WHERIGO_COMPLETION.md Normal file
View File

@@ -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
<?xml version="1.0" encoding="UTF-8"?>
<WherigoGameLog xmlns="http://www.groundspeak.com/wherigo/1.0">
<Cartridge>
<Name>The Ombos Idol</Name>
<GUID>a1b2c3d4-e5f6-7890-abcd-ef1234567890</GUID>
</Cartridge>
<Player>
<Name>Player</Name>
</Player>
<Session>
<StartTime>2026-02-13T14:30:00</StartTime>
<EndTime>2026-02-13T16:45:00</EndTime>
<Duration>8100</Duration>
</Session>
<CompletionStatus>Complete</CompletionStatus>
<Tasks>
<Task>
<Name>Find the Professor</Name>
<Completed>true</Completed>
<CompletionTime>2026-02-13T15:20:00</CompletionTime>
</Task>
<Task>
<Name>Solve the Puzzle</Name>
<Completed>true</Completed>
<CompletionTime>2026-02-13T16:30:00</CompletionTime>
</Task>
</Tasks>
<ItemsCollected>
<Item>
<Name>Key</Name>
<FoundTime>2026-02-13T15:10:00</FoundTime>
</Item>
<Item>
<Name>Map</Name>
<FoundTime>2026-02-13T15:45:00</FoundTime>
</Item>
</ItemsCollected>
<ZonesVisited>
<Zone>
<Name>Town Square</Name>
<VisitTime>2026-02-13T14:35:00</VisitTime>
</Zone>
<Zone>
<Name>Old Library</Name>
<VisitTime>2026-02-13T15:15:00</VisitTime>
</Zone>
</ZonesVisited>
<CompletionCode>A1B2C3D4E5F67890</CompletionCode>
</WherigoGameLog>
```
## 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: `<GWC-Verzeichnis>/<CartridgeName>.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)

View File

@@ -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

View File

@@ -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 <string.h>
@@ -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;
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;
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; i<n; i++) {
f->code[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; i<n; i++) {
f->lineinfo[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");
}
/*

43
main/include/app.h Normal file
View File

@@ -0,0 +1,43 @@
#pragma once
#include <cartridge/cartridge.h>
#include <wx/wx.h>
#include <memory>
#include <string>
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<cartridge::Cartridge> m_cartridge;
std::string m_cartridgePath;
};

View File

@@ -1,9 +0,0 @@
#pragma once
#include <wx/wx.h>
class cApp : public wxApp
{
public:
bool OnInit() override;
};

View File

@@ -0,0 +1,73 @@
#ifndef GAME_ENGINE_H
#define GAME_ENGINE_H
#include <wx/wx.h>
#include <wx/timer.h>
#include <wx/event.h>
#include <vector>
#include <memory>
#include <functional>
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

View File

@@ -0,0 +1,43 @@
#ifndef MEDIA_MANAGER_H
#define MEDIA_MANAGER_H
#include <cartridge/cartridge.h>
#include <map>
#include <memory>
#include <string>
#include <vector>
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<uint8_t> getMediaByName(const std::string &name);
// Get media data by index
std::vector<uint8_t> 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<std::string, int> m_nameToIndex;
};
} // namespace wherigo
#endif // MEDIA_MANAGER_H

View File

@@ -0,0 +1,45 @@
#ifndef LUA_PERSISTENCE_H
#define LUA_PERSISTENCE_H
#include <string>
#include <vector>
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<std::string>& 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<std::string> getGameGlobals(lua_State* L);
};
} // namespace wherigo
#endif // LUA_PERSISTENCE_H

View File

@@ -0,0 +1,16 @@
#ifndef WHERIGO_LUA_H
#define WHERIGO_LUA_H
extern "C" {
#include <lua.h>
}
namespace wherigo {
int luaopen_Wherigo(lua_State *L);
void resetMediaCounter();
}
#endif // WHERIGO_LUA_H

View File

@@ -0,0 +1,66 @@
#ifndef WHERIGO_COMPLETION_H
#define WHERIGO_COMPLETION_H
#include <string>
#include <vector>
#include <ctime>
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<TaskCompletion> tasks;
struct ItemFound {
std::string name;
time_t foundTime;
};
std::vector<ItemFound> items;
struct ZoneVisited {
std::string name;
time_t visitTime;
};
std::vector<ZoneVisited> 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

View File

@@ -0,0 +1,16 @@
#ifndef ZOBJECT_H
#define ZOBJECT_H
extern "C" {
#include <lua.h>
}
namespace wherigo {
int zobject_MoveTo(lua_State *L);
int zobject_Contains(lua_State *L);
}
#endif // ZOBJECT_H

17
main/include/lua/ztimer.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef ZTIMER_H
#define ZTIMER_H
extern "C" {
#include <lua.h>
}
namespace wherigo {
int ztimer_Start(lua_State *L);
int ztimer_Stop(lua_State *L);
int ztimer_Reset(lua_State *L);
}
#endif // ZTIMER_H

View File

@@ -1,14 +0,0 @@
#pragma once
#include <wx/wx.h>
class cFrame : public wxMDIParentFrame
{
public:
cFrame();
private:
void OnHello(wxCommandEvent& event);
void OnExit(wxCommandEvent& event);
void OnAbout(wxCommandEvent& event);
};

View File

@@ -0,0 +1,40 @@
#pragma once
#include <wx/wx.h>
#include <wx/listbox.h>
#include <wx/notebook.h>
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;
};

View File

@@ -0,0 +1,33 @@
#pragma once
#include <wx/wx.h>
#include <wx/statbmp.h>
#include <wx/html/htmlwin.h>
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;
};

View File

@@ -0,0 +1,50 @@
#ifndef WHERIGO_DIALOG_H
#define WHERIGO_DIALOG_H
#include <wx/wx.h>
#include <wx/dialog.h>
#include <vector>
#include <string>
#include <functional>
namespace wherigo {
struct DialogEntry {
std::string text;
std::string mediaName;
std::vector<std::string> buttons;
};
class WherigoMessageDialog : public wxDialog {
public:
WherigoMessageDialog(wxWindow *parent, const wxString &text,
const wxString &title = "Wherigo",
const std::vector<wxString> &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<void(int)> callback = nullptr);
void showDialog(const std::vector<DialogEntry> &entries,
std::function<void(int)> callback = nullptr);
private:
WherigoDialogRunner() = default;
};
} // namespace wherigo
#endif // WHERIGO_DIALOG_H

275
main/src/app.cpp Normal file
View File

@@ -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 <cartridge/parser.h>
#include <wx/dir.h>
#include <wx/filename.h>
#include <wx/stdpaths.h>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
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<const char *>(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();
}

View File

@@ -1,11 +0,0 @@
#include "cApp.h"
#include "ui/cFrame.h"
#include <cartridge/parser.h>
bool cApp::OnInit()
{
auto cart = cartridge::parseCartridge();
auto *frame = new cFrame();
return frame->Show(true);
}

View File

@@ -0,0 +1,215 @@
#include "lua/game_engine.h"
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
#include <wx/log.h>
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

View File

@@ -0,0 +1,97 @@
#include "lua/media_manager.h"
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
#include <wx/log.h>
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<uint8_t> 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<uint8_t> 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

View File

@@ -0,0 +1,228 @@
#include "lua/persistence.h"
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
#include <wx/log.h>
#include <fstream>
#include <sstream>
#include <set>
namespace wherigo {
// List of Wherigo game object classes to save
static const std::set<std::string> WHERIGO_CLASSES = {
"ZCartridge", "Zone", "ZItem", "ZCharacter",
"ZTask", "ZTimer", "ZInput", "ZMedia"
};
std::vector<std::string> LuaPersistence::getGameGlobals(lua_State* L) {
std::vector<std::string> 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<std::string>& 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

512
main/src/lua/wherigo.cpp Normal file
View File

@@ -0,0 +1,512 @@
#include "lua/wherigo.h"
#include "lua/zobject.h"
#include "lua/ztimer.h"
#include "ui/wherigo_dialog.h"
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
#include <wx/log.h>
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
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<wxString> 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<DialogEntry> 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

View File

@@ -0,0 +1,281 @@
#include "lua/wherigo_completion.h"
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
#include <wx/log.h>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <algorithm>
namespace wherigo {
std::string WherigoCompletion::escapeXML(const std::string& str) {
std::string result;
for (char c : str) {
switch (c) {
case '&': result += "&amp;"; break;
case '<': result += "&lt;"; break;
case '>': result += "&gt;"; break;
case '"': result += "&quot;"; break;
case '\'': result += "&apos;"; 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 << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
file << "<WherigoGameLog xmlns=\"http://www.groundspeak.com/wherigo/1.0\">\n";
// Cartridge info
file << " <Cartridge>\n";
file << " <Name>" << escapeXML(data.cartridgeName) << "</Name>\n";
file << " <GUID>" << escapeXML(data.cartridgeGUID) << "</GUID>\n";
file << " </Cartridge>\n";
// Player info
file << " <Player>\n";
file << " <Name>" << escapeXML(data.playerName) << "</Name>\n";
file << " </Player>\n";
// Session info
file << " <Session>\n";
file << " <StartTime>" << formatTime(data.startTime) << "</StartTime>\n";
file << " <EndTime>" << formatTime(data.endTime) << "</EndTime>\n";
file << " <Duration>" << data.duration << "</Duration>\n";
file << " </Session>\n";
// Completion status
bool allTasksComplete = std::all_of(data.tasks.begin(), data.tasks.end(),
[](const CompletionData::TaskCompletion& t) { return t.completed; });
file << " <CompletionStatus>" << (allTasksComplete ? "Complete" : "Incomplete") << "</CompletionStatus>\n";
// Tasks
if (!data.tasks.empty()) {
file << " <Tasks>\n";
for (const auto& task : data.tasks) {
file << " <Task>\n";
file << " <Name>" << escapeXML(task.name) << "</Name>\n";
file << " <Completed>" << (task.completed ? "true" : "false") << "</Completed>\n";
if (task.completed) {
file << " <CompletionTime>" << formatTime(task.completionTime) << "</CompletionTime>\n";
}
file << " </Task>\n";
}
file << " </Tasks>\n";
}
// Items collected
if (!data.items.empty()) {
file << " <ItemsCollected>\n";
for (const auto& item : data.items) {
file << " <Item>\n";
file << " <Name>" << escapeXML(item.name) << "</Name>\n";
file << " <FoundTime>" << formatTime(item.foundTime) << "</FoundTime>\n";
file << " </Item>\n";
}
file << " </ItemsCollected>\n";
}
// Zones visited
if (!data.zones.empty()) {
file << " <ZonesVisited>\n";
for (const auto& zone : data.zones) {
file << " <Zone>\n";
file << " <Name>" << escapeXML(zone.name) << "</Name>\n";
file << " <VisitTime>" << formatTime(zone.visitTime) << "</VisitTime>\n";
file << " </Zone>\n";
}
file << " </ZonesVisited>\n";
}
// Completion code (signature/verification)
file << " <CompletionCode>" << data.completionCode << "</CompletionCode>\n";
file << "</WherigoGameLog>\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

64
main/src/lua/zobject.cpp Normal file
View File

@@ -0,0 +1,64 @@
#include "lua/zobject.h"
#include "lua/game_engine.h"
extern "C" {
#include <lua.h>
}
#include <wx/log.h>
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

62
main/src/lua/ztimer.cpp Normal file
View File

@@ -0,0 +1,62 @@
#include "lua/ztimer.h"
#include "lua/game_engine.h"
extern "C" {
#include <lua.h>
}
#include <wx/log.h>
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

View File

@@ -1,4 +1,4 @@
#include "cApp.h"
#include "app.h"
#include <wx/wx.h>
wxIMPLEMENT_APP(cApp);

View File

@@ -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!");
}

939
main/src/ui/game_screen.cpp Normal file
View File

@@ -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 <lua.h>
}
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<wxString> 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<wxString> 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<wxString> 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<wxString> 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
}

View File

@@ -0,0 +1,202 @@
#include "ui/start_screen.h"
#include "app.h"
#include "lua/game_engine.h"
#include "ui/game_screen.h"
#include <wx/filedlg.h>
#include <wx/mstream.h>
#include <wx/image.h>
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", "<br/>");
m_cartridgeDesc->SetPage("<p>" + plain + "</p>");
} 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);
}

View File

@@ -0,0 +1,161 @@
#include "ui/wherigo_dialog.h"
#include "lua/media_manager.h"
#include "lua/game_engine.h"
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/statbmp.h>
#include <wx/button.h>
#include <wx/log.h>
#include <wx/mstream.h>
#include <wx/image.h>
namespace wherigo {
WherigoMessageDialog::WherigoMessageDialog(wxWindow *parent, const wxString &text,
const wxString &title,
const std::vector<wxString> &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<int>(800 * contentScaleFactor);
int maxHeight = static_cast<int>(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<void(int)> callback) {
WherigoMessageDialog dlg(nullptr, text, title);
dlg.ShowModal();
if (callback) {
callback(dlg.getSelectedButton());
}
}
void WherigoDialogRunner::showDialog(const std::vector<DialogEntry> &entries,
std::function<void(int)> callback) {
for (size_t i = 0; i < entries.size(); i++) {
const auto &entry = entries[i];
std::vector<wxString> 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