mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2024-12-28 09:58:30 +01:00
2410 lines
71 KiB
C
2410 lines
71 KiB
C
/* -*- compile-command: "cd ../wasm && make MEMDEBUG=TRUE install -j3"; -*- */
|
|
/*
|
|
* Copyright 2021 - 2023 by Eric House (xwords@eehouse.org). All rights
|
|
* reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
*/
|
|
|
|
/* TODO
|
|
*
|
|
* Downloading wordlists. Will likely need to fetch and store them as base64
|
|
* data, meaning I'll need a working btoa to use them. Here's one way to download:
|
|
* https://emscripten.org/docs/api_reference/fetch.html
|
|
*/
|
|
|
|
#include <sys/time.h>
|
|
#include <stdio.h>
|
|
#include <stdarg.h>
|
|
#include <SDL2/SDL.h>
|
|
#include <SDL2/SDL_ttf.h>
|
|
#include <emscripten.h>
|
|
#include <unistd.h>
|
|
#include <stdlib.h>
|
|
|
|
#include "game.h"
|
|
#include "device.h"
|
|
#include "mempool.h"
|
|
#include "nli.h"
|
|
#include "strutils.h"
|
|
#include "movestak.h"
|
|
#include "knownplyr.h"
|
|
#include "dbgutil.h"
|
|
#include "md5.h"
|
|
|
|
#include "main.h"
|
|
#include "wasmdraw.h"
|
|
#include "wasmutil.h"
|
|
#include "wasmdutil.h"
|
|
#include "wasmdict.h"
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
#include <emscripten.h>
|
|
#endif
|
|
|
|
#define WASM_BOARD_LEFT 0
|
|
#define WASM_HOR_SCORE_TOP 0
|
|
|
|
#define KEY_LAST_GID "cur_game"
|
|
#define KEY_PLAYER_NAME "player_name"
|
|
#define KEY_GAMES "games"
|
|
#define KEY_SUMMARY "summary"
|
|
#define KEY_GAME "game_data"
|
|
#define KEY_NAME "game_name"
|
|
#define KEY_NEXT_GAME "next_game"
|
|
#define KEY_LANG_NAME "lang_name"
|
|
#define KEY_NEWGAME_DFLTS "ng_defaults"
|
|
#define KEY_PREFS "prefs"
|
|
#define KEY_COMMON "common"
|
|
|
|
#define BUTTON_OK "OK"
|
|
#define BUTTON_CANCEL "Cancel"
|
|
#define BUTTON_REPLY "Reply"
|
|
|
|
#define BUTTONS_ID_GAME "game_buttons"
|
|
#define BUTTONS_ID_DEVICE "device_buttons"
|
|
|
|
#define BUTTON_HINTDOWN "Prev Hint"
|
|
#define BUTTON_HINTUP "Next Hint"
|
|
#define BUTTON_TRADE "Trade"
|
|
#define BUTTON_STOPTRADE "Cancel Trade"
|
|
#define BUTTON_COMMIT "Commit"
|
|
// #define BUTTON_FLIP "Flip"
|
|
#define BUTTON_REDO "Redo"
|
|
#define BUTTON_VALS "Vals"
|
|
#define BUTTON_CHAT "Chat"
|
|
#define BUTTON_INVITE "Invite"
|
|
#define BUTTON_REMATCH "Rematch"
|
|
|
|
#define BUTTON_GAME_GAMES "Switch Games"
|
|
#define BUTTON_GAME_NEW "New Game"
|
|
#define BUTTON_GAME_RENAME "Rename Game"
|
|
#define BUTTON_GAME_DELETE "Delete Game"
|
|
#define BUTTON_NAME "My Name"
|
|
#define MAX_BUTTONS 20 /* not sure what's safe here */
|
|
|
|
// I get a JS exception if I do this... So don't
|
|
// #define GLOBALS_ON_STACK
|
|
|
|
typedef struct _NewGameParams {
|
|
Globals* globals;
|
|
bool isRobot;
|
|
bool allowHints;
|
|
bool dupMode;
|
|
char langName[32];
|
|
} NewGameParams;
|
|
|
|
static void removeGameState( GameState* gs );
|
|
static void updateScreen( GameState* gs, bool doSave );
|
|
static void cleanupGame( GameState* gs );
|
|
static void saveGame( GameState* gs );
|
|
static void updateDeviceButtons( Globals* globals );
|
|
static void clearScreen( Globals* globals );
|
|
static GameState* newGameState( Globals* globals );
|
|
static GameState* getSavedGame( Globals* globals, int gameID );
|
|
static void loadAndDraw( Globals* globals, const NetLaunchInfo* invite,
|
|
int gameID, NewGameParams* params );
|
|
static GameState* getCurGame( Globals* globals );
|
|
static void nameGame( GameState* gs, const char* name );
|
|
static void ensureName( GameState* gs );
|
|
static void loadName( GameState* gs );
|
|
static void saveName( GameState* gs );
|
|
static bool isVisible( GameState* gs );
|
|
static int countLangs( Globals* globals );
|
|
|
|
typedef struct _GotDictData {
|
|
const char* lc;
|
|
const char* langName;
|
|
const char* dictName;
|
|
uint8_t* data;
|
|
int len;
|
|
} GotDictData;
|
|
|
|
typedef void (*GotDictProc)(void* closure, GotDictData* data);
|
|
|
|
EM_JS(void, call_get_dict, (const char* lc, GotDictProc proc,
|
|
void* closure), {
|
|
let langs;
|
|
if (lc) {
|
|
langs = [UTF8ToString(lc)];
|
|
} else {
|
|
langs = [navigator.language.split('-')[0]];
|
|
if (langs[0] != 'en') {
|
|
langs.push('en');
|
|
}
|
|
}
|
|
getDict(langs, proc, closure);
|
|
});
|
|
|
|
EM_JS(void, show_name, (const char* name), {
|
|
let jsname = name ? UTF8ToString(name) : "";
|
|
document.getElementById('gamename').innerHTML = jsname;
|
|
});
|
|
|
|
EM_JS(void, show_msgcount, (int count), {
|
|
let str = "\u200C"; /* space that takes up vertial space in layout */
|
|
if ( 0 < count ) {
|
|
str = "Pending msgs: " + count;
|
|
}
|
|
document.getElementById('msgcount').textContent = str;
|
|
});
|
|
|
|
EM_JS(void, show_pool, (int cur, int max), {
|
|
const msg = 'cur: ' + cur + 'b; max: ' + max + 'b';
|
|
const div = document.getElementById('mempool');
|
|
div.textContent = msg;
|
|
div.parentNode.hidden = 0;
|
|
});
|
|
|
|
EM_JS(void, call_dialog, (const char* str, const char** but_strs,
|
|
StringProc proc, void* closure), {
|
|
var buttons = [];
|
|
for ( let ii = 0; ; ++ii ) {
|
|
const mem = HEAP32[(but_strs + (ii * 4)) >> 2];
|
|
if ( 0 == mem ) {
|
|
break;
|
|
}
|
|
const str = UTF8ToString(mem);
|
|
buttons.push(str);
|
|
}
|
|
nbDialog(UTF8ToString(str), buttons, proc, closure);
|
|
} );
|
|
|
|
EM_JS(void, call_pickBlank, (const char* msg, const char** strs, int nStrs,
|
|
StringProc proc, void* closure), {
|
|
var buttons = [];
|
|
for ( let ii = 0; ii < nStrs; ++ii ) {
|
|
const mem = HEAP32[(strs + (ii * 4)) >> 2];
|
|
const str = UTF8ToString(mem);
|
|
buttons.push(str);
|
|
}
|
|
nbBlankPick(UTF8ToString(msg), buttons, proc, closure);
|
|
} );
|
|
|
|
EM_JS(void, call_pickGame, (const char* msg, char** ids, char** names,
|
|
int nEntries, StringProc proc, void* closure), {
|
|
let map = {};
|
|
for (let ii = 0; ii < nEntries; ++ii ) {
|
|
const idsMem = HEAP32[(ids + (ii * 4)) >> 2];
|
|
const namesMem = HEAP32[(names + (ii * 4)) >> 2];
|
|
let id = UTF8ToString(idsMem);
|
|
map[id] = UTF8ToString(namesMem);
|
|
}
|
|
|
|
nbGamePick(UTF8ToString(msg), map, proc, closure);
|
|
} );
|
|
|
|
EM_JS(void, call_get_string, (const char* msg, const char* dflt,
|
|
StringProc proc, void* closure), {
|
|
let jsMgs = UTF8ToString(msg);
|
|
let jsDflt = UTF8ToString(dflt);
|
|
nbGetString( jsMgs, jsDflt, proc, closure );
|
|
} );
|
|
|
|
EM_JS(void, call_setup, (void* closure, bool dbg, const char* devid,
|
|
const char* gitrev, int now,
|
|
StringProc conflictProc, StringProc focussedProc,
|
|
MsgProc msgProc,
|
|
XP_U16 nTopics, XP_UCHAR** topics), {
|
|
let jsgr = UTF8ToString(gitrev);
|
|
let subTopics = [];
|
|
for (let ii = 0; ii < nTopics; ++ii ) {
|
|
const topicMem = HEAP32[(topics + (ii * 4)) >> 2];
|
|
subTopics[ii] = UTF8ToString(topicMem);
|
|
}
|
|
jssetup(closure, dbg, UTF8ToString(devid), jsgr, now,
|
|
conflictProc, focussedProc, msgProc, subTopics);
|
|
});
|
|
|
|
EM_JS(bool, call_mqttSend, (const char* topic, const uint8_t* ptr, int len), {
|
|
let topStr = UTF8ToString(topic);
|
|
let buffer = new Uint8Array(Module.HEAPU8.buffer, ptr, len);
|
|
return mqttSend(topStr, buffer);
|
|
});
|
|
|
|
typedef void (*JSCallback)(void* closure);
|
|
|
|
EM_JS(void, jscallback_set, (JSCallback proc, void* closure, int inMS), {
|
|
let timerproc = function(closure) {
|
|
ccall('cbckVoid', null, ['number', 'number'], [proc, closure]);
|
|
};
|
|
setTimeout( timerproc, inMS, closure );
|
|
});
|
|
|
|
EM_JS(void, setButtons, (const char* id, const char** bstrs,
|
|
StringProc proc, void* closure), {
|
|
var buttons = [];
|
|
for ( let ii = 0; ; ++ii ) {
|
|
const mem = HEAP32[(bstrs + (ii * 4)) >> 2];
|
|
if ( 0 == mem ) {
|
|
break;
|
|
}
|
|
const str = UTF8ToString(mem);
|
|
buttons.push(str);
|
|
}
|
|
setDivButtons(UTF8ToString(id), buttons, proc, closure);
|
|
});
|
|
|
|
EM_JS(void, js_callNewGame, (const char* msg, void* closure,
|
|
bool allowHints, bool isRobot,
|
|
const char* langName, char** langs, int nLangs), {
|
|
let jslangName = UTF8ToString(langName);
|
|
let jsmsg = UTF8ToString(msg);
|
|
|
|
let jlangs = [];
|
|
for ( let ii = 0; ii < nLangs; ++ii ) {
|
|
const mem = HEAP32[(langs + (ii * 4)) >> 2];
|
|
let str = UTF8ToString(mem);
|
|
jlangs.push(str);
|
|
}
|
|
nbGetNewGame(closure, jsmsg, allowHints, isRobot, jlangs, jslangName );
|
|
});
|
|
|
|
/* Keep these in sync with ints used in js_getHaveNotifyPerm!!! */
|
|
typedef enum {UNSUPPORTED = 0,
|
|
UNREQUESTED = 1,
|
|
BLOCKED = 2,
|
|
GRANTED = 3,
|
|
} NOTIFY_STATE;
|
|
|
|
|
|
EM_JS(int, js_getHaveNotifyPerm, (), {
|
|
let state = 1; // UNREQUESTED;
|
|
try {
|
|
let asStr = Notification.permission;
|
|
// console.error('permission: ', asStr);
|
|
if ( asStr == 'granted' ) {
|
|
state = 3; // GRANTED;
|
|
} else if ( asStr == 'denied' ) {
|
|
state = 2; // BLOCKED;
|
|
} else {
|
|
state = 1; // UNREQUESTED;
|
|
}
|
|
} catch (ex) {
|
|
state = 0; // UNSUPPORTED
|
|
}
|
|
return state;
|
|
});
|
|
|
|
EM_JS(void, js_requestNotify, (), {
|
|
Notification.requestPermission();
|
|
});
|
|
|
|
EM_JS( void, js_notify, (const char* msg), {
|
|
const jsmsg = UTF8ToString(msg);
|
|
new Notification('WASM CrossWords',
|
|
{ body: jsmsg, renotify: true, }
|
|
);
|
|
});
|
|
|
|
EM_JS( void, js_ackMsg, (const char* sum, int gameID), {
|
|
const jsum = UTF8ToString(sum);
|
|
postAck(jsum, gameID);
|
|
})
|
|
|
|
static void
|
|
loadPrefs( Globals* globals )
|
|
{
|
|
const XP_UCHAR* keys[] = { KEY_PREFS, KEY_COMMON, NULL };
|
|
XP_U32 len = sizeof(globals->cp);
|
|
dutil_loadPtr( globals->dutil, NULL_XWE, keys, &globals->cp, &len );
|
|
}
|
|
|
|
static void
|
|
savePrefs( Globals* globals )
|
|
{
|
|
const XP_UCHAR* keys[] = { KEY_PREFS, KEY_COMMON, NULL };
|
|
dutil_storePtr( globals->dutil, NULL_XWE, keys, &globals->cp, sizeof(globals->cp) );
|
|
}
|
|
|
|
typedef struct _ConfirmState {
|
|
Globals* globals;
|
|
BoolProc proc;
|
|
void* closure;
|
|
} ConfirmState;
|
|
|
|
static void
|
|
onConfirmed( void* closure, const char* button )
|
|
{
|
|
bool confirmed = 0 == strcmp( button, BUTTON_OK );
|
|
ConfirmState* cs = (ConfirmState*)closure;
|
|
(*cs->proc)( cs->closure, confirmed );
|
|
XP_FREE( cs->globals->mpool, cs );
|
|
}
|
|
|
|
static void
|
|
call_confirm( Globals* globals, const char* msg,
|
|
BoolProc proc, void* closure )
|
|
{
|
|
const char* buttons[] = { BUTTON_CANCEL, BUTTON_OK, NULL };
|
|
ConfirmState* cs = XP_MALLOC( globals->mpool, sizeof(*cs) );
|
|
cs->globals = globals;
|
|
cs->proc = proc;
|
|
cs->closure = closure;
|
|
call_dialog( msg, buttons, onConfirmed, cs );
|
|
}
|
|
|
|
static void
|
|
call_alert( const char* msg )
|
|
{
|
|
const char* buttons[] = { BUTTON_OK, NULL };
|
|
call_dialog( msg, buttons, NULL, NULL );
|
|
}
|
|
|
|
static void
|
|
onMsgAndTopic( void* closure, const XP_UCHAR* topic,
|
|
const XP_U8* msgBuf, XP_U16 msgLen )
|
|
{
|
|
/*bool success = */
|
|
call_mqttSend( topic, msgBuf, msgLen );
|
|
}
|
|
|
|
static XP_S16
|
|
send_msgs( XWEnv xwe, const SendMsgsPacket* const msgs,
|
|
XP_U16 streamVersion, const CommsAddrRec* addr,
|
|
CommsConnType conType, XP_U32 gameID,
|
|
void* closure )
|
|
{
|
|
XP_S16 nSent = -1;
|
|
Globals* globals = (Globals*)closure;
|
|
|
|
if ( addr_hasType( addr, COMMS_CONN_MQTT ) ) {
|
|
nSent = dvc_makeMQTTMessages( globals->dutil, NULL_XWE,
|
|
onMsgAndTopic, NULL,
|
|
msgs, &addr->u.mqtt.devID,
|
|
gameID, streamVersion );
|
|
}
|
|
XP_LOGFF("(gameID=%X)=>%d", gameID, nSent );
|
|
return nSent;
|
|
}
|
|
|
|
static XP_S16
|
|
send_invite( XWEnv xwe, const NetLaunchInfo* nli,
|
|
XP_U32 createdStamp, const CommsAddrRec* addr,
|
|
CommsConnType conType, void* closure )
|
|
{
|
|
Globals* globals = (Globals*)closure;
|
|
dvc_makeMQTTInvites( globals->dutil, NULL_XWE,
|
|
onMsgAndTopic, NULL,
|
|
&addr->u.mqtt.devID, nli );
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
count_changed( XWEnv xwe, void* closure, XP_U16 msgCount )
|
|
{
|
|
show_msgcount( msgCount );
|
|
}
|
|
|
|
static void
|
|
formatGameID( char* buf, size_t len, int gameID )
|
|
{
|
|
snprintf( buf, len, "%X", gameID );
|
|
}
|
|
|
|
static void
|
|
unformatGameID( int* gameID, const char* gameIDStr )
|
|
{
|
|
sscanf( gameIDStr, "%X", gameID );
|
|
XP_LOGFF( "unformatGameID(%s) => %X", gameIDStr, *gameID );
|
|
}
|
|
|
|
static void
|
|
makeSelfAddr( Globals* globals, CommsAddrRec* addr )
|
|
{
|
|
addr_setType( addr, COMMS_CONN_MQTT );
|
|
dvc_getMQTTDevID( globals->dutil, NULL_XWE, &addr->u.mqtt.devID );
|
|
}
|
|
|
|
static void
|
|
sendInviteTo(GameState* gs, const MQTTDevID* remoteDevID)
|
|
{
|
|
Globals* globals = gs->globals;
|
|
CommsAddrRec myAddr = {0};
|
|
makeSelfAddr( globals, &myAddr );
|
|
|
|
NetLaunchInfo nli = {0}; /* include everything!!! */
|
|
nli_init( &nli, &gs->gi, &myAddr, 1, 1 );
|
|
nli_setGameName( &nli, gs->gameName );
|
|
|
|
dvc_makeMQTTInvites( globals->dutil, NULL_XWE,
|
|
onMsgAndTopic, NULL,
|
|
remoteDevID, &nli );
|
|
}
|
|
|
|
static void
|
|
onGotInviteeID( void* closure, const char* mqttid )
|
|
{
|
|
MQTTDevID remoteDevID;
|
|
if ( strToMQTTCDevID( mqttid, &remoteDevID ) ) {
|
|
CAST_GS(GameState*, gs, closure);
|
|
sendInviteTo( gs, &remoteDevID );
|
|
} else if (!!mqttid) {
|
|
call_alert( "MQTT id looks badly formed" );
|
|
}
|
|
}
|
|
|
|
typedef struct _KnownSelState {
|
|
GameState* gs;
|
|
const char** names;
|
|
XP_U16 nNames;
|
|
} KnownSelState;
|
|
|
|
static void
|
|
onKnownSelected( void* closure, const char* name )
|
|
{
|
|
XP_LOGFF( "(name=%s)", name );
|
|
KnownSelState* kss = (KnownSelState*)closure;
|
|
GameState* gs = kss->gs;
|
|
if ( 0 == strcmp( BUTTON_CANCEL, name ) ) {
|
|
call_get_string( "Invitee's MQTT Device ID?", "",
|
|
onGotInviteeID, gs );
|
|
} else {
|
|
CommsAddrRec addr;
|
|
if ( kplr_getAddr( gs->globals->dutil, NULL_XWE, name, &addr, NULL ) ) {
|
|
sendInviteTo(gs, &addr.u.mqtt.devID);
|
|
}
|
|
}
|
|
|
|
XP_FREE( gs->globals->mpool, kss->names );
|
|
XP_FREE( gs->globals->mpool, kss );
|
|
}
|
|
|
|
static void
|
|
handleInvite( GameState* gs )
|
|
{
|
|
if ( isVisible(gs) ) {
|
|
Globals* globals = gs->globals;
|
|
XW_DUtilCtxt* dutil = globals->dutil;
|
|
if ( kplr_havePlayers( dutil, NULL_XWE ) ) {
|
|
KnownSelState* kss = XP_MALLOC( globals->mpool, sizeof(*kss) );
|
|
kss->gs = gs;
|
|
kss->nNames = 0;
|
|
kplr_getNames( dutil, NULL_XWE, NULL, &kss->nNames );
|
|
kss->names = XP_CALLOC( globals->mpool,
|
|
(kss->nNames + 2) * sizeof(kss->names[0]) );
|
|
kplr_getNames( dutil, NULL_XWE, kss->names, &kss->nNames );
|
|
kss->names[kss->nNames] = BUTTON_CANCEL;
|
|
XP_ASSERT( NULL == kss->names[kss->nNames+1] );
|
|
const char* msg = "Pick an invitee you've played before, "
|
|
"or cancel to enter manually.";
|
|
call_dialog( msg, kss->names, onKnownSelected, kss );
|
|
} else {
|
|
call_get_string( "Invitee's MQTT Device ID?", "",
|
|
onGotInviteeID, gs );
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
onRematchOkd( void* closure, const char* msg )
|
|
{
|
|
if ( !!msg && msg[0] ) { /* cancel button gives null */
|
|
GameState* curGS = (GameState*)closure;
|
|
Globals* globals = curGS->globals;
|
|
GameState* newGS = newGameState( globals );
|
|
newGS->util = wasm_util_make( MPPARM(globals->mpool) &newGS->gi,
|
|
globals->dutil, newGS );
|
|
|
|
if ( game_makeRematch( &curGS->game, NULL_XWE, newGS->util,
|
|
&globals->cp, (TransportProcs*)NULL,
|
|
&newGS->game, msg ) ) {
|
|
int gameID = newGS->gi.gameID;
|
|
|
|
saveGame( newGS );
|
|
removeGameState( newGS ); /* force reload with drawCtxt etc. */
|
|
|
|
loadAndDraw( globals, (NetLaunchInfo*)NULL, gameID, (NewGameParams*)NULL );
|
|
} else {
|
|
XP_LOGFF( "failed" );
|
|
cleanupGame( newGS );
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
onChatComposed( void* closure, const char* msg )
|
|
{
|
|
if ( !!msg ) {
|
|
CAST_GS(GameState*, gs, closure);
|
|
board_sendChat( gs->game.board, NULL_XWE, msg );
|
|
}
|
|
}
|
|
|
|
static void
|
|
formatNames( GameState* gs, char buf[] )
|
|
{
|
|
const CurGameInfo* gi = &gs->gi;
|
|
buf[0] = '\0';
|
|
for ( int ii = 0; ii < gi->nPlayers; ++ii ) {
|
|
const LocalPlayer* lp = &gi->players[ii];
|
|
const XP_UCHAR* name = lp->name;
|
|
if ( 0 < ii ) {
|
|
strcat( buf, " vs. " );
|
|
}
|
|
strcat( buf, name );
|
|
}
|
|
}
|
|
|
|
static void
|
|
onGameButton( void* closure, const char* button )
|
|
{
|
|
if ( !!button ) {
|
|
CAST_GS(GameState*, gs, closure);
|
|
Globals* globals = gs->globals;
|
|
|
|
XP_Bool draw = XP_FALSE;
|
|
BoardCtxt* board = gs->game.board;
|
|
XP_Bool redo;
|
|
|
|
if ( 0 == strcmp(button, BUTTON_HINTDOWN ) ) {
|
|
draw = board_requestHint( board, NULL_XWE, XP_TRUE, &redo );
|
|
} else if ( 0 == strcmp(button, BUTTON_HINTUP) ) {
|
|
draw = board_requestHint( board, NULL_XWE, XP_FALSE, &redo );
|
|
} else if ( 0 == strcmp(button, BUTTON_CHAT) ) {
|
|
call_get_string( "Your message?", "", onChatComposed, gs );
|
|
} else if ( 0 == strcmp(button, BUTTON_TRADE ) ) {
|
|
wasm_draw_setInTrade( globals->draw, true );
|
|
board_invalAll( gs->game.board );
|
|
draw = board_beginTrade( board, NULL_XWE );
|
|
} else if ( 0 == strcmp(button, BUTTON_STOPTRADE ) ) {
|
|
wasm_draw_setInTrade( globals->draw, false );
|
|
board_invalAll( gs->game.board );
|
|
draw = board_endTrade( board );
|
|
} else if ( 0 == strcmp(button, BUTTON_COMMIT) ) {
|
|
draw = board_commitTurn( board, NULL_XWE, XP_FALSE, XP_FALSE, NULL );
|
|
/* } else if ( 0 == strcmp(button, BUTTON_FLIP) ) { */
|
|
/* draw = board_flip( board ); */
|
|
} else if ( 0 == strcmp(button, BUTTON_REDO) ) {
|
|
draw = board_redoReplacedTiles( board, NULL_XWE )
|
|
|| board_replaceTiles( board, NULL_XWE );
|
|
} else if ( 0 == strcmp(button, BUTTON_VALS) ) {
|
|
globals->cp.tvType = (globals->cp.tvType + 1) % TVT_N_ENTRIES;
|
|
savePrefs( globals );
|
|
draw = board_prefsChanged( board, &globals->cp );
|
|
} else if ( 0 == strcmp(button, BUTTON_INVITE) ) {
|
|
handleInvite(gs);
|
|
} else if ( 0 == strcmp(button, BUTTON_REMATCH) ) {
|
|
char buf[128];
|
|
formatNames( gs, buf );
|
|
call_get_string( "Rematch game name?", buf, onRematchOkd, gs );
|
|
}
|
|
|
|
if ( draw ) {
|
|
updateScreen( gs, true );
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
updateGameButtons( Globals* globals )
|
|
{
|
|
GameState* gs = getCurGame(globals);
|
|
const char* buttons[MAX_BUTTONS];
|
|
int cur = 0;
|
|
|
|
if ( gs && !!gs->util ) {
|
|
XP_U16 nPending = server_getPendingRegs( gs->game.server );
|
|
if ( 0 < nPending ) {
|
|
buttons[cur++] = BUTTON_INVITE;
|
|
} else {
|
|
GameStateInfo gsi;
|
|
game_getState( &gs->game, NULL_XWE, &gsi );
|
|
|
|
if ( gsi.canHint ) {
|
|
buttons[cur++] = BUTTON_HINTDOWN;
|
|
buttons[cur++] = BUTTON_HINTUP;
|
|
}
|
|
|
|
if ( gsi.canChat ) {
|
|
buttons[cur++] = BUTTON_CHAT;
|
|
}
|
|
|
|
if ( gsi.inTrade ) {
|
|
buttons[cur++] = BUTTON_STOPTRADE;
|
|
} else if ( gsi.canTrade ) {
|
|
buttons[cur++] = BUTTON_TRADE;
|
|
}
|
|
if ( gsi.curTurnSelected ) {
|
|
buttons[cur++] = BUTTON_COMMIT;
|
|
}
|
|
/* buttons[cur++] = BUTTON_FLIP; */
|
|
|
|
if ( gsi.canRedo ) {
|
|
buttons[cur++] = BUTTON_REDO;
|
|
}
|
|
|
|
buttons[cur++] = BUTTON_VALS;
|
|
|
|
if ( gsi.canRematch ) {
|
|
buttons[cur++] = BUTTON_REMATCH;
|
|
}
|
|
}
|
|
}
|
|
|
|
buttons[cur++] = NULL;
|
|
setButtons( BUTTONS_ID_GAME, buttons, onGameButton, gs );
|
|
}
|
|
|
|
static bool
|
|
langNameFor( Globals* globals, const char* lc, char buf[], size_t buflen )
|
|
{
|
|
const XP_UCHAR* keys[] = { KEY_DICTS, lc, KEY_LANG_NAME, NULL };
|
|
XP_U32 len = buflen;
|
|
dutil_loadPtr( globals->dutil, NULL_XWE, keys, buf, &len );
|
|
return 0 < len;
|
|
}
|
|
|
|
static void
|
|
showName( GameState* gs )
|
|
{
|
|
char title[256] = {0};
|
|
if ( !!gs ) {
|
|
Globals* globals = gs->globals;
|
|
sprintf( title, "%s", gs->gameName );
|
|
if ( 1 < countLangs( globals ) ) {
|
|
char langName[32];
|
|
if ( !langNameFor( globals, gs->gi.isoCodeStr, langName, sizeof(langName) ) ) {
|
|
strcpy( langName, "??" );
|
|
}
|
|
char* start = title + strlen(title);
|
|
sprintf( start, " (%s)", langName );
|
|
}
|
|
#ifdef DEBUG
|
|
char* start = title + strlen(title);
|
|
sprintf( start, " (gid=<a href=\"https://eehouse.org/xw4/ui/gameinfo"
|
|
"?devid=%s&gameid=%d\">%d/%X</a>)",
|
|
globals->myDevIDStr, gs->gi.gameID, gs->gi.gameID, gs->gi.gameID );
|
|
#endif
|
|
}
|
|
show_name( title );
|
|
}
|
|
|
|
static void
|
|
onGameRanamed( void* closure, const char* newName )
|
|
{
|
|
if ( !!newName ) {
|
|
CAST_GS(GameState*, gs, closure);
|
|
nameGame( gs, newName );
|
|
showName( gs );
|
|
}
|
|
}
|
|
|
|
typedef struct _NameIterState {
|
|
Globals* globals;
|
|
char curGameID[16];
|
|
int count;
|
|
char** names;
|
|
char** gids;
|
|
} NameIterState;
|
|
|
|
static void
|
|
onGameChosen( void* closure, const char* key )
|
|
{
|
|
CAST_GLOB(Globals*, globals, closure);
|
|
|
|
/* To be safe, let's make sure the game exists. We don't want to create
|
|
* another if somehow it doesn't */
|
|
int gameID;
|
|
unformatGameID( &gameID, key );
|
|
if ( !!getSavedGame(globals, gameID) ) {
|
|
loadAndDraw( globals, NULL, gameID, NULL );
|
|
}
|
|
|
|
updateDeviceButtons( globals );
|
|
}
|
|
|
|
static char*
|
|
formatForGame( Globals* globals, bool multiLangs, const XP_UCHAR* gameKey )
|
|
{
|
|
const XP_UCHAR* keys[] = {KEY_GAMES, gameKey, KEY_NAME, NULL};
|
|
char gameName[32];
|
|
XP_U32 len = sizeof(gameName);
|
|
dutil_loadPtr( globals->dutil, NULL_XWE, keys, gameName, &len );
|
|
|
|
char buf[256];
|
|
int offset = snprintf( buf, sizeof(buf), "%s;", gameName );
|
|
|
|
GameSummary summary;
|
|
len = sizeof(summary);
|
|
keys[2] = KEY_SUMMARY;
|
|
dutil_loadPtr( globals->dutil, NULL_XWE, keys, &summary, &len );
|
|
if ( len == sizeof(summary) ) {
|
|
if ( multiLangs ) {
|
|
char langName[32];
|
|
if ( langNameFor( globals, summary.isoCodeStr, langName, sizeof(langName) ) ) {
|
|
offset += snprintf( buf+offset, sizeof(buf)-offset, " (in %s)", langName );
|
|
}
|
|
}
|
|
bool inPlay = 0 <= summary.turn;
|
|
if ( inPlay ) {
|
|
offset += snprintf( buf+offset, sizeof(buf)-offset, " Opponent: %s;",
|
|
summary.opponents );
|
|
}
|
|
|
|
if ( summary.gameOver ) {
|
|
offset += snprintf( buf+offset, sizeof(buf)-offset, " Game over" );
|
|
} else if ( !inPlay ) {
|
|
offset += snprintf( buf+offset, sizeof(buf)-offset, " Game NOT in play" );
|
|
} else {
|
|
offset += snprintf( buf+offset, sizeof(buf)-offset, " My turn: %s",
|
|
0 <= summary.turn && summary.turnIsLocal ? "YES" : "NO" );
|
|
}
|
|
#ifdef DEBUG
|
|
offset += snprintf( buf+offset, sizeof(buf)-offset, "; GID: %s", gameKey );
|
|
#endif
|
|
}
|
|
char* result = NULL;
|
|
replaceStringIfDifferent( globals->mpool, &result, buf );
|
|
return result;
|
|
}
|
|
|
|
static XP_Bool
|
|
onOneGameName( void* closure, const XP_UCHAR* keys[] )
|
|
{
|
|
const char* gameIDStr = keys[1];
|
|
if ( 0 != strcmp( gameIDStr, "0" ) ) { /* temporary */
|
|
NameIterState* nis = (NameIterState*)closure;
|
|
/* we don't display the current open game as a choice */
|
|
if ( 0 != strcmp( gameIDStr, nis->curGameID ) ) {
|
|
Globals* globals = nis->globals;
|
|
bool multiLangs = 1 < countLangs(globals);
|
|
|
|
/* Make sure game exists. This may be unnecessary later */
|
|
const XP_UCHAR* dataKeys[] = { keys[0], keys[1], KEY_GAME, NULL };
|
|
XP_U32 dataLen;
|
|
dutil_loadPtr( globals->dutil, NULL_XWE, dataKeys, NULL, &dataLen );
|
|
if ( 0 < dataLen ) {
|
|
int cur = nis->count++;
|
|
nis->names = XP_REALLOC( globals->mpool, nis->names,
|
|
nis->count * sizeof(nis->names[0]) );
|
|
nis->names[cur] = formatForGame( globals, multiLangs, keys[1] );
|
|
nis->gids = XP_REALLOC( globals->mpool, nis->gids,
|
|
nis->count * sizeof(nis->gids[0]) );
|
|
nis->gids[cur] = XP_MALLOC( globals->mpool, 1 + strlen(gameIDStr) );
|
|
strcpy( nis->gids[cur], gameIDStr );
|
|
}
|
|
}
|
|
}
|
|
return true; /* keep going */
|
|
}
|
|
|
|
static void
|
|
pickGame( Globals* globals )
|
|
{
|
|
XW_DUtilCtxt* dutil = globals->dutil;
|
|
NameIterState nis = { .globals = globals, };
|
|
formatGameID( nis.curGameID, VSIZE(nis.curGameID),
|
|
globals->curGame->gi.gameID );
|
|
|
|
const XP_UCHAR* keys[] = {KEY_GAMES, KEY_WILDCARD, KEY_NAME, NULL};
|
|
dutil_forEach( dutil, NULL_XWE, keys, onOneGameName, &nis );
|
|
|
|
const char* msg = "Choose game to open:";
|
|
call_pickGame(msg, nis.gids, nis.names, nis.count, onGameChosen, globals);
|
|
|
|
for ( int ii = 0; ii < nis.count; ++ii ) {
|
|
XP_FREE( globals->mpool, nis.names[ii] );
|
|
XP_FREE( globals->mpool, nis.gids[ii] );
|
|
}
|
|
XP_FREE( globals->mpool, nis.names );
|
|
XP_FREE( globals->mpool, nis.gids );
|
|
}
|
|
|
|
static void
|
|
cleanupGame( GameState* gs )
|
|
{
|
|
if ( !!gs->util ) {
|
|
game_dispose( &gs->game, NULL_XWE );
|
|
gi_disposePlayerInfo( MPPARM(gs->globals->mpool) &gs->gi );
|
|
wasm_util_destroy( gs->util );
|
|
gs->util = NULL;
|
|
// XP_MEMSET( &gs, 0, sizeof(globals->gs) );
|
|
}
|
|
}
|
|
|
|
static void
|
|
removeGameState( GameState* gs )
|
|
{
|
|
Globals* globals = gs->globals;
|
|
GameState** prev = &globals->games;
|
|
for ( GameState* cur = globals->games; !!cur; cur = cur->next ) {
|
|
if ( gs == cur ) {
|
|
*prev = cur->next;
|
|
break;
|
|
}
|
|
prev = &cur->next;
|
|
}
|
|
}
|
|
|
|
static void
|
|
deleteGame( GameState* gs )
|
|
{
|
|
Globals* globals = gs->globals;
|
|
if ( globals->curGame == gs ) {
|
|
globals->curGame = NULL;
|
|
}
|
|
updateGameButtons( globals );
|
|
|
|
int gameID = gs->gi.gameID; /* remember it */
|
|
cleanupGame( gs );
|
|
|
|
// Remove from linked list, but don't actually free because could live in
|
|
// a js-side object still. Needs refcounting or somesuch
|
|
removeGameState( gs );
|
|
|
|
char gameIDStr[16];
|
|
formatGameID( gameIDStr, sizeof(gameIDStr), gameID );
|
|
const XP_UCHAR* keys[] = {KEY_GAMES, gameIDStr, NULL};
|
|
dutil_remove( globals->dutil, keys );
|
|
showName( NULL );
|
|
}
|
|
|
|
static void
|
|
onDeleteConfirmed( void* closure, bool confirmed )
|
|
{
|
|
if ( confirmed ) {
|
|
CAST_GS(GameState*, gs, closure);
|
|
Globals* globals = gs->globals;
|
|
bool wasVisible = gs == getCurGame(globals);
|
|
|
|
deleteGame( gs );
|
|
if ( wasVisible ) {
|
|
clearScreen( globals );
|
|
}
|
|
updateDeviceButtons( globals );
|
|
pickGame( globals );
|
|
}
|
|
}
|
|
|
|
bool
|
|
main_getLocalName( Globals* globals, char* playerName, size_t buflen )
|
|
{
|
|
XP_U32 len = buflen;
|
|
const XP_UCHAR* keys[] = {KEY_PLAYER_NAME, NULL};
|
|
dutil_loadPtr( globals->dutil, NULL_XWE, keys, playerName, &len );
|
|
bool haveName = 0 != len;
|
|
if ( !haveName ) { /* not found? */
|
|
strcpy( playerName, "Player 1" );
|
|
}
|
|
return haveName;
|
|
}
|
|
|
|
void
|
|
main_ackMQTTMsg( Globals* globals, const XP_U8* msg, XP_U16 len, int gameID )
|
|
{
|
|
MD5Result out;
|
|
calcMD5Sum( &out, (uint8_t*)msg, len );
|
|
js_ackMsg( out.output, gameID );
|
|
}
|
|
|
|
static void
|
|
onPlayerNamed( void* closure, const char* name )
|
|
{
|
|
CAST_GLOB(Globals*, globals, closure);
|
|
if ( !!name ) {
|
|
const XP_UCHAR* keys[] = {KEY_PLAYER_NAME, NULL};
|
|
dutil_storePtr( globals->dutil, NULL_XWE, keys, (void*)name, 1 + strlen(name) );
|
|
}
|
|
}
|
|
|
|
typedef struct _NewGameState {
|
|
Globals* globals;
|
|
char* langs[16];
|
|
int nLangs;
|
|
} NewGameState;
|
|
|
|
static XP_Bool
|
|
onOneLang( void* closure, const XP_UCHAR* keys[] )
|
|
{
|
|
NewGameState* ngs = (NewGameState*)closure;
|
|
|
|
char langName[32];
|
|
XP_U32 len = sizeof(langName);
|
|
dutil_loadPtr( ngs->globals->dutil, NULL_XWE, keys, langName, &len );
|
|
|
|
replaceStringIfDifferent( ngs->globals->mpool, &ngs->langs[ngs->nLangs], langName );
|
|
XP_LOGFF( "set langs[%d] %s", ngs->nLangs, ngs->langs[ngs->nLangs] );
|
|
++ngs->nLangs;
|
|
XP_ASSERT( ngs->nLangs < VSIZE(ngs->langs));
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
setNewGameDefaults( Globals* globals, NewGameParams* params )
|
|
{
|
|
XP_MEMSET( params, 0, sizeof(*params) );
|
|
params->allowHints = true;
|
|
params->isRobot = true;
|
|
strcat( params->langName, "English" );
|
|
}
|
|
|
|
static void
|
|
callNewGame( Globals* globals )
|
|
{
|
|
NewGameState ngs = {.globals = globals};
|
|
const XP_UCHAR* keys1[] = {KEY_DICTS, KEY_WILDCARD, KEY_LANG_NAME, NULL};
|
|
dutil_forEach( globals->dutil, NULL_XWE, keys1, onOneLang, &ngs );
|
|
|
|
NewGameParams params;
|
|
XP_U32 len = sizeof(params);
|
|
const XP_UCHAR* keys2[] = {KEY_NEWGAME_DFLTS, NULL};
|
|
dutil_loadPtr( globals->dutil, NULL_XWE, keys2, ¶ms, &len );
|
|
if ( len != sizeof(params) ) {
|
|
setNewGameDefaults( globals, ¶ms );
|
|
}
|
|
|
|
js_callNewGame("Configure your new game", globals, params.allowHints,
|
|
params.isRobot, params.langName, ngs.langs, ngs.nLangs );
|
|
}
|
|
|
|
static void
|
|
onDeviceButton( void* closure, const char* button )
|
|
{
|
|
CAST_GLOB(Globals*, globals, closure);
|
|
XP_LOGFF( "(button=%s)", button );
|
|
if ( 0 == strcmp(button, BUTTON_GAME_NEW) ) {
|
|
callNewGame(globals);
|
|
} else if ( 0 == strcmp(button, BUTTON_GAME_GAMES) ) {
|
|
pickGame( globals );
|
|
} else if ( 0 == strcmp(button, BUTTON_GAME_RENAME ) ) {
|
|
GameState* curGS = getCurGame( globals );
|
|
ensureName( curGS );
|
|
call_get_string( "Rename your game", curGS->gameName,
|
|
onGameRanamed, curGS );
|
|
} else if ( 0 == strcmp(button, BUTTON_GAME_DELETE) ) {
|
|
GameState* curGS = getCurGame( globals );
|
|
char msg[256];
|
|
snprintf( msg, sizeof(msg), "Are you sure you want to delete the game \"%s\""
|
|
#ifdef DEBUG
|
|
" (gid %d/%X)"
|
|
#endif
|
|
"?\nThis action cannot be undone.",
|
|
curGS->gameName
|
|
#ifdef DEBUG
|
|
, curGS->gi.gameID, curGS->gi.gameID
|
|
#endif
|
|
);
|
|
call_confirm( globals, msg, onDeleteConfirmed, curGS );
|
|
} else if ( 0 == strcmp(button, BUTTON_NAME ) ) {
|
|
char playerName[32];
|
|
main_getLocalName( globals, playerName, sizeof(playerName)-1 );
|
|
call_get_string( "Set your (local) player name", playerName,
|
|
onPlayerNamed, globals );
|
|
}
|
|
}
|
|
|
|
static XP_Bool
|
|
upCounter( void* closure, const XP_UCHAR* keys[] )
|
|
{
|
|
int* intp = (int*)closure;
|
|
++*intp;
|
|
return true;
|
|
}
|
|
|
|
static int
|
|
countGames( Globals* globals )
|
|
{
|
|
int nFound = 0;
|
|
const XP_UCHAR* keys[] = {KEY_GAMES, KEY_WILDCARD, KEY_GAME, NULL};
|
|
dutil_forEach( globals->dutil, NULL_XWE, keys, upCounter, &nFound );
|
|
return nFound;
|
|
}
|
|
|
|
static int
|
|
countLangs( Globals* globals )
|
|
{
|
|
int count = 0;
|
|
const XP_UCHAR* keys[] = {KEY_DICTS, KEY_WILDCARD, KEY_DICTS, NULL};
|
|
dutil_forEach( globals->dutil, NULL_XWE, keys, upCounter, &count );
|
|
LOG_RETURNF( "%d", count );
|
|
return count;
|
|
}
|
|
|
|
static bool
|
|
haveDictFor(Globals* globals, const char* lc)
|
|
{
|
|
int count = 0;
|
|
const XP_UCHAR* keys[] = {KEY_DICTS, lc, KEY_DICTS, KEY_WILDCARD, NULL};
|
|
dutil_forEach( globals->dutil, NULL_XWE, keys, upCounter, &count );
|
|
bool result = 0 < count;
|
|
// XP_LOGFF("(lc=%s)=>%s", lc, boolToStr(result) );
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
updateDeviceButtons( Globals* globals )
|
|
{
|
|
const char* buttons[MAX_BUTTONS];
|
|
int cur = 0;
|
|
if ( 0 < countLangs( globals ) ) {
|
|
buttons[cur++] = BUTTON_GAME_NEW;
|
|
}
|
|
if ( 1 < countGames(globals) ) {
|
|
buttons[cur++] = BUTTON_GAME_GAMES;
|
|
}
|
|
if ( !!getCurGame( globals ) ) {
|
|
buttons[cur++] = BUTTON_GAME_RENAME;
|
|
buttons[cur++] = BUTTON_GAME_DELETE;
|
|
}
|
|
buttons[cur++] = BUTTON_NAME;
|
|
buttons[cur++] = NULL;
|
|
XP_ASSERT( cur <= VSIZE(buttons) );
|
|
|
|
setButtons( BUTTONS_ID_DEVICE, buttons, onDeviceButton, globals );
|
|
}
|
|
|
|
static void
|
|
onConflict( void* closure, const char* ignored )
|
|
{
|
|
CAST_GLOB(Globals*, globals, closure);
|
|
call_alert( "Control passed to another tab" );
|
|
XP_MEMSET( globals, 0, sizeof(*globals) ); /* stop everything :-) */
|
|
}
|
|
|
|
static void
|
|
onFocussed( void* closure, const char* newState )
|
|
{
|
|
CAST_GLOB(Globals*, globals, closure);
|
|
globals->focussed = 0 == strcmp("focus", newState);
|
|
XP_LOGFF( "focussed now: %d", globals->focussed );
|
|
/* This hasn't worked.... */
|
|
GameState* gs = getCurGame( globals );
|
|
if ( !!gs ) {
|
|
board_invalAll( gs->game.board );
|
|
updateScreen( gs, false );
|
|
}
|
|
}
|
|
|
|
static void
|
|
onMqttMsg(void* closure, const char* topic, const uint8_t* data, int len )
|
|
{
|
|
CAST_GLOB(Globals*, globals, closure);
|
|
dvc_parseMQTTPacket( globals->dutil, NULL_XWE, topic, data, len );
|
|
}
|
|
|
|
static bool
|
|
storeAsDict( Globals* globals, GotDictData* gdd )
|
|
{
|
|
char shortName[32];
|
|
sprintf( shortName, "%s", gdd->dictName );
|
|
char* dot = strstr(shortName, ".xwd");
|
|
if ( !!dot ) {
|
|
*dot = '\0';
|
|
}
|
|
XP_LOGFF("shortName: %s", shortName);
|
|
|
|
/* First make a dict of it. If it doesn't work out, don't store the
|
|
data! */
|
|
DictionaryCtxt* dict =
|
|
wasm_dictionary_make( globals, shortName, gdd->data, gdd->len );
|
|
bool success = !!dict;
|
|
if ( success ) {
|
|
dict_unref( dict, NULL_XWE );
|
|
|
|
const XP_UCHAR* keys1[] = {KEY_DICTS, gdd->lc, KEY_DICTS, shortName, NULL};
|
|
dutil_storePtr( globals->dutil, NULL_XWE, keys1, gdd->data, gdd->len );
|
|
const XP_UCHAR* keys2[] = {KEY_DICTS, gdd->lc, KEY_LANG_NAME, NULL};
|
|
dutil_storePtr( globals->dutil, NULL_XWE, keys2, gdd->langName,
|
|
strlen(gdd->langName) + 1 );
|
|
|
|
char msg[128];
|
|
sprintf( msg, "Successfull installed wordlist for play in %s.",
|
|
gdd->langName );
|
|
call_alert( msg );
|
|
}
|
|
LOG_RETURNF( "%s", boolToStr(success) );
|
|
return success;
|
|
}
|
|
|
|
static void
|
|
resizeBoard( Globals* globals, GameState* gs )
|
|
{
|
|
BoardDims dims;
|
|
int useWidth = globals->useWidth;
|
|
int useHeight = globals->useHeight;
|
|
board_figureLayout( gs->game.board, NULL_XWE, &gs->gi,
|
|
WASM_BOARD_LEFT, WASM_HOR_SCORE_TOP,
|
|
useWidth, useHeight,
|
|
110, 150, 200, useWidth-25, useWidth/15,
|
|
useHeight/15, XP_FALSE, &dims );
|
|
board_applyLayout( gs->game.board, NULL_XWE, &dims );
|
|
|
|
board_invalAll( gs->game.board ); /* redraw screen on loading new game */
|
|
}
|
|
|
|
static void
|
|
resizeBoards( Globals* globals )
|
|
{
|
|
for ( GameState* cur = globals->games; !!cur; cur = cur->next ) {
|
|
resizeBoard( globals, cur );
|
|
}
|
|
}
|
|
|
|
/* We need window dimensions to draw, but other stuff too. So when we get
|
|
* dimensions, set them.
|
|
*/
|
|
static void
|
|
initWindow( Globals* globals, int winWidth, int winHeight )
|
|
{
|
|
// we're ready to go IFF we are inited.
|
|
if ( !!globals->vtMgr ) { /* inited? */
|
|
globals->useWidth = winWidth;
|
|
globals->useHeight = winHeight;
|
|
|
|
if ( !!globals->window ) {
|
|
SDL_DestroyRenderer(globals->renderer);
|
|
globals->renderer = NULL;
|
|
SDL_DestroyWindow(globals->window);
|
|
globals->window = NULL;
|
|
}
|
|
|
|
SDL_CreateWindowAndRenderer( winWidth, winHeight, 0,
|
|
&globals->window, &globals->renderer );
|
|
|
|
/* wipe the canvas to background */
|
|
SDL_SetRenderDrawColor( globals->renderer, 155, 155, 155, 255 );
|
|
SDL_RenderClear( globals->renderer );
|
|
|
|
if ( !!globals->draw ) {
|
|
wasm_draw_resize( globals->draw, winWidth, winHeight );
|
|
resizeBoards( globals );
|
|
} else {
|
|
globals->draw = wasm_draw_make( MPPARM(globals->mpool)
|
|
winWidth, winHeight );
|
|
}
|
|
|
|
GameState* gs = getCurGame( globals );
|
|
if ( !!gs ) {
|
|
board_invalAll( gs->game.board );
|
|
updateScreen( gs, true );
|
|
}
|
|
} else {
|
|
XP_ASSERT(0);
|
|
}
|
|
}
|
|
|
|
static void
|
|
initDeviceGlobals( Globals* globals )
|
|
{
|
|
globals->cp.showBoardArrow = XP_TRUE;
|
|
globals->cp.allowPeek = XP_TRUE;
|
|
globals->cp.showRobotScores = XP_TRUE;
|
|
globals->cp.sortNewTiles = XP_TRUE;
|
|
globals->cp.showColors = XP_TRUE;
|
|
|
|
globals->transportProcs.sendMsgs = send_msgs;
|
|
globals->transportProcs.sendInvt = send_invite;
|
|
globals->transportProcs.countChanged = count_changed;
|
|
globals->transportProcs.closure = globals;
|
|
|
|
#ifdef MEM_DEBUG
|
|
globals->mpool = mpool_make( "wasm" );
|
|
#endif
|
|
globals->vtMgr = make_vtablemgr( MPPARM_NOCOMMA(globals->mpool) );
|
|
globals->dutil = wasm_dutil_make( MPPARM(globals->mpool) globals->vtMgr, globals );
|
|
globals->dictMgr = dmgr_make( MPPARM_NOCOMMA(globals->mpool) );
|
|
|
|
loadPrefs( globals );
|
|
|
|
MQTTDevID devID;
|
|
dvc_getMQTTDevID( globals->dutil, NULL_XWE, &devID );
|
|
XP_SNPRINTF( globals->myDevIDStr, VSIZE(globals->myDevIDStr),
|
|
MQTTDevID_FMT, devID );
|
|
XP_LOGFF( "got mqtt devID: %s", globals->myDevIDStr );
|
|
int now = dutil_getCurSeconds( globals->dutil, NULL_XWE );
|
|
bool dbg =
|
|
#ifdef DEBUG
|
|
true
|
|
#else
|
|
false
|
|
#endif
|
|
;
|
|
|
|
XP_UCHAR storage[256];
|
|
XP_UCHAR* topics[4];
|
|
XP_U16 nTopics = VSIZE(topics);
|
|
dvc_getMQTTSubTopics( globals->dutil, NULL_XWE,
|
|
storage, VSIZE(storage),
|
|
&nTopics, topics );
|
|
for ( int ii = 0; ii < nTopics; ++ii ) {
|
|
XP_LOGFF( "got topic %d: %s", ii, topics[ii] );
|
|
}
|
|
|
|
call_setup( globals, dbg, globals->myDevIDStr, GITREV, now, onConflict,
|
|
onFocussed, onMqttMsg, nTopics, topics );
|
|
}
|
|
|
|
static void
|
|
storeCurOpen( GameState* gs )
|
|
{
|
|
const XP_UCHAR* keys[] = {KEY_LAST_GID, NULL};
|
|
dutil_storePtr( gs->globals->dutil, NULL_XWE, keys,
|
|
&gs->gi.gameID, sizeof(gs->gi.gameID) );
|
|
}
|
|
|
|
static void
|
|
startGame( GameState* gs )
|
|
{
|
|
Globals* globals = gs->globals;
|
|
globals->curGame = gs;
|
|
ensureName( gs );
|
|
XP_LOGFF( "changed curGame to %s", gs->gameName );
|
|
showName( gs );
|
|
storeCurOpen( gs );
|
|
|
|
resizeBoard( globals, gs );
|
|
|
|
if ( SERVER_ISCLIENT == gs->gi.serverRole ) {
|
|
server_initClientConnection( gs->game.server, NULL_XWE );
|
|
}
|
|
|
|
(void)server_do( gs->game.server, NULL_XWE ); /* assign tiles, etc. */
|
|
if ( !!gs->game.comms ) {
|
|
comms_resendAll( gs->game.comms, NULL_XWE, COMMS_CONN_MQTT, XP_TRUE );
|
|
}
|
|
|
|
updateScreen( gs, true );
|
|
LOG_RETURN_VOID();
|
|
}
|
|
|
|
static GameState*
|
|
newFromInvite( Globals* globals, const NetLaunchInfo* nli )
|
|
{
|
|
GameState* gs = newGameState(globals);
|
|
gs->util = wasm_util_make( MPPARM(globals->mpool) &gs->gi,
|
|
globals->dutil, gs );
|
|
|
|
CommsAddrRec selfAddr = {0};
|
|
makeSelfAddr( globals, &selfAddr );
|
|
game_makeFromInvite( &gs->game, NULL_XWE, nli,
|
|
&selfAddr, gs->util, globals->draw,
|
|
&globals->cp, &globals->transportProcs );
|
|
if ( nli->gameName[0] ) {
|
|
nameGame( gs, nli->gameName );
|
|
}
|
|
ensureName( gs );
|
|
return gs;
|
|
}
|
|
|
|
typedef struct _DictDownState {
|
|
Globals* globals;
|
|
NetLaunchInfo invite;
|
|
const char* lc;
|
|
} DictDownState;
|
|
|
|
|
|
static void
|
|
onDictForInvite( void* closure, GotDictData* gdd )
|
|
{
|
|
XP_LOGFF( "(lc=%s)", gdd->lc );
|
|
DictDownState* dds = (DictDownState*)closure;
|
|
if ( !!gdd->data
|
|
&& 0 < gdd->len
|
|
&& storeAsDict( dds->globals, gdd ) ) {
|
|
loadAndDraw( dds->globals, &dds->invite, 0, NULL );
|
|
} else {
|
|
char msg[128];
|
|
sprintf( msg, "Unable to download %s worldlist for invitation", dds->lc );
|
|
call_alert( msg );
|
|
}
|
|
XP_FREE( dds->globals->mpool, dds );
|
|
}
|
|
|
|
static void
|
|
onDictConfirmed( void* closure, bool confirmed )
|
|
{
|
|
DictDownState* dds = (DictDownState*)closure;
|
|
if ( confirmed ) {
|
|
call_get_dict( dds->lc, onDictForInvite, dds );
|
|
} else {
|
|
XP_FREE( dds->globals->mpool, dds );
|
|
}
|
|
}
|
|
|
|
/* If you launch a URL that encodes an invitation, you'll get here. If it's
|
|
* the first time (the game hasn't been created yet) you'll get a new game
|
|
* that connects to the host. If you've already created the game, you'll be
|
|
* taken to it in whatever state it's in. If you've deleted the game, bad
|
|
* situation: you'll get a new game that will be unable to connect to any
|
|
* host.
|
|
*/
|
|
static GameState*
|
|
gameFromInvite( Globals* globals, const NetLaunchInfo* nli )
|
|
{
|
|
bool needsLoad = true;
|
|
GameState* gs = getSavedGame( globals, nli->gameID );
|
|
if ( !gs ) {
|
|
const char* lc = nli->isoCodeStr;
|
|
if ( haveDictFor(globals, lc) ) {
|
|
gs = newFromInvite( globals, nli );
|
|
} else {
|
|
char msg[128];
|
|
sprintf( msg, "Invitation requires a wordlist %s for "
|
|
"locale %s; download now?", nli->dict, lc );
|
|
|
|
DictDownState* dds = XP_MALLOC( globals->mpool, sizeof(*dds) );
|
|
dds->globals = globals;
|
|
dds->invite = *nli;
|
|
dds->lc = lc;
|
|
call_confirm(globals, msg, onDictConfirmed, dds);
|
|
}
|
|
}
|
|
return gs;
|
|
}
|
|
|
|
static GameState*
|
|
newGameState( Globals* globals )
|
|
{
|
|
GameState* gs = XP_CALLOC( globals->mpool, sizeof(*gs) );
|
|
gs->globals = globals;
|
|
#ifdef DEBUG
|
|
gs->_GUARD = GUARD_GS;
|
|
#endif
|
|
gs->next = globals->games;
|
|
globals->games = gs;
|
|
|
|
return gs;
|
|
}
|
|
|
|
static GameState*
|
|
getCurGame( Globals* globals )
|
|
{
|
|
return globals->curGame;
|
|
}
|
|
|
|
static GameState*
|
|
getSavedGame( Globals* globals, int gameID )
|
|
{
|
|
GameState* gs;
|
|
|
|
/* Is it already open? */
|
|
for ( gs = globals->games; !!gs; gs = gs->next ) {
|
|
if ( gameID == gs->gi.gameID ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !gs ) {
|
|
gs = newGameState( globals );
|
|
|
|
XP_LOGFF( "gameID: %X", gameID );
|
|
bool loaded = false;
|
|
XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(globals->mpool)
|
|
globals->vtMgr );
|
|
char gameIDStr[16];
|
|
formatGameID( gameIDStr, sizeof(gameIDStr), gameID );
|
|
const XP_UCHAR* keys[] = {KEY_GAMES, gameIDStr, KEY_GAME, NULL};
|
|
dutil_loadStream( globals->dutil, NULL_XWE, keys, stream );
|
|
if ( 0 < stream_getSize( stream ) ) {
|
|
XP_ASSERT( !gs->util );
|
|
gs->util = wasm_util_make( MPPARM(globals->mpool) &gs->gi,
|
|
globals->dutil, gs );
|
|
|
|
XP_LOGFF( "there's a saved game!!" );
|
|
loaded = game_makeFromStream( MPPARM(globals->mpool) NULL_XWE, stream,
|
|
&gs->game, &gs->gi,
|
|
gs->util, globals->draw,
|
|
&globals->cp, &globals->transportProcs );
|
|
|
|
if ( loaded ) {
|
|
loadName( gs );
|
|
ensureName( gs );
|
|
updateScreen( gs, false );
|
|
} else {
|
|
removeGameState( gs );
|
|
gs = NULL;
|
|
}
|
|
} else {
|
|
XP_LOGFF( "ERROR: no saved data for game %s", gameIDStr );
|
|
XP_ASSERT( globals->games == gs );
|
|
globals->games = gs->next;
|
|
XP_FREE( globals->mpool, gs );
|
|
gs = NULL;
|
|
}
|
|
stream_destroy( stream );
|
|
}
|
|
XP_LOGFF( "(%X) => %p", gameID, gs );
|
|
return gs;
|
|
}
|
|
|
|
static void
|
|
saveName( GameState* gs )
|
|
{
|
|
char gameIDStr[32];
|
|
formatGameID( gameIDStr, sizeof(gameIDStr), gs->gi.gameID );
|
|
const XP_UCHAR* keys[] = {KEY_GAMES, gameIDStr, KEY_NAME, NULL};
|
|
dutil_storePtr( gs->globals->dutil, NULL_XWE, keys,
|
|
(XP_U8*)gs->gameName, 1 + strlen(gs->gameName) );
|
|
}
|
|
|
|
static void
|
|
loadName( GameState* gs )
|
|
{
|
|
char gameIDStr[32];
|
|
formatGameID( gameIDStr, sizeof(gameIDStr), gs->gi.gameID );
|
|
|
|
XP_U32 len = sizeof(gs->gameName);
|
|
const XP_UCHAR* keys[] = {KEY_GAMES, gameIDStr, KEY_NAME, NULL};
|
|
dutil_loadPtr( gs->globals->dutil, NULL_XWE, keys, (XP_U8*)gs->gameName, &len );
|
|
}
|
|
|
|
static void
|
|
ensureName( GameState* gs )
|
|
{
|
|
if ( '\0' == gs->gameName[0] ) {
|
|
nameGame( gs, NULL );
|
|
}
|
|
}
|
|
|
|
static int
|
|
getNextGameNo( Globals* globals )
|
|
{
|
|
int val = 0;
|
|
const XP_UCHAR* keys[] = { KEY_NEXT_GAME, NULL };
|
|
XP_U32 len = sizeof(val);
|
|
dutil_loadPtr( globals->dutil, NULL_XWE, keys, (XP_U8*)&val, &len );
|
|
++val;
|
|
dutil_storePtr( globals->dutil, NULL_XWE, keys, (XP_U8*)&val, sizeof(val) );
|
|
XP_LOGFF( "getNextGameNo() => %d", val );
|
|
return val;
|
|
}
|
|
|
|
static void
|
|
nameGame( GameState* gs, const char* name )
|
|
{
|
|
if ( !!name ) {
|
|
snprintf( gs->gameName, sizeof(gs->gameName), "%s", name );
|
|
} else {
|
|
snprintf( gs->gameName, sizeof(gs->gameName),
|
|
"Game %d", getNextGameNo(gs->globals) );
|
|
}
|
|
saveName( gs );
|
|
XP_LOGFF( "named game: %s", gs->gameName );
|
|
}
|
|
|
|
typedef struct _FindOneState {
|
|
Globals* globals;
|
|
const char* langName;
|
|
DictionaryCtxt* dict;
|
|
} FindOneState;
|
|
|
|
static XP_Bool
|
|
onOneDict( void* closure, const XP_UCHAR* keysIn[] )
|
|
{
|
|
// XP_LOGFF( "(%s/%s/%s/%s)", keysIn[0], keysIn[1], keysIn[2], keysIn[3] );
|
|
FindOneState* fos = (FindOneState*)closure;
|
|
Globals* globals = fos->globals;
|
|
XW_DUtilCtxt* dutil = globals->dutil;
|
|
/* I've got a dict directory with its language name. IF the name's a
|
|
match, or if I'm not looking for a match, load and make a dict from
|
|
it */
|
|
char langName[32];
|
|
XP_U32 len = sizeof(langName);
|
|
const XP_UCHAR* keys[] = { keysIn[0], keysIn[1], KEY_LANG_NAME, NULL };
|
|
dutil_loadPtr( dutil, NULL_XWE, keys, langName, &len );
|
|
XP_ASSERT( 0 < len );
|
|
// XP_LOGFF( "langName: %s", langName );
|
|
|
|
bool useIt = NULL == fos->langName || 0 == strcmp( fos->langName, langName );
|
|
if ( useIt ) {
|
|
const XP_UCHAR* dictName = keysIn[3];
|
|
// XP_LOGFF( "dictName: %s", dictName );
|
|
XP_ASSERT( !fos->dict );
|
|
|
|
XP_U32 len;
|
|
uint8_t* ptr = wasm_dutil_mallocAndLoad( dutil, keysIn, &len );
|
|
if ( !!ptr ) {
|
|
fos->dict = wasm_dictionary_make( globals, dictName, ptr, len );
|
|
XP_FREE( globals->mpool, ptr );
|
|
}
|
|
}
|
|
return NULL == fos->dict;
|
|
}
|
|
|
|
static DictionaryCtxt*
|
|
loadAnyDict( Globals* globals, const char* langName )
|
|
{
|
|
FindOneState fos = {.globals = globals,
|
|
.langName = langName,
|
|
};
|
|
const XP_UCHAR* keys[] = {KEY_DICTS, KEY_WILDCARD, KEY_DICTS, KEY_WILDCARD, NULL};
|
|
dutil_forEach( globals->dutil, NULL_XWE, keys, onOneDict, &fos );
|
|
LOG_RETURNF( "%p", fos.dict );
|
|
return fos.dict;
|
|
}
|
|
|
|
typedef struct _FindLCState {
|
|
Globals* globals;
|
|
const char* langName;
|
|
char* lc;
|
|
} FindLCState;
|
|
|
|
static XP_Bool
|
|
onOneLangName( void* closure, const XP_UCHAR* keys[] )
|
|
{
|
|
bool found = false;
|
|
FindLCState* lcs = (FindLCState*)closure;
|
|
|
|
char langName[32];
|
|
XP_U32 len = sizeof(langName);
|
|
dutil_loadPtr( lcs->globals->dutil, NULL_XWE, keys, langName, &len );
|
|
|
|
if ( 0 == strcmp( lcs->langName, langName ) ) {
|
|
strcpy( lcs->lc, keys[1] );
|
|
found = true;
|
|
}
|
|
return !found;
|
|
}
|
|
|
|
static void
|
|
loadAndDraw( Globals* globals, const NetLaunchInfo* nli,
|
|
int gameID, NewGameParams* params )
|
|
{
|
|
XP_LOGFF( "(gameID: %X)", gameID );
|
|
GameState* gs = NULL;
|
|
|
|
if ( !params ) {
|
|
/* First, load any saved game. We need it e.g. to confirm that an incoming
|
|
invite is a dup and should be dropped. */
|
|
if ( 0 != gameID ) {
|
|
gs = getSavedGame( globals, gameID );
|
|
XP_LOGFF( "got game id %X", gameID );
|
|
}
|
|
if ( !!nli ) { /* overwrite gs is ok: we'll likely want both games */
|
|
gs = gameFromInvite( globals, nli );
|
|
}
|
|
}
|
|
|
|
if ( !gs ) {
|
|
const char* langName = NULL == params ? NULL : params->langName;
|
|
DictionaryCtxt* dict = loadAnyDict( globals, langName );
|
|
if ( !!dict ) {
|
|
char playerName[32];
|
|
main_getLocalName( globals, playerName, sizeof(playerName) );
|
|
|
|
gs = newGameState( globals );
|
|
gs->gi.serverRole = !!params && !params->isRobot
|
|
? SERVER_ISSERVER : SERVER_STANDALONE;
|
|
|
|
gs->gi.phoniesAction = PHONIES_WARN;
|
|
gs->gi.hintsNotAllowed = !!params && !params->allowHints || false;
|
|
gs->gi.inDuplicateMode = !!params && params->dupMode;
|
|
gs->gi.gameID = 0;
|
|
XP_STRNCPY( gs->gi.isoCodeStr, dict_getISOCode(dict), VSIZE(gs->gi.isoCodeStr) );
|
|
replaceStringIfDifferent( globals->mpool, &gs->gi.dictName,
|
|
dict_getShortName(dict) );
|
|
gs->gi.nPlayers = 2;
|
|
gs->gi.boardSize = 15;
|
|
gs->gi.traySize = gs->gi.bingoMin = 7;
|
|
gs->gi.players[0].name = copyString( globals->mpool, playerName );
|
|
gs->gi.players[0].isLocal = XP_TRUE;
|
|
gs->gi.players[0].robotIQ = 0;
|
|
|
|
bool otherLocal = !!params ? params->isRobot : true;
|
|
gs->gi.players[1].name = copyString( globals->mpool,
|
|
otherLocal ? "Robot" : "(remote)" );
|
|
gs->gi.players[1].isLocal = otherLocal;
|
|
XP_LOGFF( "set isLocal[1]: %d", gs->gi.players[1].isLocal );
|
|
gs->gi.players[1].robotIQ = 99; /* doesn't matter if remote */
|
|
|
|
gs->util = wasm_util_make( MPPARM(globals->mpool) &gs->gi,
|
|
globals->dutil, gs );
|
|
|
|
XP_LOGFF( "calling game_makeNewGame()" );
|
|
const CommsAddrRec* selfAddr = NULL;
|
|
CommsAddrRec _selfAddr;
|
|
if ( SERVER_STANDALONE != gs->gi.serverRole ) {
|
|
makeSelfAddr( globals, &_selfAddr );
|
|
selfAddr = &_selfAddr;
|
|
}
|
|
|
|
const CommsAddrRec* hostAddr = NULL;
|
|
game_makeNewGame( MPPARM(globals->mpool) NULL_XWE,
|
|
&gs->game, &gs->gi,
|
|
selfAddr, hostAddr,
|
|
gs->util, globals->draw,
|
|
&globals->cp, &globals->transportProcs );
|
|
|
|
ensureName( gs );
|
|
dict_unref( dict, NULL_XWE );
|
|
}
|
|
}
|
|
if ( !!gs ) {
|
|
startGame( gs );
|
|
}
|
|
LOG_RETURN_VOID();
|
|
}
|
|
|
|
static bool
|
|
isVisible( GameState* gs )
|
|
{
|
|
return getCurGame(gs->globals) == gs;
|
|
}
|
|
|
|
void
|
|
main_gameFromInvite( Globals* globals, const NetLaunchInfo* invite )
|
|
{
|
|
GameState* gs = gameFromInvite( globals, invite );
|
|
if ( gs ) {
|
|
startGame( gs );
|
|
}
|
|
}
|
|
|
|
typedef struct _OpenForMessageState {
|
|
Globals* globals;
|
|
int gameID;
|
|
CommsAddrRec from;
|
|
XWStreamCtxt* stream;
|
|
} OpenForMessageState;
|
|
|
|
void
|
|
main_onGameMessage( Globals* globals, XP_U32 gameID,
|
|
const CommsAddrRec* from, const XP_U8* buf,
|
|
XP_U16 len )
|
|
{
|
|
GameState* gs = getSavedGame( globals, gameID );
|
|
if ( !!gs ) {
|
|
XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(gs->globals->mpool)
|
|
gs->globals->vtMgr );
|
|
stream_putBytes( stream, buf, len );
|
|
if ( game_receiveMessage( &gs->game, NULL_XWE, stream, from ) ) {
|
|
updateScreen( gs, true );
|
|
}
|
|
stream_destroy( stream );
|
|
if ( !globals->focussed && GRANTED == js_getHaveNotifyPerm() ) {
|
|
GameSummary summary;
|
|
game_summarize( &gs->game, &gs->gi, &summary );
|
|
if ( summary.turnIsLocal && 0 <= summary.turn ) {
|
|
char buf[128];
|
|
sprintf( buf, "Your turn in game %s", gs->gameName );
|
|
js_notify( buf );
|
|
}
|
|
}
|
|
} else {
|
|
dvc_makeMQTTNoSuchGames( globals->dutil, NULL_XWE,
|
|
onMsgAndTopic, NULL,
|
|
&from->u.mqtt.devID, gameID );
|
|
|
|
XP_LOGFF( "Dropping move for deleted game (id: %X/%d)",
|
|
gameID, gameID );
|
|
}
|
|
}
|
|
|
|
void
|
|
main_onCtrlReceived( Globals* globals, const XP_U8* buf, XP_U16 len )
|
|
{
|
|
XP_LOGFF("(len=%d)", len );
|
|
XP_ASSERT(0);
|
|
}
|
|
|
|
void
|
|
main_onGameGone( Globals* globals, XP_U32 gameID )
|
|
{
|
|
GameState* gs = getSavedGame( globals, gameID );
|
|
if ( !!gs ) {
|
|
char msg[128];
|
|
sprintf( msg, "The game %s (id=%X) has been deleted on the remote "
|
|
"device. Delete here too?", gs->gameName, gameID );
|
|
call_confirm( globals, msg, onDeleteConfirmed, gs );
|
|
}
|
|
}
|
|
|
|
void
|
|
main_sendOnClose( XWStreamCtxt* stream, XWEnv env, void* closure )
|
|
{
|
|
CAST_GS(GameState*, gs, closure );
|
|
XP_LOGFF( "called with msg of len %d", stream_getSize(stream) );
|
|
(void)comms_send( gs->game.comms, NULL_XWE, stream );
|
|
}
|
|
|
|
void
|
|
main_playerScoreHeld( GameState* gs, XP_U16 player )
|
|
{
|
|
LastMoveInfo lmi;
|
|
XP_UCHAR buf[128];
|
|
if ( model_getPlayersLastScore( gs->game.model, NULL_XWE, player, &lmi ) ) {
|
|
switch ( lmi.moveType ) {
|
|
case ASSIGN_TYPE:
|
|
XP_SNPRINTF( buf, sizeof(buf), "Tiles assigned to %s", lmi.names[0] );
|
|
break;
|
|
case MOVE_TYPE:
|
|
XP_SNPRINTF( buf, sizeof(buf), "%s formed %s for %d points", lmi.names[0],
|
|
lmi.word, lmi.score );
|
|
break;
|
|
case TRADE_TYPE:
|
|
XP_SNPRINTF( buf, sizeof(buf), "%s traded %d tiles", lmi.names[0],
|
|
lmi.nTiles );
|
|
break;
|
|
default:
|
|
buf[0] = '\0';
|
|
}
|
|
}
|
|
|
|
if ( buf[0] ) {
|
|
call_alert( buf );
|
|
}
|
|
}
|
|
|
|
void
|
|
main_showRemaining( GameState* gs )
|
|
{
|
|
XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(gs->globals->mpool)
|
|
gs->globals->vtMgr );
|
|
board_formatRemainingTiles( gs->game.board, NULL_XWE, stream );
|
|
stream_putU8( stream, 0 );
|
|
call_alert( (const XP_UCHAR*)stream_getPtr( stream ) );
|
|
stream_destroy( stream );
|
|
}
|
|
|
|
static void
|
|
openConfirmed(void* closure, bool confirmed)
|
|
{
|
|
if ( confirmed ) {
|
|
CAST_GS(GameState*, gs, closure);
|
|
loadAndDraw( gs->globals, NULL, gs->gi.gameID, NULL );
|
|
}
|
|
}
|
|
|
|
void
|
|
main_turnChanged( GameState* gs, int newTurn )
|
|
{
|
|
if ( 0 <= newTurn && !isVisible(gs) && gs->gi.players[newTurn].isLocal ) {
|
|
char msg[128];
|
|
snprintf( msg, sizeof(msg),
|
|
"It's your turn in game \"%s\". Would you like to open it now?",
|
|
gs->gameName );
|
|
call_confirm( gs->globals, msg, openConfirmed, gs );
|
|
}
|
|
}
|
|
|
|
static void
|
|
onChatReceiptDone( void* closure, const char* button )
|
|
{
|
|
if ( 0 == strcmp( BUTTON_REPLY, button ) ) {
|
|
call_get_string( "Your message?", "", onChatComposed, closure );
|
|
}
|
|
}
|
|
|
|
void
|
|
main_chatReceived( GameState* gs, const char* msg )
|
|
{
|
|
char buf[256];
|
|
size_t len = snprintf( buf, sizeof(buf), "Chat message received:\n%s", msg );
|
|
if ( len < sizeof(buf)) {
|
|
const char* buttons[] = { BUTTON_REPLY, BUTTON_OK, NULL };
|
|
call_dialog( buf, buttons, onChatReceiptDone, gs );
|
|
}
|
|
}
|
|
|
|
typedef struct _BlankPickState {
|
|
GameState* gs;
|
|
int col, row;
|
|
int nTiles;
|
|
int playerNum;
|
|
} BlankPickState;
|
|
|
|
static void
|
|
onBlankPicked( void* closure, const char* str )
|
|
{
|
|
XP_LOGFF( "index: %s", str );
|
|
BlankPickState* bps = (BlankPickState*)closure;
|
|
GameState* gs = bps->gs;
|
|
Globals* globals = gs->globals;
|
|
|
|
int indx = atoi(str);
|
|
if ( 0 <= indx && indx < bps->nTiles
|
|
&& board_setBlankValue( gs->game.board, bps->playerNum,
|
|
bps->col, bps->row, indx ) ) {
|
|
updateScreen( gs, true );
|
|
}
|
|
|
|
XP_FREE( globals->mpool, bps );
|
|
}
|
|
|
|
void
|
|
main_pickBlank( GameState* gs, int playerNum, int col, int row,
|
|
const char** tileFaces, int nTiles )
|
|
{
|
|
BlankPickState* bps = XP_MALLOC( gs->globals->mpool, sizeof(*bps) );
|
|
bps->gs = gs;
|
|
bps->row = row;
|
|
bps->col = col;
|
|
bps->playerNum = playerNum;
|
|
bps->nTiles = nTiles;
|
|
call_pickBlank( "Pick a tile for your blank", tileFaces, nTiles,
|
|
onBlankPicked, bps );
|
|
}
|
|
|
|
void
|
|
main_showGameOver( GameState* gs )
|
|
{
|
|
if ( isVisible(gs) ) {
|
|
XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(gs->globals->mpool)
|
|
gs->globals->vtMgr );
|
|
server_writeFinalScores( gs->game.server, NULL_XWE, stream );
|
|
stream_putU8( stream, 0 );
|
|
call_alert( (const XP_UCHAR*)stream_getPtr( stream ) );
|
|
stream_destroy( stream );
|
|
}
|
|
}
|
|
|
|
static time_t
|
|
getCurMS()
|
|
{
|
|
struct timeval tv;
|
|
gettimeofday( &tv, NULL );
|
|
time_t result = tv.tv_sec * 1000; /* convert to millis */
|
|
result += tv.tv_usec / 1000; /* convert to millis too */
|
|
// LOG_RETURNF( "%x", result );
|
|
return result;
|
|
}
|
|
|
|
void
|
|
main_clear_timer( GameState* gs, XWTimerReason why )
|
|
{
|
|
XP_LOGFF( "why: %d" );
|
|
// XP_ASSERT(0); fires when start new game
|
|
}
|
|
|
|
typedef struct _TimerClosure {
|
|
GameState* gs;
|
|
XWTimerReason why;
|
|
XWTimerProc proc;
|
|
void* closure;
|
|
} TimerClosure;
|
|
|
|
static void
|
|
onTimerFired( void* closure )
|
|
{
|
|
LOG_FUNC();
|
|
TimerClosure* tc = (TimerClosure*)closure;
|
|
XP_Bool draw = (*tc->proc)( tc->closure, NULL, tc->why );
|
|
if ( draw ) {
|
|
updateScreen( tc->gs, true );
|
|
}
|
|
XP_FREE( tc->gs->globals->mpool, tc );
|
|
}
|
|
|
|
void
|
|
main_set_timer( GameState* gs, XWTimerReason why, XP_U16 when,
|
|
XWTimerProc proc, void* closure )
|
|
{
|
|
XP_LOGFF( "why: %d", why );
|
|
TimerClosure* tc = XP_MALLOC( gs->globals->mpool, sizeof(*tc) );
|
|
tc->gs = gs;
|
|
tc->proc = proc;
|
|
tc->closure = closure;
|
|
tc->why = why;
|
|
|
|
if ( 0 == when ) {
|
|
when = 1;
|
|
}
|
|
when *= 1000; /* convert to ms */
|
|
|
|
jscallback_set( onTimerFired, tc, when );
|
|
}
|
|
|
|
typedef struct _QueryState {
|
|
GameState* gs;
|
|
QueryProc proc;
|
|
void* closure;
|
|
} QueryState;
|
|
|
|
static void
|
|
onQueryCalled( void* closure, const char* button )
|
|
{
|
|
QueryState* qs = (QueryState*)closure;
|
|
bool ok = 0 == strcmp( button, BUTTON_OK );
|
|
(*qs->proc)( qs->closure, ok );
|
|
XP_FREE( qs->gs->globals->mpool, qs );
|
|
}
|
|
|
|
void
|
|
main_query( GameState* gs, const XP_UCHAR* query, QueryProc proc, void* closure )
|
|
{
|
|
if ( isVisible(gs) ) {
|
|
QueryState* qs = XP_MALLOC( gs->globals->mpool, sizeof(*qs) );
|
|
qs->proc = proc;
|
|
qs->closure = closure;
|
|
qs->gs = gs;
|
|
|
|
const char* buttons[] = { BUTTON_CANCEL, BUTTON_OK, NULL };
|
|
call_dialog( query, buttons, onQueryCalled, qs );
|
|
}
|
|
}
|
|
|
|
void
|
|
main_alert( GameState* gs, const XP_UCHAR* msg )
|
|
{
|
|
if ( isVisible(gs) ) {
|
|
call_alert( msg );
|
|
}
|
|
}
|
|
|
|
typedef struct _IdleClosure {
|
|
GameState* gs;
|
|
IdleProc proc;
|
|
void* closure;
|
|
} IdleClosure;
|
|
|
|
static void
|
|
onIdleFired( void* closure )
|
|
{
|
|
LOG_FUNC();
|
|
IdleClosure* ic = (IdleClosure*)closure;
|
|
XP_Bool draw = (*ic->proc)(ic->closure);
|
|
if ( draw ) {
|
|
updateScreen( ic->gs, true );
|
|
}
|
|
XP_FREE( ic->gs->globals->mpool, ic );
|
|
}
|
|
|
|
void
|
|
main_set_idle( GameState* gs, IdleProc proc, void* closure )
|
|
{
|
|
LOG_FUNC();
|
|
IdleClosure* ic = XP_MALLOC( gs->globals->mpool, sizeof(*ic) );
|
|
ic->gs = gs;
|
|
ic->proc = proc;
|
|
ic->closure = closure;
|
|
|
|
jscallback_set( onIdleFired, ic, 0 );
|
|
}
|
|
|
|
static XP_Bool
|
|
checkForEvent( GameState* gs )
|
|
{
|
|
XP_Bool handled;
|
|
XP_Bool draw = XP_FALSE;
|
|
BoardCtxt* board = gs->game.board;
|
|
|
|
SDL_Event event;
|
|
if ( SDL_PollEvent(&event) ) {
|
|
switch ( event.type ) {
|
|
case SDL_MOUSEBUTTONDOWN:
|
|
draw = event.button.button == SDL_BUTTON_LEFT
|
|
&& board_handlePenDown( board, NULL_XWE,
|
|
event.button.x, event.button.y,
|
|
&handled );
|
|
break;
|
|
case SDL_MOUSEBUTTONUP:
|
|
draw = event.button.button == SDL_BUTTON_LEFT
|
|
&& board_handlePenUp( board, NULL_XWE,
|
|
event.button.x, event.button.y );
|
|
break;
|
|
case SDL_MOUSEMOTION:
|
|
draw = board_handlePenMove( board, NULL_XWE,
|
|
event.motion.x, event.motion.y );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// XP_LOGFF( "draw: %d", draw );
|
|
return draw;
|
|
}
|
|
|
|
static void
|
|
clearScreen( Globals* globals )
|
|
{
|
|
SDL_RenderClear( globals->renderer );
|
|
SDL_RenderPresent( globals->renderer );
|
|
}
|
|
|
|
static void
|
|
saveGame( GameState* gs )
|
|
{
|
|
Globals* globals = gs->globals;
|
|
XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(globals->mpool)
|
|
globals->vtMgr );
|
|
game_saveToStream( &gs->game, &gs->gi, stream, ++gs->saveToken );
|
|
|
|
GameSummary summary;
|
|
game_summarize( &gs->game, &gs->gi, &summary );
|
|
|
|
char gameIDStr[32];
|
|
formatGameID( gameIDStr, sizeof(gameIDStr), gs->gi.gameID );
|
|
const XP_UCHAR* keys[] = { KEY_GAMES, gameIDStr, KEY_GAME, NULL };
|
|
dutil_storeStream( globals->dutil, NULL_XWE, keys, stream );
|
|
stream_destroy( stream );
|
|
game_saveSucceeded( &gs->game, NULL_XWE, gs->saveToken );
|
|
|
|
keys[2] = KEY_SUMMARY;
|
|
dutil_storePtr( globals->dutil, NULL_XWE, keys, &summary, sizeof(summary) );
|
|
}
|
|
|
|
static void
|
|
updateScreen( GameState* gs, bool doSave )
|
|
{
|
|
Globals* globals = gs->globals;
|
|
if ( gs == getCurGame(globals) ) {
|
|
SDL_RenderClear( globals->renderer );
|
|
if ( !!gs->game.board ) {
|
|
board_draw( gs->game.board, NULL_XWE );
|
|
wasm_draw_render( globals->draw, globals->renderer );
|
|
} else {
|
|
XP_LOGFF( "no board for gid %X", gs->gi.gameID );
|
|
}
|
|
SDL_RenderPresent( globals->renderer );
|
|
XP_LOGFF( "SDL_RenderPresent returned" );
|
|
} else {
|
|
XP_LOGFF( "not drawing %s/%X; not visible", gs->gameName, gs->gi.gameID );
|
|
}
|
|
|
|
updateGameButtons( globals );
|
|
|
|
/* Let's save state here too, though likely too often */
|
|
if ( doSave ) {
|
|
saveGame( gs );
|
|
}
|
|
LOG_RETURN_VOID();
|
|
}
|
|
|
|
void
|
|
main_updateScreen( GameState* gs )
|
|
{
|
|
updateScreen( gs, true );
|
|
}
|
|
|
|
static void
|
|
onGotMissingDict( void* closure, GotDictData* gdd )
|
|
{
|
|
CAST_GLOB(Globals*, globals, closure);
|
|
if ( !!gdd->data
|
|
&& 0 < gdd->len ) {
|
|
storeAsDict( globals, gdd );
|
|
}
|
|
}
|
|
|
|
void
|
|
main_needDictForGame( GameState* gs, const char* lc,
|
|
const XP_UCHAR* dictName )
|
|
{
|
|
call_get_dict( lc, onGotMissingDict, gs->globals );
|
|
}
|
|
|
|
|
|
static XP_Bool
|
|
checkForChannel( void* closure, const XP_UCHAR* keysIn[] )
|
|
{
|
|
XP_Bool* foundp = (XP_Bool*)closure;
|
|
*foundp = XP_TRUE;
|
|
return XP_TRUE;
|
|
}
|
|
|
|
XP_Bool
|
|
main_haveGame( Globals* globals, XP_U32 gameID, XP_U8 channel )
|
|
{
|
|
char gameIDStr[32];
|
|
formatGameID( gameIDStr, sizeof(gameIDStr), gameID );
|
|
const XP_UCHAR* keys[] = {KEY_GAMES, gameIDStr, KEY_GAME, NULL};
|
|
XP_Bool found = XP_FALSE;
|
|
dutil_forEach( globals->dutil, NULL_XWE, keys,
|
|
checkForChannel, &found );
|
|
return found;
|
|
}
|
|
|
|
static void
|
|
looper( void* closure )
|
|
{
|
|
Globals* globals = (Globals*)closure;
|
|
GameState* gs = getCurGame( globals );
|
|
if ( !!gs ) {
|
|
if ( checkForEvent( gs ) ) {
|
|
updateScreen( gs, true );
|
|
}
|
|
}
|
|
wasm_dutil_syncIf( globals->dutil );
|
|
|
|
#ifdef MEM_DEBUG
|
|
if ( mpool_getStats( globals->mpool, &globals->mpstats ) ) {
|
|
show_pool(globals->mpstats.curBytes, globals->mpstats.maxBytes);
|
|
/* XP_LOGFF( "mempool: cur: %d; max: %d", globals->mpstats.curBytes, */
|
|
/* globals->mpstats.maxBytes ); */
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void
|
|
MQTTConnectedChanged( void* closure, bool connected )
|
|
{
|
|
if ( connected ) {
|
|
CAST_GLOB(Globals*, globals, closure);
|
|
GameState* gs = getCurGame( globals );
|
|
if ( !!gs && !!gs->game.comms ) {
|
|
comms_resendAll( gs->game.comms, NULL_XWE, COMMS_CONN_MQTT, XP_TRUE );
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
cbckMsg( MsgProc proc, void* closure, const char* topic,
|
|
int len, const uint8_t* msg )
|
|
{
|
|
(*proc)( closure, topic, msg, len );
|
|
}
|
|
|
|
static void
|
|
onAllowNotify(void* closure, bool confirmed)
|
|
{
|
|
NewGameParams* ngp = (NewGameParams*)closure;
|
|
Globals* globals = ngp->globals;
|
|
if ( confirmed ) {
|
|
js_requestNotify();
|
|
}
|
|
loadAndDraw( globals, NULL, 0, ngp );
|
|
updateDeviceButtons( globals );
|
|
XP_FREE( globals->mpool, ngp );
|
|
}
|
|
|
|
void
|
|
onNewGame( void* closure, bool isRobot, const char* langName,
|
|
bool allowHints, bool dupMode )
|
|
{
|
|
CAST_GLOB(Globals*, globals, closure);
|
|
|
|
NewGameParams* ngp = XP_CALLOC( globals->mpool, sizeof(*ngp) );
|
|
ngp->allowHints = allowHints;
|
|
ngp->isRobot = isRobot;
|
|
ngp->dupMode = dupMode;
|
|
strcpy( ngp->langName, langName );
|
|
const XP_UCHAR* keys[] = {KEY_NEWGAME_DFLTS, NULL};
|
|
dutil_storePtr( globals->dutil, NULL_XWE, keys, ngp, sizeof(*ngp) );
|
|
ngp->globals = globals;
|
|
|
|
if ( !isRobot && UNREQUESTED == js_getHaveNotifyPerm() ) {
|
|
const char* msg = "You are creating a networked game. Would you like "
|
|
"notifications when a move arrives and it becomes your turn?";
|
|
call_confirm( globals, msg, onAllowNotify, ngp );
|
|
} else {
|
|
onAllowNotify(ngp, false);
|
|
}
|
|
}
|
|
|
|
/* Called from js with a proc and closure */
|
|
void
|
|
cbckVoid( JSCallback proc, void* closure )
|
|
{
|
|
LOG_FUNC();
|
|
(*proc)(closure);
|
|
}
|
|
|
|
/* Called from js with a proc, closure, and string */
|
|
void
|
|
cbckString( StringProc proc, void* closure, const char* str )
|
|
{
|
|
if ( !!proc ) {
|
|
(*proc)( closure, str );
|
|
}
|
|
}
|
|
|
|
void
|
|
gotDictBinary( GotDictProc proc, void* closure, const char* xwd,
|
|
const char* lc, const char* langName,
|
|
uint8_t* data, int len )
|
|
{
|
|
XP_LOGFF( "lc: %s, langName: %s, xwd: %s; len: %d",
|
|
lc, langName, xwd, len );
|
|
GotDictData gdd = { .lc = lc,
|
|
.dictName = xwd,
|
|
.langName = langName,
|
|
.data = data,
|
|
.len = len,
|
|
};
|
|
(*proc)(closure, &gdd);
|
|
}
|
|
|
|
void
|
|
onResize( void* closure, int width, int height )
|
|
{
|
|
CAST_GLOB( Globals*, globals, closure );
|
|
XP_LOGFF( "width=%d, height=%d)", width, height );
|
|
initWindow( globals, width, height );
|
|
updateGameButtons( globals );
|
|
updateDeviceButtons( globals );
|
|
}
|
|
|
|
/* On first launch, we may have an invitation. Or not. We want to ask for a
|
|
* local name (but only once), then download any wordlist we need, then open
|
|
* any pre-existing game or else a new one.
|
|
*/
|
|
|
|
typedef struct _LaunchState {
|
|
Globals* globals;
|
|
NetLaunchInfo nli;
|
|
char playerName[32];
|
|
bool hadName;
|
|
} LaunchState;
|
|
|
|
static void
|
|
storeOrAlert( Globals* globals, GotDictData* gdd )
|
|
{
|
|
if ( !!gdd ) {
|
|
if ( 0 == gdd->len || !storeAsDict( globals, gdd ) ) {
|
|
call_alert( "Unable to download wordlist. Reload the page to try again?" );
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
onGotInviteDictAtLaunch( void* closure, GotDictData* gdd )
|
|
{
|
|
LaunchState* ls = (LaunchState*)closure;
|
|
Globals* globals = ls->globals;
|
|
|
|
storeOrAlert( globals, gdd );
|
|
|
|
/* We're ready to start. If we had an invitation, launch for it. Otherwise
|
|
launch the last game that was open */
|
|
NetLaunchInfo* nlip = '\0' == ls->nli.isoCodeStr[0] ? NULL : &ls->nli;
|
|
int gameID = 0;
|
|
XP_U32 len = sizeof(gameID);
|
|
const XP_UCHAR* keys[] = {KEY_LAST_GID, NULL};
|
|
dutil_loadPtr( globals->dutil, NULL_XWE, keys, (XP_U8*)&gameID, &len );
|
|
XP_LOGFF( "loaded KEY_LAST_GID: %X", gameID );
|
|
loadAndDraw( globals, nlip, gameID, NULL );
|
|
|
|
updateDeviceButtons( globals );
|
|
|
|
XP_FREE( globals->mpool, ls );
|
|
}
|
|
|
|
static void
|
|
onGotNativeDictAtLaunch( void* closure, GotDictData* gdd )
|
|
{
|
|
LaunchState* ls = (LaunchState*)closure;
|
|
Globals* globals = ls->globals;
|
|
// XP_LOGFF("(gdd: %p)", gdd );
|
|
storeOrAlert( globals, gdd );
|
|
|
|
/* Now download a wordlist if we need one */
|
|
const char* neededLC = NULL;
|
|
if ( '\0' != ls->nli.isoCodeStr[0] ) { /* 0 means unset: no invite */
|
|
if ( !haveDictFor(ls->globals, ls->nli.isoCodeStr) ) {
|
|
neededLC = ls->nli.isoCodeStr;
|
|
}
|
|
}
|
|
if ( !!neededLC ) {
|
|
call_get_dict( neededLC, onGotInviteDictAtLaunch, ls );
|
|
} else {
|
|
onGotInviteDictAtLaunch( ls, NULL);
|
|
}
|
|
}
|
|
|
|
static void
|
|
onPlayerNamedAtLaunch( void* closure, const char* responseName )
|
|
{
|
|
LaunchState* ls = (LaunchState*)closure;
|
|
Globals* globals = ls->globals;
|
|
// XP_LOGFF("(name: %s)", responseName );
|
|
|
|
/* Did user change name? Save it */
|
|
if ( !!responseName && 0 != strcmp( responseName, ls->playerName ) ) {
|
|
onPlayerNamed( globals, responseName );
|
|
} else if ( !ls->hadName ) {
|
|
onPlayerNamed( globals, ls->playerName );
|
|
|
|
char buf[128];
|
|
sprintf( buf, "Ok. Using default name %s. You can change it anytime "
|
|
"using the \"%s\" button.", ls->playerName, BUTTON_NAME );
|
|
call_alert( buf );
|
|
}
|
|
|
|
if ( 0 == countLangs( globals ) ) {
|
|
call_get_dict( NULL, onGotNativeDictAtLaunch, ls );
|
|
} else {
|
|
onGotNativeDictAtLaunch( ls, NULL );
|
|
}
|
|
}
|
|
|
|
static void
|
|
startLaunchSequence( Globals* globals, NetLaunchInfo* nli )
|
|
{
|
|
LaunchState* ls = XP_CALLOC( globals->mpool, sizeof(*ls) );
|
|
ls->globals = globals;
|
|
if ( NULL != nli ) {
|
|
ls->nli = *nli;
|
|
}
|
|
|
|
/* No saved name? Ask. Politely */
|
|
if ( main_getLocalName( globals, ls->playerName, sizeof(ls->playerName) ) ) {
|
|
ls->hadName = true;
|
|
onPlayerNamedAtLaunch( ls, NULL );
|
|
} else {
|
|
call_get_string( "Please choose a name for your player. It's what you "
|
|
"and others will see in the scoreboard", ls->playerName,
|
|
onPlayerNamedAtLaunch, ls );
|
|
}
|
|
}
|
|
|
|
void
|
|
mainPostSync( int argc, const char** argv )
|
|
{
|
|
XP_LOGFF( "(argc=%d)", argc );
|
|
time_t now = getCurMS();
|
|
srandom( now );
|
|
XP_LOGFF( "called(srandom( %x )", now );
|
|
|
|
Globals* globals;
|
|
#ifdef GLOBALS_ON_STACK
|
|
Globals _globals = {0};
|
|
globals = &_globals;
|
|
#else
|
|
globals = calloc(1, sizeof(*globals));
|
|
#endif
|
|
#ifdef DEBUG
|
|
globals->_GUARD = GUARD_GLOB;
|
|
#endif
|
|
initDeviceGlobals( globals ); /* takes care of getting mqtt devid */
|
|
|
|
NetLaunchInfo nli = {0};
|
|
NetLaunchInfo* nlip = NULL;
|
|
if ( nli_fromArgv( MPPARM(globals->mpool) &nli, argc, argv ) ) {
|
|
nlip = &nli;
|
|
}
|
|
|
|
SDL_Init( SDL_INIT_EVENTS );
|
|
TTF_Init();
|
|
|
|
startLaunchSequence( globals, nlip );
|
|
|
|
#ifdef GLOBALS_ON_STACK
|
|
emscripten_set_main_loop_arg( looper, globals, -1, 1 );
|
|
#else
|
|
emscripten_set_main_loop_arg( looper, globals, -1, 0 );
|
|
#endif
|
|
}
|
|
|
|
EM_JS( void, loadDBThen, (const char* root, int argc, const char** argv), {
|
|
let jsroot = UTF8ToString(root);
|
|
FS.mkdir(jsroot);
|
|
FS.mount(IDBFS, {}, jsroot);
|
|
FS.syncfs(true, function (err) {
|
|
if ( err ) {
|
|
console.log('fsSyncIn: err: ' + err);
|
|
}
|
|
ccall('mainPostSync', null, ['number', 'number'], [argc, argv]);
|
|
});
|
|
});
|
|
|
|
int
|
|
main( int argc, const char** argv )
|
|
{
|
|
XP_LOGFF( "MAIN ENTRY(argc=%d)", argc );
|
|
loadDBThen( ROOT_PATH, argc, argv );
|
|
return 0;
|
|
}
|