mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2024-11-18 10:08:29 +01:00
2394 lines
71 KiB
C
2394 lines
71 KiB
C
/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */
|
|
/*
|
|
* Copyright 1997 - 2002 by Eric House (fixin@peak.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.
|
|
*/
|
|
|
|
/* #include <assert.h> */
|
|
|
|
#include "comtypes.h"
|
|
#include "server.h"
|
|
#include "util.h"
|
|
#include "model.h"
|
|
#include "comms.h"
|
|
#include "memstream.h"
|
|
#include "game.h"
|
|
/* #include "board.h" */
|
|
#include "states.h"
|
|
#include "xwproto.h"
|
|
#include "util.h"
|
|
#include "pool.h"
|
|
#include "engine.h"
|
|
#include "strutils.h"
|
|
|
|
#include "LocalizedStrIncludes.h"
|
|
|
|
#ifdef CPLUS
|
|
extern "C" {
|
|
#endif
|
|
|
|
#define sEND 0x73454e44
|
|
|
|
#define MAX_PASSES 2 /* how many times can all players pass? */
|
|
|
|
#define LOCAL_ADDR NULL
|
|
|
|
#define IS_ROBOT(p) ((p)->isRobot)
|
|
#define IS_LOCAL(p) ((p)->isLocal)
|
|
|
|
enum {
|
|
END_REASON_USER_REQUEST,
|
|
END_REASON_OUT_OF_TILES,
|
|
END_REASON_TOO_MANY_PASSES
|
|
};
|
|
typedef XP_U8 GameEndReason;
|
|
|
|
typedef struct ServerPlayer {
|
|
EngineCtxt* engine; /* each needs his own so don't interfere each other */
|
|
XP_S8 deviceIndex; /* 0 means local, -1 means unknown */
|
|
} ServerPlayer;
|
|
|
|
#define UNKNOWN_DEVICE -1
|
|
#define SERVER_DEVICE 0
|
|
|
|
typedef struct RemoteAddress {
|
|
XP_PlayerAddr channelNo;
|
|
} RemoteAddress;
|
|
|
|
/* These are the parts of the server's state that needs to be preserved
|
|
across a reset/new game */
|
|
typedef struct ServerVolatiles {
|
|
ModelCtxt* model;
|
|
CommsCtxt* comms;
|
|
XW_UtilCtxt* util;
|
|
CurGameInfo* gi;
|
|
TurnChangeListener turnChangeListener;
|
|
void* turnChangeData;
|
|
GameOverListener gameOverListener;
|
|
void* gameOverData;
|
|
XWStreamCtxt* prevMoveStream; /* save it to print later */
|
|
XW_State stateAfterShow; /* do I need to serialize this? What if
|
|
someone quits before I can show the
|
|
scores? PENDING(ehouse) */
|
|
XP_Bool showPrevMove;
|
|
} ServerVolatiles;
|
|
|
|
typedef struct ServerNonvolatiles {
|
|
XP_U16 nPassesInRow;
|
|
|
|
XP_U8 nDevices;
|
|
XW_State gameState;
|
|
XP_S8 currentTurn; /* invalid when game is over */
|
|
XP_U8 pendingRegistrations;
|
|
XP_Bool showRobotScores;
|
|
|
|
RemoteAddress addresses[MAX_NUM_PLAYERS];
|
|
|
|
/* TrayTileSet tradedTiles; */
|
|
} ServerNonvolatiles;
|
|
|
|
struct ServerCtxt {
|
|
ServerVolatiles vol;
|
|
ServerNonvolatiles nv;
|
|
|
|
PoolContext* pool;
|
|
|
|
BadWordInfo illegalWordInfo;
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
XP_U16 lastMoveSource;
|
|
#endif
|
|
|
|
ServerPlayer players[MAX_NUM_PLAYERS];
|
|
XP_Bool serverDoing;
|
|
MPSLOT
|
|
};
|
|
|
|
/******************************* prototypes *******************************/
|
|
static void assignTilesToAll( ServerCtxt* server );
|
|
static void resetEngines( ServerCtxt* server );
|
|
static void nextTurn( ServerCtxt* server, XP_S16 nxtTurn );
|
|
|
|
static void doEndGame( ServerCtxt* server );
|
|
static void endGameInternal( ServerCtxt* server, GameEndReason why );
|
|
static void badWordMoveUndoAndTellUser( ServerCtxt* server,
|
|
BadWordInfo* bwi );
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
static XP_Bool handleRegistrationMsg( ServerCtxt* server,
|
|
XWStreamCtxt* stream );
|
|
static void registerRemotePlayer( ServerCtxt* server, XWStreamCtxt* stream );
|
|
static void server_sendInitialMessage( ServerCtxt* server );
|
|
static void sendBadWordMsgs( ServerCtxt* server );
|
|
static XP_Bool handleIllegalWord( ServerCtxt* server,
|
|
XWStreamCtxt* incomming );
|
|
static void tellMoveWasLegal( ServerCtxt* server );
|
|
|
|
#endif
|
|
|
|
#define PICK_NEXT -1
|
|
|
|
/*****************************************************************************
|
|
*
|
|
****************************************************************************/
|
|
static void
|
|
initServer( ServerCtxt* server )
|
|
{
|
|
XP_U16 i;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
LocalPlayer* lp;
|
|
ServerPlayer* player;
|
|
|
|
server->nv.currentTurn = -1; /* game isn't under way yet */
|
|
|
|
if ( server->vol.gi->serverRole == SERVER_ISCLIENT ) {
|
|
server->nv.gameState = XWSTATE_NONE;
|
|
} else {
|
|
server->nv.gameState = XWSTATE_BEGIN;
|
|
}
|
|
|
|
lp = gi->players;
|
|
player = server->players;
|
|
for ( i = 0; i < gi->nPlayers; ++i, ++lp, ++player ) {
|
|
if ( !lp->isLocal/* && !lp->name */ ) {
|
|
++server->nv.pendingRegistrations;
|
|
}
|
|
|
|
player->deviceIndex = lp->isLocal? SERVER_DEVICE : UNKNOWN_DEVICE;
|
|
|
|
}
|
|
|
|
server->nv.nDevices = 1; /* local device (0) is always there */
|
|
|
|
} /* initServer */
|
|
|
|
ServerCtxt*
|
|
server_make( MPFORMAL ModelCtxt* model, CommsCtxt* comms, XW_UtilCtxt* util )
|
|
{
|
|
ServerCtxt* result = (ServerCtxt*)XP_MALLOC( mpool, sizeof(*result) );
|
|
|
|
if ( result != NULL ) {
|
|
XP_MEMSET( result, 0, sizeof(*result) );
|
|
|
|
MPASSIGN(result->mpool, mpool);
|
|
|
|
result->vol.model = model;
|
|
result->vol.comms = comms;
|
|
result->vol.util = util;
|
|
result->vol.gi = util->gameInfo;
|
|
|
|
initServer( result );
|
|
}
|
|
return result;
|
|
} /* server_make */
|
|
|
|
static void
|
|
getNV( XWStreamCtxt* stream, ServerNonvolatiles* nv, XP_U16 nPlayers )
|
|
{
|
|
XP_U16 i;
|
|
|
|
nv->nPassesInRow = (XP_U16)stream_getBits( stream, 3 );
|
|
/* number of players is upper limit on device count */
|
|
nv->nDevices = (XP_U8)stream_getBits( stream, PLAYERNUM_NBITS );
|
|
|
|
XP_ASSERT( XWSTATE_GAMEOVER < 1<<4 );
|
|
nv->gameState = (XW_State)stream_getBits( stream, 4 );
|
|
|
|
nv->currentTurn = (XP_S8)stream_getBits( stream, NPLAYERS_NBITS ) - 1;
|
|
nv->pendingRegistrations = (XP_U8)stream_getBits( stream, NPLAYERS_NBITS );
|
|
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
nv->addresses[i].channelNo = (XP_PlayerAddr)stream_getBits( stream, 16 );
|
|
}
|
|
} /* getNV */
|
|
|
|
static void
|
|
putNV( XWStreamCtxt* stream, ServerNonvolatiles* nv, XP_U16 nPlayers )
|
|
{
|
|
XP_U16 i;
|
|
|
|
/* Gross hack: with four players nPassesInRow can get bigger than 8.
|
|
Rather than change the data format I'm just capping it. */
|
|
stream_putBits( stream, 3, nv->nPassesInRow & 0x0007 );
|
|
/* number of players is upper limit on device count */
|
|
stream_putBits( stream, PLAYERNUM_NBITS, nv->nDevices );
|
|
|
|
XP_ASSERT( XWSTATE_GAMEOVER < 1<<4 );
|
|
stream_putBits( stream, 4, nv->gameState );
|
|
|
|
/* +1: make -1 (NOTURN) into a positive number */
|
|
stream_putBits( stream, NPLAYERS_NBITS, nv->currentTurn+1 );
|
|
stream_putBits( stream, NPLAYERS_NBITS, nv->pendingRegistrations );
|
|
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
stream_putBits( stream, 16, nv->addresses[i].channelNo );
|
|
}
|
|
} /* putNV */
|
|
|
|
ServerCtxt*
|
|
server_makeFromStream( MPFORMAL XWStreamCtxt* stream, ModelCtxt* model,
|
|
CommsCtxt* comms, XW_UtilCtxt* util, XP_U16 nPlayers )
|
|
{
|
|
ServerCtxt* server;
|
|
short i;
|
|
CurGameInfo* gi = util->gameInfo;
|
|
|
|
server = server_make( MPPARM(mpool) model, comms, util );
|
|
getNV( stream, &server->nv, nPlayers );
|
|
|
|
if ( stream_getBits(stream, 1) != 0 ) {
|
|
server->pool = pool_makeFromStream( MPPARM(mpool) stream );
|
|
}
|
|
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
ServerPlayer* player = &server->players[i];
|
|
|
|
player->deviceIndex = stream_getU8( stream );
|
|
|
|
if ( stream_getU8( stream ) != 0 ) {
|
|
LocalPlayer* lp = &gi->players[i];
|
|
player->engine = engine_makeFromStream( MPPARM(mpool)
|
|
stream, util,
|
|
lp->isRobot );
|
|
}
|
|
}
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
server->lastMoveSource = (XP_U16)stream_getBits( stream, 2 );
|
|
#endif
|
|
|
|
XP_ASSERT( stream_getU32( stream ) == sEND );
|
|
|
|
return server;
|
|
} /* server_makeFromStream */
|
|
|
|
void
|
|
server_writeToStream( ServerCtxt* server, XWStreamCtxt* stream )
|
|
{
|
|
XP_U16 i;
|
|
XP_U16 nPlayers = server->vol.gi->nPlayers;
|
|
|
|
putNV( stream, &server->nv, nPlayers );
|
|
|
|
stream_putBits( stream, 1, server->pool != NULL );
|
|
if ( server->pool != NULL ) {
|
|
pool_writeToStream( server->pool, stream );
|
|
}
|
|
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
ServerPlayer* player = &server->players[i];
|
|
|
|
stream_putU8( stream, player->deviceIndex );
|
|
|
|
stream_putU8( stream, (XP_U8)(player->engine != NULL) );
|
|
if ( player->engine != NULL ) {
|
|
engine_writeToStream( player->engine, stream );
|
|
}
|
|
}
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
stream_putBits( stream, 2, server->lastMoveSource );
|
|
#endif
|
|
|
|
#ifdef DEBUG
|
|
stream_putU32( stream, sEND );
|
|
#endif
|
|
|
|
} /* server_writeToStream */
|
|
|
|
static void
|
|
cleanupServer( ServerCtxt* server )
|
|
{
|
|
XP_U16 i;
|
|
XP_U16 nPlayers = server->vol.gi->nPlayers;
|
|
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
ServerPlayer* player = &server->players[i];
|
|
if ( player->engine != NULL ) {
|
|
engine_destroy( player->engine );
|
|
}
|
|
}
|
|
XP_MEMSET( server->players, 0, sizeof(server->players) );
|
|
|
|
if ( server->pool != NULL ) {
|
|
pool_destroy( server->pool );
|
|
server->pool = (PoolContext*)NULL;
|
|
}
|
|
|
|
XP_MEMSET( &server->nv, 0, sizeof(server->nv) );
|
|
|
|
if ( !!server->vol.prevMoveStream ) {
|
|
stream_destroy( server->vol.prevMoveStream );
|
|
server->vol.prevMoveStream = NULL;
|
|
}
|
|
} /* cleanupServer */
|
|
|
|
void
|
|
server_reset( ServerCtxt* server )
|
|
{
|
|
ServerVolatiles vol = server->vol;
|
|
|
|
cleanupServer( server );
|
|
|
|
server->vol = vol;
|
|
initServer( server );
|
|
} /* server_reset */
|
|
|
|
void
|
|
server_destroy( ServerCtxt* server )
|
|
{
|
|
cleanupServer( server );
|
|
|
|
XP_FREE( server->mpool, server );
|
|
} /* server_destroy */
|
|
|
|
void
|
|
server_prefsChanged( ServerCtxt* server, CommonPrefs* cp )
|
|
{
|
|
server->nv.showRobotScores = cp->showRobotScores;
|
|
} /* server_prefsChanged */
|
|
|
|
XP_S16
|
|
server_countTilesInPool( ServerCtxt* server )
|
|
{
|
|
XP_S16 result = -1;
|
|
PoolContext* pool = server->pool;
|
|
if ( !!pool ) {
|
|
result = pool_getNTilesLeft( pool );
|
|
}
|
|
return result;
|
|
} /* server_countTilesInPool */
|
|
|
|
/* I'm a client device. It's my job to start the whole conversation by
|
|
* contacting the server and telling him that I exist (and some other stuff,
|
|
* including what the players here want to be called.)
|
|
*/
|
|
#define NAME_LEN_NBITS 6
|
|
#define MAX_NAME_LEN ((1<<(NAME_LEN_NBITS-1))-1)
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
void
|
|
server_initClientConnection( ServerCtxt* server, XWStreamCtxt* stream )
|
|
{
|
|
CurGameInfo* gi = server->vol.gi;
|
|
XP_U16 nLocalPlayers;
|
|
LocalPlayer* lp;
|
|
#ifdef DEBUG
|
|
XP_U16 i = 0;
|
|
#endif
|
|
|
|
XP_ASSERT( gi->serverRole == SERVER_ISCLIENT );
|
|
XP_ASSERT( stream != NULL );
|
|
XP_ASSERT( server->nv.gameState == XWSTATE_NONE );
|
|
|
|
stream_open( stream );
|
|
|
|
stream_putBits( stream, XWPROTO_NBITS, XWPROTO_DEVICE_REGISTRATION );
|
|
|
|
nLocalPlayers = gi->nPlayers;
|
|
XP_ASSERT( nLocalPlayers > 0 );
|
|
stream_putBits( stream, NPLAYERS_NBITS, nLocalPlayers );
|
|
|
|
for ( lp = gi->players; nLocalPlayers > 0; ++lp ) {
|
|
|
|
XP_ASSERT( i++ < MAX_NUM_PLAYERS );
|
|
|
|
if ( lp->isLocal ) {
|
|
|
|
XP_UCHAR* name = emptyStringIfNull(lp->name);
|
|
XP_Bool isRobot = lp->isRobot;
|
|
XP_U8 len;
|
|
stream_putBits( stream, 1, isRobot );
|
|
len = XP_STRLEN(name);
|
|
if ( len > MAX_NAME_LEN ) {
|
|
len = MAX_NAME_LEN;
|
|
}
|
|
stream_putBits( stream, NAME_LEN_NBITS, len );
|
|
stream_putBytes( stream, name, len );
|
|
|
|
XP_ASSERT( nLocalPlayers > 0 );
|
|
--nLocalPlayers;
|
|
}
|
|
}
|
|
|
|
stream_destroy( stream );
|
|
} /* server_initClientConnection */
|
|
#endif
|
|
|
|
static void
|
|
callTurnChangeListener( ServerCtxt* server )
|
|
{
|
|
if ( server->vol.turnChangeListener != NULL ) {
|
|
(*server->vol.turnChangeListener)( server->vol.turnChangeData );
|
|
}
|
|
} /* callTurnChangeListener */
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
static XP_Bool
|
|
handleRegistrationMsg( ServerCtxt* server, XWStreamCtxt* stream )
|
|
{
|
|
XP_U16 playersInMsg, i;
|
|
XP_STATUSF( "handleRegistrationMsg" );
|
|
|
|
/* code will have already been consumed */
|
|
playersInMsg = (XP_U16)stream_getBits( stream, NPLAYERS_NBITS );
|
|
XP_ASSERT( playersInMsg > 0 );
|
|
|
|
if ( server->nv.pendingRegistrations < playersInMsg ) {
|
|
util_userError( server->vol.util, ERR_REG_UNEXPECTED_USER );
|
|
return XP_FALSE;
|
|
}
|
|
|
|
for ( i = 0; i < playersInMsg; ++i ) {
|
|
registerRemotePlayer( server, stream );
|
|
|
|
/* This is abusing the semantics of turn change -- at least in the
|
|
case where there is another device yet to register -- but we
|
|
need to let the board know to redraw the scoreboard with more
|
|
players there. */
|
|
callTurnChangeListener( server );
|
|
}
|
|
|
|
if ( server->nv.pendingRegistrations == 0 ) {
|
|
assignTilesToAll( server );
|
|
server->nv.gameState = XWSTATE_RECEIVED_ALL_REG;
|
|
}
|
|
return XP_TRUE;
|
|
} /* handleRegistrationMsg */
|
|
#endif
|
|
|
|
/* Just for grins....trade in all the tiles that weren't used in the
|
|
* move the robot manage to make. This is not meant to be strategy, but
|
|
* rather to force me to make the trade-communication stuff work well.
|
|
*/
|
|
#if 0
|
|
static void
|
|
robotTradeTiles( ServerCtxt* server, MoveInfo* newMove )
|
|
{
|
|
Tile tradeTiles[MAX_TRAY_TILES];
|
|
XP_S16 turn = server->nv.currentTurn;
|
|
Tile* curTiles = model_getPlayerTiles( server->model, turn );
|
|
XP_U16 numInTray = model_getNumPlayerTiles( server->model, turn );
|
|
XP_MEMCPY( tradeTiles, curTiles, numInTray );
|
|
|
|
for ( i = 0; i < numInTray; ++i ) { /* for each tile in tray */
|
|
XP_Bool keep = XP_FALSE;
|
|
for ( j = 0; j < newMove->numTiles; ++j ) { /* for each in move */
|
|
Tile movedTile = newMove->tiles[j].tile;
|
|
if ( newMove->tiles[j].isBlank ) {
|
|
movedTile |= TILE_BLANK_BIT;
|
|
}
|
|
if ( movedTile == curTiles[i] ) { /* it's in the move */
|
|
keep = XP_TRUE;
|
|
break;
|
|
}
|
|
}
|
|
if ( !keep ) {
|
|
tradeTiles[numToTrade++] = curTiles[i];
|
|
}
|
|
}
|
|
|
|
|
|
} /* robotTradeTiles */
|
|
#endif
|
|
|
|
#define FUDGE_RANGE 10
|
|
#define MINIMUM_SCORE 5
|
|
static XP_U16
|
|
figureTargetScore( ServerCtxt* server, XP_U16 turn )
|
|
{
|
|
XP_S16 result = 1000;
|
|
XP_S16 highScore = 0;
|
|
ModelCtxt* model = server->vol.model;
|
|
XP_U16 nPlayers = server->vol.gi->nPlayers;
|
|
XP_U16 i;
|
|
|
|
XP_ASSERT( IS_ROBOT(&server->vol.gi->players[turn]) );
|
|
|
|
if ( 1 /* server->nHumanPlayers > 0 */ ) {
|
|
result = 0;
|
|
|
|
/* find the highest score anybody but the current player has */
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
if ( i != turn ) {
|
|
XP_S16 score = model_getPlayerScore( model, i );
|
|
XP_ASSERT( score >= 0 );
|
|
if ( highScore < score ) {
|
|
highScore = score;
|
|
}
|
|
}
|
|
}
|
|
|
|
result = (XP_S16)(highScore - model_getPlayerScore( model, turn )
|
|
+ (FUDGE_RANGE-(XP_RANDOM() % (FUDGE_RANGE*2))));
|
|
if ( result < 0 ) {
|
|
result = MINIMUM_SCORE;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
} /* figureTargetScore */
|
|
|
|
static XWStreamCtxt*
|
|
mkServerStream( ServerCtxt* server )
|
|
{
|
|
XWStreamCtxt* stream;
|
|
stream = mem_stream_make( MPPARM(server->mpool)
|
|
util_getVTManager(server->vol.util),
|
|
NULL, CHANNEL_NONE,
|
|
(MemStreamCloseCallback)NULL );
|
|
XP_ASSERT( !!stream );
|
|
return stream;
|
|
} /* mkServerStream */
|
|
|
|
static XP_Bool
|
|
makeRobotMove( ServerCtxt* server )
|
|
{
|
|
XP_Bool result = XP_FALSE;
|
|
XP_Bool finished;
|
|
XP_S16 turn;
|
|
const TrayTileSet* tileSet;
|
|
MoveInfo newMove;
|
|
ModelCtxt* model = server->vol.model;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
XP_Bool timerEnabled = gi->timerEnabled;
|
|
XP_Bool canMove;
|
|
XP_U32 time = 0L; /* stupid compiler.... */
|
|
XP_U16 targetScore = NO_SCORE_LIMIT;
|
|
XW_UtilCtxt* util = server->vol.util;
|
|
|
|
if ( timerEnabled ) {
|
|
time = util_getCurSeconds( util );
|
|
}
|
|
|
|
turn = server->nv.currentTurn;
|
|
XP_ASSERT( turn >= 0 );
|
|
|
|
/* If the player's been recently turned into a robot while he had some
|
|
pending tiles on the board we'll have problems. It'd be best to detect
|
|
this and put 'em back when that happens. But for now we'll just be
|
|
paranoid. PENDING(ehouse) */
|
|
model_resetCurrentTurn( model, turn );
|
|
|
|
tileSet = model_getPlayerTiles( model, turn );
|
|
|
|
if ( gi->robotSmartness == DUMB_ROBOT ) {
|
|
targetScore = figureTargetScore( server, turn );
|
|
}
|
|
|
|
XP_ASSERT( !!server_getEngineFor( server, turn ) );
|
|
finished = engine_findMove( server_getEngineFor( server, turn ),
|
|
model, model_getDictionary( model ),
|
|
tileSet->tiles, tileSet->nTiles, 0,
|
|
targetScore, &canMove, &newMove );
|
|
if ( finished ) {
|
|
XP_UCHAR* str;
|
|
XWStreamCtxt* stream = NULL;
|
|
|
|
XP_Bool trade = (newMove.nTiles == 0) && canMove &&
|
|
(server_countTilesInPool( server ) >= MAX_TRAY_TILES);
|
|
|
|
server->vol.showPrevMove = XP_TRUE;
|
|
if ( server->nv.showRobotScores ) {
|
|
stream = mkServerStream( server );
|
|
}
|
|
|
|
/* trade if unable to find a move */
|
|
if ( trade ) {
|
|
result = server_commitTrade( server, ALLTILES );
|
|
|
|
/* Quick hack to fix gremlin bug where all-robot game seen none
|
|
able to trade for tiles to move and blowing the undo stack.
|
|
This will stop them, and should have no effect if there are any
|
|
human players making real moves. */
|
|
++server->nv.nPassesInRow;
|
|
|
|
if ( !!stream ) {
|
|
XP_UCHAR tradeBuf[64];
|
|
str = util_getUserString(util, STRD_ROBOT_TRADED);
|
|
XP_SNPRINTF( tradeBuf, sizeof(tradeBuf), str, MAX_TRAY_TILES );
|
|
|
|
stream_putBytes( stream, str, (XP_U16)XP_STRLEN(str) );
|
|
XP_ASSERT( !server->vol.prevMoveStream );
|
|
server->vol.prevMoveStream = stream;
|
|
}
|
|
} else {
|
|
/* if canMove is false, this is a fake move, a pass */
|
|
model_makeTurnFromMoveInfo( model, turn, &newMove );
|
|
|
|
if ( !!stream ) {
|
|
(void)model_checkMoveLegal( model, turn, stream, NULL );
|
|
XP_ASSERT( !server->vol.prevMoveStream );
|
|
server->vol.prevMoveStream = stream;
|
|
}
|
|
result = server_commitMove( server );
|
|
}
|
|
}
|
|
|
|
if ( timerEnabled ) {
|
|
gi->players[turn].secondsUsed +=
|
|
(XP_U16)(util_getCurSeconds( util ) - time);
|
|
} else {
|
|
XP_ASSERT( gi->players[turn].secondsUsed == 0 );
|
|
}
|
|
|
|
return result; /* always return TRUE after robot move? */
|
|
} /* makeRobotMove */
|
|
|
|
static XP_Bool
|
|
robotMovePending( ServerCtxt* server )
|
|
{
|
|
XP_S16 turn = server->nv.currentTurn;
|
|
if ( turn >= 0 ) {
|
|
CurGameInfo* gi = server->vol.gi;
|
|
LocalPlayer* player = &gi->players[turn];
|
|
return IS_ROBOT(player) && IS_LOCAL(player);
|
|
}
|
|
return XP_FALSE;
|
|
} /* robotMovePending */
|
|
|
|
static void
|
|
showPrevScore( ServerCtxt* server )
|
|
{
|
|
XW_UtilCtxt* util = server->vol.util;
|
|
XWStreamCtxt* stream;
|
|
XP_UCHAR* str;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
XP_U16 nPlayers = gi->nPlayers;
|
|
XP_U16 prevTurn;
|
|
XP_U16 strCode;
|
|
LocalPlayer* lp;
|
|
XP_Bool wasRobot;
|
|
XP_Bool wasLocal;
|
|
|
|
XP_ASSERT( server->nv.showRobotScores );
|
|
|
|
prevTurn = (server->nv.currentTurn + nPlayers - 1) % nPlayers;
|
|
lp = &gi->players[prevTurn];
|
|
wasRobot = lp->isRobot;
|
|
wasLocal = lp->isLocal;
|
|
|
|
if ( wasLocal ) {
|
|
XP_ASSERT( wasRobot );
|
|
strCode = STR_ROBOT_MOVED;
|
|
} else {
|
|
strCode = STR_REMOTE_MOVED;
|
|
}
|
|
|
|
stream = mkServerStream( server );
|
|
|
|
str = util_getUserString( util, strCode );
|
|
stream_putBytes( stream, str, (XP_U16)XP_STRLEN(str) );
|
|
|
|
if ( !!server->vol.prevMoveStream ) {
|
|
XWStreamCtxt* prevStream = server->vol.prevMoveStream;
|
|
XP_U16 len = stream_getSize( prevStream );
|
|
XP_UCHAR* buf = XP_MALLOC( server->mpool, len );
|
|
|
|
stream_getBytes( prevStream, buf, len );
|
|
stream_destroy( prevStream );
|
|
server->vol.prevMoveStream = NULL;
|
|
|
|
stream_putBytes( stream, buf, len );
|
|
XP_FREE( server->mpool, buf );
|
|
}
|
|
|
|
(void)util_userQuery( util, QUERY_ROBOT_MOVE, stream );
|
|
stream_destroy( stream );
|
|
|
|
server->nv.gameState = server->vol.stateAfterShow;
|
|
} /* showPrevScore */
|
|
|
|
XP_Bool
|
|
server_do( ServerCtxt* server )
|
|
{
|
|
XP_Bool result = XP_TRUE;
|
|
XP_Bool moreToDo = XP_FALSE;
|
|
|
|
if ( server->serverDoing ) {
|
|
return XP_FALSE;
|
|
}
|
|
server->serverDoing = XP_TRUE;
|
|
|
|
switch( server->nv.gameState ) {
|
|
case XWSTATE_BEGIN:
|
|
if ( server->nv.pendingRegistrations == 0 ) { /* all players on device */
|
|
assignTilesToAll( server );
|
|
server->nv.gameState = XWSTATE_INTURN;
|
|
server->nv.currentTurn = 0;
|
|
moreToDo = XP_TRUE;
|
|
}
|
|
break;
|
|
|
|
case XWSTATE_NEEDSEND_BADWORD_INFO:
|
|
XP_ASSERT( server->vol.gi->serverRole == SERVER_ISSERVER );
|
|
badWordMoveUndoAndTellUser( server, &server->illegalWordInfo );
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
sendBadWordMsgs( server );
|
|
#endif
|
|
nextTurn( server, PICK_NEXT ); /* sets server->nv.gameState */
|
|
//moreToDo = XP_TRUE; /* why? */
|
|
break;
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
case XWSTATE_RECEIVED_ALL_REG:
|
|
server_sendInitialMessage( server );
|
|
/* PENDING isn't INTURN_OFFDEVICE possible too? Or just INTURN? */
|
|
server->nv.gameState = XWSTATE_INTURN;
|
|
server->nv.currentTurn = 0;
|
|
moreToDo = XP_TRUE;
|
|
break;
|
|
|
|
case XWSTATE_MOVE_CONFIRM_MUSTSEND:
|
|
XP_ASSERT( server->vol.gi->serverRole == SERVER_ISSERVER );
|
|
tellMoveWasLegal( server );
|
|
nextTurn( server, PICK_NEXT );
|
|
break;
|
|
|
|
#endif /* XWFEATURE_STANDALONE_ONLY */
|
|
|
|
case XWSTATE_NEEDSEND_ENDGAME:
|
|
endGameInternal( server, END_REASON_OUT_OF_TILES );
|
|
break;
|
|
|
|
case XWSTATE_NEED_SHOWSCORE:
|
|
showPrevScore( server );
|
|
moreToDo = XP_TRUE; /* either process turn or end game... */
|
|
break;
|
|
case XWSTATE_INTURN:
|
|
if ( robotMovePending( server ) ) {
|
|
result = makeRobotMove( server );
|
|
/* if robot was interrupted, we need to schedule again */
|
|
moreToDo = !result || robotMovePending( server );
|
|
}
|
|
break;
|
|
|
|
default:
|
|
result = XP_FALSE;
|
|
break;
|
|
}
|
|
if ( moreToDo ) {
|
|
util_requestTime( server->vol.util );
|
|
}
|
|
|
|
server->serverDoing = XP_FALSE;
|
|
return result;
|
|
} /* server_do */
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
static XP_S8
|
|
getIndexForDevice( ServerCtxt* server, XP_PlayerAddr channelNo )
|
|
{
|
|
short i;
|
|
XP_S8 result = -1;
|
|
|
|
for ( i = 0; i < server->nv.nDevices; ++i ) {
|
|
RemoteAddress* addr = &server->nv.addresses[i];
|
|
if ( addr->channelNo == channelNo ) {
|
|
result = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
} /* getIndexForDevice */
|
|
|
|
static LocalPlayer*
|
|
findFirstPending( ServerCtxt* server, ServerPlayer** playerP )
|
|
{
|
|
LocalPlayer* lp;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
XP_U16 nPlayers = gi->nPlayers;
|
|
XP_U16 nPending = server->nv.pendingRegistrations;
|
|
|
|
XP_ASSERT( nPlayers > 0 );
|
|
lp = gi->players + nPlayers;
|
|
|
|
while ( --lp >= gi->players ) {
|
|
--nPlayers;
|
|
if ( !lp->isLocal ) {
|
|
if ( --nPending == 0 ) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
XP_ASSERT( lp >= gi->players ); /* did we find a slot? */
|
|
*playerP = server->players + nPlayers;
|
|
return lp;
|
|
} /* findFirstPending */
|
|
|
|
void
|
|
registerRemotePlayer( ServerCtxt* server, XWStreamCtxt* stream )
|
|
{
|
|
XP_S8 deviceIndex;
|
|
XP_PlayerAddr channelNo;
|
|
XP_UCHAR* name;
|
|
XP_U16 nameLen;
|
|
LocalPlayer* lp;
|
|
ServerPlayer* player = (ServerPlayer*)NULL;
|
|
|
|
/* The player must already be there with a null name, or it's an error.
|
|
Take the first empty slot. */
|
|
XP_ASSERT( server->nv.pendingRegistrations > 0 );
|
|
|
|
/* find the slot to use */
|
|
lp = findFirstPending( server, &player );
|
|
|
|
/* get data from stream */
|
|
lp->isRobot = stream_getBits( stream, 1 );
|
|
nameLen = stream_getBits( stream, NAME_LEN_NBITS );
|
|
name = (XP_UCHAR*)XP_MALLOC( server->mpool, nameLen + 1 );
|
|
stream_getBytes( stream, name, nameLen );
|
|
name[nameLen] = '\0';
|
|
|
|
replaceStringIfDifferent( MPPARM(server->mpool) &lp->name, name );
|
|
XP_FREE( server->mpool, name );
|
|
|
|
channelNo = stream_getAddress( stream );
|
|
deviceIndex = getIndexForDevice( server, channelNo );
|
|
|
|
--server->nv.pendingRegistrations;
|
|
|
|
if ( deviceIndex == -1 ) {
|
|
RemoteAddress* addr;
|
|
addr = &server->nv.addresses[server->nv.nDevices];
|
|
|
|
deviceIndex = server->nv.nDevices++;
|
|
|
|
XP_ASSERT( channelNo != 0 );
|
|
addr->channelNo = channelNo;
|
|
}
|
|
|
|
player->deviceIndex = deviceIndex;
|
|
|
|
} /* registerRemotePlayer */
|
|
|
|
static void
|
|
clearLocalRobots( ServerCtxt* server )
|
|
{
|
|
XP_U16 i;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
XP_U16 nPlayers = gi->nPlayers;
|
|
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
LocalPlayer* player = &gi->players[i];
|
|
if ( IS_LOCAL( player ) ) {
|
|
player->isRobot = XP_FALSE;
|
|
}
|
|
}
|
|
} /* clearLocalRobots */
|
|
#endif
|
|
|
|
/* Called in response to message from server listing all the names of
|
|
* players in the game (in server-assigned order) and their initial
|
|
* tray contents.
|
|
*/
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
static XP_Bool
|
|
client_readInitialMessage( ServerCtxt* server, XWStreamCtxt* stream )
|
|
{
|
|
DictionaryCtxt* newDict;
|
|
DictionaryCtxt* curDict;
|
|
XP_U16 nPlayers, nCols;
|
|
XP_PlayerAddr channelNo;
|
|
short i;
|
|
ModelCtxt* model = server->vol.model;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
CurGameInfo localGI;
|
|
XP_U32 gameID;
|
|
PoolContext* pool;
|
|
|
|
/* version */
|
|
XP_U8 streamVersion = stream_getU8( stream );
|
|
XP_ASSERT( streamVersion == CUR_STREAM_VERS );
|
|
if ( streamVersion != CUR_STREAM_VERS ) {
|
|
return XP_FALSE;
|
|
}
|
|
|
|
gameID = stream_getBits( stream, 32 );
|
|
XP_STATUSF( "read gameID of %ld; calling comms_setConnID", gameID );
|
|
server->vol.gi->gameID = gameID;
|
|
comms_setConnID( server->vol.comms, gameID );
|
|
|
|
XP_MEMSET( &localGI, 0, sizeof(localGI) );
|
|
gi_readFromStream( MPPARM(server->mpool) stream, streamVersion, &localGI );
|
|
localGI.serverRole = SERVER_ISCLIENT;
|
|
|
|
/* so it's not lost (HACK!). Without this, a client won't have a default
|
|
dict name when a new game is started. */
|
|
localGI.dictName = copyString( MPPARM(server->mpool) gi->dictName );
|
|
gi_copy( MPPARM(server->mpool) gi, &localGI );
|
|
|
|
nCols = localGI.boardSize;
|
|
|
|
newDict = util_makeEmptyDict( server->vol.util );
|
|
dict_loadFromStream( newDict, stream );
|
|
|
|
channelNo = stream_getAddress( stream );
|
|
XP_ASSERT( channelNo != 0 );
|
|
XP_ASSERT( server->nv.addresses[0].channelNo == 0 );
|
|
server->nv.addresses[0].channelNo = channelNo;
|
|
|
|
/* PENDING init's a bit harsh for setting the size */
|
|
model_init( model, nCols, nCols );
|
|
|
|
nPlayers = localGI.nPlayers;
|
|
XP_STATUSF( "reading in %d players", localGI.nPlayers );
|
|
|
|
gi_disposePlayerInfo( MPPARM(server->mpool) &localGI );
|
|
|
|
gi->nPlayers = nPlayers;
|
|
model_setNPlayers( model, nPlayers );
|
|
|
|
curDict = model_getDictionary( model );
|
|
|
|
XP_ASSERT( !!newDict );
|
|
|
|
if ( curDict == NULL ) {
|
|
model_setDictionary( model, newDict );
|
|
} else if ( dict_tilesAreSame( newDict, curDict ) ) {
|
|
/* keep the dict the local user installed */
|
|
dict_destroy( newDict );
|
|
} else {
|
|
dict_destroy( curDict );
|
|
model_setDictionary( model, newDict );
|
|
util_userError( server->vol.util, ERR_SERVER_DICT_WINS );
|
|
clearLocalRobots( server );
|
|
}
|
|
|
|
XP_ASSERT( !server->pool );
|
|
pool = server->pool = pool_make( MPPARM_NOCOMMA(server->mpool) );
|
|
pool_initFromDict( server->pool, model_getDictionary(model));
|
|
|
|
/* now read the assigned tiles for each player from the stream, and remove
|
|
them from the newly-created local pool. */
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
TrayTileSet tiles;
|
|
|
|
traySetFromStream( stream, &tiles );
|
|
XP_ASSERT( tiles.nTiles <= MAX_TRAY_TILES );
|
|
|
|
XP_STATUSF( "got %d tiles for player %d", tiles.nTiles, i );
|
|
|
|
model_assignPlayerTiles( model, i, &tiles );
|
|
|
|
/* remove what the server's assigned so we won't conflict later. */
|
|
pool_removeTiles( pool, &tiles );
|
|
}
|
|
|
|
if ( gi->players->isLocal ) {
|
|
server->nv.gameState = XWSTATE_INTURN;
|
|
}
|
|
server->nv.currentTurn = 0;
|
|
|
|
/* Give board a chance to redraw self with the full compliment of
|
|
known players */
|
|
callTurnChangeListener( server );
|
|
|
|
return XP_TRUE;
|
|
} /* client_readInitialMessage */
|
|
#endif
|
|
|
|
/* For each remote device, send a message containing the dictionary and the
|
|
* names of all the players in the game (including those on the device itself,
|
|
* since they might have been changed in the case of conflicts), in the order
|
|
* that all must use for the game. Then for each player on the device give
|
|
* the starting tray.
|
|
*/
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
|
|
static void
|
|
makeSendableGICopy( ServerCtxt* server, CurGameInfo* giCopy,
|
|
XP_U16 deviceIndex )
|
|
{
|
|
XP_U16 nPlayers;
|
|
LocalPlayer* clientPl;
|
|
XP_U16 i;
|
|
XP_MEMCPY( giCopy, server->vol.gi, sizeof(*giCopy) );
|
|
|
|
nPlayers = giCopy->nPlayers;
|
|
|
|
for ( clientPl = giCopy->players, i = 0;
|
|
i < nPlayers; ++clientPl, ++i ) {
|
|
/* adjust isLocal to client's perspective */
|
|
clientPl->isLocal = server->players[i].deviceIndex == deviceIndex;
|
|
}
|
|
|
|
giCopy->dictName = (XP_UCHAR*)NULL; /* so we don't sent the bytes; Isn't this
|
|
a leak? PENDING */
|
|
} /* makeSendableGICopy */
|
|
|
|
static void
|
|
server_sendInitialMessage( ServerCtxt* server )
|
|
{
|
|
short i;
|
|
XP_U16 deviceIndex;
|
|
ModelCtxt* model = server->vol.model;
|
|
XP_U16 nPlayers = server->vol.gi->nPlayers;
|
|
CurGameInfo localGI;
|
|
XP_U32 gameID = server->vol.gi->gameID;
|
|
|
|
XP_STATUSF( "server_sendInitialMessage" );
|
|
|
|
for ( deviceIndex = 1; deviceIndex < server->nv.nDevices;
|
|
++deviceIndex ) {
|
|
RemoteAddress* addr = &server->nv.addresses[deviceIndex];
|
|
XWStreamCtxt* stream = util_makeStreamFromAddr( server->vol.util,
|
|
addr->channelNo );
|
|
XP_ASSERT( !!stream );
|
|
stream_open( stream );
|
|
stream_putBits( stream, XWPROTO_NBITS, XWPROTO_CLIENT_SETUP );
|
|
|
|
/* write version for server's benefit */
|
|
stream_putU8( stream, CUR_STREAM_VERS );
|
|
|
|
XP_STATUSF( "putting gameID %ld into msg", gameID );
|
|
stream_putU32( stream, gameID );
|
|
|
|
makeSendableGICopy( server, &localGI, deviceIndex );
|
|
gi_writeToStream( stream, &localGI );
|
|
|
|
dict_writeToStream( model_getDictionary(model), stream );
|
|
|
|
/* send tiles currently in tray */
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
model_trayToStream( model, i, stream );
|
|
}
|
|
|
|
stream_destroy( stream );
|
|
}
|
|
|
|
comms_setConnID( server->vol.comms, gameID );
|
|
} /* server_sendInitialMessage */
|
|
#endif
|
|
|
|
static void
|
|
freeBWI( MPFORMAL BadWordInfo* bwi )
|
|
{
|
|
/* BadWordInfo* bwi = &server->illegalWordInfo; */
|
|
XP_U16 nWords = bwi->nWords;
|
|
|
|
while ( nWords-- ) {
|
|
XP_FREE( mpool, bwi->words[nWords] );
|
|
bwi->words[nWords] = (XP_UCHAR*)NULL;
|
|
}
|
|
|
|
bwi->nWords = 0;
|
|
} /* freeBWI */
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
static void
|
|
bwiToStream( XWStreamCtxt* stream, BadWordInfo* bwi )
|
|
{
|
|
XP_U16 nWords = bwi->nWords;
|
|
XP_UCHAR** sp;
|
|
|
|
stream_putBits( stream, 4, nWords );
|
|
|
|
for ( sp = bwi->words; nWords > 0; --nWords, ++sp ) {
|
|
stringToStream( stream, *sp );
|
|
}
|
|
|
|
} /* bwiToStream */
|
|
|
|
static void
|
|
bwiFromStream( MPFORMAL XWStreamCtxt* stream, BadWordInfo* bwi )
|
|
{
|
|
XP_U16 nWords = stream_getBits( stream, 4 );
|
|
XP_UCHAR** sp = bwi->words;
|
|
|
|
bwi->nWords = nWords;
|
|
for ( sp = bwi->words; nWords; ++sp, --nWords ) {
|
|
*sp = stringFromStream( MPPARM(mpool) stream );
|
|
}
|
|
} /* bwiFromStream */
|
|
|
|
#ifdef DEBUG
|
|
#define caseStr(var, s) case s: var = #s; break;
|
|
static void
|
|
printCode(char* intro, XW_Proto code)
|
|
{
|
|
char* str = (char*)NULL;
|
|
|
|
switch( code ) {
|
|
caseStr( str, XWPROTO_ERROR );
|
|
caseStr( str, XWPROTO_CHAT );
|
|
caseStr( str, XWPROTO_DEVICE_REGISTRATION );
|
|
caseStr( str, XWPROTO_CLIENT_SETUP );
|
|
caseStr( str, XWPROTO_MOVEMADE_INFO_CLIENT );
|
|
caseStr( str, XWPROTO_MOVEMADE_INFO_SERVER );
|
|
caseStr( str, XWPROTO_UNDO_INFO_CLIENT );
|
|
caseStr( str, XWPROTO_UNDO_INFO_SERVER );
|
|
caseStr( str, XWPROTO_BADWORD_INFO );
|
|
caseStr( str, XWPROTO_MOVE_CONFIRM );
|
|
caseStr( str, XWPROTO_CLIENT_REQ_END_GAME );
|
|
caseStr( str, XWPROTO_END_GAME );
|
|
}
|
|
|
|
XP_STATUSF( "\t%s for %s", intro, str );
|
|
} /* printCode */
|
|
#undef caseStr
|
|
#else
|
|
#define printCode(intro, code)
|
|
#endif
|
|
|
|
static XWStreamCtxt*
|
|
messageStreamWithHeader( ServerCtxt* server, XP_U16 devIndex, XW_Proto code )
|
|
{
|
|
XWStreamCtxt* stream;
|
|
XP_PlayerAddr channelNo = server->nv.addresses[devIndex].channelNo;
|
|
|
|
printCode("making", code);
|
|
|
|
stream = util_makeStreamFromAddr( server->vol.util, channelNo );
|
|
|
|
stream_open( stream );
|
|
stream_putBits( stream, XWPROTO_NBITS, code );
|
|
|
|
return stream;
|
|
} /* messageStreamWithHeader */
|
|
|
|
/* Check that the message belongs to this game, whatever. Pull out the data
|
|
* put in by messageStreamWithHeader -- except for the code, which will have
|
|
* already come out.
|
|
*/
|
|
static XP_Bool
|
|
readStreamHeader( ServerCtxt* server, XWStreamCtxt* stream )
|
|
{
|
|
return XP_TRUE;
|
|
} /* readStreamHeader */
|
|
|
|
static void
|
|
sendBadWordMsgs( ServerCtxt* server )
|
|
{
|
|
XWStreamCtxt* stream;
|
|
|
|
stream = messageStreamWithHeader( server, server->lastMoveSource,
|
|
XWPROTO_BADWORD_INFO );
|
|
stream_putBits( stream, PLAYERNUM_NBITS, server->nv.currentTurn );
|
|
|
|
XP_ASSERT( server->illegalWordInfo.nWords > 0 );
|
|
bwiToStream( stream, &server->illegalWordInfo );
|
|
|
|
stream_destroy( stream );
|
|
|
|
freeBWI( MPPARM(server->mpool) &server->illegalWordInfo );
|
|
} /* sendBadWordMsgs */
|
|
#endif
|
|
|
|
static void
|
|
badWordMoveUndoAndTellUser( ServerCtxt* server, BadWordInfo* bwi )
|
|
{
|
|
XP_U16 turn;
|
|
ModelCtxt* model = server->vol.model;
|
|
/* I'm the server. I need to send a message to everybody else telling
|
|
them the move's rejected. Then undo it on this side, replacing it with
|
|
model_commitRejectedPhony(); */
|
|
|
|
model_rejectPreviousMove( model, server->pool, &turn );
|
|
|
|
util_warnIllegalWord( server->vol.util, bwi, turn, XP_TRUE );
|
|
} /* badWordMoveUndoAndTellUser */
|
|
|
|
EngineCtxt*
|
|
server_getEngineFor( ServerCtxt* server, XP_U16 playerNum )
|
|
{
|
|
ServerPlayer* player;
|
|
EngineCtxt* engine;
|
|
|
|
XP_ASSERT( playerNum < server->vol.gi->nPlayers );
|
|
|
|
player = &server->players[playerNum];
|
|
engine = player->engine;
|
|
if ( !engine && server->vol.gi->players[playerNum].isLocal ) {
|
|
engine = engine_make( MPPARM(server->mpool)
|
|
server->vol.util,
|
|
server->vol.gi->players[playerNum].isRobot );
|
|
player->engine = engine;
|
|
}
|
|
|
|
return engine;
|
|
} /* server_getEngineFor */
|
|
|
|
void
|
|
server_resetEngine( ServerCtxt* server, XP_U16 playerNum )
|
|
{
|
|
ServerPlayer* player = &server->players[playerNum];
|
|
if ( !!player->engine ) {
|
|
XP_ASSERT( player->deviceIndex == 0 );
|
|
engine_reset( player->engine );
|
|
}
|
|
} /* server_resetEngine */
|
|
|
|
static void
|
|
resetEngines( ServerCtxt* server )
|
|
{
|
|
XP_U16 i;
|
|
XP_U16 nPlayers = server->vol.gi->nPlayers;
|
|
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
server_resetEngine( server, i );
|
|
}
|
|
} /* resetEngines */
|
|
|
|
#ifdef TEST_ROBOT_TRADE
|
|
static void
|
|
makeNotAVowel( ServerCtxt* server, Tile* newTile )
|
|
{
|
|
char face[4];
|
|
Tile tile = *newTile;
|
|
PoolContext* pool = server->pool;
|
|
TrayTileSet set;
|
|
DictionaryCtxt* dict = model_getDictionary( server->vol.model );
|
|
XP_U8 numGot = 1;
|
|
|
|
set.nTiles = 1;
|
|
|
|
for ( ; ; ) {
|
|
|
|
XP_U16 len = dict_tilesToString( dict, &tile, 1, face );
|
|
|
|
if ( len == 1 ) {
|
|
switch ( face[0] ) {
|
|
case 'A':
|
|
case 'E':
|
|
case 'I':
|
|
case 'O':
|
|
case 'U':
|
|
case '_':
|
|
break;
|
|
default:
|
|
*newTile = tile;
|
|
return;
|
|
}
|
|
}
|
|
|
|
set.tiles[0] = tile;
|
|
pool_replaceTiles( pool, &set );
|
|
|
|
pool_requestTiles( pool, &tile, &numGot );
|
|
|
|
}
|
|
|
|
} /* makeNotAVowel */
|
|
#endif
|
|
|
|
static void
|
|
curTrayAsTexts( ServerCtxt* server, XP_U16 turn, const TrayTileSet* notInTray,
|
|
XP_U16* nUsedP, XP_UCHAR4* curTrayText )
|
|
{
|
|
const TrayTileSet* tileSet = model_getPlayerTiles( server->vol.model, turn );
|
|
DictionaryCtxt* dict = model_getDictionary( server->vol.model );
|
|
XP_U16 i, j;
|
|
XP_U16 size = tileSet->nTiles;
|
|
Tile* tp = tileSet->tiles;
|
|
XP_U16 tradedTiles[MAX_TRAY_TILES];
|
|
XP_U16 nNotInTray = 0;
|
|
XP_U16 nUsed = 0;
|
|
|
|
XP_MEMSET( tradedTiles, 0xFF, sizeof(tradedTiles) );
|
|
if ( !!notInTray ) {
|
|
Tile* tp = notInTray->tiles;
|
|
nNotInTray = notInTray->nTiles;
|
|
for ( i = 0; i < nNotInTray; ++i ) {
|
|
tradedTiles[i] = *tp++;
|
|
}
|
|
}
|
|
|
|
for ( i = 0; i < size; ++i ) {
|
|
Tile tile = *tp++;
|
|
XP_Bool toBeTraded = XP_FALSE;
|
|
|
|
for ( j = 0; j < nNotInTray; ++j ) {
|
|
if ( tradedTiles[j] == tile ) {
|
|
tradedTiles[j] = 0xFFFF;
|
|
toBeTraded = XP_TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !toBeTraded ) {
|
|
dict_tilesToString( dict, &tile, 1, (XP_UCHAR*)&curTrayText[nUsed++] );
|
|
}
|
|
}
|
|
*nUsedP = nUsed;
|
|
} /* curTrayAsTexts */
|
|
|
|
/* Get tiles for one user. If picking is available, let user pick until
|
|
* cancels. Otherwise, and after cancel, pick for 'im.
|
|
*/
|
|
static void
|
|
fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch,
|
|
const TrayTileSet* tradedTiles, TrayTileSet* resultTiles )
|
|
{
|
|
XP_Bool ask;
|
|
XP_U16 nSoFar = 0;
|
|
XP_U16 nLeft;
|
|
PoolContext* pool = server->pool;
|
|
TrayTileSet oneTile;
|
|
PickInfo pi;
|
|
XP_UCHAR4 curTray[MAX_TRAY_TILES];
|
|
#ifdef FEATURE_TRAY_EDIT
|
|
DictionaryCtxt* dict = model_getDictionary( server->vol.model );
|
|
#endif
|
|
|
|
XP_ASSERT( !!pool );
|
|
#ifdef FEATURE_TRAY_EDIT
|
|
ask = server->vol.gi->allowPickTiles
|
|
&& !server->vol.gi->players[playerNum].isRobot;
|
|
#else
|
|
ask = XP_FALSE;
|
|
#endif
|
|
|
|
nLeft = pool_getNTilesLeft( pool );
|
|
if ( nLeft < nToFetch ) {
|
|
nToFetch = nLeft;
|
|
}
|
|
|
|
oneTile.nTiles = 1;
|
|
|
|
pi.why = PICK_FOR_CHEAT;
|
|
pi.nTotal = nToFetch;
|
|
pi.thisPick = 0;
|
|
pi.curTiles = curTray;
|
|
|
|
curTrayAsTexts( server, playerNum, tradedTiles, &pi.nCurTiles, curTray );
|
|
|
|
#ifdef FEATURE_TRAY_EDIT /* good compiler would note ask==0, but... */
|
|
/* First ask until cancelled */
|
|
for ( nSoFar = 0; ask && nSoFar < nToFetch; ) {
|
|
XP_UCHAR4 texts[MAX_UNIQUE_TILES];
|
|
Tile tiles[MAX_UNIQUE_TILES];
|
|
XP_S16 chosen;
|
|
XP_U16 nUsed = MAX_UNIQUE_TILES;
|
|
|
|
model_packTilesUtil( server->vol.model, pool,
|
|
XP_TRUE, &nUsed, texts, tiles );
|
|
|
|
++pi.thisPick;
|
|
chosen = util_userPickTile( server->vol.util, &pi,
|
|
playerNum, texts, nUsed );
|
|
|
|
if ( chosen < 0 ) {
|
|
ask = XP_FALSE;
|
|
} else {
|
|
Tile tile = tiles[chosen];
|
|
oneTile.tiles[0] = tile;
|
|
pool_removeTiles( pool, &oneTile );
|
|
|
|
(void)dict_tilesToString( dict, &tile, 1, (XP_UCHAR*)&curTray[pi.nCurTiles++] );
|
|
|
|
resultTiles->tiles[nSoFar++] = tile;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Then fetch the rest without asking */
|
|
if ( nSoFar < nToFetch ) {
|
|
XP_U8 nLeft = nToFetch - nSoFar;
|
|
Tile tiles[MAX_TRAY_TILES];
|
|
|
|
pool_requestTiles( pool, tiles, &nLeft );
|
|
|
|
XP_MEMCPY( &resultTiles->tiles[nSoFar], tiles,
|
|
nLeft * sizeof(resultTiles->tiles[0]) );
|
|
nSoFar += nLeft;
|
|
}
|
|
|
|
XP_ASSERT( nSoFar < 0x00FF );
|
|
resultTiles->nTiles = (XP_U8)nSoFar;
|
|
} /* fetchTiles */
|
|
|
|
static void
|
|
assignTilesToAll( ServerCtxt* server )
|
|
{
|
|
XP_U16 numAssigned;
|
|
short i;
|
|
ModelCtxt* model = server->vol.model;
|
|
XP_U16 nPlayers = server->vol.gi->nPlayers;
|
|
|
|
XP_ASSERT( server->vol.gi->serverRole != SERVER_ISCLIENT );
|
|
XP_ASSERT( model_getDictionary(model) != NULL );
|
|
if ( server->pool == NULL ) {
|
|
server->pool = pool_make( MPPARM_NOCOMMA(server->mpool) );
|
|
XP_STATUSF( "initing pool" );
|
|
pool_initFromDict( server->pool, model_getDictionary(model));
|
|
}
|
|
|
|
XP_STATUSF( "assignTilesToAll" );
|
|
|
|
model_setNPlayers( model, nPlayers );
|
|
|
|
numAssigned = pool_getNTilesLeft( server->pool ) / nPlayers;
|
|
if ( numAssigned > MAX_TRAY_TILES ) {
|
|
numAssigned = MAX_TRAY_TILES;
|
|
}
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
TrayTileSet newTiles;
|
|
fetchTiles( server, i, numAssigned, NULL, &newTiles );
|
|
model_assignPlayerTiles( model, i, &newTiles );
|
|
}
|
|
|
|
} /* assignTilesToAll */
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
static void
|
|
getPlayerTime( ServerCtxt* server, XWStreamCtxt* stream, XP_U16 turn )
|
|
{
|
|
CurGameInfo* gi = server->vol.gi;
|
|
|
|
if ( gi->timerEnabled ) {
|
|
XP_U16 secondsUsed = stream_getU16( stream );
|
|
|
|
gi->players[turn].secondsUsed = secondsUsed;
|
|
}
|
|
} /* getPlayerTime */
|
|
#endif
|
|
|
|
static void
|
|
nextTurn( ServerCtxt* server, XP_S16 nxtTurn )
|
|
{
|
|
ServerPlayer* player;
|
|
XP_U16 nPlayers = server->vol.gi->nPlayers;
|
|
XP_U16 playerTilesLeft;
|
|
XP_S16 currentTurn = server->nv.currentTurn;
|
|
XP_Bool moreToDo = XP_FALSE;
|
|
|
|
if ( nxtTurn == PICK_NEXT ) {
|
|
XP_ASSERT( currentTurn >= 0 );
|
|
playerTilesLeft = model_getNumTilesTotal(server->vol.model,
|
|
currentTurn);
|
|
nxtTurn = (currentTurn+1) % nPlayers;
|
|
} else {
|
|
/* We're doing an undo, and so won't bother figuring out who the
|
|
previous turn was or how many tiles he had: it's a sure thing he
|
|
"has" enough to be allowed to take the turn just undone. */
|
|
playerTilesLeft = MAX_TRAY_TILES;
|
|
/* prevent game from ending immediately if this was already too high.
|
|
The right fix is to decrement it by the number being undone while
|
|
still keeping it >= 0, but that's too much code for what's a very
|
|
obscure bug.*/
|
|
server->nv.nPassesInRow = 0;
|
|
}
|
|
|
|
server->nv.gameState = XWSTATE_INTURN; /* unless game over */
|
|
|
|
if ( playerTilesLeft > 0
|
|
&& (server->nv.nPassesInRow / nPlayers < MAX_PASSES) ) {
|
|
|
|
player = &server->players[nxtTurn];
|
|
server->nv.currentTurn = (XP_U8)nxtTurn;
|
|
|
|
} else {
|
|
/* I discover that the game should end. If I'm the client, though,
|
|
should I wait for the server to deduce this and send out a message.
|
|
I think so. */
|
|
if ( server->vol.gi->serverRole != SERVER_ISCLIENT ) {
|
|
server->nv.gameState = XWSTATE_NEEDSEND_ENDGAME;
|
|
moreToDo = XP_TRUE;
|
|
}
|
|
}
|
|
|
|
if ( server->vol.showPrevMove ) {
|
|
server->vol.showPrevMove = XP_FALSE;
|
|
if ( server->nv.showRobotScores ) {
|
|
server->vol.stateAfterShow = server->nv.gameState;
|
|
server->nv.gameState = XWSTATE_NEED_SHOWSCORE;
|
|
moreToDo = XP_TRUE;
|
|
}
|
|
}
|
|
|
|
/* It's safer, if perhaps not always necessary, to do this here. */
|
|
resetEngines( server );
|
|
|
|
if ( server->nv.gameState != XWSTATE_GAMEOVER ) {
|
|
callTurnChangeListener( server );
|
|
} else {
|
|
XP_ASSERT(0);
|
|
}
|
|
|
|
if ( robotMovePending(server) ) {
|
|
moreToDo = XP_TRUE;
|
|
}
|
|
|
|
if ( moreToDo ) {
|
|
util_requestTime( server->vol.util );
|
|
}
|
|
} /* nextTurn */
|
|
|
|
void
|
|
server_setTurnChangeListener( ServerCtxt* server, TurnChangeListener tl,
|
|
void* data )
|
|
{
|
|
server->vol.turnChangeListener = tl;
|
|
server->vol.turnChangeData = data;
|
|
} /* server_setTurnChangeListener */
|
|
|
|
void
|
|
server_setGameOverListener( ServerCtxt* server, GameOverListener gol,
|
|
void* data )
|
|
{
|
|
server->vol.gameOverListener = gol;
|
|
server->vol.gameOverData = data;
|
|
} /* server_setTurnChangeListener */
|
|
|
|
static XP_Bool
|
|
storeBadWords( XP_UCHAR* word, void* closure )
|
|
{
|
|
ServerCtxt* server = (ServerCtxt*)closure;
|
|
|
|
XP_STATUSF( "storeBadWords called with \"%s\"", word );
|
|
|
|
server->illegalWordInfo.words[server->illegalWordInfo.nWords++]
|
|
= copyString( MPPARM(server->mpool) word );
|
|
|
|
return XP_TRUE;
|
|
} /* storeBadWords */
|
|
|
|
static XP_Bool
|
|
checkMoveAllowed( ServerCtxt* server, XP_U16 playerNum )
|
|
{
|
|
CurGameInfo* gi = server->vol.gi;
|
|
XP_ASSERT( server->illegalWordInfo.nWords == 0 );
|
|
|
|
if ( gi->phoniesAction == PHONIES_DISALLOW ) {
|
|
WordNotifierInfo info;
|
|
info.proc = storeBadWords;
|
|
info.closure = server;
|
|
(void)model_checkMoveLegal( server->vol.model, playerNum,
|
|
(XWStreamCtxt*)NULL, &info );
|
|
}
|
|
|
|
return server->illegalWordInfo.nWords == 0;
|
|
} /* checkMoveAllowed */
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
static void
|
|
sendMoveTo( ServerCtxt* server, XP_U16 devIndex, XP_U16 turn,
|
|
XP_Bool legal, TrayTileSet* newTiles,
|
|
TrayTileSet* tradedTiles ) /* null if a move, set if a trade */
|
|
{
|
|
XWStreamCtxt* stream;
|
|
XP_Bool isTrade = !!tradedTiles;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
XW_Proto code = gi->serverRole == SERVER_ISCLIENT?
|
|
XWPROTO_MOVEMADE_INFO_CLIENT : XWPROTO_MOVEMADE_INFO_SERVER;
|
|
|
|
stream = messageStreamWithHeader( server, devIndex, code );
|
|
|
|
stream_putBits( stream, PLAYERNUM_NBITS, turn ); /* who made the move */
|
|
|
|
traySetToStream( stream, newTiles );
|
|
|
|
stream_putBits( stream, 1, isTrade );
|
|
|
|
if ( isTrade ) {
|
|
|
|
traySetToStream( stream, tradedTiles );
|
|
|
|
} else {
|
|
stream_putBits( stream, 1, legal );
|
|
|
|
model_currentMoveToStream( server->vol.model, turn, stream );
|
|
|
|
if ( gi->timerEnabled ) {
|
|
stream_putU16( stream, gi->players[turn].secondsUsed );
|
|
XP_STATUSF("*** wrote secondsUsed for player %d: %d",
|
|
turn, gi->players[turn].secondsUsed );
|
|
} else {
|
|
XP_ASSERT( gi->players[turn].secondsUsed == 0 );
|
|
}
|
|
|
|
if ( !legal ) {
|
|
XP_ASSERT( server->illegalWordInfo.nWords > 0 );
|
|
bwiToStream( stream, &server->illegalWordInfo );
|
|
}
|
|
}
|
|
|
|
stream_destroy( stream );
|
|
} /* sendMoveTo */
|
|
|
|
static void
|
|
readMoveInfo( ServerCtxt* server, XWStreamCtxt* stream,
|
|
XP_U16* whoMovedP, XP_Bool* isTradeP,
|
|
TrayTileSet* newTiles, TrayTileSet* tradedTiles,
|
|
XP_Bool* legalP )
|
|
{
|
|
XP_U16 whoMoved = stream_getBits( stream, PLAYERNUM_NBITS );
|
|
XP_Bool legalMove = XP_TRUE;
|
|
XP_Bool isTrade;
|
|
|
|
traySetFromStream( stream, newTiles );
|
|
isTrade = stream_getBits( stream, 1 );
|
|
|
|
if ( isTrade ) {
|
|
traySetFromStream( stream, tradedTiles );
|
|
} else {
|
|
legalMove = stream_getBits( stream, 1 );
|
|
model_makeTurnFromStream( server->vol.model, whoMoved, stream );
|
|
|
|
getPlayerTime( server, stream, whoMoved );
|
|
}
|
|
|
|
pool_removeTiles( server->pool, newTiles );
|
|
|
|
*whoMovedP = whoMoved;
|
|
*isTradeP = isTrade;
|
|
*legalP = legalMove;
|
|
} /* readMoveInfo */
|
|
|
|
static void
|
|
sendMoveToClientsExcept( ServerCtxt* server, XP_U16 whoMoved, XP_Bool legal,
|
|
TrayTileSet* newTiles, TrayTileSet* tradedTiles,
|
|
XP_U16 skip )
|
|
{
|
|
XP_U16 devIndex;
|
|
|
|
for ( devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) {
|
|
if ( devIndex != skip ) {
|
|
sendMoveTo( server, devIndex, whoMoved, legal,
|
|
newTiles, tradedTiles );
|
|
}
|
|
}
|
|
} /* sendMoveToClientsExcept */
|
|
|
|
/* Client is reporting a move made, complete with new tiles and time taken by
|
|
* the player. Update the model with that information as a tentative move,
|
|
* then sent info about it to all the clients, and finally commit the move
|
|
* here.
|
|
*/
|
|
static XP_Bool
|
|
reflectMoveAndInform( ServerCtxt* server, XWStreamCtxt* stream )
|
|
{
|
|
ModelCtxt* model = server->vol.model;
|
|
XP_U16 whoMoved;
|
|
XP_U16 nTilesMoved = 0; /* trade case */
|
|
XP_Bool isTrade;
|
|
XP_Bool isLegalMove;
|
|
XP_Bool doRequest = XP_FALSE;
|
|
TrayTileSet newTiles;
|
|
TrayTileSet tradedTiles;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
XP_U16 sourceClientIndex =
|
|
getIndexForDevice( server, stream_getAddress( stream ) );
|
|
XWStreamCtxt* mvStream = NULL;
|
|
|
|
XP_ASSERT( gi->serverRole == SERVER_ISSERVER );
|
|
|
|
readMoveInfo( server, stream, &whoMoved, &isTrade, &newTiles,
|
|
&tradedTiles, &isLegalMove );
|
|
XP_ASSERT( isLegalMove ); /* client should always report as true */
|
|
isLegalMove = XP_TRUE;
|
|
|
|
if ( isTrade ) {
|
|
|
|
sendMoveToClientsExcept( server, whoMoved, XP_TRUE, &newTiles,
|
|
&tradedTiles, sourceClientIndex );
|
|
|
|
model_makeTileTrade( model, whoMoved,
|
|
&tradedTiles, &newTiles );
|
|
pool_replaceTiles( server->pool, &tradedTiles );
|
|
|
|
server->vol.showPrevMove = XP_TRUE;
|
|
if ( server->nv.showRobotScores ) {
|
|
XP_UCHAR tradeBuf[64];
|
|
XP_UCHAR* tradeStr = util_getUserString( server->vol.util,
|
|
STRD_ROBOT_TRADED );
|
|
XP_SNPRINTF( tradeBuf, sizeof(tradeBuf),
|
|
tradeStr, tradedTiles.nTiles );
|
|
mvStream = mkServerStream( server );
|
|
stream_putBytes( mvStream, tradeBuf, XP_STRLEN(tradeBuf) );
|
|
}
|
|
|
|
} else {
|
|
nTilesMoved = model_getCurrentMoveCount( model, whoMoved );
|
|
isLegalMove = (nTilesMoved == 0)
|
|
|| checkMoveAllowed( server, whoMoved );
|
|
|
|
/* I don't think this will work if there are more than two devices in
|
|
a palm game; need to change state and get out of here before
|
|
returning to send additional messages. PENDING(ehouse) */
|
|
sendMoveToClientsExcept( server, whoMoved, isLegalMove, &newTiles,
|
|
(TrayTileSet*)NULL, sourceClientIndex );
|
|
|
|
server->vol.showPrevMove = XP_TRUE;
|
|
if ( server->nv.showRobotScores ) {
|
|
mvStream = mkServerStream( server );
|
|
(void)model_checkMoveLegal( server->vol.model,
|
|
server->nv.currentTurn, mvStream,
|
|
NULL );
|
|
}
|
|
|
|
model_commitTurn( model, whoMoved, &newTiles );
|
|
resetEngines( server );
|
|
}
|
|
|
|
if ( isLegalMove ) {
|
|
XP_U16 nTilesLeft = model_getNumTilesTotal( model, whoMoved );
|
|
|
|
if ( (gi->phoniesAction == PHONIES_DISALLOW) && (nTilesMoved > 0) ) {
|
|
server->lastMoveSource = sourceClientIndex;
|
|
server->nv.gameState = XWSTATE_MOVE_CONFIRM_MUSTSEND;
|
|
doRequest = XP_TRUE;
|
|
} else if ( nTilesLeft > 0 ) {
|
|
nextTurn( server, PICK_NEXT );
|
|
} else {
|
|
server->nv.gameState = XWSTATE_NEEDSEND_ENDGAME;
|
|
doRequest = XP_TRUE;
|
|
}
|
|
|
|
if ( !!mvStream ) {
|
|
XP_ASSERT( !server->vol.prevMoveStream );
|
|
server->vol.prevMoveStream = mvStream;
|
|
}
|
|
|
|
} else {
|
|
/* The client from which the move came still needs to be told. But we
|
|
can't send a message now since we're burried in a message handler.
|
|
(Palm, at least, won't manage.) So set up state to tell that
|
|
client again in a minute. */
|
|
server->nv.gameState = XWSTATE_NEEDSEND_BADWORD_INFO;
|
|
server->lastMoveSource = sourceClientIndex;
|
|
doRequest = XP_TRUE;
|
|
}
|
|
|
|
if ( doRequest ) {
|
|
util_requestTime( server->vol.util );
|
|
}
|
|
|
|
return XP_TRUE;
|
|
} /* reflectMoveAndInform */
|
|
|
|
static XP_Bool
|
|
reflectMove( ServerCtxt* server, XWStreamCtxt* stream )
|
|
{
|
|
XP_Bool isTrade;
|
|
XP_Bool isLegal;
|
|
XP_U16 whoMoved;
|
|
TrayTileSet newTiles;
|
|
TrayTileSet tradedTiles;
|
|
ModelCtxt* model = server->vol.model;
|
|
|
|
readMoveInfo( server, stream, &whoMoved, &isTrade, &newTiles,
|
|
&tradedTiles, &isLegal );
|
|
|
|
if ( isTrade ) {
|
|
|
|
model_makeTileTrade( model, whoMoved, &tradedTiles, &newTiles );
|
|
pool_replaceTiles( server->pool, &tradedTiles );
|
|
/* pool_removeTiles( server->pool, &newTiles ); */
|
|
server->vol.showPrevMove = XP_TRUE;
|
|
} else {
|
|
server->vol.showPrevMove = XP_TRUE;
|
|
model_commitTurn( model, whoMoved, &newTiles );
|
|
}
|
|
|
|
resetEngines( server );
|
|
|
|
if ( !isLegal ) {
|
|
XP_ASSERT( server->vol.gi->serverRole == SERVER_ISCLIENT );
|
|
handleIllegalWord( server, stream );
|
|
}
|
|
|
|
return XP_TRUE;
|
|
} /* reflectMove */
|
|
#endif
|
|
|
|
/* A local player is done with his turn. If a client device, broadcast
|
|
* the move to the server (after which a response should be coming soon.)
|
|
* If the server, then that step can be skipped: go straight to doing what
|
|
* the server does after learning of a move on a remote device.
|
|
*
|
|
* Second cut. Whether server or client, be responsible for checking the
|
|
* basic legality of the move and taking new tiles out of the pool. If
|
|
* client, send the move and new tiles to the server. If the server, fall
|
|
* back to what will do after hearing from client: tell everybody who doesn't
|
|
* already know what's happened: move and new tiles together.
|
|
*
|
|
* What about phonies when DISALLOW is set? The server's ultimately
|
|
* responsible, since it has the dictionary, so the client can't check. So
|
|
* when server, check and send move together with a flag indicating legality.
|
|
* Client is responsible for first posting the move to the model and then
|
|
* undoing it. When client, send the move and go into a state waiting to hear
|
|
* if it was legal -- but only if DISALLOW is set.
|
|
*/
|
|
XP_Bool
|
|
server_commitMove( ServerCtxt* server )
|
|
{
|
|
XP_S16 turn = server->nv.currentTurn;
|
|
ModelCtxt* model = server->vol.model;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
TrayTileSet newTiles;
|
|
XP_U16 nTilesMoved;
|
|
XP_Bool isLegalMove = XP_TRUE;
|
|
XP_Bool isClient = gi->serverRole == SERVER_ISCLIENT;
|
|
|
|
#ifdef DEBUG
|
|
if ( IS_ROBOT( &gi->players[turn] ) ) {
|
|
XP_ASSERT( model_checkMoveLegal( model, turn, (XWStreamCtxt*)NULL,
|
|
(WordNotifierInfo*)NULL ) );
|
|
}
|
|
#endif
|
|
|
|
/* commit the move. get new tiles. if server, send to everybody.
|
|
if client, send to server. */
|
|
XP_ASSERT( turn >= 0 );
|
|
|
|
nTilesMoved = model_getCurrentMoveCount( model, turn );
|
|
if ( nTilesMoved > 0 ) {
|
|
server->nv.nPassesInRow = 0;
|
|
} else {
|
|
++server->nv.nPassesInRow;
|
|
}
|
|
|
|
fetchTiles( server, turn, nTilesMoved, NULL, &newTiles );
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
if ( isClient ) {
|
|
/* just send to server */
|
|
sendMoveTo( server, SERVER_DEVICE, turn, XP_TRUE, &newTiles,
|
|
(TrayTileSet*)NULL );
|
|
} else {
|
|
isLegalMove = checkMoveAllowed( server, turn );
|
|
sendMoveToClientsExcept( server, turn, isLegalMove, &newTiles,
|
|
(TrayTileSet*)NULL, SERVER_DEVICE );
|
|
}
|
|
#else
|
|
isLegalMove = checkMoveAllowed( server, turn );
|
|
#endif
|
|
|
|
model_commitTurn( model, turn, &newTiles );
|
|
|
|
if ( !isLegalMove && !isClient ) {
|
|
badWordMoveUndoAndTellUser( server, &server->illegalWordInfo );
|
|
/* It's ok to free these guys. I'm the server, and the move was made
|
|
here, so I've notified all clients already by setting the flag (and
|
|
passing the word) in sendMoveToClientsExcept. */
|
|
freeBWI( MPPARM(server->mpool) &server->illegalWordInfo );
|
|
}
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
if (isClient && (gi->phoniesAction == PHONIES_DISALLOW)
|
|
&& nTilesMoved > 0 ) {
|
|
server->nv.gameState = XWSTATE_MOVE_CONFIRM_WAIT;
|
|
} else {
|
|
nextTurn( server, PICK_NEXT );
|
|
}
|
|
#else
|
|
nextTurn( server, PICK_NEXT );
|
|
#endif
|
|
|
|
return XP_TRUE;
|
|
} /* server_commitMove */
|
|
|
|
static void
|
|
removeTradedTiles( ServerCtxt* server, TileBit selBits, TrayTileSet* tiles )
|
|
{
|
|
XP_U8 nTiles = 0;
|
|
XP_S16 index;
|
|
XP_S16 turn = server->nv.currentTurn;
|
|
|
|
/* selBits: It's gross that server knows this much about tray's
|
|
implementation. PENDING(ehouse) */
|
|
|
|
for ( index = 0; selBits != 0; selBits >>= 1, ++index ) {
|
|
if ( (selBits & 0x01) != 0 ) {
|
|
Tile tile = model_getPlayerTile( server->vol.model, turn, index );
|
|
tiles->tiles[nTiles++] = tile;
|
|
}
|
|
}
|
|
tiles->nTiles = nTiles;
|
|
} /* saveTradedTiles */
|
|
|
|
XP_Bool
|
|
server_commitTrade( ServerCtxt* server, TileBit selBits )
|
|
{
|
|
TrayTileSet oldTiles;
|
|
TrayTileSet newTiles;
|
|
XP_U16 turn = server->nv.currentTurn;
|
|
|
|
removeTradedTiles( server, selBits, &oldTiles );
|
|
|
|
fetchTiles( server, turn, oldTiles.nTiles, &oldTiles, &newTiles );
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
if ( server->vol.gi->serverRole == SERVER_ISCLIENT ) {
|
|
/* just send to server */
|
|
sendMoveTo(server, SERVER_DEVICE, turn, XP_TRUE, &newTiles, &oldTiles);
|
|
} else {
|
|
sendMoveToClientsExcept( server, turn, XP_TRUE, &newTiles, &oldTiles,
|
|
SERVER_DEVICE );
|
|
}
|
|
#endif
|
|
|
|
pool_replaceTiles( server->pool, &oldTiles );
|
|
model_makeTileTrade( server->vol.model, server->nv.currentTurn,
|
|
&oldTiles, &newTiles );
|
|
nextTurn( server, PICK_NEXT );
|
|
return XP_TRUE;
|
|
} /* server_commitTrade */
|
|
|
|
XP_S16
|
|
server_getCurrentTurn( ServerCtxt* server )
|
|
{
|
|
return server->nv.currentTurn;
|
|
} /* server_getCurrentTurn */
|
|
|
|
XP_Bool
|
|
server_getGameIsOver( ServerCtxt* server )
|
|
{
|
|
return server->nv.gameState == XWSTATE_GAMEOVER;
|
|
} /* server_getGameIsOver */
|
|
|
|
static void
|
|
doEndGame( ServerCtxt* server )
|
|
{
|
|
server->nv.gameState = XWSTATE_GAMEOVER;
|
|
server->nv.currentTurn = -1;
|
|
|
|
(*server->vol.gameOverListener)( server->vol.gameOverData );
|
|
} /* doEndGame */
|
|
|
|
/* Somebody wants to end the game.
|
|
*
|
|
* If I'm the server, I send a END_GAME message to all clients. If I'm a
|
|
* client, I send the GAME_OVER_REQUEST message to the server. If I'm the
|
|
* server and this is called in response to a GAME_OVER_REQUEST, send the
|
|
* GAME_OVER message to all clients including the one that requested it.
|
|
*/
|
|
static void
|
|
endGameInternal( ServerCtxt* server, GameEndReason why )
|
|
{
|
|
XP_ASSERT( server->nv.gameState != XWSTATE_GAMEOVER );
|
|
|
|
if ( server->vol.gi->serverRole != SERVER_ISCLIENT ) {
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
XP_U16 devIndex;
|
|
for ( devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) {
|
|
XWStreamCtxt* stream;
|
|
stream = messageStreamWithHeader( server, devIndex,
|
|
XWPROTO_END_GAME );
|
|
stream_destroy( stream );
|
|
}
|
|
#endif
|
|
doEndGame( server );
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
} else {
|
|
XWStreamCtxt* stream;
|
|
stream = messageStreamWithHeader( server, SERVER_DEVICE,
|
|
XWPROTO_CLIENT_REQ_END_GAME );
|
|
stream_destroy( stream );
|
|
|
|
/* Do I want to change the state I'm in? I don't think so.... */
|
|
#endif
|
|
}
|
|
} /* endGameInternal */
|
|
|
|
void
|
|
server_endGame( ServerCtxt* server )
|
|
{
|
|
XW_State gameState = server->nv.gameState;
|
|
if ( gameState < XWSTATE_GAMEOVER && gameState >= XWSTATE_INTURN ) {
|
|
endGameInternal( server, END_REASON_USER_REQUEST );
|
|
}
|
|
} /* server_endGame */
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
static void
|
|
tellMoveWasLegal( ServerCtxt* server )
|
|
{
|
|
XWStreamCtxt* stream;
|
|
|
|
stream = messageStreamWithHeader( server, server->lastMoveSource,
|
|
XWPROTO_MOVE_CONFIRM );
|
|
stream_destroy( stream );
|
|
} /* tellMoveWasLegal */
|
|
|
|
static XP_Bool
|
|
handleIllegalWord( ServerCtxt* server, XWStreamCtxt* incomming )
|
|
{
|
|
BadWordInfo bwi;
|
|
XP_U16 whichPlayer;
|
|
|
|
whichPlayer = stream_getBits( incomming, PLAYERNUM_NBITS );
|
|
bwiFromStream( MPPARM(server->mpool) incomming, &bwi );
|
|
|
|
badWordMoveUndoAndTellUser( server, &bwi );
|
|
|
|
freeBWI( MPPARM(server->mpool) &bwi );
|
|
|
|
return XP_TRUE;
|
|
} /* handleIllegalWord */
|
|
|
|
static XP_Bool
|
|
handleMoveOk( ServerCtxt* server, XWStreamCtxt* incomming /* unused */ )
|
|
{
|
|
XP_Bool accepted = XP_TRUE;
|
|
XP_ASSERT( server->vol.gi->serverRole == SERVER_ISCLIENT );
|
|
XP_ASSERT( server->nv.gameState == XWSTATE_MOVE_CONFIRM_WAIT );
|
|
|
|
nextTurn( server, PICK_NEXT );
|
|
|
|
return accepted;
|
|
} /* handleMoveOk */
|
|
|
|
static void
|
|
sendUndoTo( ServerCtxt* server, XP_U16 devIndex, XP_U16 nUndone,
|
|
XP_U16 lastUndone )
|
|
{
|
|
XWStreamCtxt* stream;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
XW_Proto code = gi->serverRole == SERVER_ISCLIENT?
|
|
XWPROTO_UNDO_INFO_CLIENT : XWPROTO_UNDO_INFO_SERVER;
|
|
|
|
stream = messageStreamWithHeader( server, devIndex, code );
|
|
|
|
stream_putU16( stream, nUndone );
|
|
stream_putU16( stream, lastUndone );
|
|
|
|
stream_destroy( stream );
|
|
} /* sendUndoTo */
|
|
|
|
static void
|
|
sendUndoToClientsExcept( ServerCtxt* server, XP_U16 skip,
|
|
XP_U16 nUndone, XP_U16 lastUndone )
|
|
{
|
|
XP_U16 devIndex;
|
|
|
|
for ( devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) {
|
|
if ( devIndex != skip ) {
|
|
sendUndoTo( server, devIndex, nUndone, lastUndone );
|
|
}
|
|
}
|
|
} /* sendUndoToClientsExcept */
|
|
|
|
static XP_Bool
|
|
reflectUndos( ServerCtxt* server, XWStreamCtxt* stream, XW_Proto code )
|
|
{
|
|
XP_U16 nUndone, lastUndone;
|
|
XP_S16 moveNum;
|
|
XP_U16 turn;
|
|
ModelCtxt* model = server->vol.model;
|
|
XP_Bool success = XP_TRUE;
|
|
|
|
nUndone = stream_getU16( stream );
|
|
lastUndone = stream_getU16( stream );
|
|
|
|
moveNum = lastUndone + nUndone - 1;
|
|
|
|
success = model_undoLatestMoves(model, server->pool, nUndone, &turn,
|
|
&moveNum);
|
|
|
|
if ( success ) {
|
|
XP_ASSERT( moveNum == lastUndone );
|
|
|
|
if ( code == XWPROTO_UNDO_INFO_CLIENT ) { /* need to inform */
|
|
XP_U16 sourceClientIndex = getIndexForDevice(
|
|
server, stream_getAddress( stream ) );
|
|
|
|
sendUndoToClientsExcept( server, sourceClientIndex, nUndone,
|
|
lastUndone );
|
|
|
|
}
|
|
nextTurn( server, turn );
|
|
}
|
|
|
|
return success;
|
|
} /* reflectUndos */
|
|
#endif
|
|
|
|
XP_Bool
|
|
server_handleUndo( ServerCtxt* server )
|
|
{
|
|
XP_Bool result = XP_FALSE;
|
|
XP_U16 lastTurnUndone = 0; /* quiet compiler good */
|
|
XP_U16 nUndone = 0;
|
|
ModelCtxt* model;
|
|
CurGameInfo* gi;
|
|
XP_U16 lastUndone = 0xFFFF;
|
|
|
|
model = server->vol.model;
|
|
gi = server->vol.gi;
|
|
XP_ASSERT( !!model );
|
|
|
|
/* Undo until we find we've just undone a non-robot move. The point is
|
|
not to stop with a robot about to act (since that's a bit pointless.)
|
|
The exception is that if the first move was a robot move we'll stop
|
|
there, and it will immediately move again. */
|
|
for ( ; ; ) {
|
|
XP_S16 moveNum = -1; /* don't need it checked */
|
|
if ( !model_undoLatestMoves( model, server->pool, 1, &lastTurnUndone,
|
|
&moveNum ) ) {
|
|
break;
|
|
}
|
|
++nUndone;
|
|
XP_ASSERT( moveNum >= 0 );
|
|
lastUndone = moveNum;
|
|
if ( !IS_ROBOT(&gi->players[lastTurnUndone]) ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
result = nUndone > 0 ;
|
|
if ( result ) {
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
XP_ASSERT( lastUndone != 0xFFFF );
|
|
if ( server->vol.gi->serverRole == SERVER_ISCLIENT ) {
|
|
sendUndoTo( server, SERVER_DEVICE, nUndone, lastUndone );
|
|
} else {
|
|
sendUndoToClientsExcept( server, SERVER_DEVICE, nUndone,
|
|
lastUndone );
|
|
}
|
|
#endif
|
|
nextTurn( server, lastTurnUndone );
|
|
}
|
|
|
|
return result;
|
|
} /* server_handleUndo */
|
|
|
|
#ifndef XWFEATURE_STANDALONE_ONLY
|
|
XP_Bool
|
|
server_receiveMessage( ServerCtxt* server, XWStreamCtxt* incomming )
|
|
{
|
|
XW_Proto code;
|
|
XP_Bool accepted = XP_FALSE;
|
|
|
|
code = (XW_Proto)stream_getBits( incomming, XWPROTO_NBITS );
|
|
|
|
printCode("Receiving", code);
|
|
|
|
if ( code == XWPROTO_DEVICE_REGISTRATION ) {
|
|
/* This message is special: doesn't have the header that's possible
|
|
once the game's in progress and communication's been
|
|
established. */
|
|
XP_STATUSF( "somebody's registering!!!" );
|
|
accepted = handleRegistrationMsg( server, incomming );
|
|
|
|
} else if ( code == XWPROTO_CLIENT_SETUP ) {
|
|
|
|
XP_STATUSF( "client got XWPROTO_CLIENT_SETUP" );
|
|
XP_ASSERT( server->vol.gi->serverRole == SERVER_ISCLIENT );
|
|
accepted = client_readInitialMessage( server, incomming );
|
|
|
|
} else if ( readStreamHeader( server, incomming ) ) {
|
|
|
|
switch( code ) {
|
|
/* case XWPROTO_MOVEMADE_INFO: */
|
|
/* accepted = client_reflectMoveMade( server, incomming ); */
|
|
/* if ( accepted ) { */
|
|
/* nextTurn( server ); */
|
|
/* } */
|
|
/* break; */
|
|
/* case XWPROTO_TRADEMADE_INFO: */
|
|
/* accepted = client_reflectTradeMade( server, incomming ); */
|
|
/* if ( accepted ) { */
|
|
/* nextTurn( server ); */
|
|
/* } */
|
|
/* break; */
|
|
/* case XWPROTO_CLIENT_MOVE_INFO: */
|
|
/* accepted = handleClientMoved( server, incomming ); */
|
|
/* break; */
|
|
/* case XWPROTO_CLIENT_TRADE_INFO: */
|
|
/* accepted = handleClientTraded( server, incomming ); */
|
|
/* break; */
|
|
|
|
case XWPROTO_MOVEMADE_INFO_CLIENT: /* client is reporting a move */
|
|
accepted = reflectMoveAndInform( server, incomming );
|
|
break;
|
|
|
|
case XWPROTO_MOVEMADE_INFO_SERVER: /* server telling me about a move */
|
|
accepted = reflectMove( server, incomming );
|
|
if ( accepted ) {
|
|
nextTurn( server, PICK_NEXT );
|
|
}
|
|
break;
|
|
|
|
case XWPROTO_UNDO_INFO_CLIENT:
|
|
case XWPROTO_UNDO_INFO_SERVER:
|
|
accepted = reflectUndos( server, incomming, code );
|
|
/* nextTurn is called by reflectUndos */
|
|
break;
|
|
|
|
case XWPROTO_BADWORD_INFO:
|
|
accepted = handleIllegalWord( server, incomming );
|
|
if ( accepted && server->nv.gameState != XWSTATE_GAMEOVER ) {
|
|
nextTurn( server, PICK_NEXT );
|
|
}
|
|
break;
|
|
|
|
case XWPROTO_MOVE_CONFIRM:
|
|
accepted = handleMoveOk( server, incomming );
|
|
break;
|
|
|
|
case XWPROTO_CLIENT_REQ_END_GAME:
|
|
endGameInternal( server, END_REASON_USER_REQUEST );
|
|
accepted = XP_TRUE;
|
|
break;
|
|
case XWPROTO_END_GAME:
|
|
doEndGame( server );
|
|
accepted = XP_TRUE;
|
|
break;
|
|
default:
|
|
XP_WARNF( "Unknown code on incomming message: %d\n", code );
|
|
break;
|
|
} /* switch */
|
|
}
|
|
|
|
stream_close( incomming );
|
|
return accepted;
|
|
} /* server_receiveMessage */
|
|
#endif
|
|
|
|
void
|
|
server_formatPoolCounts( ServerCtxt* server, XWStreamCtxt* stream,
|
|
XP_U16 nCols )
|
|
{
|
|
DictionaryCtxt* dict;
|
|
Tile tile;
|
|
XP_U16 nChars, nPrinted;
|
|
XP_Bool hasBlank;
|
|
Tile blank = 0; /* shut compiler up */
|
|
XP_U16 counts[MAX_UNIQUE_TILES+1]; /* 1 for the blank */
|
|
PoolContext* pool = server->pool;
|
|
|
|
if ( !pool ) {
|
|
return; /* might want to print an explanation in the stream */
|
|
}
|
|
|
|
XP_ASSERT( !!server->vol.model );
|
|
|
|
XP_MEMSET( counts, 0, sizeof(counts) );
|
|
model_countAllTrayTiles( server->vol.model, counts );
|
|
|
|
dict = model_getDictionary( server->vol.model );
|
|
nChars = dict_numTileFaces( dict );
|
|
hasBlank = dict_hasBlankTile( dict );
|
|
if ( hasBlank ) {
|
|
blank = dict_getBlankTile( dict );
|
|
}
|
|
|
|
for ( tile = 0, nPrinted = 0; ; ) {
|
|
XP_UCHAR buf[24];
|
|
XP_UCHAR face[4];
|
|
XP_U16 count, value;
|
|
XP_S16 nRemaining; /* signed so assertion will work */
|
|
|
|
count = dict_numTiles( dict, tile );
|
|
|
|
if ( count > 0 ) {
|
|
dict_tilesToString( dict, &tile, 1, face );
|
|
nRemaining = pool_getNTilesLeftFor( pool, tile ) + counts[tile];
|
|
XP_ASSERT( nRemaining >= 0 );
|
|
value = dict_getTileValue( dict, tile );
|
|
|
|
XP_SNPRINTF( buf, sizeof(buf), (XP_UCHAR*)"%s: %d[%d] %d",
|
|
face, count, nRemaining, value );
|
|
stream_putBytes( stream, buf, (XP_U16)XP_STRLEN(buf) );
|
|
}
|
|
|
|
if ( ++tile >= nChars ) {
|
|
break;
|
|
} else if ( count > 0 ) {
|
|
if ( ++nPrinted % nCols == 0 ) {
|
|
stream_putBytes( stream, XP_CR, (XP_U16)XP_STRLEN(XP_CR) );
|
|
} else {
|
|
stream_putBytes( stream, (void*)" ", 3 );
|
|
}
|
|
}
|
|
}
|
|
} /* server_formatPoolCounts */
|
|
|
|
#define IMPOSSIBLY_LOW_SCORE -1000
|
|
void
|
|
server_writeFinalScores( ServerCtxt* server, XWStreamCtxt* stream )
|
|
{
|
|
XP_S16 scores[MAX_NUM_PLAYERS];
|
|
XP_S16 tilePenalties[MAX_NUM_PLAYERS];
|
|
XP_S16 highestIndex;
|
|
XP_S16 highestScore;
|
|
XP_U16 place, nPlayers, i;
|
|
XP_S16 curScore;
|
|
ModelCtxt* model = server->vol.model;
|
|
XP_UCHAR* addString = util_getUserString( server->vol.util,
|
|
STRD_REMAINING_TILES_ADD );
|
|
XP_UCHAR* subString = util_getUserString( server->vol.util,
|
|
STRD_UNUSED_TILES_SUB );
|
|
XP_UCHAR timeBuf[16];
|
|
XP_UCHAR* timeStr;
|
|
CurGameInfo* gi = server->vol.gi;
|
|
|
|
XP_ASSERT( server->nv.gameState == XWSTATE_GAMEOVER );
|
|
|
|
model_figureFinalScores( model, scores, tilePenalties );
|
|
|
|
nPlayers = gi->nPlayers;
|
|
|
|
for ( place = 1; ; ++place ) {
|
|
XP_UCHAR tmpbuf[48];
|
|
XP_UCHAR buf[128];
|
|
XP_Bool firstDone;
|
|
|
|
highestScore = IMPOSSIBLY_LOW_SCORE;
|
|
highestIndex = -1;
|
|
|
|
for ( i = 0; i < nPlayers; ++i ) {
|
|
if ( scores[i] > highestScore ) {
|
|
highestIndex = i;
|
|
highestScore = scores[i];
|
|
}
|
|
}
|
|
|
|
if ( highestIndex == -1 ) {
|
|
break; /* we're done */
|
|
} else if ( place > 1 ) {
|
|
stream_putBytes( stream, XP_CR, (XP_U16)XP_STRLEN(XP_CR) );
|
|
}
|
|
scores[highestIndex] = IMPOSSIBLY_LOW_SCORE;
|
|
|
|
curScore = model_getPlayerScore( model, highestIndex );
|
|
|
|
timeStr = (XP_UCHAR*)"";
|
|
if ( gi->timerEnabled ) {
|
|
XP_U16 penalty = player_timePenalty( gi, highestIndex );
|
|
if ( penalty > 0 ) {
|
|
XP_SNPRINTF( timeBuf, sizeof(timeBuf),
|
|
util_getUserString(
|
|
server->vol.util,
|
|
STRD_TIME_PENALTY_SUB ),
|
|
penalty ); /* positive for formatting */
|
|
timeStr = timeBuf;
|
|
}
|
|
}
|
|
|
|
firstDone = model_getNumTilesTotal( model, highestIndex) == 0;
|
|
XP_SNPRINTF( tmpbuf, sizeof(tmpbuf),
|
|
(firstDone? addString:subString),
|
|
firstDone?
|
|
tilePenalties[highestIndex]:
|
|
-tilePenalties[highestIndex] );
|
|
|
|
XP_SNPRINTF( buf, sizeof(buf),
|
|
(XP_UCHAR*)"[%d] %s: %d" XP_CR " (%d %s%s)",
|
|
place,
|
|
emptyStringIfNull(gi->players[highestIndex].name),
|
|
highestScore, curScore, tmpbuf, timeStr );
|
|
stream_putBytes( stream, buf, (XP_U16)XP_STRLEN(buf) );
|
|
}
|
|
} /* server_writeFinalScores */
|
|
|
|
#ifdef CPLUS
|
|
}
|
|
#endif
|