diff --git a/xwords4/android/app/build.gradle b/xwords4/android/app/build.gradle
index d7d5b6d67..e26731bd8 100644
--- a/xwords4/android/app/build.gradle
+++ b/xwords4/android/app/build.gradle
@@ -4,9 +4,28 @@ def VERSION_NAME = '4.4.154'
def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY")
def BUILD_INFO_NAME = "build-info.txt"
+// Not all variants use the same BT_UUID. Those with the same talk to
+// each other
+def XW_UUID = '"7be0d084-ff89-4d6d-9c78-594773a6f963"' // from comms.h
+def XWD_UUID = '"b079b640-35fe-11e5-a432-0002a5d5c51b"' // from comms.h
+def XWDUP_UUID = '"92602f84-1bc5-11ea-978f-2e728ce88125"'
+def BT_UUIDS = [
+ 'xw4fdroidDebug' : XW_UUID,
+ 'xw4fdroidRelease' : XW_UUID,
+ 'xw4NoSMSDebug' : XW_UUID,
+ 'xw4NoSMSRelease' : XW_UUID,
+ 'xw4SMSDebug' : XW_UUID,
+ 'xw4SMSRelease' : XW_UUID,
+
+ 'xw4dupDebug' : XWDUP_UUID,
+ 'xw4dupRelease' : XWDUP_UUID,
+ 'xw4dupNoSMSDebug' : XWDUP_UUID,
+ 'xw4dupNoSMSRelease' : XWDUP_UUID,
+]
+
// AID must start with F (first 4 bits) and be from 5 to 16 bytes long
def NFC_AID_XW4 = "FC8FF510B360"
-def NFC_AID_XW4d = "FDDA0A3EB5E5"
+def NFC_AID_XW4dup = "F8960736B33C"
boolean forFDroid = hasProperty('forFDroid')
@@ -79,7 +98,7 @@ android {
buildConfigField "boolean", "UDP_ENABLED", "true"
buildConfigField "boolean", "REPORT_LOCKS", "false"
buildConfigField "boolean", "LOG_LIFECYLE", "false"
- buildConfigField "boolean", "MOVE_VIA_NFC", "false"
+ buildConfigField "String", "KEY_FCMID", "\"FBMService_fcmid\""
buildConfigField "boolean", "ATTACH_SUPPORTED", "false"
}
@@ -111,38 +130,37 @@ android {
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4"
}
- xw4d {
+ xw4dup {
dimension "variant"
- buildConfigField "String", "DB_NAME", "\"xwddb\""
- applicationId "org.eehouse.android.xw4dbg"
+ buildConfigField "String", "DB_NAME", "\"xwddb\"";
+ applicationId "org.eehouse.android.xw4dup"
manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ]
- resValue "string", "app_name", "CrossDbg"
+ resValue "string", "app_name", "CrossDup"
resValue "string", "nbs_port", "3345"
buildConfigField "boolean", "WIDIR_ENABLED", "true"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "true"
buildConfigField "String", "VARIANT_NAME", "\"Dev/Debug\""
buildConfigField "int", "VARIANT_CODE", "3"
buildConfigField "boolean", "REPORT_LOCKS", "true"
- buildConfigField "boolean", "MOVE_VIA_NFC", "true"
- buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4d}\""
- resValue "string", "nfc_aid", "$NFC_AID_XW4d"
+ buildConfigField "String", "KEY_FCMID", "\"FBMService_fcmid1\""
+ buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4dup}\""
+ resValue "string", "nfc_aid", "$NFC_AID_XW4dup"
}
- xw4dNoSMS {
+ xw4dupNoSMS {
dimension "variant"
+ applicationId "org.eehouse.android.xw4dup"
buildConfigField "String", "DB_NAME", "\"xwddb\""
- applicationId "org.eehouse.android.xw4dbg"
manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ]
- resValue "string", "app_name", "CrossDbg"
+ resValue "string", "app_name", "CrossDup"
resValue "string", "nbs_port", "3345"
buildConfigField "boolean", "WIDIR_ENABLED", "true"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "true"
buildConfigField "String", "VARIANT_NAME", "\"Dev/Debug NoSMS\""
buildConfigField "int", "VARIANT_CODE", "4"
buildConfigField "boolean", "REPORT_LOCKS", "true"
- buildConfigField "boolean", "MOVE_VIA_NFC", "true"
- buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4d}\""
- resValue "string", "nfc_aid", "$NFC_AID_XW4d"
+ buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4dup}\""
+ resValue "string", "nfc_aid", "$NFC_AID_XW4dup"
}
xw4SMS {
@@ -223,20 +241,20 @@ android {
jniLibs.srcDir "../libs-xw4NoSMSDebug"
}
}
- xw4d {
+ xw4dup {
release {
- jniLibs.srcDir "../libs-xw4dRelease"
+ jniLibs.srcDir "../libs-xw4dupRelease"
}
debug {
- jniLibs.srcDir "../libs-xw4dDebug"
+ jniLibs.srcDir "../libs-xw4dupDebug"
}
}
- xw4dNoSMS {
+ xw4dupNoSMS {
release {
- jniLibs.srcDir "../libs-xw4dNoSMSRelease"
+ jniLibs.srcDir "../libs-xw4dupNoSMSRelease"
}
debug {
- jniLibs.srcDir "../libs-xw4dNoSMSDebug"
+ jniLibs.srcDir "../libs-xw4dupNoSMSDebug"
}
}
xw4SMS {
@@ -284,10 +302,10 @@ dependencies {
// 2.6.8 is probably as far forward as I can go without upping my
// min-supported SDK version
- xw4dImplementation('com.crashlytics.sdk.android:crashlytics:2.6.3@aar') { // rm-for-fdroid
+ xw4dupImplementation('com.crashlytics.sdk.android:crashlytics:2.6.3@aar') { // rm-for-fdroid
transitive = true // rm-for-fdroid
} // rm-for-fdroid
- xw4dNoSMSImplementation('com.crashlytics.sdk.android:crashlytics:2.6.3@aar') { // rm-for-fdroid
+ xw4dupNoSMSImplementation('com.crashlytics.sdk.android:crashlytics:2.6.3@aar') { // rm-for-fdroid
transitive = true // rm-for-fdroid
} // rm-for-fdroid
@@ -357,12 +375,13 @@ afterEvaluate {
String nameLC = variant.getBuildType().getName().toLowerCase()
String lib = "libs-${variant.name}"
String ndkBuildTask = "ndkBuild${variantCaps}"
+ String btUUID = BT_UUIDS[variant.name]
task "$ndkBuildTask"(type: Exec) {
- workingDir '../'
- commandLine './scripts/ndkbuild.sh', '-j3',
- "BUILD_TARGET=${nameLC}", "INITIAL_CLIENT_VERS=$INITIAL_CLIENT_VERS",
- "VARIANT=${FLAVOR}", "NDK_LIBS_OUT=${lib}",
- "NDK_OUT=./obj-${variant.name}"
+ workingDir '../'
+ commandLine './scripts/ndkbuild.sh', '-j3',
+ "BUILD_TARGET=${nameLC}", "INITIAL_CLIENT_VERS=$INITIAL_CLIENT_VERS",
+ "VARIANT=${FLAVOR}", "NDK_LIBS_OUT=${lib}",
+ "NDK_OUT=./obj-${variant.name}", "XW_BT_UUID=\"${btUUID}\""
}
String compileTask = "compile${variantCaps}Ndk"
@@ -383,18 +402,18 @@ afterEvaluate {
String copyStringsTask = "copyStringsXw4D"
task "$copyStringsTask"(type: Exec) {
workingDir './'
- environment.put('APPNAME', 'CrossDbg')
+ environment.put('APPNAME', 'CrossDup')
commandLine 'make', '-f', '../scripts/Variant.mk',
- "src/xw4d/res/values/strings.xml"
+ "src/xw4dup/res/values/strings.xml"
}
preBuild.dependsOn copyStringsTask
String copyStringsTaskNoSMS = "copyStringsXw4DNoSMS"
task "$copyStringsTaskNoSMS"(type: Exec) {
workingDir './'
- environment.put('APPNAME', 'CrossDbg')
+ environment.put('APPNAME', 'CrossDup')
commandLine 'make', '-f', '../scripts/Variant.mk',
- "src/xw4dNoSMS/res/values/strings.xml"
+ "src/xw4dupNoSMS/res/values/strings.xml"
}
preBuild.dependsOn copyStringsTaskNoSMS
}
diff --git a/xwords4/android/app/src/main/AndroidManifest.xml b/xwords4/android/app/src/main/AndroidManifest.xml
index 4043fd8ab..8d1e0d07b 100644
--- a/xwords4/android/app/src/main/AndroidManifest.xml
+++ b/xwords4/android/app/src/main/AndroidManifest.xml
@@ -138,6 +138,7 @@
+
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardCanvas.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardCanvas.java
index cd6adf173..9725b64ff 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardCanvas.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardCanvas.java
@@ -80,6 +80,7 @@ public class BoardCanvas extends Canvas implements DrawCtx {
private CommonPrefs m_prefs;
private int m_lastSecsLeft;
private int m_lastTimerPlayer;
+ private boolean m_lastTimerTurnDone;
private boolean m_inTrade;
private boolean m_darkOnLight;
private Drawable m_origin;
@@ -334,18 +335,48 @@ public class BoardCanvas extends Canvas implements DrawCtx {
}
@Override
+ // public void drawTimer( Rect rect, int player, int secondsLeft,
+ // boolean turnDone )
+ // {
+ // if ( m_lastSecsLeft != secondsLeft
+ // || m_lastTimerPlayer != player
+ // || m_lastTimerTurnDone != turnDone ) {
+ // if ( null != m_activity && null != m_jniThread ) {
+ // Rect rectCopy = new Rect(rect);
+ // m_lastSecsLeft = secondsLeft;
+ // m_lastTimerPlayer = player;
+ // m_lastTimerTurnDone = turnDone;
+
+ // String negSign = secondsLeft < 0? "-" : "";
+ // secondsLeft = Math.abs( secondsLeft );
+ // String time = String.format( "%s%d:%02d", negSign,
+ // secondsLeft/60, secondsLeft%60 );
+
+ // fillRectOther( rectCopy, CommonPrefs.COLOR_BACKGRND );
+
+ // int color = m_playerColors[player];
+ // if ( turnDone ) {
+ // color &= NOT_TURN_ALPHA;
+ // }
+ // m_fillPaint.setColor( color );
public void drawTimer( Rect rect, final int player,
- int secondsLeft )
+ int secondsLeft, final boolean turnDone )
{
- if ( m_lastSecsLeft != secondsLeft || m_lastTimerPlayer != player ) {
+ Activity activity = m_activity;
+ if ( null == activity ) {
+ // Do nothing
+ } else if ( m_lastSecsLeft != secondsLeft
+ || m_lastTimerPlayer != player
+ || m_lastTimerTurnDone != turnDone ) {
final Rect rectCopy = new Rect(rect);
final int secondsLeftCopy = secondsLeft;
- m_activity.runOnUiThread( new Runnable() {
+ activity.runOnUiThread( new Runnable() {
@Override
public void run() {
if ( null != m_jniThread ) {
m_lastSecsLeft = secondsLeftCopy;
m_lastTimerPlayer = player;
+ m_lastTimerTurnDone = turnDone;
String negSign = secondsLeftCopy < 0? "-":"";
int secondsLeft = Math.abs( secondsLeftCopy );
@@ -519,9 +550,9 @@ public class BoardCanvas extends Canvas implements DrawCtx {
@Override
public void score_pendingScore( Rect rect, int score, int playerNum,
- int curTurn, int flags )
+ boolean curTurn, int flags )
{
- Log.d( TAG, "pendingScore(playerNum=%d, curTurn=%d)",
+ Log.d( TAG, "pendingScore(playerNum=%d, curTurn=%b)",
playerNum, curTurn );
int otherIndx = (0 == (flags & CELL_ISCURSOR))
@@ -530,7 +561,7 @@ public class BoardCanvas extends Canvas implements DrawCtx {
fillRectOther( rect, otherIndx );
int playerColor = m_playerColors[playerNum];
- if ( playerNum != curTurn ) {
+ if ( !curTurn ) {
playerColor &= NOT_TURN_ALPHA;
}
m_fillPaint.setColor( playerColor );
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java
index b05f32151..4e0e770d9 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java
@@ -1,6 +1,6 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
- * Copyright 2009 - 2017 by Eric House (xwords@eehouse.org). All rights
+ * Copyright 2009 - 2019 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
@@ -33,6 +33,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
+import android.text.format.DateUtils;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
@@ -59,6 +60,7 @@ import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;
import org.eehouse.android.xw4.jni.CommsAddrRec;
import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
import org.eehouse.android.xw4.jni.CurGameInfo;
+import org.eehouse.android.xw4.jni.DUtilCtxt;
import org.eehouse.android.xw4.jni.GameSummary;
import org.eehouse.android.xw4.jni.JNIThread.JNICmd;
import org.eehouse.android.xw4.jni.JNIThread;
@@ -85,6 +87,8 @@ public class BoardDelegate extends DelegateBase
private static final String SAVE_MYSIS = TAG + "/MYSIS";
+ static final String PAUSER_KEY = TAG + "/pauser";
+
private Activity m_activity;
private BoardView m_view;
private GamePtr m_jniGamePtr;
@@ -366,6 +370,31 @@ public class BoardDelegate extends DelegateBase
}
break;
+ case ASK_DUP_PAUSE: {
+ final boolean isPause = (Boolean)params[0];
+ final ConfirmPauseView pauseView =
+ ((ConfirmPauseView)inflate( R.layout.pause_view ))
+ .setIsPause( isPause );
+ int buttonId = isPause ? R.string.board_menu_game_pause
+ : R.string.board_menu_game_unpause;
+ dialog = ab
+ .setTitle(isPause ? R.string.pause_title : R.string.unpause_title)
+ .setView( pauseView )
+ .setPositiveButton( buttonId, new OnClickListener() {
+ @Override
+ public void
+ onClick( DialogInterface dlg,
+ int whichButton ) {
+ String msg = pauseView.getMsg();
+ handleViaThread( isPause ? JNICmd.CMD_PAUSE
+ : JNICmd.CMD_UNPAUSE, msg );
+ }
+ })
+ .setNegativeButton( android.R.string.cancel, null )
+ .create();
+ }
+ break;
+
case QUERY_ENDGAME:
dialog = ab.setTitle( R.string.query_title )
.setMessage( R.string.ids_endnow )
@@ -565,8 +594,8 @@ public class BoardDelegate extends DelegateBase
mNFCWrapper = Wrapper.init( m_activity, this, devID );
m_utils = new BoardUtilCtxt();
- m_timers = new TimerRunnable[4]; // needs to be in sync with
- // XWTimerReason
+ // needs to be in sync with XWTimerReason
+ m_timers = new TimerRunnable[UtilCtxt.NUM_TIMERS_PLUS_ONE];
m_view = (BoardView)findViewById( R.id.board_view );
if ( ! ABUtils.haveActionBar() ) {
m_tradeButtons = findViewById( R.id.exchange_buttons );
@@ -644,6 +673,7 @@ public class BoardDelegate extends DelegateBase
}
}
+ @Override
protected void onPause()
{
Wrapper.setResumed( mNFCWrapper, false );
@@ -672,7 +702,6 @@ public class BoardDelegate extends DelegateBase
if ( null != m_jniThreadRef ) {
m_jniThreadRef.release();
m_jniThreadRef = null;
- // Assert.assertNull( m_jniThreadRef ); // firing
}
GamesListDelegate.boardDestroyed( m_rowid );
super.onDestroy();
@@ -770,7 +799,11 @@ public class BoardDelegate extends DelegateBase
@Override
protected void setTitle()
{
- setTitle( GameUtils.getName( m_activity, m_rowid ) );
+ String title = GameUtils.getName( m_activity, m_rowid );
+ if ( null != m_gi && m_gi.inDuplicateMode ) {
+ title = LocUtils.getString( m_activity, R.string.dupe_title_fmt, title );
+ }
+ setTitle( title );
}
private void initToolbar()
@@ -849,6 +882,11 @@ public class BoardDelegate extends DelegateBase
m_gsi.canTrade );
Utils.setItemVisible( menu, R.id.board_menu_undo_last,
m_gsi.canUndo );
+
+ Utils.setItemVisible( menu, R.id.board_menu_game_pause,
+ m_gsi.canPause );
+ Utils.setItemVisible( menu, R.id.board_menu_game_unpause,
+ m_gsi.canUnpause );
}
Utils.setItemVisible( menu, R.id.board_menu_trade_cancel, inTrade );
@@ -905,7 +943,7 @@ public class BoardDelegate extends DelegateBase
JNICmd cmd = JNICmd.CMD_NONE;
Runnable proc = null;
- int id = item.getItemId();
+ final int id = item.getItemId();
switch ( id ) {
case R.id.board_menu_done:
int nTiles = XwJNI.model_getNumTilesInTray( m_jniGamePtr,
@@ -985,6 +1023,11 @@ public class BoardDelegate extends DelegateBase
.show();
break;
+ case R.id.board_menu_game_pause:
+ case R.id.board_menu_game_unpause:
+ getConfirmPause( R.id.board_menu_game_pause == id );
+ break;
+
// small devices only
case R.id.board_menu_dict:
String dictName = m_gi.dictName( m_view.getCurPlayer() );
@@ -1600,13 +1643,13 @@ public class BoardDelegate extends DelegateBase
private void deleteAndClose( int gameID )
{
- if ( gameID == m_gi.gameID ) {
- GameUtils.deleteGame( m_activity, m_jniThread.getLock(), false );
- waitCloseGame( false );
- finish();
+ if ( null != m_gi && gameID == m_gi.gameID ) {
+ GameUtils.deleteGame( m_activity, m_jniThread.getLock(), false, false );
} else {
Log.e( TAG, "deleteAndClose() called with wrong gameID %d", gameID );
}
+ waitCloseGame( false );
+ finish();
}
private void askDropRelay()
@@ -1757,6 +1800,19 @@ public class BoardDelegate extends DelegateBase
handleViaThread( JNICmd.CMD_REMAINING, R.string.tiles_left_title );
}
+ @Override
+ public void timerSelected( boolean inDuplicateMode, final boolean canPause )
+ {
+ if ( inDuplicateMode ) {
+ runOnUiThread( new Runnable() {
+ @Override
+ public void run() {
+ getConfirmPause( canPause );
+ }
+ } );
+ }
+ }
+
@Override
public void setIsServer( boolean isServer )
{
@@ -1843,6 +1899,7 @@ public class BoardDelegate extends DelegateBase
int inHowLong;
switch ( why ) {
case UtilCtxt.TIMER_COMMS:
+ case UtilCtxt.TIMER_DUP_TIMERCHECK:
inHowLong = when * 1000;
break;
case UtilCtxt.TIMER_TIMERTICK:
@@ -1942,6 +1999,20 @@ public class BoardDelegate extends DelegateBase
showDialogFragment( DlgID.QUERY_TRADE, dlgBytes );
}
+ @Override
+ public void notifyDupStatus( boolean amHost, final String msg )
+ {
+ final int key = amHost ? R.string.key_na_dupstatus_host
+ : R.string.key_na_dupstatus_guest;
+ runOnUiThread( new Runnable() {
+ @Override
+ public void run() {
+ makeNotAgainBuilder( msg, key )
+ .show();
+ }
+ } );
+ }
+
@Override
public void userError( int code )
{
@@ -2166,6 +2237,38 @@ public class BoardDelegate extends DelegateBase
}
} );
}
+
+ @Override
+ public String formatPauseHistory( int pauseTyp, int player,
+ int whenPrev, int whenCur, String msg )
+ {
+ Log.d( TAG, "formatPauseHistory(prev: %d, cur: %d)", whenPrev, whenCur );
+ String result = null;
+ String name = 0 > player ? null : m_gi.players[player].name;
+ switch ( pauseTyp ) {
+ case DUtilCtxt.UNPAUSED:
+ String interval = DateUtils
+ .formatElapsedTime( whenCur - whenPrev )
+ .toString();
+ result = LocUtils.getString( m_activity, R.string.history_unpause_fmt,
+ name, interval );
+ break;
+ case DUtilCtxt.PAUSED:
+ result = LocUtils.getString( m_activity, R.string.history_pause_fmt,
+ name );
+ break;
+ case DUtilCtxt.AUTOPAUSED:
+ result = LocUtils.getString( m_activity, R.string.history_autopause );
+ break;
+ }
+
+ if ( null != msg ) {
+ result += " " + LocUtils
+ .getString( m_activity, R.string.history_msg_fmt, msg );
+ }
+
+ return result;
+ }
} // class BoardUtilCtxt
private void doResume( boolean isStart )
@@ -2225,11 +2328,6 @@ public class BoardDelegate extends DelegateBase
invalidateOptionsMenuIf();
}
break;
- case JNIThread.GOT_WORDS:
- CurGameInfo gi = m_jniThreadRef.getGI();
- launchLookup( wordsToArray((String)msg.obj),
- gi.dictLang );
- break;
case JNIThread.GAME_OVER:
if ( m_isFirstLaunch ) {
runOnUiThread( new Runnable() {
@@ -2249,6 +2347,16 @@ public class BoardDelegate extends DelegateBase
showToast( getQuantityString( R.plurals.resent_msgs_fmt,
nSent, nSent ) );
break;
+
+ case JNIThread.GOT_PAUSE:
+ runOnUiThread( new Runnable() {
+ @Override
+ public void run() {
+ makeOkOnlyBuilder( (String)msg.obj )
+ .show();
+ }
+ } );
+ break;
}
}
};
@@ -2303,10 +2411,11 @@ public class BoardDelegate extends DelegateBase
}
}
if ( 0 != flags ) {
- DBUtils.setMsgFlags( m_rowid, GameSummary.MSG_FLAGS_NONE );
+ DBUtils.setMsgFlags( m_activity, m_rowid,
+ GameSummary.MSG_FLAGS_NONE );
}
- Utils.cancelNotification( m_activity, (int)m_rowid );
+ Utils.cancelNotification( m_activity, m_rowid );
askNBSPermissions();
@@ -2315,6 +2424,11 @@ public class BoardDelegate extends DelegateBase
tickle( isStart );
tryInvites();
}
+
+ Bundle args = getArguments();
+ if ( args.getBoolean( PAUSER_KEY, false ) ) {
+ getConfirmPause( true );
+ }
}
} // resumeGame
@@ -2547,6 +2661,11 @@ public class BoardDelegate extends DelegateBase
names, locs );
}
+ private void getConfirmPause( boolean isPause )
+ {
+ showDialogFragment( DlgID.ASK_DUP_PAUSE, isPause );
+ }
+
private void closeIfFinishing( boolean force )
{
if ( null == m_handler ) {
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardView.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardView.java
index 00e06ec0b..3466b4c80 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardView.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardView.java
@@ -209,7 +209,7 @@ public class BoardView extends View implements BoardHandler, SyncedDraw {
} else {
Bitmap bitmap = s_bitmap;
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
- bitmap = Bitmap.createBitmap(bitmap);
+ bitmap = Bitmap.createBitmap( bitmap );
}
canvas.drawBitmap( bitmap, 0, 0, new Paint() );
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Channels.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Channels.java
index 1c65c247f..38b9d426c 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Channels.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Channels.java
@@ -20,22 +20,27 @@
package org.eehouse.android.xw4;
-import android.os.Build;
-import android.content.Context;
-import java.util.HashSet;
-import java.util.Set;
import android.app.NotificationChannel;
import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
public class Channels {
+ private static final String TAG = Channels.class.getSimpleName();
- enum ID {
- NBSPROXY(R.string.nbsproxy_channel_expl,
- NotificationManager.IMPORTANCE_LOW),
- GAME_EVENT(R.string.gameevent_channel_expl,
- NotificationManager.IMPORTANCE_LOW),
- SERVICE_STALL(R.string.servicestall_channel_expl,
- NotificationManager.IMPORTANCE_LOW);
+ public enum ID {
+ NBSPROXY( R.string.nbsproxy_channel_expl )
+ ,GAME_EVENT( R.string.gameevent_channel_expl )
+ ,SERVICE_STALL( R.string.servicestall_channel_expl )
+ ,DUP_TIMER_RUNNING( R.string.dup_timer_expl )
+ ,DUP_PAUSED( R.string.dup_paused_expl )
+ ;
private int mExpl;
private int mImportance;
@@ -45,7 +50,15 @@ public class Channels {
mImportance = imp;
}
+ private ID( int expl )
+ {
+ this( expl, NotificationManager.IMPORTANCE_LOW );
+ }
+
public int getDesc() { return mExpl; }
+ public int idFor( long rowid ) {
+ return notificationId( rowid, this );
+ }
private int getImportance() { return mImportance; }
}
@@ -71,4 +84,63 @@ public class Channels {
}
return name;
}
+
+ private static final String IDS_KEY = TAG + "/ids_key";
+
+ private static class IdsData implements Serializable {
+ HashMap> mMap = new HashMap<>();
+ HashSet mInts = new HashSet<>();
+
+ int newID()
+ {
+ int result;
+ for ( ; ; ) {
+ int one = Utils.nextRandomInt();
+ if ( !mInts.contains( one ) ) {
+ mInts.add( one );
+ result = one;
+ break;
+ }
+ }
+ return result;
+ }
+ }
+ private static IdsData sData;
+
+ // I want each rowid to be able to have a notification active for it for
+ // each channel. So let's try generating and storing random ints.
+ private static int notificationId( long rowid, ID channel )
+ {
+ Context context = XWApp.getContext();
+ int result;
+ synchronized ( Channels.class ) {
+ if ( null == sData ) {
+ sData = (IdsData)DBUtils.getSerializableFor( context, IDS_KEY );
+ if ( null == sData ) {
+ sData = new IdsData();
+ }
+ }
+ }
+
+ synchronized ( sData ) {
+ boolean dirty = false;
+ if ( ! sData.mMap.containsKey( channel ) ) {
+ sData.mMap.put( channel, new HashMap() );
+ dirty = true;
+ }
+ Map map = sData.mMap.get( channel );
+ if ( ! map.containsKey( rowid ) ) {
+ map.put( rowid, sData.newID() );
+ dirty = true;
+ }
+
+ if ( dirty ) {
+ DBUtils.setSerializableFor( context, IDS_KEY, sData );
+ }
+
+ result = map.get( rowid );
+ }
+ Log.d( TAG, "notificationId(%s, %d) => %d", channel, rowid, result );
+ return result;
+ }
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConfirmPauseView.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConfirmPauseView.java
new file mode 100644
index 000000000..476cbed8c
--- /dev/null
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConfirmPauseView.java
@@ -0,0 +1,165 @@
+/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
+/*
+ * Copyright 2019 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.
+ */
+
+package org.eehouse.android.xw4;
+
+import android.text.Editable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.util.HashSet;
+
+// Edit text should start out empty
+
+public class ConfirmPauseView extends LinearLayout
+ implements View.OnClickListener, OnItemSelectedListener, EditWClear.TextWatcher {
+ private static final String TAG = ConfirmPauseView.class.getSimpleName();
+ private static final String PAUSE_MSGS_KEY = TAG + "/pause_msgs";
+ private static final String UNPAUSE_MSGS_KEY = TAG + "/unpause_msgs";
+
+ private Boolean mIsPause;
+ private boolean mInflateFinished;
+ private boolean mInited;
+ private HashSet mSavedMsgs;
+ private Button mForgetButton;
+ private Button mRememberButton;
+ private Spinner mSpinner;
+ private EditWClear mMsgEdit;
+
+ public ConfirmPauseView( Context context, AttributeSet as ) {
+ super( context, as );
+ }
+
+ @Override
+ protected void onFinishInflate()
+ {
+ mInflateFinished = true;
+ initIfReady();
+ }
+
+ private void initIfReady()
+ {
+ if ( !mInited && mInflateFinished && null != mIsPause ) {
+ mInited = true;
+
+ Context context = getContext();
+
+ int id = mIsPause ? R.string.pause_expl : R.string.unpause_expl;
+ ((TextView)findViewById(R.id.confirm_pause_expl))
+ .setText( id );
+
+ mForgetButton = (Button)findViewById( R.id.pause_forget_msg );
+ mForgetButton.setOnClickListener( this );
+ mRememberButton = (Button)findViewById( R.id.pause_save_msg );
+ mRememberButton.setOnClickListener( this );
+ mSpinner = (Spinner)findViewById( R.id.saved_msgs );
+ mMsgEdit = (EditWClear)findViewById( R.id.msg_edit );
+ mMsgEdit.addTextChangedListener( this );
+
+ String key = mIsPause ? PAUSE_MSGS_KEY : UNPAUSE_MSGS_KEY;
+ mSavedMsgs = (HashSet)DBUtils
+ .getSerializableFor( context, key );
+ if ( null == mSavedMsgs ) {
+ mSavedMsgs = new HashSet<>();
+ }
+
+ populateSpinner();
+ mSpinner.setOnItemSelectedListener( this );
+ setMsg( "" );
+ // onTextChanged( "" );
+ }
+ }
+
+ private void populateSpinner()
+ {
+ final ArrayAdapter adapter =
+ new ArrayAdapter<>( getContext(), android.R.layout.simple_spinner_item );
+ for ( String msg : mSavedMsgs ) {
+ adapter.add( msg );
+ }
+ mSpinner.setAdapter( adapter );
+ }
+
+ @Override
+ public void onItemSelected( AdapterView> parent, View spinner,
+ int position, long id )
+ {
+ String msg = (String)parent.getAdapter().getItem( position );
+ setMsg( msg );
+ onTextChanged( msg );
+ }
+
+ @Override
+ public void onNothingSelected( AdapterView> p ) {}
+
+ @Override
+ public void onClick( View view )
+ {
+ Log.d( TAG, "onClick() called" );
+ String msg = getMsg();
+ if ( view == mRememberButton && 0 < msg.length() ) {
+ mSavedMsgs.add( msg );
+ } else if ( view == mForgetButton ) {
+ mSavedMsgs.remove( msg );
+ setMsg( "" );
+ } else {
+ Assert.assertFalse( BuildConfig.DEBUG );
+ }
+ String key = mIsPause ? PAUSE_MSGS_KEY : UNPAUSE_MSGS_KEY;
+ DBUtils.setSerializableFor( getContext(), key, mSavedMsgs );
+ populateSpinner();
+ }
+
+ // from EditWClear.TextWatcher
+ @Override
+ public void onTextChanged( String msg )
+ {
+ Log.d( TAG, "onTextChanged(%s)", msg );
+ boolean hasText = 0 < msg.length();
+ boolean matches = mSavedMsgs.contains( msg );
+ mForgetButton.setEnabled( hasText && matches );
+ mRememberButton.setEnabled( hasText && !matches );
+ }
+
+ ConfirmPauseView setIsPause( boolean isPause )
+ {
+ mIsPause = isPause;
+ initIfReady();
+ return this;
+ }
+
+ String getMsg()
+ {
+ return mMsgEdit.getText().toString();
+ }
+
+ private void setMsg( String msg )
+ {
+ mMsgEdit.setText( msg );
+ }
+}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBHelper.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBHelper.java
index d13738faa..9e81a6113 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBHelper.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBHelper.java
@@ -1,6 +1,6 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
- * Copyright 2009-2016 by Eric House (xwords@eehouse.org). All
+ * Copyright 2009 - 2019 by Eric House (xwords@eehouse.org). All
* rights reserved.
*
* This program is free software; you can redistribute it and/or
@@ -56,7 +56,7 @@ public class DBHelper extends SQLiteOpenHelper {
private int addedVersion() { return mAddedVersion; }
}
private static final String DB_NAME = BuildConfig.DB_NAME;
- private static final int DB_VERSION = 29;
+ private static final int DB_VERSION = 30;
public static final String GAME_NAME = "GAME_NAME";
public static final String VISID = "VISID";
@@ -90,6 +90,7 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String SEED = "SEED";
public static final String SMSPHONE = "SMSPHONE"; // unused -- so far
public static final String LASTMOVE = "LASTMOVE";
+ public static final String NEXTDUPTIMER = "NEXTDUPTIMER";
public static final String NEXTNAG = "NEXTNAG";
public static final String GROUPID = "GROUPID";
public static final String NPACKETSPENDING = "NPACKETSPENDING";
@@ -159,6 +160,7 @@ public class DBHelper extends SQLiteOpenHelper {
,{ REMOTEDEVS, "TEXT" }
,{ EXTRAS, "TEXT" } // json data, most likely
,{ LASTMOVE, "INTEGER DEFAULT 0" }
+ ,{ NEXTDUPTIMER, "INTEGER DEFAULT 0" }
,{ NEXTNAG, "INTEGER DEFAULT 0" }
,{ GROUPID, "INTEGER" }
// HASMSGS: sqlite doesn't have bool; use 0 and 1
@@ -347,6 +349,10 @@ public class DBHelper extends SQLiteOpenHelper {
if ( !madeChatTable ) {
addColumn( db, TABLE_NAMES.CHAT, s_chatsSchema, CHATTIME );
}
+ case 29:
+ if ( !madeSumTable ) {
+ addSumColumn( db, NEXTDUPTIMER );
+ }
break;
default:
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java
index 3c7ab8322..25f53e470 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java
@@ -85,7 +85,8 @@ public class DBUtils {
};
public static interface DBChangeListener {
- public void gameSaved( long rowid, GameChangeType change );
+ public void gameSaved( Context context, long rowid,
+ GameChangeType change );
}
private static HashSet s_listeners =
new HashSet();
@@ -146,7 +147,7 @@ public class DBUtils {
DBHelper.SCORES,
DBHelper.LASTPLAY_TIME, DBHelper.REMOTEDEVS,
DBHelper.LASTMOVE, DBHelper.NPACKETSPENDING,
- DBHelper.EXTRAS,
+ DBHelper.EXTRAS, DBHelper.NEXTDUPTIMER,
};
String selection = String.format( ROW_ID_FMT, lock.getRowid() );
@@ -194,6 +195,8 @@ public class DBUtils {
summary.gameOver = tmp != 0;
summary.lastMoveTime =
cursor.getInt(cursor.getColumnIndex(DBHelper.LASTMOVE));
+ summary.dupTimerExpires =
+ cursor.getInt(cursor.getColumnIndex(DBHelper.NEXTDUPTIMER));
String str = cursor
.getString(cursor.getColumnIndex(DBHelper.EXTRAS));
summary.setExtras( str );
@@ -298,6 +301,7 @@ public class DBUtils {
values.put( DBHelper.GAMEID, summary.gameID );
values.put( DBHelper.GAME_OVER, summary.gameOver? 1 : 0 );
values.put( DBHelper.LASTMOVE, summary.lastMoveTime );
+ values.put( DBHelper.NEXTDUPTIMER, summary.dupTimerExpires );
// Don't overwrite extras! Sometimes this method is called from
// JNIThread which has created the summary from common code that
@@ -357,7 +361,7 @@ public class DBUtils {
long result = update( TABLE_NAMES.SUM, values, selection );
Assert.assertTrue( result >= 0 );
}
- notifyListeners( rowid, GameChangeType.GAME_CHANGED );
+ notifyListeners( context, rowid, GameChangeType.GAME_CHANGED );
invalGroupsCache();
}
@@ -663,10 +667,10 @@ public class DBUtils {
updateRow( null, TABLE_NAMES.SUM, rowid, values );
}
- public static void setMsgFlags( long rowid, int flags )
+ public static void setMsgFlags( Context context, long rowid, int flags )
{
setSummaryInt( rowid, DBHelper.HASMSGS, flags );
- notifyListeners( rowid, GameChangeType.GAME_CHANGED );
+ notifyListeners( context, rowid, GameChangeType.GAME_CHANGED );
}
public static void setExpanded( long rowid, boolean expanded )
@@ -730,7 +734,7 @@ public class DBUtils {
Assert.assertTrue( result >= 0 );
- notifyListeners( rowid, GameChangeType.GAME_CHANGED );
+ notifyListeners( context, rowid, GameChangeType.GAME_CHANGED );
}
}
@@ -742,7 +746,7 @@ public class DBUtils {
synchronized( s_dbHelper ) {
long result = update( TABLE_NAMES.SUM, values, null );
- notifyListeners( ROWIDS_ALL, GameChangeType.GAME_CHANGED );
+ notifyListeners( context, ROWIDS_ALL, GameChangeType.GAME_CHANGED );
}
}
@@ -1066,7 +1070,7 @@ public class DBUtils {
lock = GameLock.tryLock( rowid );
Assert.assertNotNull( lock );
- notifyListeners( rowid, GameChangeType.GAME_CREATED );
+ notifyListeners( context, rowid, GameChangeType.GAME_CREATED );
}
invalGroupsCache(); // then again after
@@ -1092,7 +1096,7 @@ public class DBUtils {
setCached( rowid, null ); // force reread
if ( ROWID_NOTFOUND != rowid ) { // Means new game?
- notifyListeners( rowid, GameChangeType.GAME_CHANGED );
+ notifyListeners( context, rowid, GameChangeType.GAME_CHANGED );
}
invalGroupsCache();
return rowid;
@@ -1154,7 +1158,7 @@ public class DBUtils {
deleteCurChatsSync( s_db, rowid );
}
- notifyListeners( lock.getRowid(), GameChangeType.GAME_DELETED );
+ notifyListeners( context, lock.getRowid(), GameChangeType.GAME_DELETED );
invalGroupsCache();
}
@@ -1775,7 +1779,47 @@ public class DBUtils {
values.put( DBHelper.GROUPID, groupID );
updateRow( context, TABLE_NAMES.SUM, rowid, values );
invalGroupsCache();
- notifyListeners( rowid, GameChangeType.GAME_MOVED );
+ notifyListeners( context, rowid, GameChangeType.GAME_MOVED );
+ }
+
+ public static Map getDupModeGames( Context context )
+ {
+ return getDupModeGames( context, ROWID_NOTFOUND );
+ }
+
+ // Return all games whose DUP_MODE_MASK bit is set. Return also (as map
+ // value) the nextTimer value, which will be negative if the game's
+ // paused. As a bit of a hack, set it to 0 if the local player has already
+ // committed his turn so caller (DupeModeTimer) will know not to show a
+ // notification.
+ public static Map getDupModeGames( Context context, long rowid )
+ {
+ // select giflags from summaries where 0x100 & giflags != 0;
+ Map result = new HashMap<>();
+ String[] columns = { ROW_ID, DBHelper.NEXTDUPTIMER, DBHelper.TURN_LOCAL };
+ String selection = String.format( "%d & %s != 0",
+ GameSummary.DUP_MODE_MASK,
+ DBHelper.GIFLAGS );
+ if ( ROWID_NOTFOUND != rowid ) {
+ selection += String.format( " AND %s = %d", ROW_ID, rowid );
+ }
+
+ initDB( context );
+ synchronized( s_dbHelper ) {
+ Cursor cursor = query( TABLE_NAMES.SUM, columns, selection );
+ int count = cursor.getCount();
+ int indxRowid = cursor.getColumnIndex( ROW_ID );
+ int indxTimer = cursor.getColumnIndex( DBHelper.NEXTDUPTIMER );
+ int indxIsLocal = cursor.getColumnIndex( DBHelper.TURN_LOCAL );
+ while ( cursor.moveToNext() ) {
+ boolean isLocal = 0 != cursor.getInt( indxIsLocal );
+ int timer = isLocal ? cursor.getInt( indxTimer ) : 0;
+ result.put( cursor.getLong( indxRowid ), timer );
+ }
+ cursor.close();
+ }
+ Log.d( TAG, "getDupModeGames(%d) => %s", rowid, result );
+ return result;
}
private static String getChatHistoryStr( Context context, long rowid )
@@ -2592,12 +2636,13 @@ public class DBUtils {
}
}
- private static void notifyListeners( long rowid, GameChangeType change )
+ private static void notifyListeners( Context context, long rowid,
+ GameChangeType change )
{
synchronized( s_listeners ) {
Iterator iter = s_listeners.iterator();
while ( iter.hasNext() ) {
- iter.next().gameSaved( rowid, change );
+ iter.next().gameSaved( context, rowid, change );
}
}
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DbgUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DbgUtils.java
index e4fcc33ff..e7f107ca5 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DbgUtils.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DbgUtils.java
@@ -99,18 +99,23 @@ public class DbgUtils {
Log.d( tag, stackTrace );
}
- static String extrasToString( Intent intent )
+ static String extrasToString( Bundle extras )
{
- Bundle bundle = intent.getExtras();
ArrayList al = new ArrayList();
- if ( null != bundle ) {
- for ( String key : bundle.keySet() ) {
- al.add( key + ":" + bundle.get(key) );
+ if ( null != extras ) {
+ for ( String key : extras.keySet() ) {
+ al.add( key + ":" + extras.get(key) );
}
}
return TextUtils.join( ", ", al );
}
+ static String extrasToString( Intent intent )
+ {
+ Bundle bundle = intent.getExtras();
+ return extrasToString( bundle );
+ }
+
public static void dumpCursor( Cursor cursor )
{
String dump = DatabaseUtils.dumpCursorToString( cursor );
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictLangCache.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictLangCache.java
index 8c239ccce..9c6995154 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictLangCache.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictLangCache.java
@@ -45,8 +45,8 @@ import java.util.Set;
public class DictLangCache {
private static final String TAG = DictLangCache.class.getSimpleName();
- private static String[] s_langNames;
- private static HashMap s_langCodes;
+ private static Map s_langNames;
+ private static Map s_langCodes;
private static int s_adaptedLang = -1;
private static LangsArrayAdapter s_langsAdapter;
@@ -151,11 +151,12 @@ public class DictLangCache {
public static String getLangName( Context context, int code )
{
- String[] namesArray = getLangNames( context );
- if ( code < 0 || code >= namesArray.length ) {
- code = 0;
+ Map namesArray = getLangNames( context );
+ String name = namesArray.get( code );
+ if ( null == name ) {
+ name = namesArray.get( 0 );
}
- return namesArray[code];
+ return name;
}
// This populates the cache and will take significant time if it's
@@ -248,7 +249,7 @@ public class DictLangCache {
for ( DictAndLoc dal : dals ) {
DictInfo info = getInfo( context, dal );
int langCode = info.langCode;
- if ( langCode >= s_langNames.length ) {
+ if ( !s_langNames.containsKey( langCode ) ) {
langCode = 0;
}
if ( null != info && code == langCode ) {
@@ -305,13 +306,11 @@ public class DictLangCache {
public static int getLangLangCode( Context context, String lang )
{
- int code = 0;
- String[] namesArray = getLangNames( context );
- for ( int ii = 0; ii < namesArray.length; ++ii ) {
- if ( namesArray[ii].equals( lang ) ) {
- code = ii;
- break;
- }
+ getLangNames( context ); /* inits s_langCodes */
+
+ Integer code = s_langCodes.get( lang );
+ if ( null == code ) {
+ code = 0;
}
return code;
}
@@ -431,16 +430,24 @@ public class DictLangCache {
return s_dictsAdapter;
}
- public static String[] getLangNames( Context context )
+ private static Map getLangNames( Context context )
{
if ( null == s_langNames ) {
Resources res = context.getResources();
- s_langNames = res.getStringArray( R.array.language_names );
+ String[] names = res.getStringArray( R.array.language_names );
- s_langCodes = new HashMap();
- for ( int ii = 0; ii < s_langNames.length; ++ii ) {
- s_langCodes.put( s_langNames[ii], ii );
+ s_langCodes = new HashMap<>();
+ s_langNames = new HashMap<>();
+ for ( int ii = 0; ii < names.length; ++ii ) {
+ String name = names[ii];
+ s_langCodes.put( name, ii );
+ s_langNames.put( ii, name );
}
+
+ // Hex is out-of-order, so can't be in the res-based array. Hard
+ // code it: it's a hack anyway.
+ s_langCodes.put( "Hex", 127 );
+ s_langNames.put( 127, "Hex" );
}
return s_langNames;
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictsDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictsDelegate.java
index aefc2cea2..766a39095 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictsDelegate.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictsDelegate.java
@@ -503,7 +503,7 @@ public class DictsDelegate extends ListDelegateBase
int lang = args.getInt( DICT_LANG_EXTRA, 0 );
if ( 0 < lang ) {
- m_filterLang = DictLangCache.getLangNames( m_activity )[lang];
+ m_filterLang = DictLangCache.getLangName( m_activity, lang );
m_closedLangs.remove( m_filterLang );
}
String name = args.getString( DICT_NAME_EXTRA );
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java
index d8d9b7759..226600b65 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java
@@ -67,6 +67,7 @@ public enum DlgID {
, GAMES_LIST_NEWGAME
, CHANGE_CONN
, GAMES_LIST_NAME_REMATCH
+ , ASK_DUP_PAUSE
;
private boolean m_addToStack;
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DupeModeTimer.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DupeModeTimer.java
new file mode 100644
index 000000000..55aab7c3d
--- /dev/null
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DupeModeTimer.java
@@ -0,0 +1,294 @@
+/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
+/*
+ * Copyright 2019 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.
+ */
+
+package org.eehouse.android.xw4;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.SystemClock;
+
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.eehouse.android.xw4.DBUtils.GameChangeType;
+import org.eehouse.android.xw4.jni.CurGameInfo;
+import org.eehouse.android.xw4.jni.JNIThread;
+import org.eehouse.android.xw4.jni.XwJNI.GamePtr;
+import org.eehouse.android.xw4.jni.XwJNI;
+import org.eehouse.android.xw4.loc.LocUtils;
+
+/**
+ * This class owns the problem of timers in duplicate-mode games. Unlike the
+ * existing timers that run only when a game is open and visible, they run
+ * with the clock for any game where it's a local player's turn. So this
+ * module needs to be aware of all games in that state and to be counting
+ * their timers down at all times. For each game for which a timer's running
+ * it's either 1) sending updates to the game (if it's open) OR 2) keeping an
+ * unhideable notification open with the relevant time counting down.
+ */
+
+public class DupeModeTimer extends BroadcastReceiver {
+ private static final String TAG = DupeModeTimer.class.getSimpleName();
+
+ private static final Channels.ID sMyChannel = Channels.ID.DUP_TIMER_RUNNING;
+
+ private static RowidQueue sQueue;
+ private static Map sDirtyVals = new HashMap<>();
+
+ private static DateFormat s_df =
+ DateFormat.getTimeInstance( /*DateFormat.SHORT*/ );
+ private static long sCurTimer = Long.MAX_VALUE;
+
+ static {
+ sQueue = new RowidQueue();
+ sQueue.start();
+
+ DBUtils.setDBChangeListener( new DBUtils.DBChangeListener() {
+ @Override
+ public void gameSaved( Context context, long rowid,
+ GameChangeType change )
+ {
+ Log.d( TAG, "gameSaved(rowid=%d,change=%s) called", rowid, change );
+ switch( change ) {
+ case GAME_CHANGED:
+ case GAME_CREATED:
+ synchronized ( sDirtyVals ) {
+ if ( sDirtyVals.containsKey( rowid ) ) {
+ sQueue.addOne( context, rowid );
+ } else {
+ Log.d( TAG, "skipping; not dirty" );
+ }
+ }
+ break;
+ case GAME_DELETED:
+ cancelNotification( context, rowid );
+ break;
+ }
+ }
+ } );
+
+ }
+
+ @Override
+ public void onReceive( Context context, Intent intent )
+ {
+ Log.d( TAG, "onReceive()" );
+ sCurTimer = Long.MAX_VALUE; // clear so we'll set again
+ sQueue.addAll( context );
+ }
+
+ /**
+ * Called when
+ */
+ static void init( Context context )
+ {
+ Log.d( TAG, "init()" );
+ sQueue.addAll( context );
+ }
+
+ public static void gameOpened( Context context, long rowid )
+ {
+ Log.d( TAG, "gameOpened(%s, %d)", context, rowid );
+ sQueue.addOne( context, rowid );
+ }
+
+ public static void gameClosed( Context context, long rowid )
+ {
+ Log.d( TAG, "gameClosed(%s, %d)", context, rowid );
+ sQueue.addOne( context, rowid );
+ }
+
+ // public static void timerPauseChanged( Context context, long rowid )
+ // {
+ // sQueue.addOne( context, rowid );
+ // }
+
+ public static void timerChanged( Context context, int gameID, int newVal )
+ {
+ long[] rowids = DBUtils.getRowIDsFor( context, gameID );
+ for ( long rowid : rowids ) {
+ Log.d( TAG, "timerChanged(rowid=%d, newVal=%d)", rowid, newVal );
+ synchronized ( sDirtyVals ) {
+ sDirtyVals.put( rowid, newVal );
+ }
+ }
+ }
+
+ private static void postNotification( Context context, long rowid, long when )
+ {
+ Log.d( TAG, "postNotification(rowid=%d)", rowid );
+ if ( !JNIThread.gameIsOpen( rowid ) ) {
+ String title = LocUtils.getString( context, R.string.dup_notif_title );
+ if ( BuildConfig.DEBUG ) {
+ title += " (" + rowid + ")";
+ }
+ String body = context.getString( R.string.dup_notif_title_fmt,
+ s_df.format( new Date( 1000 * when ) ) );
+ Intent intent = GamesListDelegate.makeRowidIntent( context, rowid );
+
+ Intent pauseIntent = GamesListDelegate.makeRowidIntent( context, rowid );
+ pauseIntent.putExtra( BoardDelegate.PAUSER_KEY, true );
+ Utils.postOngoingNotification( context, intent, title, body,
+ rowid, sMyChannel,
+ pauseIntent, R.string.board_menu_game_pause );
+ } else {
+ Log.d( TAG, "postOngoingNotification(%d): open, so skipping", rowid );
+ }
+ }
+
+ private static void cancelNotification( Context context, long rowid )
+ {
+ Log.d( TAG, "cancelNotification(rowid=%d)", rowid );
+ Utils.cancelNotification( context, sMyChannel, rowid );
+ }
+
+ private static void setTimer( Context context, long whenSeconds )
+ {
+ if ( whenSeconds < sCurTimer ) {
+ sCurTimer = whenSeconds;
+ Intent intent = new Intent( context, DupeModeTimer.class );
+ PendingIntent pi = PendingIntent.getBroadcast( context, 0, intent, 0 );
+
+ long now = Utils.getCurSeconds();
+ long fire_millis = SystemClock.elapsedRealtime()
+ + (1000 * (whenSeconds - now));
+
+ ((AlarmManager)context.getSystemService( Context.ALARM_SERVICE ))
+ .set( AlarmManager.ELAPSED_REALTIME, fire_millis, pi );
+ }
+ }
+
+ private static class RowidQueue extends Thread {
+ private Set mSet = new HashSet<>();
+ private Context mContext;
+
+ void addAll( Context context )
+ {
+ addOne( context, 0 );
+ }
+
+ synchronized void addOne( Context context, long rowid )
+ {
+ mContext = context;
+ synchronized ( mSet ) {
+ mSet.add( rowid );
+ mSet.notify();
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ long rowid = DBUtils.ROWID_NOTFOUND;
+ for ( ; ; ) {
+ synchronized( mSet ) {
+ mSet.remove( rowid );
+ if ( 0 == mSet.size() ) {
+ try {
+ mSet.wait();
+ Assert.assertTrue( 0 < mSet.size() );
+ } catch ( InterruptedException ie ) {
+ break;
+ }
+ }
+ rowid = mSet.iterator().next();
+ }
+ inventoryGames( rowid );
+ }
+ }
+
+ private void inventoryGames( long onerow )
+ {
+ Log.d( TAG, "inventoryGames(%d)", onerow );
+ Map dupeGames = onerow == 0
+ ? DBUtils.getDupModeGames( mContext )
+ : DBUtils.getDupModeGames( mContext, onerow );
+
+ Log.d( TAG, "inventoryGames(%s)", dupeGames );
+ long now = Utils.getCurSeconds();
+ long minTimer = sCurTimer;
+
+ for ( long rowid : dupeGames.keySet() ) {
+ int timerFires = dupeGames.get( rowid );
+
+ synchronized ( sDirtyVals ) {
+ if ( sDirtyVals.containsKey(rowid) && timerFires == sDirtyVals.get(rowid) ) {
+ sDirtyVals.remove(rowid);
+ }
+ }
+
+ if ( timerFires > now ) {
+ Log.d( TAG, "found dupe game with %d seconds left",
+ timerFires - now );
+ postNotification( mContext, rowid, timerFires );
+ if ( timerFires < minTimer ) {
+ minTimer = timerFires;
+ }
+ } else {
+ cancelNotification( mContext, rowid );
+ Log.d( TAG, "found dupe game with expired or inactive timer" );
+ if ( timerFires > 0 ) {
+ giveGameTime( rowid );
+ }
+ }
+ }
+
+ setTimer( mContext, minTimer );
+ }
+
+ private void giveGameTime( long rowid )
+ {
+ Log.d( TAG, "giveGameTime(%d)() starting", rowid );
+ try ( GameLock lock = GameLock.tryLock( rowid ) ) {
+ if ( null != lock ) {
+ CurGameInfo gi = new CurGameInfo( mContext );
+ MultiMsgSink sink = new MultiMsgSink( mContext, rowid );
+ try ( final XwJNI.GamePtr gamePtr = GameUtils
+ .loadMakeGame( mContext, gi, sink, lock ) ) { // calls getJNI()
+ Log.d( TAG, "got gamePtr: %H", gamePtr );
+ if ( null != gamePtr ) {
+ boolean draw = false;
+ for ( int ii = 0; ii < 3; ++ii ) {
+ draw = XwJNI.server_do( gamePtr ) || draw;
+ }
+
+ GameUtils.saveGame( mContext, gamePtr, gi, lock, false );
+
+ if ( draw && XWPrefs.getThumbEnabled( mContext ) ) {
+ Bitmap bitmap = GameUtils
+ .takeSnapshot( mContext, gamePtr, gi );
+ DBUtils.saveThumbnail( mContext, lock, bitmap );
+ }
+ }
+ }
+ }
+ }
+ Log.d( TAG, "giveGameTime(%d)() DONE", rowid );
+ }
+ }
+}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/EditWClear.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/EditWClear.java
index a88a5a16d..d94e754b8 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/EditWClear.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/EditWClear.java
@@ -23,14 +23,33 @@ import android.widget.SearchView;
import android.content.Context;
import android.util.AttributeSet;
-public class EditWClear extends SearchView {
+import java.util.HashSet;
+import java.util.Set;
+
+public class EditWClear extends SearchView
+ implements SearchView.OnQueryTextListener {
private static final String TAG = EditWClear.class.getSimpleName();
+ private Set mWatchers;
+
+ public interface TextWatcher {
+ void onTextChanged( String newText );
+ }
+
public EditWClear( Context context, AttributeSet as )
{
super( context, as );
}
+ synchronized void addTextChangedListener( TextWatcher proc )
+ {
+ if ( null == mWatchers ) {
+ mWatchers = new HashSet<>();
+ setOnQueryTextListener( this );
+ }
+ mWatchers.add( proc );
+ }
+
void setText( String txt )
{
super.setQuery( txt, false );
@@ -40,4 +59,22 @@ public class EditWClear extends SearchView {
{
return super.getQuery();
}
+
+ // from SearchView.OnQueryTextListener
+ @Override
+ public synchronized boolean onQueryTextChange( String newText )
+ {
+ for ( TextWatcher proc : mWatchers ) {
+ proc.onTextChanged( newText );
+ }
+ return true;
+ }
+
+ // from SearchView.OnQueryTextListener
+ @Override
+ public boolean onQueryTextSubmit( String query )
+ {
+ Assert.assertFalse( BuildConfig.DEBUG );
+ return true;
+ }
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java
index 923d2c3be..2969243c3 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java
@@ -123,6 +123,7 @@ public class GameConfigDelegate extends DelegateBase
R.id.room_spinner,
R.id.refresh_button,
R.id.hints_allowed,
+ R.id.duplicate_check,
R.id.pick_faceup,
R.id.boardsize_spinner,
R.id.use_timer,
@@ -620,26 +621,66 @@ public class GameConfigDelegate extends DelegateBase
setSmartnessSpinner();
+ tweakTimerStuff();
+
setChecked( R.id.hints_allowed, !m_gi.hintsNotAllowed );
setChecked( R.id.pick_faceup, m_gi.allowPickTiles );
- setInt( R.id.timer_minutes_edit,
- m_gi.gameSeconds/60/m_gi.nPlayers );
+ setBoardsizeSpinner();
+ }
+ } // loadGame
+
+ private boolean mTimerStuffInited = false;
+ private void tweakTimerStuff()
+ {
+ // one-time only stuff
+ if ( ! mTimerStuffInited ) {
+ mTimerStuffInited = true;
+
+ // dupe-mode check is GONE by default (in the .xml)
+ if ( CommonPrefs.getDupModeHidden( m_activity ) ) {
+ setChecked( R.id.duplicate_check, false );
+ } else {
+ CheckBox check = (CheckBox)findViewById( R.id.duplicate_check );
+ check.setVisibility( View.VISIBLE );
+ check.setChecked( m_gi.inDuplicateMode );
+ check.setOnCheckedChangeListener( new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged( CompoundButton buttonView,
+ boolean checked ) {
+ tweakTimerStuff();
+ }
+ } );
+ }
CheckBox check = (CheckBox)findViewById( R.id.use_timer );
OnCheckedChangeListener lstnr =
new OnCheckedChangeListener() {
public void onCheckedChanged( CompoundButton buttonView,
boolean checked ) {
- showTimerSet( checked );
+ tweakTimerStuff();
}
};
check.setOnCheckedChangeListener( lstnr );
- setChecked( R.id.use_timer, m_gi.timerEnabled );
- showTimerSet( m_gi.timerEnabled );
-
- setBoardsizeSpinner();
+ check.setChecked( m_gi.timerEnabled );
}
- } // loadGame
+
+ boolean dupModeChecked = getChecked( R.id.duplicate_check );
+ CheckBox check = (CheckBox)findViewById( R.id.use_timer );
+ check.setText( dupModeChecked ? R.string.use_duptimer : R.string.use_timer );
+
+ boolean timerSet = getChecked( R.id.use_timer );
+ showTimerSet( timerSet );
+
+ int id = dupModeChecked ? R.string.dup_minutes_label : R.string.minutes_label;
+ TextView label = (TextView)findViewById(R.id.timer_label );
+ label.setText( id );
+
+ // setInt( R.id.timer_minutes_edit,
+ // m_gi.gameSeconds/60/m_gi.nPlayers );
+
+ // setChecked( R.id.use_timer, m_gi.timerEnabled );
+ // showTimerSet( m_gi.timerEnabled );
+ }
private void showTimerSet( boolean show )
{
@@ -852,7 +893,7 @@ public class GameConfigDelegate extends DelegateBase
private void deleteGame()
{
- GameUtils.deleteGame( m_activity, m_rowid, false );
+ GameUtils.deleteGame( m_activity, m_rowid, false, false );
}
private void loadPlayersList()
@@ -1199,11 +1240,19 @@ public class GameConfigDelegate extends DelegateBase
}
}
+ m_gi.inDuplicateMode = getChecked( R.id.duplicate_check );
m_gi.hintsNotAllowed = !getChecked( R.id.hints_allowed );
m_gi.allowPickTiles = getChecked( R.id.pick_faceup );
m_gi.timerEnabled = getChecked( R.id.use_timer );
- m_gi.gameSeconds =
- 60 * m_gi.nPlayers * getInt( R.id.timer_minutes_edit );
+
+ // Get timer value. It's per-move minutes in duplicate mode, otherwise
+ // it's for the whole game.
+ int seconds = 60 * getInt( R.id.timer_minutes_edit );
+ if ( m_gi.inDuplicateMode ) {
+ m_gi.gameSeconds = seconds;
+ } else {
+ m_gi.gameSeconds = seconds * m_gi.nPlayers;
+ }
int position = m_phoniesSpinner.getSelectedItemPosition();
m_gi.phoniesAction = CurGameInfo.XWPhoniesChoice.values()[position];
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java
index 53a310e65..21b49355c 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java
@@ -348,6 +348,9 @@ public class GameListItem extends LinearLayout
m_role.setText( roleSummary );
}
+ findViewById( R.id.dup_tag )
+ .setVisibility( summary.inDuplicateMode() ? View.VISIBLE : View.GONE );
+
update( expanded, summary.lastMoveTime, haveATurn,
haveALocalTurn );
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameUtils.java
index 2048127a1..d3cf43356 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameUtils.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameUtils.java
@@ -179,7 +179,7 @@ public class GameUtils {
tellDied( context, lock, true );
resetGame( context, lock, lock, DBUtils.GROUPID_UNSPEC, false );
- Utils.cancelNotification( context, (int)rowidIn );
+ Utils.cancelNotification( context, rowidIn );
success = true;
} else {
DbgUtils.toastNoLock( TAG, context, rowidIn,
@@ -299,11 +299,13 @@ public class GameUtils {
}
public static void deleteGame( Context context, GameLock lock,
- boolean informNow )
+ boolean informNow, boolean skipTell )
{
if ( null != lock ) {
- tellDied( context, lock, informNow );
- Utils.cancelNotification( context, (int)lock.getRowid() );
+ if ( !skipTell ) {
+ tellDied( context, lock, informNow );
+ }
+ Utils.cancelNotification( context, lock.getRowid() );
DBUtils.deleteGame( context, lock );
} else {
Log.e( TAG, "deleteGame(): null lock; doing nothing" );
@@ -311,13 +313,13 @@ public class GameUtils {
}
public static boolean deleteGame( Context context, long rowid,
- boolean informNow )
+ boolean informNow, boolean skipTell )
{
boolean success;
// does this need to be synchronized?
try ( GameLock lock = GameLock.tryLock( rowid ) ) {
if ( null != lock ) {
- deleteGame( context, lock, informNow );
+ deleteGame( context, lock, informNow, skipTell );
success = true;
} else {
DbgUtils.toastNoLock( TAG, context, rowid,
@@ -334,7 +336,7 @@ public class GameUtils {
int nSuccesses = 0;
long[] rowids = DBUtils.getGroupGames( context, groupid );
for ( int ii = rowids.length - 1; ii >= 0; --ii ) {
- if ( deleteGame( context, rowids[ii], ii == 0 ) ) {
+ if ( deleteGame( context, rowids[ii], ii == 0, false ) ) {
++nSuccesses;
}
}
@@ -938,8 +940,18 @@ public class GameUtils {
public static void launchGame( Delegator delegator, long rowid,
boolean invited )
+ {
+ launchGame( delegator, rowid, invited, null );
+ }
+
+ public static void launchGame( Delegator delegator, long rowid,
+ boolean invited, Bundle moreExtras )
{
Bundle extras = makeLaunchExtras( rowid, invited );
+ if ( null != moreExtras ) {
+ extras.putAll( moreExtras );
+ }
+
if ( delegator.inDPMode() ) {
delegator.addFragment( BoardFrag.newInstance( delegator ), extras );
} else {
@@ -1055,7 +1067,7 @@ public class GameUtils {
if ( GameSummary.MSG_FLAGS_NONE != flags ) {
draw = true;
int curFlags = DBUtils.getMsgFlags( context, rowid );
- DBUtils.setMsgFlags( rowid, flags | curFlags );
+ DBUtils.setMsgFlags( context, rowid, flags | curFlags );
}
}
}
@@ -1245,7 +1257,7 @@ public class GameUtils {
if ( 0 != titleID ) {
String title = LocUtils.getString( context, titleID,
getName( context, rowid ) );
- Utils.postNotification( context, intent, title, msg, (int)rowid );
+ Utils.postNotification( context, intent, title, msg, rowid );
}
} else {
Log.d( TAG, "postMoveNotification(): posting nothing for lack"
@@ -1258,7 +1270,7 @@ public class GameUtils {
{
Intent intent = GamesListDelegate.makeGameIDIntent( context, gameID );
Utils.postNotification( context, intent, R.string.invite_notice_title,
- body, (int)rowid );
+ body, rowid );
}
private static void tellDied( Context context, GameLock lock,
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java
index 40422657c..55d393796 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java
@@ -1,7 +1,7 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
- * Copyright 2009 - 2016 by Eric House (xwords@eehouse.org). All
- * rights reserved.
+ * Copyright 2009 - 2019 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
@@ -1012,6 +1012,17 @@ public class GamesListDelegate extends ListDelegateBase
} else if ( isFirstLaunch ) {
warnSMSBannedIf();
}
+
+ Set dupModeGames = DBUtils.getDupModeGames( m_activity ).keySet();
+ long[] asArray = new long[dupModeGames.size()];
+ int ii = 0;
+ for ( long rowid : dupModeGames ) {
+ Log.d( TAG, "row %d is dup-mode", rowid );
+ asArray[ii++] = rowid;
+ }
+ if ( false ) {
+ deleteGames( asArray, true );
+ }
} // init
@Override
@@ -1172,7 +1183,9 @@ public class GamesListDelegate extends ListDelegateBase
//////////////////////////////////////////////////////////////////////
// DBUtils.DBChangeListener interface
//////////////////////////////////////////////////////////////////////
- public void gameSaved( final long rowid, final GameChangeType change )
+ @Override
+ public void gameSaved( Context context, final long rowid,
+ final GameChangeType change )
{
post( new Runnable() {
public void run() {
@@ -1324,7 +1337,7 @@ public class GamesListDelegate extends ListDelegateBase
mkListAdapter();
break;
case DELETE_GAMES:
- deleteGames( (long[])params[0] );
+ deleteGames( (long[])params[0], false );
break;
case OPEN_GAME:
doOpenGame( params );
@@ -2140,7 +2153,7 @@ public class GamesListDelegate extends ListDelegateBase
return launched;
}
- private boolean startFirstHasDict( final long rowid )
+ private boolean startFirstHasDict( final long rowid, final Bundle extras )
{
boolean handled = -1 != rowid && DBUtils.haveGame( m_activity, rowid );
if ( handled ) {
@@ -2154,7 +2167,7 @@ public class GamesListDelegate extends ListDelegateBase
.gameDictsHere( m_activity, lock );
lock.release();
if ( haveDict ) {
- launchGame( rowid );
+ launchGame( rowid, extras );
}
}
}
@@ -2171,7 +2184,7 @@ public class GamesListDelegate extends ListDelegateBase
String[] relayIDs = intent.getStringArrayExtra( RELAYIDS_EXTRA );
if ( !startFirstHasDict( relayIDs ) ) {
long rowid = intent.getLongExtra( ROWID_EXTRA, -1 );
- result = startFirstHasDict( rowid );
+ result = startFirstHasDict( rowid, intent.getExtras() );
}
}
return result;
@@ -2455,10 +2468,10 @@ public class GamesListDelegate extends ListDelegateBase
}
}
- private void deleteGames( long[] rowids )
+ private void deleteGames( long[] rowids, boolean skipTell )
{
for ( long rowid : rowids ) {
- GameUtils.deleteGame( m_activity, rowid, false );
+ GameUtils.deleteGame( m_activity, rowid, false, skipTell );
m_mySIS.selGames.remove( rowid );
}
invalidateOptionsMenuIf();
@@ -2547,7 +2560,7 @@ public class GamesListDelegate extends ListDelegateBase
return madeGame;
}
- private void launchGame( long rowid, boolean invited )
+ private void launchGame( long rowid, boolean invited, Bundle extras )
{
if ( DBUtils.ROWID_NOTFOUND == rowid ) {
Log.d( TAG, "launchGame(): dropping bad rowid" );
@@ -2556,20 +2569,25 @@ public class GamesListDelegate extends ListDelegateBase
if ( m_adapter.inExpandedGroup( rowid ) ) {
setSelGame( rowid );
}
- GameUtils.launchGame( getDelegator(), rowid, invited );
+ GameUtils.launchGame( getDelegator(), rowid, invited, extras );
}
}
private void launchGame( long rowid )
{
- launchGame( rowid, false );
+ launchGame( rowid, false, null );
+ }
+
+ private void launchGame( long rowid, Bundle extras )
+ {
+ launchGame( rowid, false, extras );
}
private void makeNewNetGame( NetLaunchInfo nli )
{
long rowid = DBUtils.ROWID_NOTFOUND;
rowid = GameUtils.makeNewMultiGame( m_activity, nli );
- launchGame( rowid, true );
+ launchGame( rowid, true, null );
}
private void tryStartsFromIntent( Intent intent )
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiService.java
index 220a64935..1b33a4cf1 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiService.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiService.java
@@ -51,6 +51,7 @@ public class MultiService {
public static final String BT_ADDRESS = "BT_ADDRESS";
public static final String P2P_MAC_ADDRESS = "P2P_MAC_ADDRESS";
private static final String NLI_DATA = "nli";
+ public static final String DUPEMODE = "du";
public enum DictFetchOwner { _NONE,
OWNER_SMS,
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NagTurnReceiver.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NagTurnReceiver.java
index e7cd05885..2e3d15b5b 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NagTurnReceiver.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NagTurnReceiver.java
@@ -101,7 +101,7 @@ public class NagTurnReceiver extends BroadcastReceiver {
}
Utils.postNotification( context, msgIntent,
R.string.nag_title, body,
- (int)rowid );
+ rowid );
}
DBUtils.updateNeedNagging( context, needNagging );
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetLaunchInfo.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetLaunchInfo.java
index d118120b9..6151437cc 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetLaunchInfo.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetLaunchInfo.java
@@ -65,6 +65,7 @@ public class NetLaunchInfo implements Serializable {
private static final String FORCECHANNEL_KEY = "fc";
private static final String NAME_KEY = "nm";
private static final String P2P_MAC_KEY = "p2";
+ private static final String DUPMODE_KEY = "du";
protected String gameName;
protected String dict;
@@ -87,6 +88,7 @@ public class NetLaunchInfo implements Serializable {
private CommsConnTypeSet m_addrs;
private boolean m_valid;
private String inviteID;
+ private boolean dupeMode;
public NetLaunchInfo()
{
@@ -244,6 +246,8 @@ public class NetLaunchInfo implements Serializable {
val = data.getQueryParameter( FORCECHANNEL_KEY );
forceChannel = null == val ? 0 : Integer.decode( val );
gameName = data.getQueryParameter( NAME_KEY );
+ val = data.getQueryParameter( DUPMODE_KEY );
+ dupeMode = null != val && Integer.decode(val) != 0;
}
calcValid();
} catch ( Exception e ) {
@@ -254,7 +258,7 @@ public class NetLaunchInfo implements Serializable {
}
private NetLaunchInfo( int gamID, String gamNam, int dictLang,
- String dictName, int nPlayers )
+ String dictName, int nPlayers, boolean dupMode )
{
this();
gameName = gamNam;
@@ -263,6 +267,7 @@ public class NetLaunchInfo implements Serializable {
nPlayersT = nPlayers;
nPlayersH = 1;
gameID = gamID;
+ dupeMode = dupMode;
}
public NetLaunchInfo( Context context, GameSummary summary, CurGameInfo gi,
@@ -275,7 +280,8 @@ public class NetLaunchInfo implements Serializable {
public NetLaunchInfo( CurGameInfo gi )
{
- this( gi.gameID, gi.getName(), gi.dictLang, gi.dictName, gi.nPlayers );
+ this( gi.gameID, gi.getName(), gi.dictLang, gi.dictName, gi.nPlayers,
+ gi.inDuplicateMode );
}
public NetLaunchInfo( Context context, GameSummary summary, CurGameInfo gi )
@@ -350,12 +356,17 @@ public class NetLaunchInfo implements Serializable {
bundle.putString( MultiService.GAMENAME, gameName );
bundle.putInt( MultiService.NPLAYERST, nPlayersT );
bundle.putInt( MultiService.NPLAYERSH, nPlayersH );
- bundle.putBoolean( MultiService.REMOTES_ROBOTS, remotesAreRobots );
+ if ( remotesAreRobots ) {
+ bundle.putBoolean( MultiService.REMOTES_ROBOTS, true );
+ }
bundle.putInt( MultiService.GAMEID, gameID() );
bundle.putString( MultiService.BT_NAME, btName );
bundle.putString( MultiService.BT_ADDRESS, btAddress );
bundle.putString( MultiService.P2P_MAC_ADDRESS, p2pMacAddress );
bundle.putInt( MultiService.FORCECHANNEL, forceChannel );
+ if ( dupeMode ) {
+ bundle.putBoolean( MultiService.DUPEMODE, true );
+ }
int flags = m_addrs.toInt();
bundle.putInt( ADDRS_KEY, flags );
@@ -374,6 +385,7 @@ public class NetLaunchInfo implements Serializable {
&& forceChannel == other.forceChannel
&& nPlayersT == other.nPlayersT
&& nPlayersH == other.nPlayersH
+ && dupeMode == other.dupeMode
&& remotesAreRobots == other.remotesAreRobots
&& TextUtils.equals( room, other.room )
&& TextUtils.equals( btName, other.btName )
@@ -406,7 +418,12 @@ public class NetLaunchInfo implements Serializable {
.put( MultiService.NPLAYERSH, nPlayersH )
.put( MultiService.REMOTES_ROBOTS, remotesAreRobots )
.put( MultiService.GAMEID, gameID() )
- .put( MultiService.FORCECHANNEL, forceChannel );
+ .put( MultiService.FORCECHANNEL, forceChannel )
+ ;
+
+ if ( dupeMode ) {
+ obj.put( MultiService.DUPEMODE, dupeMode );
+ }
if ( m_addrs.contains( CommsConnType.COMMS_CONN_RELAY ) ) {
obj.put( MultiService.ROOM, room )
@@ -477,6 +494,7 @@ public class NetLaunchInfo implements Serializable {
lang = json.optInt( MultiService.LANG, -1 );
forceChannel = json.optInt( MultiService.FORCECHANNEL, 0 );
+ dupeMode = json.optBoolean( MultiService.DUPEMODE, false );
dict = json.optString( MultiService.DICT );
gameName = json.optString( MultiService.GAMENAME );
nPlayersT = json.optInt( MultiService.NPLAYERST, -1 );
@@ -549,6 +567,9 @@ public class NetLaunchInfo implements Serializable {
appendInt( ub, FORCECHANNEL_KEY, forceChannel );
appendInt( ub, ADDRS_KEY, addrs );
ub.appendQueryParameter( NAME_KEY, gameName );
+ if ( dupeMode ) {
+ appendInt( ub, DUPMODE_KEY, 1 );
+ }
if ( null != dict ) {
ub.appendQueryParameter( WORDLIST_KEY, dict );
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java
index af465ae55..dc188fc52 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java
@@ -36,8 +36,8 @@ import android.preference.PreferenceScreen;
import android.view.View;
import android.widget.Button;
-
import org.eehouse.android.xw4.DlgDelegate.Action;
+import org.eehouse.android.xw4.jni.CommonPrefs;
import org.eehouse.android.xw4.loc.LocUtils;
import java.io.File;
@@ -387,6 +387,10 @@ public class PrefsDelegate extends DelegateBase
if ( null == FBMService.getFCMDevID( m_activity ) ) {
hideOne( R.string.key_show_fcm, R.string.pref_group_relay_title );
}
+
+ if ( CommonPrefs.getDupModeHidden( m_activity ) ) {
+ hideOne( R.string.key_init_dupmodeon, R.string.key_prefs_defaults );
+ }
}
public static void launch( Context context )
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java
index 65e39dbe5..b0af8bd73 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java
@@ -1330,6 +1330,10 @@ public class RelayService extends XWJIService
if ( null == udpSocket ) {
// will be null if e.g. device or emulator doesn't have network
udpSocket = getService().connectSocketOnce(); // block until this is done
+ // Assert.assertTrue( null != udpSocket || !BuildConfig.DEBUG ); // firing
+ if ( null == udpSocket ) {
+ Log.e( TAG, "connectSocketOnce() failed; no socket" );
+ }
}
byte[] buf = new byte[1024];
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/StudyListDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/StudyListDelegate.java
index cf4575ebf..b2ab06d33 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/StudyListDelegate.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/StudyListDelegate.java
@@ -326,11 +326,11 @@ public class StudyListDelegate extends ListDelegateBase
startLang = startIntent.getIntExtra( START_LANG, NO_LANG );
}
- String[] names = DictLangCache.getLangNames( m_activity );
String[] myNames = new String[m_langCodes.length];
for ( int ii = 0; ii < m_langCodes.length; ++ii ) {
int lang = m_langCodes[ii];
- myNames[ii] = xlateLang( names[lang], true );
+ String name = DictLangCache.getLangName( m_activity, lang );
+ myNames[ii] = xlateLang( name, true );
if ( lang == startLang ) {
startIndex = ii;
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java
index 00765770d..d461b4b7e 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java
@@ -88,6 +88,8 @@ public class Utils {
private static final String FIRST_VERSION_KEY = "FIRST_VERSION_KEY";
private static final String SHOWN_VERSION_KEY = "SHOWN_VERSION_KEY";
+ private static final Channels.ID sDefaultChannel = Channels.ID.GAME_EVENT;
+
private static Boolean s_isFirstBootThisVersion = null;
private static Boolean s_firstVersion = null;
private static Boolean s_isFirstBootEver = null;
@@ -244,24 +246,68 @@ public class Utils {
LocUtils.getString( context, bodyID ), id );
}
+ public static void postNotification( Context context, Intent intent,
+ String title, String body, long rowid )
+ {
+ int id = sDefaultChannel.idFor( rowid );
+ postNotification( context, intent, title, body, id );
+ }
+
+ public static void postNotification( Context context, Intent intent,
+ int titleId, String body, long rowid )
+ {
+ postNotification( context, intent, titleId, body, rowid,
+ sDefaultChannel );
+ }
+
public static void postNotification( Context context, Intent intent,
int titleID, String body, int id )
+ {
+ postNotification( context, intent, titleID, body, id,
+ sDefaultChannel );
+ }
+
+ public static void postNotification( Context context, Intent intent,
+ int titleID, String body, long rowid,
+ Channels.ID channel )
+ {
+ int id = channel.idFor( rowid );
+ postNotification( context, intent, titleID, body, id, channel );
+ }
+
+ private static void postNotification( Context context, Intent intent,
+ int titleID, String body, int id,
+ Channels.ID channel )
{
String title = LocUtils.getString( context, titleID );
- postNotification( context, intent, title, body, id );
+ // Log.d( TAG, "posting with title %s", title );
+ postNotification( context, intent, title, body, id, channel, false,
+ null, 0 );
}
public static void postNotification( Context context, Intent intent,
String title, String body,
int id )
{
- String channelID = Channels.getChannelID( context, Channels.ID.GAME_EVENT );
- postNotification( context, intent, title, body, id, channelID );
+ postNotification( context, intent, title, body, id,
+ sDefaultChannel, false, null, 0 );
+ }
+
+ static void postOngoingNotification( Context context, Intent intent,
+ String title, String body,
+ long rowid, Channels.ID channel,
+ Intent actionIntent,
+ int actionString )
+ {
+ int id = channel.idFor( rowid );
+ postNotification( context, intent, title, body, id, channel, true,
+ actionIntent, actionString );
}
private static void postNotification( Context context, Intent intent,
String title, String body,
- int id, String channelID )
+ int id, Channels.ID channel, boolean ongoing,
+ Intent actionIntent, int actionString )
{
/* nextRandomInt: per this link
http://stackoverflow.com/questions/10561419/scheduling-more-than-one-pendingintent-to-same-activity-using-alarmmanager
@@ -269,9 +315,8 @@ public class Utils {
Intents is to send a different second param each time,
though the docs say that param's ignored.
*/
- PendingIntent pi = null == intent ? null
- : PendingIntent.getActivity( context, nextRandomInt(), intent,
- PendingIntent.FLAG_ONE_SHOT );
+ PendingIntent pi = null == intent
+ ? null : getPendingIntent( context, intent );
int defaults = Notification.FLAG_AUTO_CANCEL;
if ( CommonPrefs.getSoundNotify( context ) ) {
@@ -281,42 +326,63 @@ public class Utils {
defaults |= Notification.DEFAULT_VIBRATE;
}
- Notification notification =
+ String channelID = Channels.getChannelID( context, channel );
+ NotificationCompat.Builder builder =
new NotificationCompat.Builder( context, channelID )
.setContentIntent( pi )
.setSmallIcon( R.drawable.notify )
//.setTicker(body)
//.setWhen(time)
+ .setOngoing( ongoing )
.setAutoCancel( true )
.setDefaults( defaults )
.setContentTitle( title )
.setContentText( body )
- .build();
+ ;
+
+ if ( null != actionIntent ) {
+ PendingIntent actionPI = getPendingIntent( context, actionIntent );
+ builder.addAction( 0, LocUtils.getString(context, actionString),
+ actionPI );
+ }
+
+ Notification notification = builder.build();
NotificationManager nm = (NotificationManager)
context.getSystemService( Context.NOTIFICATION_SERVICE );
nm.notify( id, notification );
}
+ private static PendingIntent getPendingIntent( Context context, Intent intent )
+ {
+ PendingIntent pi = PendingIntent
+ .getActivity( context, Utils.nextRandomInt(), intent,
+ PendingIntent.FLAG_ONE_SHOT );
+ return pi;
+ }
+
private static final String KEY_LAST_STALL_NOT = TAG + ".last_stall_note";
private static final long MIN_STALL_NOTE_INTERVAL_MS = 1000 * 60 * 30;
- public static void showStallNotification( Context context, long ageMS )
+ public static void showStallNotification( Context context, String typ,
+ long ageMS )
{
+ String body = LocUtils.getString( context, R.string.notify_stall_body_fmt,
+ typ, (ageMS + 500) / 1000,
+ MIN_STALL_NOTE_INTERVAL_MS / (1000 * 60));
+
long now = System.currentTimeMillis();
long lastStallNotify = DBUtils.getLongFor( context, KEY_LAST_STALL_NOT, 0 );
if ( now - lastStallNotify > MIN_STALL_NOTE_INTERVAL_MS ) {
String title = LocUtils.getString( context, R.string.notify_stall_title );
- String body = LocUtils.getString( context, R.string.notify_stall_body_fmt,
- (ageMS + 500) / 1000,
- MIN_STALL_NOTE_INTERVAL_MS / (1000 * 60));
- String channelID = Channels.getChannelID( context,
- Channels.ID.SERVICE_STALL );
-
Intent intent = GamesListDelegate
.makeAlertWithEmailIntent( context, body );
postNotification( context, intent, title, body,
- R.string.notify_stall_title, channelID );
+ R.string.notify_stall_title,
+ Channels.ID.SERVICE_STALL, false, null, 0 );
DBUtils.setLongFor( context, KEY_LAST_STALL_NOT, now );
+ } else {
+ Log.e( TAG, "stalled, but too recent for notification: %s",
+ body );
}
}
@@ -328,6 +394,18 @@ public class Utils {
cancelNotification( context, R.string.notify_stall_title );
}
+ public static void cancelNotification( Context context, Channels.ID channel,
+ long rowid )
+ {
+ int id = channel.idFor( rowid );
+ cancelNotification( context, id );
+ }
+
+ public static void cancelNotification( Context context, long rowid )
+ {
+ cancelNotification( context, sDefaultChannel, rowid );
+ }
+
public static void cancelNotification( Context context, int id )
{
NotificationManager nm = (NotificationManager)
@@ -522,6 +600,7 @@ public class Utils {
return result;
}
+ // Called from andutils.c in the jni world
public static long getCurSeconds()
{
// Note: an int is big enough for *seconds* (not milliseconds) since 1970
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWApp.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWApp.java
index b459101a2..b0d993c2a 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWApp.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWApp.java
@@ -97,6 +97,8 @@ public class XWApp extends Application
mPort = Short.valueOf( getString( R.string.nbs_port ) );
NBSProxy.register( this, mPort, BuildConfig.APPLICATION_ID, this );
+
+ DupeModeTimer.init( this );
}
@OnLifecycleEvent(ON_ANY)
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWJIService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWJIService.java
index 05bdf5a4e..3307c93c5 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWJIService.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWJIService.java
@@ -132,6 +132,7 @@ abstract class XWJIService extends JobIntentService {
if ( stallCheckEnabled( context ) ) {
long now = System.currentTimeMillis();
long maxAge = 0;
+ String maxName = null;
synchronized ( sPendingIntents ) {
for ( String simpleName : sPendingIntents.keySet() ) {
List intents = sPendingIntents.get( simpleName );
@@ -141,6 +142,7 @@ abstract class XWJIService extends JobIntentService {
long age = now - timestamp;
if ( age > maxAge ) {
maxAge = age;
+ maxName = simpleName;
}
}
}
@@ -148,7 +150,7 @@ abstract class XWJIService extends JobIntentService {
if ( maxAge > AGE_THRESHOLD_MS ) {
// ConnStatusHandler.noteStall( sTypes.get( clazz ), maxAge );
- Utils.showStallNotification( context, maxAge );
+ Utils.showStallNotification( context, maxName, maxAge );
}
}
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CommonPrefs.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CommonPrefs.java
index ec56b5658..c1014ab3a 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CommonPrefs.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CommonPrefs.java
@@ -237,6 +237,16 @@ public class CommonPrefs extends XWPrefs {
return getPrefsBoolean( context, key, true );
}
+ public static boolean getDefaultDupMode( Context context )
+ {
+ return getPrefsBoolean( context, R.string.key_init_dupmodeon, false );
+ }
+
+ public static boolean getDupModeHidden( Context context )
+ {
+ return !getPrefsBoolean( context, R.string.key_unhide_dupmode, false );
+ }
+
public static boolean getAutoJuggle( Context context )
{
return getPrefsBoolean( context, R.string.key_init_autojuggle, false );
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CurGameInfo.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CurGameInfo.java
index eedeaa81e..7fc26fecc 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CurGameInfo.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CurGameInfo.java
@@ -49,6 +49,7 @@ public class CurGameInfo implements Serializable {
private static final String TIMER = "TIMER";
private static final String ALLOW_PICK = "ALLOW_PICK";
private static final String PHONIES = "PHONIES";
+ private static final String DUP = "DUP";
public enum XWPhoniesChoice { PHONIES_IGNORE, PHONIES_WARN, PHONIES_DISALLOW };
public enum DeviceRole { SERVER_STANDALONE, SERVER_ISSERVER, SERVER_ISCLIENT };
@@ -63,6 +64,7 @@ public class CurGameInfo implements Serializable {
public int forceChannel;
public DeviceRole serverRole;
+ public boolean inDuplicateMode;
public boolean hintsNotAllowed;
public boolean timerEnabled;
public boolean allowPickTiles;
@@ -83,8 +85,9 @@ public class CurGameInfo implements Serializable {
{
boolean isNetworked = null != inviteID;
nPlayers = 2;
- gameSeconds = 60 * nPlayers *
- CommonPrefs.getDefaultPlayerMinutes( context );
+ inDuplicateMode = CommonPrefs.getDefaultDupMode( context );
+ gameSeconds = inDuplicateMode ? (5 * 60)
+ : 60 * nPlayers * CommonPrefs.getDefaultPlayerMinutes( context );
boardSize = CommonPrefs.getDefaultBoardSize( context );
players = new LocalPlayer[MAX_NUM_PLAYERS];
serverRole = isNetworked ? DeviceRole.SERVER_ISCLIENT
@@ -142,6 +145,7 @@ public class CurGameInfo implements Serializable {
dictName = src.dictName;
dictLang = src.dictLang;
hintsNotAllowed = src.hintsNotAllowed;
+ inDuplicateMode = src.inDuplicateMode;
phoniesAction = src.phoniesAction;
timerEnabled = src.timerEnabled;
allowPickTiles = src.allowPickTiles;
@@ -169,6 +173,8 @@ public class CurGameInfo implements Serializable {
}
sb.append( "], gameID: ").append( gameID )
.append( ", hashCode: ").append( hashCode() )
+ .append( ", timerEnabled: ").append( timerEnabled )
+ .append( ", gameSeconds: ").append( gameSeconds )
.append('}');
result = sb.toString();
@@ -185,6 +191,7 @@ public class CurGameInfo implements Serializable {
JSONObject obj = new JSONObject()
.put( BOARD_SIZE, boardSize )
.put( NO_HINTS, hintsNotAllowed )
+ .put( DUP, inDuplicateMode )
.put( TIMER, timerEnabled )
.put( ALLOW_PICK, allowPickTiles )
.put( PHONIES, phoniesAction.ordinal() )
@@ -204,6 +211,7 @@ public class CurGameInfo implements Serializable {
JSONObject obj = new JSONObject( jsonData );
boardSize = obj.optInt( BOARD_SIZE, boardSize );
hintsNotAllowed = obj.optBoolean( NO_HINTS, hintsNotAllowed );
+ inDuplicateMode = obj.optBoolean( DUP, inDuplicateMode );
timerEnabled = obj.optBoolean( TIMER, timerEnabled );
allowPickTiles = obj.optBoolean( ALLOW_PICK, allowPickTiles );
int tmp = obj.optInt( PHONIES, phoniesAction.ordinal() );
@@ -280,6 +288,7 @@ public class CurGameInfo implements Serializable {
|| dictLang != other.dictLang
|| boardSize != other.boardSize
|| hintsNotAllowed != other.hintsNotAllowed
+ || inDuplicateMode != other.inDuplicateMode
|| allowPickTiles != other.allowPickTiles
|| phoniesAction != other.phoniesAction;
@@ -313,6 +322,7 @@ public class CurGameInfo implements Serializable {
&& boardSize == other.boardSize
&& forceChannel == other.forceChannel
&& hintsNotAllowed == other.hintsNotAllowed
+ && inDuplicateMode == other.inDuplicateMode
&& timerEnabled == other.timerEnabled
&& allowPickTiles == other.allowPickTiles
&& allowHintRect == other.allowHintRect
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DUtilCtxt.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DUtilCtxt.java
index 14ecd7525..3ccbcd4ba 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DUtilCtxt.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DUtilCtxt.java
@@ -21,14 +21,20 @@
package org.eehouse.android.xw4.jni;
import android.content.Context;
+import android.content.Intent;
import android.telephony.PhoneNumberUtils;
import org.eehouse.android.xw4.Assert;
+import org.eehouse.android.xw4.Channels;
import org.eehouse.android.xw4.DBUtils;
import org.eehouse.android.xw4.DevID;
+import org.eehouse.android.xw4.DupeModeTimer;
import org.eehouse.android.xw4.FBMService;
+import org.eehouse.android.xw4.GameUtils;
+import org.eehouse.android.xw4.GamesListDelegate;
import org.eehouse.android.xw4.Log;
import org.eehouse.android.xw4.R;
+import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.XWApp;
import org.eehouse.android.xw4.loc.LocUtils;
@@ -102,15 +108,17 @@ public class DUtilCtxt {
static final int STRD_CUMULATIVE_SCORE = 14;
static final int STRS_NEW_TILES = 15;
static final int STR_COMMIT_CONFIRM = 16;
- static final int STR_BONUS_ALL = 17;
- static final int STRD_TURN_SCORE = 18;
- static final int STRD_REMAINS_HEADER = 19;
- static final int STRD_REMAINS_EXPL = 20;
- static final int STRSD_RESIGNED = 21;
- static final int STRSD_WINNER = 22;
- static final int STRDSD_PLACER = 23;
+ static final int STR_SUBMIT_CONFIRM = 17;
+ static final int STR_BONUS_ALL = 18;
+ static final int STRD_TURN_SCORE = 19;
+ static final int STRD_REMAINS_HEADER = 20;
+ static final int STRD_REMAINS_EXPL = 21;
+ static final int STRSD_RESIGNED = 22;
+ static final int STRSD_WINNER = 23;
+ static final int STRDSD_PLACER = 24;
+ static final int STR_DUP_CLIENT_SENT = 25;
+ static final int STRDD_DUP_HOST_RECEIVED = 26;
-
public String getUserString( int stringCode )
{
Log.d( TAG, "getUserString(%d)", stringCode );
@@ -161,6 +169,9 @@ public class DUtilCtxt {
case STR_COMMIT_CONFIRM:
id = R.string.str_commit_confirm;
break;
+ case STR_SUBMIT_CONFIRM:
+ id = R.string.str_submit_confirm;
+ break;
case STR_BONUS_ALL:
id = R.string.str_bonus_all;
break;
@@ -177,6 +188,14 @@ public class DUtilCtxt {
id = R.string.str_placer_fmt;
break;
+ case STR_DUP_CLIENT_SENT:
+ id = R.string.dup_client_sent;
+ break;
+ case STRDD_DUP_HOST_RECEIVED:
+ id = R.string.dup_host_received_fmt;
+ break;
+
+
default:
Log.w( TAG, "no such stringCode: %d", stringCode );
}
@@ -236,4 +255,71 @@ public class DUtilCtxt {
Log.d( TAG, "load(%s) returning %d bytes", key, resultLen );
return result;
}
+
+
+ // Must match enum DupPauseType
+ public static final int UNPAUSED = 0;
+ public static final int PAUSED = 1;
+ public static final int AUTOPAUSED = 2;
+
+ // A pause can come in when a game's open or when it's not. If it's open,
+ // we want to post an alert. If it's not, we want to post a notification,
+ // or at least kick off DupeModeTimer to cancel or start the timer-running
+ // notification.
+ public void notifyPause( int gameID, int pauseType, int pauser,
+ String pauserName, String expl )
+ {
+ long[] rowids = DBUtils.getRowIDsFor( m_context, gameID );
+ Log.d( TAG, "got %d games with gameid", null == rowids ? 0 : rowids.length );
+
+ final boolean isPause = UNPAUSED != pauseType;
+
+ for ( long rowid : rowids ) {
+ String msg = msgForPause( rowid, pauseType, pauserName, expl );
+ try ( JNIThread thread = JNIThread.getRetained( rowid ) ) {
+ if ( null != thread ) {
+ thread.notifyPause( pauser, isPause, msg );
+ } else {
+ Intent intent = GamesListDelegate
+ .makeRowidIntent( m_context, rowid );
+ int titleID = isPause ? R.string.game_paused_title
+ : R.string.game_unpaused_title;
+ Channels.ID channelID = Channels.ID.DUP_PAUSED;
+ Utils.postNotification( m_context, intent, titleID, msg,
+ rowid, channelID );
+
+ // DupeModeTimer.timerPauseChanged( m_context, rowid );
+ }
+ }
+ }
+ }
+
+ private String msgForPause( long rowid, int pauseType, String pauserName, String expl )
+ {
+ String msg;
+ final String gameName = GameUtils.getName( m_context, rowid );
+ if ( AUTOPAUSED == pauseType ) {
+ msg = LocUtils.getString( m_context, R.string.autopause_expl_fmt,
+ gameName );
+ } else {
+ boolean isPause = PAUSED == pauseType;
+ if ( null != expl && 0 < expl.length() ) {
+ msg = LocUtils.getString( m_context,
+ isPause ? R.string.pause_notify_expl_fmt
+ : R.string.unpause_notify_expl_fmt,
+ pauserName, expl );
+ } else {
+ msg = LocUtils.getString( m_context,
+ isPause ? R.string.pause_notify_fmt
+ : R.string.unpause_notify_fmt,
+ pauserName );
+ }
+ }
+ return msg;
+ }
+
+ public void onDupTimerChanged( int gameID, int oldVal, int newVal )
+ {
+ DupeModeTimer.timerChanged( m_context, gameID, newVal );
+ }
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DrawCtx.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DrawCtx.java
index f41b45262..9eb93ffed 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DrawCtx.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DrawCtx.java
@@ -57,7 +57,7 @@ public interface DrawCtx {
// void score_drawPlayers( Rect scoreRect, DrawScoreInfo[] playerData,
// Rect[] playerRects );
- void drawTimer( Rect rect, int player, int secondsLeft );
+ void drawTimer( Rect rect, int player, int secondsLeft, boolean inDuplicateMode );
boolean drawCell( Rect rect, String text, int tile, int value,
int owner, int bonus, int hintAtts, int flags );
@@ -69,8 +69,8 @@ public interface DrawCtx {
int flags );
boolean drawTileBack( Rect rect, int flags );
void drawTrayDivider( Rect rect, int flags );
- void score_pendingScore( Rect rect, int score, int playerNum, int curTurn,
- int flags );
+ void score_pendingScore( Rect rect, int score, int playerNum,
+ boolean curTurn, int flags );
public static final int BONUS_NONE = 0;
public static final int BONUS_DOUBLE_LETTER = 1;
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/GameSummary.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/GameSummary.java
index 09ef06ca0..c51f3acfe 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/GameSummary.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/GameSummary.java
@@ -37,6 +37,7 @@ import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.XWApp;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;
+import org.eehouse.android.xw4.jni.CurGameInfo;
import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
import org.eehouse.android.xw4.loc.LocUtils;
@@ -55,8 +56,10 @@ public class GameSummary implements Serializable {
public static final int MSG_FLAGS_CHAT = 2;
public static final int MSG_FLAGS_GAMEOVER = 4;
public static final int MSG_FLAGS_ALL = 7;
+ public static final int DUP_MODE_MASK = 1 << (CurGameInfo.MAX_NUM_PLAYERS * 2);
public int lastMoveTime; // set by jni's server.c on move receipt
+ public int dupTimerExpires;
public int nMoves;
public int turn;
public boolean turnIsLocal;
@@ -110,6 +113,7 @@ public class GameSummary implements Serializable {
GameSummary other = (GameSummary)obj;
result = lastMoveTime == other.lastMoveTime
&& nMoves == other.nMoves
+ && dupTimerExpires == other.dupTimerExpires
&& turn == other.turn
&& turnIsLocal == other.turnIsLocal
&& nPlayers == other.nPlayers
@@ -361,10 +365,21 @@ public class GameSummary implements Serializable {
result |= 1 << (ii * 2);
}
}
+
+ Assert.assertTrue( (result & DUP_MODE_MASK) == 0 );
+ if ( m_gi.inDuplicateMode ) {
+ result |= DUP_MODE_MASK;
+ }
}
return result;
}
+ public boolean inDuplicateMode()
+ {
+ int flags = giflags();
+ return (flags & DUP_MODE_MASK) != 0;
+ }
+
public void setGiFlags( int flags )
{
m_giFlags = new Integer( flags );
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/JNIThread.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/JNIThread.java
index bd820e255..925a65a10 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/JNIThread.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/JNIThread.java
@@ -1,6 +1,6 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
- * Copyright 2009 - 2017 by Eric House (xwords@eehouse.org). All rights
+ * Copyright 2009 - 2019 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
@@ -33,11 +33,12 @@ import org.eehouse.android.xw4.ConnStatusHandler;
import org.eehouse.android.xw4.DBUtils;
import org.eehouse.android.xw4.DbgUtils;
import org.eehouse.android.xw4.DictUtils;
+import org.eehouse.android.xw4.DupeModeTimer;
import org.eehouse.android.xw4.GameLock;
import org.eehouse.android.xw4.GameUtils;
-import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.Log;
import org.eehouse.android.xw4.R;
+import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.XWPrefs;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
@@ -97,6 +98,8 @@ public class JNIThread extends Thread implements AutoCloseable {
CMD_NETSTATS,
CMD_PASS_PASSWD,
CMD_SET_BLANK,
+ CMD_PAUSE,
+ CMD_UNPAUSE,
// CMD_DRAW_CONNS_STATUS,
// CMD_DRAW_BT_STATUS,
// CMD_DRAW_SMS_STATUS,
@@ -106,7 +109,7 @@ public class JNIThread extends Thread implements AutoCloseable {
public static final int DIALOG = 2;
public static final int QUERY_ENDGAME = 3;
public static final int TOOLBAR_STATES = 4;
- public static final int GOT_WORDS = 5;
+ public static final int GOT_PAUSE = 5;
public static final int GAME_OVER = 6;
public static final int MSGS_SENT = 7;
@@ -124,6 +127,8 @@ public class JNIThread extends Thread implements AutoCloseable {
public boolean curTurnSelected;
public boolean canHideRack;
public boolean canTrade;
+ public boolean canPause;
+ public boolean canUnpause;
public GameStateInfo clone() {
GameStateInfo obj = null;
try {
@@ -167,7 +172,7 @@ public class JNIThread extends Thread implements AutoCloseable {
{
m_lock = lock.retain();
m_rowid = lock.getRowid();
- m_queue = new LinkedBlockingQueue();
+ m_queue = new LinkedBlockingQueue<>();
}
public boolean configure( Context context, SyncedDraw drawer,
@@ -239,6 +244,8 @@ public class JNIThread extends Thread implements AutoCloseable {
}
m_lastSavedState = Arrays.hashCode( stream );
+
+ DupeModeTimer.gameOpened( m_context, m_rowid );
}
Log.d( TAG, "configure() => %b", success );
return success;
@@ -729,6 +736,12 @@ public class JNIThread extends Thread implements AutoCloseable {
((Integer)args[2]).intValue() );
break;
+ case CMD_PAUSE:
+ XwJNI.board_pause( m_jniGamePtr, ((String)args[0]) );
+ break;
+ case CMD_UNPAUSE:
+ XwJNI.board_unpause( m_jniGamePtr, ((String)args[0]) );
+ break;
case CMD_NONE: // ignored
break;
default:
@@ -785,6 +798,12 @@ public class JNIThread extends Thread implements AutoCloseable {
handle( JNICmd.CMD_SENDCHAT, chat );
}
+ public void notifyPause( int pauser, boolean isPause, String msg )
+ {
+ Message.obtain( m_handler, GOT_PAUSE, msg )
+ .sendToTarget();
+ }
+
public void handle( JNICmd cmd, Object... args )
{
if ( m_stopped && ! JNICmd.CMD_NONE.equals(cmd) ) {
@@ -837,6 +856,7 @@ public class JNIThread extends Thread implements AutoCloseable {
if ( stop ) {
waitToStop( true );
+ DupeModeTimer.gameClosed( m_context, m_rowid );
} else if ( save && 0 != m_lastSavedState ) { // has configure() run?
handle( JNICmd.CMD_SAVE ); // in case releaser has made changes
}
@@ -873,4 +893,14 @@ public class JNIThread extends Thread implements AutoCloseable {
}
return result;
}
+
+ public static boolean gameIsOpen( long rowid )
+ {
+ boolean result = false;
+ try ( JNIThread thread = JNIThread.getRetained( rowid ) ) {
+ result = null != thread;
+ }
+ Log.d( TAG, "gameIsOpen(%d) => %b", rowid, result );
+ return result;
+ }
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/LastMoveInfo.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/LastMoveInfo.java
index bd738654e..f20ba573b 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/LastMoveInfo.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/LastMoveInfo.java
@@ -1,6 +1,7 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
- * Copyright 2014 by Eric House (xwords@eehouse.org). All rights reserved.
+ * Copyright 2014 - 2019 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
@@ -20,6 +21,7 @@
package org.eehouse.android.xw4.jni;
import android.content.Context;
+import android.text.TextUtils;
import org.eehouse.android.xw4.R;
import org.eehouse.android.xw4.loc.LocUtils;
@@ -33,7 +35,8 @@ public class LastMoveInfo {
private static final int PHONY_TYPE = 3;
public boolean isValid = false; // modified in jni world
- public String name;
+ public boolean inDuplicateMode;
+ public String[] names;
public int moveType;
public int score;
public int nTiles;
@@ -45,25 +48,42 @@ public class LastMoveInfo {
if ( isValid ) {
switch( moveType ) {
case ASSIGN_TYPE:
- result = LocUtils.getString( context, R.string.lmi_tiles_fmt, name );
+ result = inDuplicateMode
+ ? LocUtils.getString( context, R.string.lmi_tiles_dup )
+ : LocUtils.getString( context, R.string.lmi_tiles_fmt, names[0] );
break;
case MOVE_TYPE:
if ( 0 == nTiles ) {
- result = LocUtils.getString( context, R.string.lmi_pass_fmt,
- name );
+ result = inDuplicateMode
+ // Nobody scoring in dup mode is usually followed
+ // automatically by a trade. So this first will be
+ // rare.
+ ? LocUtils.getString( context, R.string.lmi_pass_dup )
+ : LocUtils.getString( context, R.string.lmi_pass_fmt, names[0] );
+ } else if ( inDuplicateMode ) {
+ if ( names.length == 1 ) {
+ result = LocUtils.getString( context, R.string.lmi_move_one_dup_fmt,
+ names[0], word, score );
+ } else {
+ String joiner = LocUtils.getString( context, R.string.name_concat_dup );
+ String players = TextUtils.join( joiner, names);
+ result = LocUtils.getString( context, R.string.lmi_move_tie_dup_fmt,
+ players, score, word );
+ }
} else {
result = LocUtils.getQuantityString( context, R.plurals.lmi_move_fmt,
- score, name, word, score );
+ score, names[0], word, score );
}
break;
case TRADE_TYPE:
- result = LocUtils
- .getQuantityString( context, R.plurals.lmi_trade_fmt,
- nTiles, name, nTiles );
+ result = inDuplicateMode
+ ? LocUtils.getString( context, R.string.lmi_trade_dup_fmt, nTiles )
+ : LocUtils.getQuantityString( context, R.plurals.lmi_trade_fmt,
+ nTiles, names[0], nTiles );
break;
case PHONY_TYPE:
result = LocUtils.getString( context, R.string.lmi_phony_fmt,
- name );
+ names[0] );
break;
}
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxt.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxt.java
index 996bee710..34262648d 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxt.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxt.java
@@ -53,11 +53,14 @@ public interface UtilCtxt {
public static final int TIMER_TIMERTICK = 2;
public static final int TIMER_COMMS = 3;
public static final int TIMER_SLOWROBOT = 4;
+ public static final int TIMER_DUP_TIMERCHECK = 5;
+ public static final int NUM_TIMERS_PLUS_ONE = 6;
void setTimer( int why, int when, int handle );
void clearTimer( int why );
void requestTime();
void remSelected();
+ void timerSelected( boolean inDuplicateMode, boolean canPause );
void setIsServer( boolean isServer );
void bonusSquareHeld( int bonus );
@@ -66,6 +69,7 @@ public interface UtilCtxt {
void notifyMove( String query );
void notifyTrade( String[] tiles );
+ void notifyDupStatus( boolean amHost, String msg );
// These can't be an ENUM! The set is open-ended, with arbitrary values
// added to ERR_RELAY_BASE, so no way to create an enum from an int in the
@@ -110,4 +114,7 @@ public interface UtilCtxt {
boolean turnLost );
void showChat( String msg, int fromIndx, String fromName, int tsSeconds );
+
+ String formatPauseHistory( int pauseTyp, int player, int whenPrev,
+ int whenCur, String msg );
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxtImpl.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxtImpl.java
index 890dfbe39..6f4528069 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxtImpl.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxtImpl.java
@@ -94,6 +94,12 @@ public class UtilCtxtImpl implements UtilCtxt {
subclassOverride( "remSelected" );
}
+ @Override
+ public void timerSelected( boolean inDuplicateMode, boolean canPause )
+ {
+ subclassOverride( "timerSelected" );
+ }
+
@Override
public void setIsServer( boolean isServer )
{
@@ -127,6 +133,12 @@ public class UtilCtxtImpl implements UtilCtxt {
subclassOverride( "notifyTrade" );
}
+ @Override
+ public void notifyDupStatus( boolean amHost, String msg )
+ {
+ subclassOverride( "notifyDupStatus" );
+ }
+
@Override
public void userError( int id )
{
@@ -183,6 +195,14 @@ public class UtilCtxtImpl implements UtilCtxt {
subclassOverride( "showChat" );
}
+ @Override
+ public String formatPauseHistory( int pauseTyp, int player, int whenPrev,
+ int whenCur, String msg )
+ {
+ subclassOverride( "formatPauseHistory" );
+ return null;
+ }
+
private void subclassOverride( String name ) {
// DbgUtils.logf( "%s::%s() called", getClass().getName(), name );
}
diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java
index 47bb6675c..302f4ec6a 100644
--- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java
+++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java
@@ -350,6 +350,9 @@ public class XwJNI {
public static native String board_formatRemainingTiles( GamePtr gamePtr );
public static native void board_sendChat( GamePtr gamePtr, String msg );
+ // Duplicate mode to start and stop timer
+ public static native void board_pause( GamePtr gamePtr, String msg );
+ public static native void board_unpause( GamePtr gamePtr, String msg );
public enum XP_Key {
XP_KEY_NONE,
diff --git a/xwords4/android/app/src/main/res/layout/game_config.xml b/xwords4/android/app/src/main/res/layout/game_config.xml
index 6a950c28f..205f2540b 100644
--- a/xwords4/android/app/src/main/res/layout/game_config.xml
+++ b/xwords4/android/app/src/main/res/layout/game_config.xml
@@ -189,63 +189,75 @@
android:layout_marginTop="15dp"
/>
-
+ android:orientation="horizontal"
+ >
-
-
+
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/xwords4/android/app/src/main/res/menu/board_menu.xml b/xwords4/android/app/src/main/res/menu/board_menu.xml
index 4814192e3..3142bb404 100644
--- a/xwords4/android/app/src/main/res/menu/board_menu.xml
+++ b/xwords4/android/app/src/main/res/menu/board_menu.xml
@@ -82,4 +82,9 @@
+
+
+
diff --git a/xwords4/android/app/src/main/res/values/common_rsrc.xml b/xwords4/android/app/src/main/res/values/common_rsrc.xml
index f213c014e..9cc38af9c 100644
--- a/xwords4/android/app/src/main/res/values/common_rsrc.xml
+++ b/xwords4/android/app/src/main/res/values/common_rsrc.xml
@@ -46,6 +46,8 @@
key_logging_on
key_show_sms
key_init_hintsallowed
+ key_init_dupmodeon
+ key_unhide_dupmode
key_init_nethintsallowed
key_init_autojuggle
key_board_size
@@ -65,6 +67,7 @@
key_enable_nbs
key_enable_p2p
key_enable_stallnotify
+ key_prefs_defaults
key_network_behavior
key_keep_screenon
key_thumbsize3
@@ -147,6 +150,9 @@
key_na_longtap_lookup
key_na_perms_phonestate
+ key_na_dupstatus_host
+ key_na_dupstatus_guest
+
xwords@eehouse.org
diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml
index cdd65ca34..5e330dc4d 100644
--- a/xwords4/android/app/src/main/res/values/strings.xml
+++ b/xwords4/android/app/src/main/res/values/strings.xml
@@ -314,10 +314,24 @@
for network play will by default have the hint feature
enabled. -->
Allow hints (networked)
-
+
+
Enable game timer
-
+
+
+ Enable per-move timer
+
+
Minutes per player
+
+ Minutes per turn
+
+
+ Robot IQ
+
+
+ Phonies
+
@@ -2113,7 +2127,8 @@
This game must be
configured before it can be opened.
(You will have a chance to
- invite other players when it is open.)
+ invite other players after it is created.)
+
Use defaults
Number on this device
@@ -2386,6 +2401,7 @@
\nIf it happens again, e-mail the developer logs and info about your device.
\n
\nThis message will be seen at most once every %2$d minutes.
+
The Google Play version of CrossWords no longer supports invitations or play via data SMS.
Read more
This game is set up to communicate via data SMS, but apps from the Google Play Store are no longer allowed to do so (with rare exceptions). You can still open the game, but it may not be able to send or receive moves.
@@ -2399,7 +2415,104 @@
they\'re committed as moves -- by long-tapping, same as committed
words.\n\nUse this feature to check the validity of words you\'re
thinking of playing, or to look up an unfamiliar word provided as a
- hint.
+ hint.
- For transmitting CrossWords moves
+ For transmitting CrossWords moves
+
+
+
+ Duplicate mode
+
+
+
+ Dup
+
+ %1$s (dup.)
+
+ Submit the current move?\n
+
+
+ Play in \"duplicate-mode\"
+ (Experimental!!!) Style of play
+ where all players have the same tiles
+
+
+ Unhide duplicate-mode options
+ (It\'s too experimental right now)
+
+
+ Same tiles assigned to all players
+
+ No players found a move (all passed)
+
+ %1$d tiles exchanged for all players
+
+ %1$s won this round playing %2$s for %3$d points
+
+ Players %1$s tied with %2$d
+ points each. %3$s was played.
+
+ \u0020and\u0020
+
+
+ This device has sent its moves to
+ the host. When all devices have sent their moves it will be
+ your turn again.
+
+ %1$d of %2$d players have
+ reported their moves. When all moves have been received it will be
+ your turn again.
+
+
+ Duplicate-mode timers running
+
+
+ Notice of games being paused and un-paused
+
+ Timer running
+ You have until %1$s to move.
+
+ Pause
+ Un-pause
+
+ Pausing this game will stop the timers, and hide the game, on all devices.
+ Un-Pausing this game will restart the timers on all devices.
+
+
+ Game paused
+
+ Game un-paused
+
+
+ Game pause
+
+ Game Unpause
+
+ Remember message
+
+ Forget message
+
+ Why I\'m doing this
+
+ Player %1$s has paused this game.
+ Player %1$s has un-paused this game.
+ Player %1$s has paused this game, with this explanation: %2$s
+ Player %1$s has un-paused
+ this game, with this explanation: %2$s
+
+ The timer for game \"%1$s\" expired without
+ any moves being made, so it has been paused for you.
+
+ Unpaused after %2$s by: %1$s.
+ Paused by: %1$s.
+ Message: %1$s.
+ Auto-paused.
diff --git a/xwords4/android/app/src/main/res/values/styles.xml b/xwords4/android/app/src/main/res/values/styles.xml
index 25d48fe76..2324d9df2 100644
--- a/xwords4/android/app/src/main/res/values/styles.xml
+++ b/xwords4/android/app/src/main/res/values/styles.xml
@@ -111,4 +111,25 @@
- @null
- @null
+
+
+
+
+
+
diff --git a/xwords4/android/app/src/main/res/xml/xwprefs.xml b/xwords4/android/app/src/main/res/xml/xwprefs.xml
index 755ac4bbf..159235d74 100644
--- a/xwords4/android/app/src/main/res/xml/xwprefs.xml
+++ b/xwords4/android/app/src/main/res/xml/xwprefs.xml
@@ -5,6 +5,7 @@
+
+
+
+
+
0 || !BuildConfig.DEBUG );
- DBUtils.setStringFor( context, KEY_FCMID, token );
+ DBUtils.setStringFor( context, BuildConfig.KEY_FCMID, token );
DevID.setFCMDevID( context, token );
RelayService.fcmConfirmed( context, true );
diff --git a/xwords4/android/app/src/xw4dup/AndroidManifest.xml b/xwords4/android/app/src/xw4dup/AndroidManifest.xml
new file mode 120000
index 000000000..d28246254
--- /dev/null
+++ b/xwords4/android/app/src/xw4dup/AndroidManifest.xml
@@ -0,0 +1 @@
+../xw4d/AndroidManifest.xml
\ No newline at end of file
diff --git a/xwords4/android/app/src/xw4dup/google-services.json b/xwords4/android/app/src/xw4dup/google-services.json
new file mode 100644
index 000000000..b7dc071a5
--- /dev/null
+++ b/xwords4/android/app/src/xw4dup/google-services.json
@@ -0,0 +1,127 @@
+{
+ "project_info": {
+ "project_number": "801272813571",
+ "firebase_url": "https://fcmtest-9fe99.firebaseio.com",
+ "project_id": "fcmtest-9fe99",
+ "storage_bucket": "fcmtest-9fe99.appspot.com"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:801272813571:android:15f4eb80a9b07720",
+ "android_client_info": {
+ "package_name": "com.google.firebase.fiamquickstart"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCl3lfUITEX0EscF2aeDZY4G-DNL2xeEZ8"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:801272813571:android:2d4684b9d573e182",
+ "android_client_info": {
+ "package_name": "org.eehouse.android.xw4"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCl3lfUITEX0EscF2aeDZY4G-DNL2xeEZ8"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:801272813571:android:8c4ed916336414b2",
+ "android_client_info": {
+ "package_name": "org.eehouse.android.xw4dbg"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCl3lfUITEX0EscF2aeDZY4G-DNL2xeEZ8"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:801272813571:android:ded92ad1a9f5b318d41636",
+ "android_client_info": {
+ "package_name": "org.eehouse.android.xw4dup"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCl3lfUITEX0EscF2aeDZY4G-DNL2xeEZ8"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/xwords4/android/app/src/xw4dup/java/org/eehouse/android/xw4/CrashTrack.java b/xwords4/android/app/src/xw4dup/java/org/eehouse/android/xw4/CrashTrack.java
new file mode 120000
index 000000000..a6d84264d
--- /dev/null
+++ b/xwords4/android/app/src/xw4dup/java/org/eehouse/android/xw4/CrashTrack.java
@@ -0,0 +1 @@
+../../../../../../xw4NoSMS/java/org/eehouse/android/xw4/CrashTrack.java
\ No newline at end of file
diff --git a/xwords4/android/app/src/xw4dup/java/org/eehouse/android/xw4/FBMService.java b/xwords4/android/app/src/xw4dup/java/org/eehouse/android/xw4/FBMService.java
new file mode 120000
index 000000000..d9f24f9b7
--- /dev/null
+++ b/xwords4/android/app/src/xw4dup/java/org/eehouse/android/xw4/FBMService.java
@@ -0,0 +1 @@
+../../../../../../xw4NoSMS/java/org/eehouse/android/xw4/FBMService.java
\ No newline at end of file
diff --git a/xwords4/android/app/src/xw4dup/res/drawable/icon48x48.png b/xwords4/android/app/src/xw4dup/res/drawable/icon48x48.png
new file mode 100644
index 000000000..6b582204d
Binary files /dev/null and b/xwords4/android/app/src/xw4dup/res/drawable/icon48x48.png differ
diff --git a/xwords4/android/app/src/xw4dup/res/drawable/notify.png b/xwords4/android/app/src/xw4dup/res/drawable/notify.png
new file mode 100644
index 000000000..c464c20c9
Binary files /dev/null and b/xwords4/android/app/src/xw4dup/res/drawable/notify.png differ
diff --git a/xwords4/android/app/src/xw4dupNoSMS/java/org/eehouse/android/xw4/CrashTrack.java b/xwords4/android/app/src/xw4dupNoSMS/java/org/eehouse/android/xw4/CrashTrack.java
new file mode 120000
index 000000000..a60914f3f
--- /dev/null
+++ b/xwords4/android/app/src/xw4dupNoSMS/java/org/eehouse/android/xw4/CrashTrack.java
@@ -0,0 +1 @@
+../../../../../../../src/xw4d/java/org/eehouse/android/xw4/CrashTrack.java
\ No newline at end of file
diff --git a/xwords4/android/app/src/xw4dupNoSMS/java/org/eehouse/android/xw4/FBMService.java b/xwords4/android/app/src/xw4dupNoSMS/java/org/eehouse/android/xw4/FBMService.java
new file mode 120000
index 000000000..a55ea0f4c
--- /dev/null
+++ b/xwords4/android/app/src/xw4dupNoSMS/java/org/eehouse/android/xw4/FBMService.java
@@ -0,0 +1 @@
+../../../../../../../src/xw4NoSMS/java/org/eehouse/android/xw4/FBMService.java
\ No newline at end of file
diff --git a/xwords4/android/app/src/xw4fdroid/java/org/eehouse/android/xw4/FBMService.java b/xwords4/android/app/src/xw4fdroid/java/org/eehouse/android/xw4/FBMService.java
index 4545cfb29..ed9dfb82f 100644
--- a/xwords4/android/app/src/xw4fdroid/java/org/eehouse/android/xw4/FBMService.java
+++ b/xwords4/android/app/src/xw4fdroid/java/org/eehouse/android/xw4/FBMService.java
@@ -24,7 +24,6 @@ import android.content.Intent;
public class FBMService {
private static final String TAG = FBMService.class.getSimpleName();
- private static final String KEY_FCMID = TAG + "_fcmid";
public static void init( Context context )
{
diff --git a/xwords4/android/jni/Android.mk b/xwords4/android/jni/Android.mk
index 9146685ae..1eef27e0d 100644
--- a/xwords4/android/jni/Android.mk
+++ b/xwords4/android/jni/Android.mk
@@ -44,6 +44,7 @@ LOCAL_DEFINES += \
-DNATIVE_NLI \
-DCOMMS_VERSION=1 \
-DINITIAL_CLIENT_VERS=${INITIAL_CLIENT_VERS} \
+ -DXW_BT_UUID=\"${XW_BT_UUID}\" \
-DVARIANT_${VARIANT} \
-DRELAY_ROOM_DEFAULT=\"\" \
-D__LITTLE_ENDIAN \
diff --git a/xwords4/android/jni/LocalizedStrIncludes.h b/xwords4/android/jni/LocalizedStrIncludes.h
index 9e04820fb..30a004be4 100644
--- a/xwords4/android/jni/LocalizedStrIncludes.h
+++ b/xwords4/android/jni/LocalizedStrIncludes.h
@@ -20,13 +20,16 @@
# define STRD_CUMULATIVE_SCORE 14
# define STRS_NEW_TILES 15
# define STR_COMMIT_CONFIRM 16
-# define STR_BONUS_ALL 17
-# define STRD_TURN_SCORE 18
-# define STRD_REMAINS_HEADER 19
-# define STRD_REMAINS_EXPL 20
-# define STRSD_RESIGNED 21
-# define STRSD_WINNER 22
-# define STRDSD_PLACER 23
+# define STR_SUBMIT_CONFIRM 17
+# define STR_BONUS_ALL 18
+# define STRD_TURN_SCORE 19
+# define STRD_REMAINS_HEADER 20
+# define STRD_REMAINS_EXPL 21
+# define STRSD_RESIGNED 22
+# define STRSD_WINNER 23
+# define STRDSD_PLACER 24
+# define STR_DUP_CLIENT_SENT 25
+# define STRDD_DUP_HOST_RECEIVED 26
-# define N_AND_USER_STRINGS 23
+# define N_AND_USER_STRINGS 26
#endif
diff --git a/xwords4/android/jni/andutils.c b/xwords4/android/jni/andutils.c
index 61bdd2ac1..e52f136e9 100644
--- a/xwords4/android/jni/andutils.c
+++ b/xwords4/android/jni/andutils.c
@@ -31,9 +31,9 @@ void
and_assert( const char* test, int line, const char* file, const char* func )
{
XP_LOGF( "assertion \"%s\" failed: line %d in %s() in %s",
- test, line, file, func );
+ test, line, func, file );
__android_log_assert( test, "ASSERT", "line %d in %s() in %s",
- line, file, func );
+ line, func, file );
}
#ifdef __LITTLE_ENDIAN
@@ -115,7 +115,7 @@ getInts( JNIEnv* env, void* cobj, jobject jobj, const SetInfo* sis, XP_U16 nSis
void
setInt( JNIEnv* env, jobject obj, const char* name, int value )
{
- // XP_LOGF( "%s(name=%s)", __func__, name );
+ // XP_LOGF( "%s(name=%s, val=%d)", __func__, name, value );
jclass cls = (*env)->GetObjectClass( env, obj );
XP_ASSERT( !!cls );
jfieldID fid = (*env)->GetFieldID( env, cls, name, "I");
@@ -341,6 +341,15 @@ makeIntArray( JNIEnv *env, int count, const void* vals, size_t elemSize )
return array;
}
+void
+setIntArray( JNIEnv *env, jobject jowner, const char* fieldName,
+ int count, const void* vals, size_t elemSize )
+{
+ jintArray jarr = makeIntArray( env, count, vals, elemSize );
+ setObject( env, jowner, fieldName, "[I", jarr );
+ deleteLocalRef( env, jarr );
+}
+
jbyteArray
makeByteArray( JNIEnv *env, int siz, const jbyte* vals )
{
@@ -413,14 +422,14 @@ setIntInArray( JNIEnv* env, jintArray arr, int index, int val )
}
jobjectArray
-makeStringArray( JNIEnv *env, int siz, const XP_UCHAR** vals )
+makeStringArray( JNIEnv *env, const int count, const XP_UCHAR** vals )
{
jclass clas = (*env)->FindClass(env, "java/lang/String");
jstring empty = (*env)->NewStringUTF( env, "" );
- jobjectArray jarray = (*env)->NewObjectArray( env, siz, clas, empty );
+ jobjectArray jarray = (*env)->NewObjectArray( env, count, clas, empty );
deleteLocalRefs( env, clas, empty, DELETE_NO_REF );
- for ( int ii = 0; !!vals && ii < siz; ++ii ) {
+ for ( int ii = 0; !!vals && ii < count; ++ii ) {
jstring jstr = (*env)->NewStringUTF( env, vals[ii] );
(*env)->SetObjectArrayElement( env, jarray, ii, jstr );
deleteLocalRef( env, jstr );
@@ -429,6 +438,15 @@ makeStringArray( JNIEnv *env, int siz, const XP_UCHAR** vals )
return jarray;
}
+void
+setStringArray( JNIEnv *env, jobject jowner, const char* ownerField,
+ int count, const XP_UCHAR** vals )
+{
+ jobjectArray jaddrs = makeStringArray( env, count, vals );
+ setObject( env, jowner, ownerField, "[Ljava/lang/String;", jaddrs );
+ deleteLocalRef( env, jaddrs );
+}
+
jobjectArray
makeByteArrayArray( JNIEnv *env, int siz )
{
@@ -771,6 +789,8 @@ android_debugf( const char* format, ... )
"xw4"
# elif defined VARIANT_xw4d || defined VARIANT_xw4dNoSMS
"x4bg"
+# elif defined VARIANT_xw4dup || defined VARIANT_xw4dupNoSMS
+ "x4du"
# endif
, buf );
}
diff --git a/xwords4/android/jni/andutils.h b/xwords4/android/jni/andutils.h
index 7b5e572b6..a19eef58f 100644
--- a/xwords4/android/jni/andutils.h
+++ b/xwords4/android/jni/andutils.h
@@ -69,6 +69,8 @@ bool getObject( JNIEnv* env, jobject obj, const char* name, const char* sig,
jobject* ret );
jintArray makeIntArray( JNIEnv *env, int size, const void* vals, size_t elemSize );
+void setIntArray( JNIEnv *env, jobject jowner, const char* ownerField,
+ int count, const void* vals, size_t elemSize );
void getIntsFromArray( JNIEnv* env, int dest[], jintArray arr, int count, bool del );
void setIntInArray( JNIEnv* env, jintArray arr, int index, int val );
@@ -80,6 +82,9 @@ void setBoolArray( JNIEnv* env, jbooleanArray jarr, int count,
const jboolean* vals );
jobjectArray makeStringArray( JNIEnv *env, int size, const XP_UCHAR** vals );
+void setStringArray( JNIEnv *env, jobject jowner, const char* ownerField,
+ int count, const XP_UCHAR** vals );
+
jstring streamToJString( JNIEnv* env, XWStreamCtxt* stream );
jbyteArray streamToBArray( JNIEnv *env, XWStreamCtxt* stream );
diff --git a/xwords4/android/jni/drawwrapper.c b/xwords4/android/jni/drawwrapper.c
index 8d79c9e79..1195f4231 100644
--- a/xwords4/android/jni/drawwrapper.c
+++ b/xwords4/android/jni/drawwrapper.c
@@ -369,16 +369,16 @@ and_draw_score_drawPlayer( DrawCtx* dctx, const XP_Rect* rInner,
static void
and_draw_drawTimer( DrawCtx* dctx, const XP_Rect* rect, XP_U16 player,
- XP_S16 secondsLeft )
+ XP_S16 secondsLeft, XP_Bool inDuplicateMode )
{
if ( rect->width == 0 ) {
XP_LOGF( "%s: exiting b/c rect empty", __func__ );
} else {
- DRAW_CBK_HEADER("drawTimer", "(Landroid/graphics/Rect;II)V" );
+ DRAW_CBK_HEADER("drawTimer", "(Landroid/graphics/Rect;IIZ)V" );
jobject jrect = makeJRect( draw, JCACHE_RECT0, rect );
(*env)->CallVoidMethod( env, draw->jdraw, mid,
- jrect, player, secondsLeft );
+ jrect, player, secondsLeft, inDuplicateMode );
returnJRect( draw, JCACHE_RECT0, jrect );
}
}
@@ -526,9 +526,9 @@ and_draw_drawTrayDivider( DrawCtx* dctx, const XP_Rect* rect, CellFlags flags )
static void
and_draw_score_pendingScore( DrawCtx* dctx, const XP_Rect* rect,
XP_S16 score, XP_U16 playerNum,
- XP_S16 curTurn, CellFlags flags )
+ XP_Bool curTurn, CellFlags flags )
{
- DRAW_CBK_HEADER( "score_pendingScore", "(Landroid/graphics/Rect;IIII)V" );
+ DRAW_CBK_HEADER( "score_pendingScore", "(Landroid/graphics/Rect;IIZI)V" );
jobject jrect = makeJRect( draw, JCACHE_RECT0, rect );
diff --git a/xwords4/android/jni/utilwrapper.c b/xwords4/android/jni/utilwrapper.c
index 0e0373dcc..c8aece2eb 100644
--- a/xwords4/android/jni/utilwrapper.c
+++ b/xwords4/android/jni/utilwrapper.c
@@ -242,6 +242,16 @@ and_util_informMove( XW_UtilCtxt* uc, XP_S16 turn, XWStreamCtxt* expl,
UTIL_CBK_TAIL();
}
+static void
+and_util_notifyDupStatus( XW_UtilCtxt* uc, XP_Bool amHost, const XP_UCHAR* msg )
+{
+ UTIL_CBK_HEADER( "notifyDupStatus", "(ZLjava/lang/String;)V" );
+ jstring jmsg = (*env)->NewStringUTF( env, msg );
+ (*env)->CallVoidMethod( env, util->jutil, mid, amHost, jmsg );
+ deleteLocalRefs( env, jmsg, DELETE_NO_REF );
+ UTIL_CBK_TAIL();
+}
+
static void
and_util_informUndo( XW_UtilCtxt* uc )
{
@@ -581,6 +591,34 @@ and_util_remSelected(XW_UtilCtxt* uc)
UTIL_CBK_TAIL();
}
+static void
+and_util_timerSelected( XW_UtilCtxt* uc, XP_Bool inDuplicateMode, XP_Bool canPause )
+{
+ UTIL_CBK_HEADER("timerSelected", "(ZZ)V" );
+ (*env)->CallVoidMethod( env, util->jutil, mid, inDuplicateMode, canPause );
+ UTIL_CBK_TAIL();
+}
+
+static void
+and_util_formatPauseHistory( XW_UtilCtxt* uc, XWStreamCtxt* stream,
+ DupPauseType typ, XP_S16 turn,
+ XP_U32 secsPrev, XP_U32 secsCur,
+ const XP_UCHAR* msg )
+{
+ UTIL_CBK_HEADER( "formatPauseHistory",
+ "(IIIILjava/lang/String;)Ljava/lang/String;" );
+ jstring jmsg = !! msg ? (*env)->NewStringUTF( env, msg ) : NULL;
+
+ jstring jresult = (*env)->CallObjectMethod( env, util->jutil, mid, typ,
+ turn, secsPrev, secsCur, jmsg );
+
+ const char* jchars = (*env)->GetStringUTFChars( env, jresult, NULL );
+ stream_catString( stream, jchars );
+ (*env)->ReleaseStringUTFChars( env, jresult, jchars );
+ deleteLocalRefs( env, jresult, jmsg, DELETE_NO_REF );
+ UTIL_CBK_TAIL();
+}
+
#ifndef XWFEATURE_MINIWIN
static void
and_util_bonusSquareHeld( XW_UtilCtxt* uc, XWBonusType bonus )
@@ -768,6 +806,28 @@ and_dutil_md5sum( XW_DUtilCtxt* duc, const XP_U8* ptr, XP_U16 len )
}
#endif
+static void
+and_dutil_notifyPause( XW_DUtilCtxt* duc, XP_U32 gameID, DupPauseType pauseTyp,
+ XP_U16 pauser, const XP_UCHAR* name,
+ const XP_UCHAR* msg )
+{
+ DUTIL_CBK_HEADER( "notifyPause", "(IIILjava/lang/String;Ljava/lang/String;)V" );
+ jstring jname = (*env)->NewStringUTF( env, name );
+ jstring jmsg = (*env)->NewStringUTF( env, msg );
+ (*env)->CallVoidMethod( env, dutil->jdutil, mid, gameID, pauseTyp, pauser,
+ jname, jmsg );
+ deleteLocalRefs( env, jname, jmsg, DELETE_NO_REF );
+ DUTIL_CBK_TAIL();
+}
+
+static void
+and_dutil_onDupTimerChanged( XW_DUtilCtxt* duc, XP_U32 gameID,
+ XP_U32 oldVal, XP_U32 newVal )
+{
+ DUTIL_CBK_HEADER( "onDupTimerChanged", "(III)V" );
+ (*env)->CallVoidMethod( env, dutil->jdutil, mid, gameID, oldVal, newVal );
+ DUTIL_CBK_TAIL();
+}
XW_UtilCtxt*
makeUtil( MPFORMAL EnvThreadInfo* ti, jobject jutil, CurGameInfo* gi,
@@ -803,6 +863,7 @@ makeUtil( MPFORMAL EnvThreadInfo* ti, jobject jutil, CurGameInfo* gi,
SET_PROC(turnChanged);
#endif
SET_PROC(informMove);
+ SET_PROC(notifyDupStatus);
SET_PROC(informUndo);
SET_PROC(informNetDict);
SET_PROC(notifyGameOver);
@@ -820,6 +881,8 @@ makeUtil( MPFORMAL EnvThreadInfo* ti, jobject jutil, CurGameInfo* gi,
SET_PROC(showChat);
#endif
SET_PROC(remSelected);
+ SET_PROC(timerSelected);
+ SET_PROC(formatPauseHistory);
#ifndef XWFEATURE_MINIWIN
SET_PROC(bonusSquareHeld);
@@ -899,6 +962,8 @@ makeDUtil( MPFORMAL EnvThreadInfo* ti, jobject jdutil, VTableMgr* vtMgr,
#ifdef COMMS_CHECKSUM
SET_DPROC(md5sum);
#endif
+ SET_DPROC(notifyPause);
+ SET_DPROC(onDupTimerChanged);
return &dutil->dutil;
}
diff --git a/xwords4/android/jni/utilwrapper.h b/xwords4/android/jni/utilwrapper.h
index b317d4ff5..883a88a32 100644
--- a/xwords4/android/jni/utilwrapper.h
+++ b/xwords4/android/jni/utilwrapper.h
@@ -40,6 +40,4 @@ void destroyUtil( XW_UtilCtxt** util );
bool utilTimerFired( XW_UtilCtxt* util, XWTimerReason why, int handle );
-XP_U32 and_util_getCurSeconds( XW_UtilCtxt* uc ); /* uc can be NULL */
-
#endif
diff --git a/xwords4/android/jni/xwjni.c b/xwords4/android/jni/xwjni.c
index 473895808..cab372cc2 100644
--- a/xwords4/android/jni/xwjni.c
+++ b/xwords4/android/jni/xwjni.c
@@ -52,6 +52,7 @@
typedef struct _EnvThreadEntry {
JNIEnv* env;
pthread_t owner;
+ XP_U16 refcount;
#ifdef LOG_MAPPING
const char* ownerFunc;
#endif
@@ -186,6 +187,8 @@ map_thread_prv( EnvThreadInfo* ti, JNIEnv* env, const char* caller )
XP_ASSERT( !!firstEmpty );
firstEmpty->owner = self;
firstEmpty->env = env;
+ XP_ASSERT( 0 == firstEmpty->refcount );
+ ++firstEmpty->refcount;
#ifdef LOG_MAPPING
firstEmpty->ownerFunc = caller;
XP_LOGF( "%s: entry %zu: mapped env %p to thread %x", __func__,
@@ -216,12 +219,15 @@ map_remove_prv( EnvThreadInfo* ti, JNIEnv* env, const char* func )
EnvThreadEntry* entry = &ti->entries[ii];
found = env == entry->env;
if ( found ) {
+ XP_ASSERT( pthread_self() == entry->owner );
#ifdef LOG_MAPPING
XP_LOGF( "%s: UNMAPPED env %p to thread %x (from %s; mapped by %s)", __func__,
entry->env, (int)entry->owner, func, entry->ownerFunc );
XP_LOGF( "%s: %d entries left", __func__, countUsed( ti ) );
entry->ownerFunc = NULL;
#endif
+ XP_ASSERT( 1 == entry->refcount );
+ --entry->refcount;
entry->env = NULL;
entry->owner = 0;
}
@@ -359,6 +365,7 @@ static const SetInfo gi_bools[] = {
,ARR_MEMBER( CurGameInfo, timerEnabled )
,ARR_MEMBER( CurGameInfo, allowPickTiles )
,ARR_MEMBER( CurGameInfo, allowHintRect )
+ ,ARR_MEMBER( CurGameInfo, inDuplicateMode )
};
static const SetInfo pl_ints[] = {
@@ -701,13 +708,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_comms_1getUUID
{
jstring jstr =
#ifdef XWFEATURE_BLUETOOTH
- (*env)->NewStringUTF( env,
-# if defined VARIANT_xw4NoSMS || defined VARIANT_xw4fdroid || defined VARIANT_xw4SMS
- XW_BT_UUID
-# elif defined VARIANT_xw4d || defined VARIANT_xw4dNoSMS
- XW_BT_UUID_DBG
-# endif
- )
+ (*env)->NewStringUTF( env, XW_BT_UUID )
#else
NULL
#endif
@@ -1227,19 +1228,6 @@ Java_org_eehouse_android_xw4_jni_XwJNI_board_1setScoreboardLoc
XWJNI_END();
}
-JNIEXPORT void JNICALL
-Java_org_eehouse_android_xw4_jni_XwJNI_board_1setTimerLoc
-( JNIEnv* env, jclass C, GamePtrType gamePtr, jint timerLeft, jint timerTop,
- jint timerWidth, jint timerHeight )
-{
- XWJNI_START();
- XP_LOGF( "%s(%d,%d,%d,%d)", __func__, timerLeft, timerTop,
- timerWidth, timerHeight );
- board_setTimerLoc( state->game.board, timerLeft, timerTop,
- timerWidth, timerHeight );
- XWJNI_END();
-}
-
JNIEXPORT void JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_board_1setTrayLoc
( JNIEnv *env, jclass C, GamePtrType gamePtr, jint left, jint top,
@@ -1692,10 +1680,11 @@ Java_org_eehouse_android_xw4_jni_XwJNI_model_1getPlayersLastScore
player, &lmi );
setBool( env, jlmi, "isValid", valid );
if ( valid ) {
+ setBool( env, jlmi, "inDuplicateMode", lmi.inDuplicateMode );
setInt( env, jlmi, "score", lmi.score );
setInt( env, jlmi, "nTiles", lmi.nTiles );
setInt( env, jlmi, "moveType", lmi.moveType );
- setString( env, jlmi, "name", lmi.name );
+ setStringArray( env, jlmi, "names", lmi.nWinners, lmi.names );
setString( env, jlmi, "word", lmi.word );
}
XWJNI_END();
@@ -1861,16 +1850,19 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1summarize
{
XWJNI_START();
ModelCtxt* model = state->game.model;
+ ServerCtxt* server = state->game.server;
XP_S16 nMoves = model_getNMoves( model );
setInt( env, jsummary, "nMoves", nMoves );
- XP_Bool gameOver = server_getGameIsOver( state->game.server );
+ XP_Bool gameOver = server_getGameIsOver( server );
setBool( env, jsummary, "gameOver", gameOver );
XP_Bool isLocal = XP_FALSE;
setInt( env, jsummary, "turn",
- server_getCurrentTurn( state->game.server, &isLocal ) );
+ server_getCurrentTurn( server, &isLocal ) );
setBool( env, jsummary, "turnIsLocal", isLocal );
setInt( env, jsummary, "lastMoveTime",
- server_getLastMoveTime(state->game.server) );
+ server_getLastMoveTime(server) );
+ setInt( env, jsummary, "dupTimerExpires",
+ server_getDupTimerExpires(server) );
if ( !!state->game.comms ) {
CommsAddrRec addr;
@@ -1878,7 +1870,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1summarize
comms_getAddr( comms, &addr );
setInt( env, jsummary, "seed", comms_getChannelSeed( comms ) );
setInt( env, jsummary, "missingPlayers",
- server_getMissingPlayers( state->game.server ) );
+ server_getMissingPlayers( server ) );
setInt( env, jsummary, "nPacketsPending",
comms_countPendingPackets( state->game.comms ) );
@@ -1917,10 +1909,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1summarize
}
XP_LOGF( "%s: adding btaddr/phone/mac %s", __func__, addrps[ii] );
}
- jobjectArray jaddrs = makeStringArray( env, count, addrps );
- setObject( env, jsummary, "remoteDevs", "[Ljava/lang/String;",
- jaddrs );
- deleteLocalRef( env, jaddrs );
+ setStringArray( env, jsummary, "remoteDevs", count, addrps );
}
break;
#endif
@@ -1943,9 +1932,8 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1summarize
jvals[ii] = model_getPlayerScore( model, ii );
}
}
- jintArray jarr = makeIntArray( env, nPlayers, jvals, sizeof(jvals[0]) );
- setObject( env, jsummary, "scores", "[I", jarr );
- deleteLocalRef( env, jarr );
+
+ setIntArray( env, jsummary, "scores", nPlayers, jvals, sizeof(jvals[0]) );
XWJNI_END();
}
@@ -2039,6 +2027,8 @@ static const SetInfo gsi_bools[] = {
ARR_MEMBER( GameStateInfo, curTurnSelected ),
ARR_MEMBER( GameStateInfo, canHideRack ),
ARR_MEMBER( GameStateInfo, canTrade ),
+ ARR_MEMBER( GameStateInfo, canPause ),
+ ARR_MEMBER( GameStateInfo, canUnpause ),
};
JNIEXPORT void JNICALL
@@ -2268,6 +2258,30 @@ Java_org_eehouse_android_xw4_jni_XwJNI_server_1endGame
XWJNI_END();
}
+JNIEXPORT void JNICALL Java_org_eehouse_android_xw4_jni_XwJNI_board_1pause
+( JNIEnv* env, jclass C, GamePtrType gamePtr, jstring jmsg )
+{
+ XWJNI_START();
+ XP_ASSERT( !!state->game.board );
+
+ const char* msg = (*env)->GetStringUTFChars( env, jmsg, NULL );
+ board_pause( state->game.board, msg );
+ (*env)->ReleaseStringUTFChars( env, jmsg, msg );
+
+ XWJNI_END();
+}
+
+JNIEXPORT void JNICALL Java_org_eehouse_android_xw4_jni_XwJNI_board_1unpause
+( JNIEnv* env, jclass C, GamePtrType gamePtr, jstring jmsg )
+{
+ XWJNI_START();
+ XP_ASSERT( !!state->game.board );
+ const char* msg = (*env)->GetStringUTFChars( env, jmsg, NULL );
+ board_unpause( state->game.board, msg );
+ (*env)->ReleaseStringUTFChars( env, jmsg, msg );
+ XWJNI_END();
+}
+
#ifdef XWFEATURE_CHAT
JNIEXPORT void JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_board_1sendChat
diff --git a/xwords4/common/board.c b/xwords4/common/board.c
index 1ade81c48..343fa78af 100644
--- a/xwords4/common/board.c
+++ b/xwords4/common/board.c
@@ -96,7 +96,9 @@ static void boardTurnChanged( void* board );
static void boardGameOver( void* board, XP_S16 quitter );
static void setArrow( BoardCtxt* board, XP_U16 row, XP_U16 col, XP_Bool* vp );
static XP_Bool setArrowVisible( BoardCtxt* board, XP_Bool visible );
-
+static void board_setTimerLoc( BoardCtxt* board,
+ XP_U16 timerLeft, XP_U16 timerTop,
+ XP_U16 timerWidth, XP_U16 timerHeight );
#ifdef XWFEATURE_MINIWIN
static void invalTradeWindow( BoardCtxt* board, XP_S16 turn, XP_Bool redraw );
#else
@@ -657,7 +659,7 @@ board_setPos( BoardCtxt* board, XP_U16 left, XP_U16 top,
figureBoardRect( board );
} /* board_setPos */
-void
+static void
board_setTimerLoc( BoardCtxt* board,
XP_U16 timerLeft, XP_U16 timerTop,
XP_U16 timerWidth, XP_U16 timerHeight )
@@ -812,7 +814,7 @@ board_getYOffset( const BoardCtxt* board )
XP_Bool
board_curTurnSelected( const BoardCtxt* board )
{
- return MY_TURN( board );
+ return server_isPlayersTurn( board->server, board->selPlayer );
}
XP_U16
@@ -822,6 +824,21 @@ board_visTileCount( const BoardCtxt* board )
TRAY_REVEALED == board->trayVisState );
}
+void
+board_pause( BoardCtxt* board, const XP_UCHAR* msg )
+{
+ server_pause( board->server, board->selPlayer, msg );
+ board_invalAll( board );
+}
+
+void
+board_unpause( BoardCtxt* board, const XP_UCHAR* msg )
+{
+ server_unpause( board->server, board->selPlayer, msg );
+ setTimerIf( board );
+ board_invalAll( board );
+}
+
XP_Bool
board_canShuffle( const BoardCtxt* board )
{
@@ -841,6 +858,7 @@ XP_Bool
board_canTrade( BoardCtxt* board )
{
XP_Bool result = preflight( board, XP_FALSE )
+ && !board->gi->inDuplicateMode
&& MIN_TRADE_TILES(board) <= server_countTilesInPool( board->server );
return result;
}
@@ -1060,17 +1078,18 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
{
XP_Bool result = XP_FALSE;
const XP_S16 turn = server_getCurrentTurn( board->server, NULL );
- PerTurnInfo* pti = board->pti + turn;
+ const XP_U16 selPlayer = board->selPlayer;
ModelCtxt* model = board->model;
if ( board->gameOver || turn < 0 ) {
/* do nothing */
- } else if ( turn != board->selPlayer ) {
+ } else if ( !server_isPlayersTurn( board->server, selPlayer ) ) {
util_userError( board->util, ERR_NOT_YOUR_TURN );
- } else if ( 0 == model_getNumTilesTotal( model, turn ) ) {
+ } else if ( 0 == model_getNumTilesTotal( model, selPlayer ) ) {
/* game's over but still undoable so turn hasn't changed; do
nothing */
} else if ( phoniesConfirmed || turnConfirmed || checkRevealTray( board ) ) {
+ PerTurnInfo* pti = board->pti + selPlayer;
if ( pti->tradeInProgress ) {
TileBit traySelBits = pti->traySelBits;
int count = 0;
@@ -1088,7 +1107,7 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
TrayTileSet selTiles;
getSelTiles( board, traySelBits, &selTiles );
if ( turnConfirmed ) {
- if ( !server_askPickTiles( board->server, turn, newTiles,
+ if ( !server_askPickTiles( board->server, selPlayer, newTiles,
selTiles.nTiles ) ) {
/* server_commitTrade() changes selPlayer, so board_endTrade
must be called first() */
@@ -1111,8 +1130,9 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
stream = mem_stream_make_raw( MPPARM(board->mpool)
dutil_getVTManager(board->dutil) );
- const XP_UCHAR* str = dutil_getUserString( board->dutil,
- STR_COMMIT_CONFIRM );
+ XP_U16 stringCode = board->gi->inDuplicateMode
+ ? STR_SUBMIT_CONFIRM : STR_COMMIT_CONFIRM;
+ const XP_UCHAR* str = dutil_getUserString( board->dutil, stringCode );
stream_catString( stream, str );
XP_Bool warn = board->util->gameInfo->phoniesAction == PHONIES_WARN;
@@ -1121,14 +1141,14 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
info.proc = saveBadWords;
info.closure = &bwl;
}
- legal = model_checkMoveLegal( model, turn, stream,
+ legal = model_checkMoveLegal( model, selPlayer, stream,
warn? &info:(WordNotifierInfo*)NULL);
}
if ( 0 < bwl.bwi.nWords && !phoniesConfirmed ) {
bwl.bwi.dictName =
- dict_getShortName( model_getPlayerDict( model, turn ) );
- util_notifyIllegalWords( board->util, &bwl.bwi, turn, XP_FALSE );
+ dict_getShortName( model_getPlayerDict( model, selPlayer ) );
+ util_notifyIllegalWords( board->util, &bwl.bwi, selPlayer, XP_FALSE );
} else {
/* Hide the tray so no peeking. Leave it hidden even if user
cancels as otherwise another player could get around
@@ -1140,10 +1160,11 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
if ( board->skipCommitConfirm || turnConfirmed ) {
XP_U16 nToPick = MAX_TRAY_TILES -
- model_getNumTilesInTray( model, turn );
- if ( !server_askPickTiles( board->server, turn, newTiles,
+ model_getNumTilesInTray( model, selPlayer );
+ if ( !server_askPickTiles( board->server, selPlayer, newTiles,
nToPick ) ) {
- result = server_commitMove( board->server, newTiles )
+ result = server_commitMove( board->server, selPlayer,
+ newTiles )
|| result;
/* invalidate all tiles in case we'll be drawing this tray
again rather than some other -- as is the case in a
@@ -1163,7 +1184,7 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
}
if ( result ) {
- setArrowVisibleFor( board, turn, XP_FALSE );
+ setArrowVisibleFor( board, selPlayer, XP_FALSE );
}
}
}
@@ -1386,7 +1407,9 @@ timerFiredForPen( BoardCtxt* board )
static void
setTimerIf( BoardCtxt* board )
{
- XP_Bool timerWanted = board->gi->timerEnabled && !board->gameOver;
+ XP_Bool timerWanted = board->gi->timerEnabled
+ && !board->gameOver
+ && !server_canUnpause( board->server );
if ( timerWanted && !board->timerPending ) {
util_setTimer( board->util, TIMER_TIMERTICK, 0,
@@ -1399,16 +1422,20 @@ static void
timerFiredForTimer( BoardCtxt* board )
{
board->timerPending = XP_FALSE;
- if ( !board->gameOver ) {
- XP_S16 turn = server_getCurrentTurn( board->server, NULL );
+ if ( !board->gameOver || !server_canUnpause( board->server ) ) {
+ XP_Bool doDraw = board->gi->inDuplicateMode;
+ if ( !doDraw ) {
+ XP_S16 turn = server_getCurrentTurn( board->server, NULL );
- if ( turn >= 0 ) {
- ++board->gi->players[turn].secondsUsed;
+ if ( turn >= 0 ) {
+ ++board->gi->players[turn].secondsUsed;
- if ( turn == board->selPlayer ) {
- drawTimer( board );
+ doDraw = turn == board->selPlayer;
}
}
+ if ( doDraw ) {
+ drawTimer( board );
+ }
}
setTimerIf( board );
} /* timerFiredForTimer */
@@ -2084,7 +2111,7 @@ static XP_Bool
preflight( BoardCtxt* board, XP_Bool reveal )
{
return !board->gameOver
- && server_getCurrentTurn(board->server, NULL) >= 0
+ && server_getCurrentTurn( board->server, NULL) >= 0
&& ( !reveal || checkRevealTray( board ) )
&& !TRADE_IN_PROGRESS(board);
} /* preflight */
@@ -2098,6 +2125,24 @@ MIN_TRADE_TILES( const BoardCtxt* board )
return 6 == langCode ? 1 : MAX_TRAY_TILES;
}
+#ifdef DEBUG
+/* static void */
+/* assertTilesInTiles( const BoardCtxt* board, const MoveInfo* mi, */
+/* const Tile* tiles, XP_U16 nTiles ) */
+/* { */
+/* Tile blank = dict_getBlankTile( model_getDictionary( board->model ) ); */
+/* for ( XP_U16 ii = 0; ii < mi->nTiles; ++ii ) { */
+/* Tile tile = mi->tiles[ii].tile; */
+/* XP_Bool found = XP_FALSE; */
+/* for ( XP_U16 jj = 0; !found && jj < nTiles; ++jj ) { */
+/* found = tiles[jj] == tile */
+/* || (tiles[jj] == blank && IS_BLANK(tile)); */
+/* } */
+/* XP_ASSERT( found ); */
+/* } */
+/* } */
+#endif
+
/* Refuse with error message if any tiles are currently on board in this turn.
* Then call the engine, and display the first move. Return true if there's
* any redrawing to be done.
@@ -2190,7 +2235,7 @@ board_requestHint( BoardCtxt* board,
# endif
#endif
searchComplete =
- engine_findMove( engine, model, selPlayer, XP_FALSE,
+ engine_findMove( engine, model, selPlayer, XP_FALSE, XP_FALSE,
tiles, nTiles, usePrev,
#ifdef XWFEATURE_BONUSALL
allTilesBonus,
@@ -2199,12 +2244,13 @@ board_requestHint( BoardCtxt* board,
lp, useTileLimits,
#endif
0, /* 0: not a robot */
- &canMove, &newMove );
+ &canMove, &newMove, NULL );
board_popTimerSave( board );
if ( searchComplete && canMove ) {
+ // assertTilesInTiles( board, &newMove, tiles, nTiles );
juggleMoveIfDebug( &newMove );
- model_makeTurnFromMoveInfo( model, selPlayer, &newMove);
+ model_makeTurnFromMoveInfo( model, selPlayer, &newMove );
} else {
result = XP_FALSE;
XP_STATUSF( "unable to complete hint request\n" );
@@ -2492,6 +2538,9 @@ pointOnSomething( const BoardCtxt* board, XP_U16 xx, XP_U16 yy,
*wp = OBJ_TRAY;
} else if ( rectContainsPt( &board->scoreBdBounds, xx, yy ) ) {
*wp = OBJ_SCORE;
+ } else if ( board->gi->timerEnabled
+ && rectContainsPt( &board->timerBounds, xx, yy ) ) {
+ *wp = OBJ_TIMER;
} else {
*wp = OBJ_NONE;
result = XP_FALSE;
@@ -3055,6 +3104,10 @@ handlePenUpInternal( BoardCtxt* board, XP_U16 xx, XP_U16 yy, XP_Bool isPen,
draw = handlePenUpTray( board, xx, yy ) || draw;
}
break;
+ case OBJ_TIMER:
+ util_timerSelected( board->util, board->gi->inDuplicateMode,
+ server_canPause( board->server ) );
+ break;
default:
XP_ASSERT( XP_FALSE );
}
@@ -3117,6 +3170,7 @@ focusToCoords( BoardCtxt* board, XP_U16* xp, XP_U16* yp )
if ( result ) {
switch( board->focussed ) {
case OBJ_NONE:
+ case OBJ_TIMER:
result = XP_FALSE;
break;
case OBJ_BOARD:
@@ -3380,6 +3434,7 @@ invalFocusOwner( BoardCtxt* board )
}
break;
case OBJ_NONE:
+ case OBJ_TIMER:
draw = XP_FALSE;
break;
}
@@ -3606,17 +3661,18 @@ board_moveCursor( BoardCtxt* board, XP_Key cursorKey, XP_Bool preflightOnly,
#endif
XP_Bool
-rectContainsPt( const XP_Rect* rect, XP_S16 x, XP_S16 y )
+rectContainsPt( const XP_Rect* rect, XP_S16 xx, XP_S16 yy )
{
/* 7/4 Made <= into <, etc., because a tap on the right boundary of the
board was still mapped onto the board but dividing by scale put it in
the 15th column. If this causes other problems and the '=' chars have
to be put back then deal with that, probably by forcing an
out-of-bounds col/row to the nearest possible. */
- return ( rect->top <= y
- && rect->left <= x
- && (rect->top + rect->height) >= y
- && (rect->left + rect->width) >= x );
+ XP_Bool result = ( rect->top <= yy
+ && rect->left <= xx
+ && (rect->top + rect->height) >= yy
+ && (rect->left + rect->width) >= xx );
+ return result;
} /* rectContainsPt */
XP_Bool
diff --git a/xwords4/common/board.h b/xwords4/common/board.h
index 6d9e59858..b600f8484 100644
--- a/xwords4/common/board.h
+++ b/xwords4/common/board.h
@@ -117,9 +117,6 @@ void board_setScoreboardLoc( BoardCtxt* board,
XP_U16 scoreLeft, XP_U16 scoreTop,
XP_U16 scoreWidth, XP_U16 scoreHeight,
XP_Bool divideHorizontally );
-void board_setTimerLoc( BoardCtxt* board,
- XP_U16 timerLeft, XP_U16 timerTop,
- XP_U16 timerWidth, XP_U16 timerHeight );
void board_setTrayLoc( BoardCtxt* board, XP_U16 trayLeft, XP_U16 trayTop,
XP_U16 trayWidth, XP_U16 trayHeight );
@@ -129,6 +126,8 @@ XP_U16 board_getYOffset( const BoardCtxt* board );
XP_Bool board_curTurnSelected( const BoardCtxt* board );
XP_U16 board_visTileCount( const BoardCtxt* board );
+void board_pause( BoardCtxt* board, const XP_UCHAR* msg );
+void board_unpause( BoardCtxt* board, const XP_UCHAR* msg );
XP_Bool board_canShuffle( const BoardCtxt* board );
XP_Bool board_canHideRack( const BoardCtxt* board );
XP_Bool board_canTrade( BoardCtxt* board );
diff --git a/xwords4/common/boarddrw.c b/xwords4/common/boarddrw.c
index 7010dfd1b..0a815894d 100644
--- a/xwords4/common/boarddrw.c
+++ b/xwords4/common/boarddrw.c
@@ -373,7 +373,7 @@ drawCell( BoardCtxt* board, const XP_U16 col, const XP_U16 row, XP_Bool skipBlan
XP_Bool success = XP_TRUE;
XP_Rect cellRect = {0};
Tile tile;
- XP_Bool isBlank, isEmpty, recent = XP_FALSE, pending = XP_FALSE;
+ XP_Bool isBlank, isEmpty, pending = XP_FALSE;
XWBonusType bonus;
ModelCtxt* model = board->model;
DictionaryCtxt* dict = model_getDictionary( model );
@@ -395,6 +395,7 @@ drawCell( BoardCtxt* board, const XP_U16 col, const XP_U16 row, XP_Bool skipBlan
/* This 'while' is only here so I can 'break' below */
while ( board->trayVisState == TRAY_HIDDEN ||
!rectContainsRect( &board->trayBounds, &cellRect ) ) {
+ XP_Bool recent = XP_FALSE;
XP_UCHAR ch[4] = {'\0'};
XP_S16 owner = -1;
XP_Bitmaps bitmaps;
diff --git a/xwords4/common/boardp.h b/xwords4/common/boardp.h
index b9e3e9462..ae86428a4 100644
--- a/xwords4/common/boardp.h
+++ b/xwords4/common/boardp.h
@@ -240,7 +240,6 @@ struct BoardCtxt {
# define valHintMiniWindowActive( board ) \
((XP_Bool)((board)->miniWindowStuff[MINIWINDOW_VALHINT].text != NULL))
#endif
-#define MY_TURN(b) ((b)->selPlayer == server_getCurrentTurn( (b)->server, NULL ))
#define TRADE_IN_PROGRESS(b) ((b)->selInfo->tradeInProgress==XP_TRUE)
/* tray-related functions */
diff --git a/xwords4/common/comms.h b/xwords4/common/comms.h
index 4547647d4..9f60a5495 100644
--- a/xwords4/common/comms.h
+++ b/xwords4/common/comms.h
@@ -63,8 +63,9 @@ typedef enum {
} CommsRelayState;
#ifdef XWFEATURE_BLUETOOTH
-# define XW_BT_UUID "7be0d084-ff89-4d6d-9c78-594773a6f963"
-# define XW_BT_UUID_DBG "b079b640-35fe-11e5-a432-0002a5d5c51b"
+# ifndef XW_BT_UUID
+# define XW_BT_UUID "7be0d084-ff89-4d6d-9c78-594773a6f963"
+# endif
# define XW_BT_NAME "CrossWords"
#endif
diff --git a/xwords4/common/comtypes.h b/xwords4/common/comtypes.h
index 42130e2ae..a46227600 100644
--- a/xwords4/common/comtypes.h
+++ b/xwords4/common/comtypes.h
@@ -127,7 +127,8 @@ typedef enum {
OBJ_NONE,
OBJ_BOARD,
OBJ_SCORE,
- OBJ_TRAY
+ OBJ_TRAY,
+ OBJ_TIMER,
} BoardObjectType;
enum {
@@ -177,6 +178,7 @@ typedef enum {
#ifdef XWFEATURE_SLOW_ROBOT
TIMER_SLOWROBOT,
#endif
+ TIMER_DUP_TIMERCHECK,
NUM_TIMERS_PLUS_ONE /* must be last */
} XWTimerReason;
diff --git a/xwords4/common/dbgutil.c b/xwords4/common/dbgutil.c
index d6bbec783..6c6a94e5d 100644
--- a/xwords4/common/dbgutil.c
+++ b/xwords4/common/dbgutil.c
@@ -79,7 +79,10 @@ StackMoveType_2str( StackMoveType typ )
CASESTR(MOVE_TYPE);
CASESTR(TRADE_TYPE);
CASESTR(PHONY_TYPE);
- default: return FUNC(__func__) " unknown";
+ CASESTR(PAUSE_TYPE);
+ default:
+ XP_ASSERT(0);
+ return "";
}
}
diff --git a/xwords4/common/draw.h b/xwords4/common/draw.h
index 9b986d723..cca8916ae 100644
--- a/xwords4/common/draw.h
+++ b/xwords4/common/draw.h
@@ -169,11 +169,12 @@ typedef struct DrawCtxVTable {
const XP_Rect* rect,
XP_S16 score,
XP_U16 playerNum,
- XP_S16 curTurn,
+ XP_Bool curTurn,
CellFlags flags );
void DRAW_VTABLE_NAME(drawTimer) ( DrawCtx* dctx, const XP_Rect* rect,
- XP_U16 player, XP_S16 secondsLeft );
+ XP_U16 player, XP_S16 secondsLeft,
+ XP_Bool turnDone );
XP_Bool DRAW_VTABLE_NAME(drawCell) ( DrawCtx* dctx, const XP_Rect* rect,
/* at least one of these two will be
@@ -307,8 +308,8 @@ struct DrawCtx {
#endif
#define draw_score_pendingScore(dc, r, s, p, t, f ) \
CALL_DRAW_NAME5(score_pendingScore,(dc), (r), (s), (p), (t), (f))
-#define draw_drawTimer( dc, r, plyr, sec ) \
- CALL_DRAW_NAME3(drawTimer,(dc),(r),(plyr),(sec))
+#define draw_drawTimer( dc, r, plyr, sec, dm ) \
+ CALL_DRAW_NAME4(drawTimer,(dc),(r),(plyr),(sec), (dm))
#define draw_drawCell( dc, rect, txt, bmap, t, v,o, bon, hi, f ) \
CALL_DRAW_NAME9(drawCell,(dc),(rect),(txt),(bmap),(t),(v),(o),(bon),(hi), \
(f))
diff --git a/xwords4/common/dutil.h b/xwords4/common/dutil.h
index 9771e8a09..31de7879e 100644
--- a/xwords4/common/dutil.h
+++ b/xwords4/common/dutil.h
@@ -26,6 +26,11 @@
#include "xwrelay.h"
#include "vtabmgr.h"
+typedef enum { UNPAUSED,
+ PAUSED,
+ AUTOPAUSED,
+} DupPauseType;
+
typedef struct _DUtilVtable {
XP_U32 (*m_dutil_getCurSeconds)( XW_DUtilCtxt* duc );
const XP_UCHAR* (*m_dutil_getUserString)( XW_DUtilCtxt* duc,
@@ -34,7 +39,7 @@ typedef struct _DUtilVtable {
XP_U16 stringCode,
XP_U16 quantity );
void (*m_dutil_storeStream)( XW_DUtilCtxt* duc, const XP_UCHAR* key,
- XWStreamCtxt* data );
+ XWStreamCtxt* data );
/* Pass in an empty stream, and it'll be returned full */
void (*m_dutil_loadStream)( XW_DUtilCtxt* duc, const XP_UCHAR* key,
XWStreamCtxt* inOut );
@@ -56,6 +61,12 @@ typedef struct _DUtilVtable {
#ifdef COMMS_CHECKSUM
XP_UCHAR* (*m_dutil_md5sum)( XW_DUtilCtxt* duc, const XP_U8* ptr, XP_U16 len );
#endif
+
+ void (*m_dutil_notifyPause)( XW_DUtilCtxt* duc, XP_U32 gameID,
+ DupPauseType pauseTyp, XP_U16 pauser,
+ const XP_UCHAR* name, const XP_UCHAR* msg );
+ void (*m_dutil_onDupTimerChanged)( XW_DUtilCtxt* duc, XP_U32 gameID,
+ XP_U32 oldVal, XP_U32 newVal );
} DUtilVtable;
struct XW_DUtilCtxt {
@@ -101,4 +112,9 @@ struct XW_DUtilCtxt {
# define dutil_md5sum( duc, p, l ) (duc)->vtable.m_dutil_md5sum((duc), (p), (l))
#endif
+#define dutil_notifyPause( duc, id, ip, p, n, m ) \
+ (duc)->vtable.m_dutil_notifyPause( (duc), (id), (ip), (p), (n), (m) )
+
+#define dutil_onDupTimerChanged(duc, id, ov, nv) \
+ (duc)->vtable.m_dutil_onDupTimerChanged( (duc), (id), (ov), (nv))
#endif
diff --git a/xwords4/common/engine.c b/xwords4/common/engine.c
index 71cda9104..496241a0e 100644
--- a/xwords4/common/engine.c
+++ b/xwords4/common/engine.c
@@ -95,6 +95,7 @@ struct EngineCtxt {
XP_U16 nMovesToSave;
XP_U16 star_row;
XP_Bool returnNOW;
+ XP_Bool skipProgressCallback;
XP_Bool isRobot;
XP_Bool includePending;
MoveIterationData miData;
@@ -376,7 +377,7 @@ normalizeIQ( EngineCtxt* engine, XP_U16 iq )
*/
XP_Bool
engine_findMove( EngineCtxt* engine, const ModelCtxt* model,
- XP_S16 turn, XP_Bool includePending,
+ XP_S16 turn, XP_Bool includePending, XP_Bool skipCallback,
const Tile* tiles, const XP_U16 nTiles, XP_Bool usePrev,
#ifdef XWFEATURE_BONUSALL
XP_U16 allTilesBonus,
@@ -385,7 +386,8 @@ engine_findMove( EngineCtxt* engine, const ModelCtxt* model,
const BdHintLimits* searchLimits,
XP_Bool useTileLimits,
#endif
- XP_U16 robotIQ, XP_Bool* canMoveP, MoveInfo* newMove )
+ XP_U16 robotIQ, XP_Bool* canMoveP, MoveInfo* newMove,
+ XP_U16* score )
{
XP_Bool result = XP_TRUE;
XP_U16 star_row;
@@ -432,6 +434,7 @@ engine_findMove( EngineCtxt* engine, const ModelCtxt* model,
engine->usePrev = usePrev;
engine->blankTile = dict_getBlankTile( engine->dict );
engine->returnNOW = XP_FALSE;
+ engine->skipProgressCallback = skipCallback;
#ifdef XWFEATURE_SEARCHLIMIT
engine->searchLimits = searchLimits;
#endif
@@ -525,6 +528,9 @@ engine_findMove( EngineCtxt* engine, const ModelCtxt* model,
if ( chooseMove( engine, &move ) ) {
XP_ASSERT( !!newMove );
XP_MEMCPY( newMove, &move->moveInfo, sizeof(*newMove) );
+ if ( !!score ) {
+ *score = move->score;
+ }
} else {
newMove->nTiles = 0;
canMove = XP_FALSE;
@@ -1060,7 +1066,7 @@ considerMove( EngineCtxt* engine, Tile* tiles, XP_S16 tileLength,
short col;
BlankTuple blankTuples[MAX_NUM_BLANKS];
- if ( !util_engineProgressCallback( engine->util ) ) {
+ if ( !engine->skipProgressCallback && !util_engineProgressCallback( engine->util ) ) {
engine->returnNOW = XP_TRUE;
} else {
diff --git a/xwords4/common/engine.h b/xwords4/common/engine.h
index 639163eed..514d90db8 100644
--- a/xwords4/common/engine.h
+++ b/xwords4/common/engine.h
@@ -48,8 +48,11 @@ void engine_init( EngineCtxt* ctxt );
void engine_reset( EngineCtxt* ctxt );
void engine_destroy( EngineCtxt* ctxt );
-XP_Bool engine_findMove( EngineCtxt* ctxt, const ModelCtxt* model,
- XP_S16 turn, XP_Bool includePending,
+XP_Bool engine_findMove( EngineCtxt* ctxt, const ModelCtxt* model, XP_S16 turn,
+ /* includePending: include pending tiles as part of words */
+ XP_Bool includePending,
+ /* skipCallback: skip the callback that lets client cancel */
+ XP_Bool skipCallback,
const Tile* tiles, XP_U16 nTiles, XP_Bool usePrev,
#ifdef XWFEATURE_BONUSALL
XP_U16 allTilesBonus,
@@ -58,7 +61,8 @@ XP_Bool engine_findMove( EngineCtxt* ctxt, const ModelCtxt* model,
const BdHintLimits* boardLimits,
XP_Bool useTileLimits,
#endif
- XP_U16 robotIQ, XP_Bool* canMove, MoveInfo* result );
+ XP_U16 robotIQ, XP_Bool* canMove,
+ MoveInfo* result, XP_U16* score );
XP_Bool engine_check( DictionaryCtxt* dict, Tile* buf, XP_U16 buflen );
#ifdef CPLUS
diff --git a/xwords4/common/game.c b/xwords4/common/game.c
index cc531e3e6..3ddc9dc99 100644
--- a/xwords4/common/game.c
+++ b/xwords4/common/game.c
@@ -85,6 +85,26 @@ makeGameID( XW_UtilCtxt* util )
return gameID;
}
+static void
+timerChangeListener( void* data, const XP_U32 gameID,
+ XP_S32 oldVal, XP_S32 newVal )
+{
+ XWGame* game = (XWGame*)data;
+ const CurGameInfo* gi = game->util->gameInfo;
+ XP_ASSERT( gi->gameID == gameID );
+ XP_LOGF( "%s(oldVal=%d, newVal=%d, id=%d)", __func__, oldVal, newVal, gameID );
+ dutil_onDupTimerChanged( util_getDevUtilCtxt( game->util ),
+ gameID, oldVal, newVal );
+}
+
+static void
+setListeners( XWGame* game, const CommonPrefs* cp )
+{
+ server_prefsChanged( game->server, cp );
+ board_prefsChanged( game->board, cp );
+ server_setTimerChangeListener( game->server, timerChangeListener, game );
+}
+
void
game_makeNewGame( MPFORMAL XWGame* game, CurGameInfo* gi,
XW_UtilCtxt* util, DrawCtx* draw,
@@ -105,6 +125,8 @@ game_makeNewGame( MPFORMAL XWGame* game, CurGameInfo* gi,
gi->gameID = makeGameID( util );
}
+ game->util = util;
+
game->model = model_make( MPPARM(mpool) (DictionaryCtxt*)NULL, NULL, util,
gi->boardSize );
@@ -133,16 +155,15 @@ game_makeNewGame( MPFORMAL XWGame* game, CurGameInfo* gi,
NULL, util );
board_setCallbacks( game->board );
- server_prefsChanged( game->server, cp );
- board_prefsChanged( game->board, cp );
board_setDraw( game->board, draw );
+ setListeners( game, cp );
} /* game_makeNewGame */
XP_Bool
-game_reset( MPFORMAL XWGame* game, CurGameInfo* gi,
- XW_UtilCtxt* XP_UNUSED_STANDALONE(util),
+game_reset( MPFORMAL XWGame* game, CurGameInfo* gi, XW_UtilCtxt* util,
CommonPrefs* cp, const TransportProcs* procs )
{
+ XP_ASSERT( util == game->util );
XP_Bool result = XP_FALSE;
XP_U16 ii;
@@ -195,8 +216,7 @@ game_reset( MPFORMAL XWGame* game, CurGameInfo* gi,
gi->players[ii].secondsUsed = 0;
}
- server_prefsChanged( game->server, cp );
- board_prefsChanged( game->board, cp );
+ setListeners( game, cp );
result = XP_TRUE;
}
return result;
@@ -242,6 +262,7 @@ game_makeFromStream( MPFORMAL XWStreamCtxt* stream, XWGame* game,
XP_LOGF( "%s: gi was all we got; failing.", __func__ );
break;
}
+ game->util = util;
/* Previous stream versions didn't save anything if built
* standalone. Now we always save something. But we need to know
@@ -274,8 +295,7 @@ game_makeFromStream( MPFORMAL XWStreamCtxt* stream, XWGame* game,
game->board = board_makeFromStream( MPPARM(mpool) stream,
game->model, game->server,
NULL, util, gi->nPlayers );
- server_prefsChanged( game->server, cp );
- board_prefsChanged( game->board, cp );
+ setListeners( game, cp );
board_setDraw( game->board, draw );
success = XP_TRUE;
} while( XP_FALSE );
@@ -370,9 +390,10 @@ game_receiveMessage( XWGame* game, XWStreamCtxt* stream,
void
game_getState( const XWGame* game, GameStateInfo* gsi )
{
- XP_Bool gameOver = server_getGameIsOver( game->server );
-
+ const ServerCtxt* server = game->server;
BoardCtxt* board = game->board;
+
+ XP_Bool gameOver = server_getGameIsOver( server );
gsi->curTurnSelected = board_curTurnSelected( board );
gsi->trayVisState = board_getTrayVisState( board );
gsi->visTileCount = board_visTileCount( board );
@@ -386,6 +407,9 @@ game_getState( const XWGame* game, GameStateInfo* gsi )
gsi->canTrade = board_canTrade( board );
gsi->nPendingMessages = !!game->comms ?
comms_countPendingPackets(game->comms) : 0;
+
+ gsi->canPause = server_canPause( server );
+ gsi->canUnpause = server_canUnpause( server );
}
XP_Bool
diff --git a/xwords4/common/game.h b/xwords4/common/game.h
index 969fca0b4..e43a7d03e 100644
--- a/xwords4/common/game.h
+++ b/xwords4/common/game.h
@@ -47,9 +47,12 @@ typedef struct _GameStateInfo {
XP_Bool curTurnSelected;
XP_Bool canHideRack;
XP_Bool canTrade;
+ XP_Bool canPause; /* duplicate-mode only */
+ XP_Bool canUnpause; /* duplicate-mode only */
} GameStateInfo;
typedef struct XWGame {
+ XW_UtilCtxt* util;
BoardCtxt* board;
ModelCtxt* model;
ServerCtxt* server;
diff --git a/xwords4/common/memstream.c b/xwords4/common/memstream.c
index 399e0978c..d0eda7187 100644
--- a/xwords4/common/memstream.c
+++ b/xwords4/common/memstream.c
@@ -404,6 +404,8 @@ mem_stream_getHash( const XWStreamCtxt* p_sctx, XWStreamPos pos,
--len;
}
+ log_hex( ptr, len, __func__ );
+
hash = augmentHash( 0, ptr, len );
if ( 0 != bits ) {
XP_U8 byt = ptr[len];
@@ -413,10 +415,11 @@ mem_stream_getHash( const XWStreamCtxt* p_sctx, XWStreamPos pos,
byt &= 1 << bits;
}
hash = augmentHash( hash, &byt, 1 );
+ log_hex( &byt, 1, __func__ );
}
hash = finishHash( hash );
- LOG_RETURNF( "%X (nBytes=%d;nBits=%d)", hash, len, bits );
+ XP_LOGF( "%s(nBytes=%d, nBits=%d) => %X", __func__, len, bits, hash );
return hash;
} /* mem_stream_getHash */
diff --git a/xwords4/common/model.c b/xwords4/common/model.c
index a4e834c02..5f29ed137 100644
--- a/xwords4/common/model.c
+++ b/xwords4/common/model.c
@@ -59,8 +59,6 @@ static CellTile getModelTileRaw( const ModelCtxt* model, XP_U16 col,
XP_U16 row );
static void setModelTileRaw( ModelCtxt* model, XP_U16 col, XP_U16 row,
CellTile tile );
-static void assignPlayerTiles( ModelCtxt* model, XP_S16 turn,
- const TrayTileSet* tiles );
static void makeTileTrade( ModelCtxt* model, XP_S16 player,
const TrayTileSet* oldTiles,
const TrayTileSet* newTiles );
@@ -112,11 +110,11 @@ model_make( MPFORMAL DictionaryCtxt* dict, const PlayerDicts* dicts,
result->vol.wni.proc = recordWord;
result->vol.wni.closure = &result->vol.rwi;
- model_setSize( result, nCols );
-
XP_ASSERT( !!util->gameInfo );
result->vol.gi = util->gameInfo;
+ model_setSize( result, nCols );
+
model_setDictionary( result, dict );
model_setPlayerDicts( result, dicts );
}
@@ -177,7 +175,6 @@ model_makeFromStream( MPFORMAL XWStreamCtxt* stream, DictionaryCtxt* dict,
stack_loadFromStream( model->vol.stack, stream );
-
MovePrintFuncPre pre = NULL;
MovePrintFuncPost post = NULL;
void* closure = NULL;
@@ -293,10 +290,11 @@ model_setSize( ModelCtxt* model, XP_U16 nCols )
XP_MEMSET( model->vol.tiles, TILE_EMPTY_BIT, TILES_SIZE(model, nCols) );
if ( !!model->vol.stack ) {
- stack_init( model->vol.stack );
+ stack_init( model->vol.stack, model->vol.gi->inDuplicateMode );
} else {
model->vol.stack = stack_make( MPPARM(model->vol.mpool)
- dutil_getVTManager(model->vol.dutil));
+ dutil_getVTManager(model->vol.dutil),
+ model->vol.gi->inDuplicateMode );
}
} /* model_setSize */
@@ -348,6 +346,7 @@ model_popToHash( ModelCtxt* model, const XP_U32 hash, PoolContext* pool )
foundAt = ii;
break;
}
+
if ( ! stack_popEntry( stack, &entries[ii] ) ) {
break;
}
@@ -356,6 +355,7 @@ model_popToHash( ModelCtxt* model, const XP_U32 hash, PoolContext* pool )
for ( XP_S16 ii = nPopped - 1; ii >= 0; --ii ) {
stack_redo( stack, &entries[ii] );
+ stack_freeEntry( stack, &entries[ii] );
}
XP_Bool found = -1 != foundAt;
@@ -378,8 +378,9 @@ model_popToHash( ModelCtxt* model, const XP_U32 hash, PoolContext* pool )
XP_LOGF( "%s(%X) => %s (nEntries=%d)", __func__, hash, boolToStr(found),
nEntries );
#endif
+
return found;
-}
+} /* model_popToHash */
#ifdef STREAM_VERS_BIGBOARD
void
@@ -445,6 +446,40 @@ model_getSquareBonus( const ModelCtxt* model, XP_U16 col, XP_U16 row )
return result;
}
+static XP_U16
+makeAndCommit( ModelCtxt* model, XP_U16 turn, const MoveInfo* mi,
+ const TrayTileSet* tiles, XWStreamCtxt* stream,
+ XP_Bool useStack, WordNotifierInfo* wni )
+{
+ model_makeTurnFromMoveInfo( model, turn, mi );
+ XP_U16 moveScore = commitTurn( model, turn, tiles,
+ stream, wni, useStack );
+ return moveScore;
+}
+
+static void
+dupe_adjustScores( ModelCtxt* model, XP_Bool add, XP_U16 nScores, const XP_U16* scores )
+{
+ XP_S16 mult = add ? 1 : -1;
+ for ( XP_U16 ii = 0; ii < nScores; ++ii ) {
+ model->players[ii].score += mult * scores[ii];
+ }
+}
+
+void
+model_cloneDupeTrays( ModelCtxt* model )
+{
+ XP_ASSERT( model->vol.gi->inDuplicateMode );
+ XP_U16 nTiles = model->players[DUP_PLAYER].trayTiles.nTiles;
+ for ( XP_U16 ii = 0; ii < model->nPlayers; ++ii ) {
+ if ( ii != DUP_PLAYER ) {
+ model_resetCurrentTurn( model, ii );
+ model->players[ii].trayTiles = model->players[DUP_PLAYER].trayTiles;
+ notifyTrayListeners( model, ii, 0, nTiles );
+ }
+ }
+}
+
static void
modelAddEntry( ModelCtxt* model, XP_U16 indx, const StackEntry* entry,
XP_Bool useStack, XWStreamCtxt* stream,
@@ -458,20 +493,29 @@ modelAddEntry( ModelCtxt* model, XP_U16 indx, const StackEntry* entry,
switch ( entry->moveType ) {
case MOVE_TYPE:
-
- model_makeTurnFromMoveInfo( model, entry->playerNum,
- &entry->u.move.moveInfo);
- moveScore = commitTurn( model, entry->playerNum,
- &entry->u.move.newTiles,
- stream, wni, useStack );
+ moveScore = makeAndCommit( model, entry->playerNum, &entry->u.move.moveInfo,
+ &entry->u.move.newTiles, stream, useStack, wni );
+ if ( model->vol.gi->inDuplicateMode ) {
+ XP_ASSERT( DUP_PLAYER == entry->playerNum );
+ dupe_adjustScores( model, XP_TRUE, entry->u.move.dup.nScores,
+ entry->u.move.dup.scores );
+ model_cloneDupeTrays( model );
+ }
break;
case TRADE_TYPE:
makeTileTrade( model, entry->playerNum, &entry->u.trade.oldTiles,
&entry->u.trade.newTiles );
+ if ( model->vol.gi->inDuplicateMode ) {
+ XP_ASSERT( DUP_PLAYER == entry->playerNum );
+ model_cloneDupeTrays( model );
+ }
break;
case ASSIGN_TYPE:
- assignPlayerTiles( model, entry->playerNum,
- &entry->u.assign.tiles );
+ model_addNewTiles( model, entry->playerNum, &entry->u.assign.tiles );
+ if ( model->vol.gi->inDuplicateMode ) {
+ XP_ASSERT( DUP_PLAYER == entry->playerNum );
+ model_cloneDupeTrays( model );
+ }
break;
case PHONY_TYPE: /* nothing to add */
model_makeTurnFromMoveInfo( model, entry->playerNum,
@@ -482,6 +526,9 @@ modelAddEntry( ModelCtxt* model, XP_U16 indx, const StackEntry* entry,
moveScore = 0;
model_resetCurrentTurn( model, entry->playerNum );
+ break;
+ case PAUSE_TYPE:
+ // XP_LOGF( "%s(): nothing to do with PAUSE_TYPE", __func__ );
break;
default:
XP_ASSERT(0);
@@ -499,11 +546,10 @@ buildModelFromStack( ModelCtxt* model, StackCtxt* stack, XP_Bool useStack,
MovePrintFuncPost mpf_post, void* closure )
{
StackEntry entry;
- XP_U16 ii;
-
- for ( ii = initial; stack_getNthEntry( stack, ii, &entry ); ++ii ) {
+ for ( XP_U16 ii = initial; stack_getNthEntry( stack, ii, &entry ); ++ii ) {
modelAddEntry( model, ii, &entry, useStack, stream, wni,
mpf_pre, mpf_post, closure );
+ stack_freeEntry( stack, &entry );
}
} /* buildModelFromStack */
@@ -794,20 +840,19 @@ getModelTileRaw( const ModelCtxt* model, XP_U16 col, XP_U16 row )
} /* getModelTileRaw */
static void
-undoFromMoveInfo( ModelCtxt* model, XP_U16 turn, Tile blankTile, MoveInfo* mi )
+undoFromMove( ModelCtxt* model, XP_U16 turn, Tile blankTile, MoveRec* move )
{
- XP_U16 col, row, ii;
- XP_U16* other;
- MoveInfoTile* tinfo;
+ const MoveInfo* mi = &move->moveInfo;
+ XP_U16 col, row;
col = row = mi->commonCoord;
- other = mi->isHorizontal? &col: &row;
+ XP_U16* other = mi->isHorizontal? &col: &row;
+ const MoveInfoTile* tinfo;
+ XP_U16 ii;
for ( tinfo = mi->tiles, ii = 0; ii < mi->nTiles; ++tinfo, ++ii ) {
- Tile tile;
-
+ Tile tile = tinfo->tile;
*other = tinfo->varCoord;
- tile = tinfo->tile;
setModelTileRaw( model, col, row, EMPTY_TILE );
notifyBoardListeners( model, turn, col, row, XP_FALSE );
@@ -818,8 +863,13 @@ undoFromMoveInfo( ModelCtxt* model, XP_U16 turn, Tile blankTile, MoveInfo* mi )
}
model_addPlayerTile( model, turn, -1, tile );
}
- adjustScoreForUndone( model, mi, turn );
-} /* undoFromMoveInfo */
+
+ if ( model->vol.gi->inDuplicateMode ) {
+ dupe_adjustScores( model, XP_FALSE, move->dup.nScores, move->dup.scores );
+ } else {
+ adjustScoreForUndone( model, mi, turn );
+ }
+} /* undoFromMove */
/* Remove tiles in a set from tray and put them back in the pool.
*/
@@ -857,12 +907,13 @@ model_rejectPreviousMove( ModelCtxt* model, PoolContext* pool, XP_U16* turn )
XP_ASSERT( entry.moveType == MOVE_TYPE );
replaceNewTiles( model, pool, entry.playerNum, &entry.u.move.newTiles );
- undoFromMoveInfo( model, entry.playerNum, blankTile,
- &entry.u.move.moveInfo );
+ XP_ASSERT( !model->vol.gi->inDuplicateMode );
+ undoFromMove( model, entry.playerNum, blankTile, &entry.u.move );
stack_addPhony( stack, entry.playerNum, &entry.u.phony.moveInfo );
*turn = entry.playerNum;
+ stack_freeEntry( stack, &entry );
} /* model_rejectPreviousMove */
XP_Bool
@@ -872,7 +923,8 @@ model_canUndo( const ModelCtxt* model )
XP_U16 nStackEntries = stack_getNEntries( stack );
/* More than just tile assignment? */
- XP_Bool result = nStackEntries > model->nPlayers;
+ XP_U16 assignCount = model->vol.gi->inDuplicateMode ? 1 : model->nPlayers;
+ XP_Bool result = nStackEntries > assignCount;
return result;
}
@@ -887,10 +939,12 @@ model_undoLatestMoves( ModelCtxt* model, PoolContext* pool,
XP_Bool success;
XP_S16 moveSought = !!moveNumP ? *moveNumP : -1;
XP_U16 nStackEntries = stack_getNEntries( stack );
+ const XP_U16 assignCount = model->vol.gi->inDuplicateMode
+ ? 1 : model->nPlayers;
if ( (0 <= moveSought && moveSought >= nStackEntries)
|| ( nStackEntries < nMovesSought )
- || ( nStackEntries <= model->nPlayers ) ) {
+ || ( nStackEntries <= assignCount ) ) {
success = XP_FALSE;
} else {
XP_U16 nMovesUndone = 0;
@@ -911,17 +965,22 @@ model_undoLatestMoves( ModelCtxt* model, PoolContext* pool,
if ( entry.moveType == MOVE_TYPE ) {
/* get the tiles out of player's tray and back into the
pool */
- replaceNewTiles( model, pool, turn, &entry.u.move.newTiles);
+ replaceNewTiles( model, pool, turn, &entry.u.move.newTiles );
- undoFromMoveInfo( model, turn, blankTile,
- &entry.u.move.moveInfo );
+ undoFromMove( model, turn, blankTile, &entry.u.move );
+ model_sortTiles( model, turn );
+
+ if ( model->vol.gi->inDuplicateMode ) {
+ XP_ASSERT( DUP_PLAYER == turn );
+ model_cloneDupeTrays( model );
+ }
} else if ( entry.moveType == TRADE_TYPE ) {
replaceNewTiles( model, pool, turn,
&entry.u.trade.newTiles );
if ( pool != NULL ) {
pool_removeTiles( pool, &entry.u.trade.oldTiles );
}
- assignPlayerTiles( model, turn, &entry.u.trade.oldTiles );
+ model_addNewTiles( model, turn, &entry.u.trade.oldTiles );
} else if ( entry.moveType == PHONY_TYPE ) {
/* nothing to do, since nothing happened */
} else {
@@ -940,6 +999,7 @@ model_undoLatestMoves( ModelCtxt* model, PoolContext* pool,
} else if ( nMovesSought == nMovesUndone ) {
break;
}
+ stack_freeEntry( stack, &entry );
}
/* Find the first MOVE still on the stack and highlight its tiles since
@@ -977,6 +1037,7 @@ model_undoLatestMoves( ModelCtxt* model, PoolContext* pool,
}
break;
}
+ stack_freeEntry( stack, &entry );
}
/* We fail if we didn't undo as many as requested UNLESS the lower
@@ -1020,23 +1081,63 @@ model_trayToStream( ModelCtxt* model, XP_S16 turn, XWStreamCtxt* stream )
traySetToStream( stream, &player->trayTiles );
} /* model_trayToStream */
+void
+model_currentMoveToMoveInfo( ModelCtxt* model, XP_S16 turn,
+ MoveInfo* moveInfo )
+{
+ XP_ASSERT( turn >= 0 );
+ const XP_S16 numTiles = model->players[turn].nPending;
+ moveInfo->nTiles = numTiles;
+
+ XP_U16 cols[MAX_TRAY_TILES];
+ XP_U16 rows[MAX_TRAY_TILES];
+ for ( XP_S16 ii = 0; ii < numTiles; ++ii ) {
+ XP_Bool isBlank;
+ Tile tile;
+ model_getCurrentMoveTile( model, turn, &ii, &tile,
+ &cols[ii], &rows[ii], &isBlank );
+ if ( isBlank ) {
+ tile |= TILE_BLANK_BIT;
+ }
+ moveInfo->tiles[ii].tile = tile;
+ }
+
+ XP_Bool isHorizontal = XP_TRUE;
+ if ( 1 == numTiles ) { /* horizonal/vertical makes no sense */
+ moveInfo->tiles[0].varCoord = cols[0];
+ moveInfo->commonCoord = rows[0];
+ } else if ( 1 < numTiles ) {
+ isHorizontal = rows[0] == rows[1];
+ moveInfo->commonCoord = isHorizontal ? rows[0] : cols[0];
+ for ( XP_U16 ii = 0; ii < numTiles; ++ii ) {
+ moveInfo->tiles[ii].varCoord =
+ isHorizontal ? cols[ii] : rows[ii];
+ /* MoveInfo assumes legal moves! Check here */
+ if ( isHorizontal ) {
+ XP_ASSERT( rows[ii] == rows[0] );
+ } else {
+ XP_ASSERT( cols[ii] == cols[0] );
+ }
+ }
+ }
+ moveInfo->isHorizontal = isHorizontal;
+
+ normalizeMI( moveInfo, moveInfo );
+}
+
void
model_currentMoveToStream( ModelCtxt* model, XP_S16 turn,
XWStreamCtxt* stream )
{
- PlayerCtxt* player;
- XP_S16 numTiles;
- XP_U16 nColsNBits;
#ifdef STREAM_VERS_BIGBOARD
- nColsNBits = 16 <= model_numCols( model ) ? NUMCOLS_NBITS_5
+ XP_U16 nColsNBits = 16 <= model_numCols( model ) ? NUMCOLS_NBITS_5
: NUMCOLS_NBITS_4;
#else
- nColsNBits = NUMCOLS_NBITS_4;
+ XP_U16 nColsNBits = NUMCOLS_NBITS_4;
#endif
XP_ASSERT( turn >= 0 );
- player = &model->players[turn];
- numTiles = player->nPending;
+ XP_S16 numTiles = model->players[turn].nPending;
stream_putBits( stream, NTILES_NBITS, numTiles );
@@ -1065,66 +1166,63 @@ XP_Bool
model_makeTurnFromStream( ModelCtxt* model, XP_U16 playerNum,
XWStreamCtxt* stream )
{
- XP_Bool success = XP_TRUE;
- XP_U16 numTiles, ii;
Tile blank = dict_getBlankTile( model_getDictionary(model) );
- XP_U16 nColsNBits;
+ XP_U16 nColsNBits =
#ifdef STREAM_VERS_BIGBOARD
- nColsNBits = 16 <= model_numCols( model ) ? NUMCOLS_NBITS_5
- : NUMCOLS_NBITS_4;
+ 16 <= model_numCols( model ) ? NUMCOLS_NBITS_5 : NUMCOLS_NBITS_4
#else
- nColsNBits = NUMCOLS_NBITS_4;
+ NUMCOLS_NBITS_4
#endif
+ ;
model_resetCurrentTurn( model, playerNum );
- if ( success ) {
- numTiles = (XP_U16)stream_getBits( stream, NTILES_NBITS );
- XP_LOGF( "%s: numTiles=%d", __func__, numTiles );
+ XP_U16 numTiles = (XP_U16)stream_getBits( stream, NTILES_NBITS );
+ XP_LOGF( "%s: numTiles=%d", __func__, numTiles );
- Tile tileFaces[numTiles];
- XP_U16 cols[numTiles];
- XP_U16 rows[numTiles];
- XP_Bool isBlanks[numTiles];
- Tile moveTiles[numTiles];
- TrayTileSet curTiles = *model_getPlayerTiles( model, playerNum );
+ Tile tileFaces[numTiles];
+ XP_U16 cols[numTiles];
+ XP_U16 rows[numTiles];
+ XP_Bool isBlanks[numTiles];
+ Tile moveTiles[numTiles];
+ TrayTileSet curTiles = *model_getPlayerTiles( model, playerNum );
- for ( ii = 0; success && ii < numTiles; ++ii ) {
- tileFaces[ii] = (Tile)stream_getBits( stream, TILE_NBITS );
- cols[ii] = (XP_U16)stream_getBits( stream, nColsNBits );
- rows[ii] = (XP_U16)stream_getBits( stream, nColsNBits );
- isBlanks[ii] = stream_getBits( stream, 1 );
+ XP_Bool success = XP_TRUE;
+ for ( XP_U16 ii = 0; success && ii < numTiles; ++ii ) {
+ tileFaces[ii] = (Tile)stream_getBits( stream, TILE_NBITS );
+ cols[ii] = (XP_U16)stream_getBits( stream, nColsNBits );
+ rows[ii] = (XP_U16)stream_getBits( stream, nColsNBits );
+ isBlanks[ii] = stream_getBits( stream, 1 );
- if ( isBlanks[ii] ) {
- moveTiles[ii] = blank;
- } else {
- moveTiles[ii] = tileFaces[ii];
- }
-
- XP_S16 index = setContains( &curTiles, moveTiles[ii] );
- if ( 0 <= index ) {
- removeTile( &curTiles, index );
- } else {
- success = XP_FALSE;
- }
+ if ( isBlanks[ii] ) {
+ moveTiles[ii] = blank;
+ } else {
+ moveTiles[ii] = tileFaces[ii];
}
- if ( success ) {
- for ( ii = 0; ii < numTiles; ++ii ) {
- XP_S16 foundAt = model_trayContains( model, playerNum, moveTiles[ii] );
- if ( foundAt == -1 ) {
- XP_ASSERT( EMPTY_TILE == model_getPlayerTile(model, playerNum,
- 0));
- /* Does this ever happen? */
- XP_LOGF( "%s: found empty tile and it's ok", __func__ );
+ XP_S16 index = setContains( &curTiles, moveTiles[ii] );
+ if ( 0 <= index ) {
+ removeTile( &curTiles, index );
+ } else {
+ success = XP_FALSE;
+ }
+ }
- (void)model_removePlayerTile( model, playerNum, -1 );
- model_addPlayerTile( model, playerNum, -1, moveTiles[ii] );
- }
+ if ( success ) {
+ for ( XP_U16 ii = 0; ii < numTiles; ++ii ) {
+ XP_S16 foundAt = model_trayContains( model, playerNum, moveTiles[ii] );
+ if ( foundAt == -1 ) {
+ XP_ASSERT( EMPTY_TILE == model_getPlayerTile(model, playerNum,
+ 0));
+ /* Does this ever happen? */
+ XP_LOGF( "%s: found empty tile and it's ok", __func__ );
- model_moveTrayToBoard( model, playerNum, cols[ii], rows[ii], foundAt,
- tileFaces[ii] );
+ (void)model_removePlayerTile( model, playerNum, -1 );
+ model_addPlayerTile( model, playerNum, -1, moveTiles[ii] );
}
+
+ model_moveTrayToBoard( model, playerNum, cols[ii], rows[ii], foundAt,
+ tileFaces[ii] );
}
}
return success;
@@ -1326,6 +1424,45 @@ model_removePlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index )
return tile;
} /* model_removePlayerTile */
+void
+model_removePlayerTiles( ModelCtxt* model, XP_S16 turn, const MoveInfo* mi )
+{
+ XP_ASSERT( turn >= 0 );
+ PlayerCtxt* player = &model->players[turn];
+ for ( XP_U16 ii = 0; ii < mi->nTiles; ++ii ) {
+ Tile tile = mi->tiles[ii].tile;
+ if ( IS_BLANK( tile ) ) {
+ tile = dict_getBlankTile( model_getDictionary(model) );
+ }
+ XP_S16 index = -1;
+ for ( XP_U16 jj = 0; index < 0 && jj < player->trayTiles.nTiles; ++jj ) {
+ if ( tile == player->trayTiles.tiles[jj] ) {
+ index = jj;
+ }
+ }
+ XP_ASSERT( index >= 0 );
+ model_removePlayerTile( model, turn, index );
+ }
+}
+
+void
+model_removePlayerTiles2( ModelCtxt* model, XP_S16 turn, const TrayTileSet* tiles )
+{
+ XP_ASSERT( turn >= 0 );
+ PlayerCtxt* player = &model->players[turn];
+ for ( XP_U16 ii = 0; ii < tiles->nTiles; ++ii ) {
+ Tile tile = tiles->tiles[ii];
+ XP_S16 index = -1;
+ for ( XP_U16 jj = 0; index < 0 && jj < player->trayTiles.nTiles; ++jj ) {
+ if ( tile == player->trayTiles.tiles[jj] ) {
+ index = jj;
+ }
+ }
+ XP_ASSERT( index >= 0 );
+ model_removePlayerTile( model, turn, index );
+ }
+}
+
void
model_packTilesUtil( ModelCtxt* model, PoolContext* pool,
XP_Bool includeBlank,
@@ -1593,8 +1730,8 @@ model_resetCurrentTurn( ModelCtxt* model, XP_S16 whose )
XP_S16
model_getNMoves( const ModelCtxt* model )
{
- XP_U16 result = stack_getNEntries( model->vol.stack );
- result -= model->nPlayers; /* tile assignment doesn't count */
+ XP_U16 nAssigns = model->vol.gi->inDuplicateMode ? 1 : model->nPlayers;
+ XP_U16 result = stack_getNEntries( model->vol.stack ) - nAssigns;
return result;
}
@@ -1689,6 +1826,14 @@ putBackOtherPlayersTiles( ModelCtxt* model, XP_U16 notMyTurn,
}
} /* putBackOtherPlayersTiles */
+static void
+invalidateScores( ModelCtxt* model )
+{
+ for ( int ii = 0; ii < model->nPlayers; ++ii ) {
+ invalidateScore( model, ii );
+ }
+}
+
/* Make those tiles placed by 'turn' a permanent part of the board. If any
* other players have placed pending tiles on those same squares, replace them
* in their trays.
@@ -1720,7 +1865,7 @@ commitTurn( ModelCtxt* model, XP_S16 turn, const TrayTileSet* newTiles,
XP_ASSERT( inLine );
MoveInfo moveInfo = {0};
normalizeMoves( model, turn, isHorizontal, &moveInfo );
-
+
stack_addMove( model->vol.stack, turn, &moveInfo, newTiles );
}
@@ -1751,13 +1896,12 @@ commitTurn( ModelCtxt* model, XP_S16 turn, const TrayTileSet* newTiles,
(void)getCurrentMoveScoreIfLegal( model, turn, stream, wni, &score );
XP_ASSERT( score >= 0 );
- player->score += score;
-
- /* Why is this next loop necessary? */
- for ( int ii = 0; ii < model->nPlayers; ++ii ) {
- invalidateScore( model, ii );
+ if ( ! model->vol.gi->inDuplicateMode ) {
+ player->score += score;
}
+ invalidateScores( model );
+
player->nPending = 0;
player->nUndone = 0;
@@ -1776,6 +1920,34 @@ model_commitTurn( ModelCtxt* model, XP_S16 turn, TrayTileSet* newTiles )
return 0 <= score;
} /* model_commitTurn */
+void
+model_commitDupeTurn( ModelCtxt* model, const MoveInfo* moveInfo,
+ XP_U16 nScores, XP_U16* scores, TrayTileSet* newTiles )
+{
+ model_resetCurrentTurn( model, DUP_PLAYER );
+ model_makeTurnFromMoveInfo( model, DUP_PLAYER, moveInfo );
+ (void)commitTurn( model, DUP_PLAYER, newTiles, NULL, NULL, XP_FALSE );
+ dupe_adjustScores( model, XP_TRUE, nScores, scores );
+ invalidateScores( model );
+
+ stack_addDupMove( model->vol.stack, moveInfo, nScores, scores, newTiles );
+}
+
+void
+model_commitDupeTrade( ModelCtxt* model, const TrayTileSet* oldTiles,
+ const TrayTileSet* newTiles )
+{
+ stack_addDupTrade( model->vol.stack, oldTiles, newTiles );
+}
+
+void
+model_noteDupePause( ModelCtxt* model, DupPauseType typ, XP_S16 turn,
+ const XP_UCHAR* msg )
+{
+ XP_U32 when = dutil_getCurSeconds( model->vol.dutil );
+ stack_addPause( model->vol.stack, typ, turn, when, msg );
+}
+
/* Given a rack of new tiles and of old, remove all the old from the tray and
* replace them with new. Replace in the same place so that user sees an
* in-place change.
@@ -1784,13 +1956,11 @@ static void
makeTileTrade( ModelCtxt* model, XP_S16 player, const TrayTileSet* oldTiles,
const TrayTileSet* newTiles )
{
- XP_U16 ii;
- XP_U16 nTiles;
-
XP_ASSERT( newTiles->nTiles == oldTiles->nTiles );
XP_ASSERT( oldTiles != &model->players[player].trayTiles );
- for ( nTiles = newTiles->nTiles, ii = 0; ii < nTiles; ++ii ) {
+ const XP_U16 nTiles = newTiles->nTiles;
+ for ( XP_U16 ii = 0; ii < nTiles; ++ii ) {
Tile oldTile = oldTiles->tiles[ii];
XP_S16 tileIndex = model_trayContains( model, player, oldTile );
@@ -1911,8 +2081,8 @@ model_setDividerLoc( ModelCtxt* model, XP_S16 turn, XP_U16 loc )
player->dividerLoc = (XP_U8)loc;
}
-static void
-assignPlayerTiles( ModelCtxt* model, XP_S16 turn, const TrayTileSet* tiles )
+void
+model_addNewTiles( ModelCtxt* model, XP_S16 turn, const TrayTileSet* tiles )
{
const Tile* tilep = tiles->tiles;
XP_U16 nTiles = tiles->nTiles;
@@ -1926,13 +2096,21 @@ model_assignPlayerTiles( ModelCtxt* model, XP_S16 turn,
const TrayTileSet* tiles )
{
XP_ASSERT( turn >= 0 );
+ XP_ASSERT( turn == DUP_PLAYER || !model->vol.gi->inDuplicateMode );
TrayTileSet sorted;
sortTiles( &sorted, tiles, 0 );
stack_addAssign( model->vol.stack, turn, &sorted );
- assignPlayerTiles( model, turn, tiles );
+ model_addNewTiles( model, turn, &sorted );
} /* model_assignPlayerTiles */
+void
+model_assignDupeTiles( ModelCtxt* model, const TrayTileSet* tiles )
+{
+ model_assignPlayerTiles( model, DUP_PLAYER, tiles );
+ model_cloneDupeTrays( model );
+}
+
void
model_sortTiles( ModelCtxt* model, XP_S16 turn )
{
@@ -1946,7 +2124,7 @@ model_sortTiles( ModelCtxt* model, XP_S16 turn )
removePlayerTile( model, turn, --nTiles );
}
- assignPlayerTiles( model, turn, &sorted );
+ model_addNewTiles( model, turn, &sorted );
}
} /* model_sortTiles */
@@ -1956,7 +2134,9 @@ model_getNumTilesInTray( ModelCtxt* model, XP_S16 turn )
PlayerCtxt* player;
XP_ASSERT( turn >= 0 );
player = &model->players[turn];
- return player->trayTiles.nTiles;
+ XP_U16 result = player->trayTiles.nTiles;
+ // XP_LOGF( "%s(turn=%d) => %d", __func__, turn, result );
+ return result;
} /* model_getNumTilesInTray */
XP_U16
@@ -2060,6 +2240,7 @@ typedef struct MovePrintClosure {
DictionaryCtxt* dict;
XP_U16 nPrinted;
XP_Bool keepHidden;
+ XP_U32 lastPauseWhen;
} MovePrintClosure;
static void
@@ -2077,8 +2258,11 @@ printMovePre( ModelCtxt* model, XP_U16 XP_UNUSED(moveN), const StackEntry* entry
entry->playerNum+1 );
printString( stream, (XP_UCHAR*)buf );
- if ( entry->moveType == TRADE_TYPE ) {
- } else {
+ switch ( entry->moveType ) {
+ case TRADE_TYPE:
+ case PAUSE_TYPE:
+ break;
+ default: {
XP_UCHAR letter[2] = {'\0','\0'};
XP_Bool isHorizontal = entry->u.move.moveInfo.isHorizontal;
XP_U16 col, row;
@@ -2116,15 +2300,17 @@ printMovePre( ModelCtxt* model, XP_U16 XP_UNUSED(moveN), const StackEntry* entry
XP_SNPRINTF( buf, sizeof(buf), format, traybuf );
}
printString( stream, (XP_UCHAR*)buf );
- }
- if ( !closure->keepHidden ) {
- format = dutil_getUserString( model->vol.dutil, STRS_TRAY_AT_START );
- formatTray( model_getPlayerTiles( model, entry->playerNum ),
- closure->dict, (XP_UCHAR*)traybuf, sizeof(traybuf),
- XP_FALSE );
- XP_SNPRINTF( buf, sizeof(buf), format, traybuf );
- printString( stream, buf );
+ if ( !closure->keepHidden ) {
+ format = dutil_getUserString( model->vol.dutil, STRS_TRAY_AT_START );
+ formatTray( model_getPlayerTiles( model, entry->playerNum ),
+ closure->dict, (XP_UCHAR*)traybuf, sizeof(traybuf),
+ XP_FALSE );
+ XP_SNPRINTF( buf, sizeof(buf), format, traybuf );
+ printString( stream, buf );
+ }
+ }
+ break;
}
}
} /* printMovePre */
@@ -2146,6 +2332,7 @@ printMovePost( ModelCtxt* model, XP_U16 XP_UNUSED(moveN),
XP_UCHAR traybuf2[MAX_TRAY_TILES+1];
const MoveInfo* mi;
XP_S16 totalScore = model_getPlayerScore( model, entry->playerNum );
+ XP_Bool addCR = XP_FALSE;
switch( entry->moveType ) {
case TRADE_TYPE:
@@ -2157,13 +2344,25 @@ printMovePost( ModelCtxt* model, XP_U16 XP_UNUSED(moveN),
format = dutil_getUserString( model->vol.dutil, STRSS_TRADED_FOR );
XP_SNPRINTF( buf, sizeof(buf), format, traybuf1, traybuf2 );
printString( stream, buf );
- printString( stream, (XP_UCHAR*)XP_CR );
+ addCR = XP_TRUE;
break;
case PHONY_TYPE:
format = dutil_getUserString( model->vol.dutil, STR_PHONY_REJECTED );
printString( stream, format );
+ /* FALLTHRU */
case MOVE_TYPE:
+ /* Duplicate case */
+ if ( model->vol.gi->inDuplicateMode ) {
+ XP_U16 offset = XP_SNPRINTF( buf, VSIZE(buf), "%s", "All scores: " );
+ for ( XP_U16 ii = 0; ii < entry->u.move.dup.nScores; ++ii ) {
+ offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset, "%d,",
+ entry->u.move.dup.scores[ii] );
+ }
+ buf[offset-1] = '\n'; /* replace last ',' */
+ printString( stream, buf );
+ }
+
format = dutil_getUserString( model->vol.dutil, STRD_CUMULATIVE_SCORE );
XP_SNPRINTF( buf, sizeof(buf), format, totalScore );
printString( stream, buf );
@@ -2185,11 +2384,25 @@ printMovePost( ModelCtxt* model, XP_U16 XP_UNUSED(moveN),
traybuf1, sizeof(traybuf1),
XP_FALSE ) );
printString( stream, buf );
- stream_catString( stream, (XP_UCHAR*)XP_CR );
+ addCR = XP_TRUE;
}
}
break;
+ case PAUSE_TYPE:
+ util_formatPauseHistory( model->vol.util, stream, entry->u.pause.pauseType,
+ entry->playerNum, closure->lastPauseWhen,
+ entry->u.pause.when, entry->u.pause.msg );
+ closure->lastPauseWhen = entry->u.pause.when;
+ addCR = XP_TRUE;
+ break;
+
+ default:
+ XP_ASSERT( 0 );
+ }
+
+ if ( addCR ) {
+ printString( stream, (XP_UCHAR*)XP_CR );
}
printString( stream, (XP_UCHAR*)XP_CR );
@@ -2231,16 +2444,15 @@ void
model_writeGameHistory( ModelCtxt* model, XWStreamCtxt* stream,
ServerCtxt* server, XP_Bool gameOver )
{
- ModelCtxt* tmpModel;
- MovePrintClosure closure;
+ MovePrintClosure closure = {
+ .stream = stream,
+ .dict = model_getDictionary( model ),
+ .keepHidden = !gameOver && !model->vol.gi->inDuplicateMode,
+ .nPrinted = 0
+ };
- closure.stream = stream;
- closure.dict = model_getDictionary( model );
- closure.keepHidden = !gameOver;
- closure.nPrinted = 0;
-
- tmpModel = makeTmpModel( model, stream, printMovePre, printMovePost,
- &closure );
+ ModelCtxt* tmpModel = makeTmpModel( model, stream, printMovePre,
+ printMovePost, &closure );
model_destroy( tmpModel );
if ( gameOver ) {
@@ -2305,23 +2517,25 @@ model_getRecentPassCount( ModelCtxt* model )
{
StackCtxt* stack = model->vol.stack;
XP_U16 nPasses = 0;
- XP_S16 nEntries, which;
- StackEntry entry;
XP_ASSERT( !!stack );
- nEntries = stack_getNEntries( stack );
- for ( which = nEntries - 1; which >= 0; --which ) {
- if ( stack_getNthEntry( stack, which, &entry ) ) {
- if ( entry.moveType == MOVE_TYPE
- && entry.u.move.moveInfo.nTiles == 0 ) {
- ++nPasses;
- } else {
- break;
- }
- } else {
+ XP_S16 nEntries = stack_getNEntries( stack );
+ for ( XP_S16 which = nEntries - 1; which >= 0; --which ) {
+ StackEntry entry;
+ if ( !stack_getNthEntry( stack, which, &entry ) ) {
break;
}
+ switch ( entry.moveType ) {
+ case MOVE_TYPE:
+ if ( entry.u.move.moveInfo.nTiles == 0 ) {
+ ++nPasses;
+ }
+ break;
+ default:
+ break;
+ }
+ stack_freeEntry( stack, &entry );
}
return nPasses;
} /* model_getRecentPassCount */
@@ -2330,7 +2544,10 @@ XP_Bool
model_recentPassCountOk( ModelCtxt* model )
{
XP_U16 count = model_getRecentPassCount( model );
- XP_U16 okCount = model->nPlayers * MAX_PASSES;
+ XP_U16 okCount = MAX_PASSES;
+ if ( !model->vol.gi->inDuplicateMode ) {
+ okCount *= model->nPlayers;
+ }
XP_ASSERT( count <= okCount ); /* should never be more than 1 over */
return count < okCount;
}
@@ -2410,8 +2627,15 @@ model_listWordsThrough( ModelCtxt* model, XP_U16 col, XP_U16 row,
MoveInfo moveInfo = {0};
normalizeMoves( model, turn, isHorizontal, &moveInfo );
model_makeTurnFromMoveInfo( tmpModel, turn, &moveInfo );
- TrayTileSet newTiles = {0};
- commitTurn( tmpModel, turn, &newTiles, NULL, NULL, XP_TRUE );
+
+ /* Might not be a legal move. If isn't, don't add it! */
+ if ( getCurrentMoveScoreIfLegal( tmpModel, turn, (XWStreamCtxt*)NULL,
+ (WordNotifierInfo*)NULL, NULL ) ) {
+ TrayTileSet newTiles = {.nTiles = 0};
+ commitTurn( tmpModel, turn, &newTiles, NULL, NULL, XP_TRUE );
+ } else {
+ model_resetCurrentTurn( tmpModel, turn );
+ }
}
XP_ASSERT( !!stream );
@@ -2451,6 +2675,26 @@ model_listWordsThrough( ModelCtxt* model, XP_U16 col, XP_U16 row,
} /* model_listWordsThrough */
#endif
+/* Set array of 1-4 (>1 in case of tie) with highest scores' owners */
+static void
+listHighestScores( const ModelCtxt* model, LastMoveInfo* lmi, MoveRec* move )
+{
+ /* find highest */
+ XP_U16 max = 0;
+ lmi->nWinners = 0;
+ for ( XP_U16 ii = 0; ii < move->dup.nScores; ++ii ) {
+ XP_U16 score = move->dup.scores[ii];
+ if ( 0 == score || score < max ) {
+ continue;
+ } else if ( score > max ) {
+ max = score;
+ lmi->nWinners = 0;
+ lmi->score = score;
+ }
+ lmi->names[lmi->nWinners++] = model->vol.gi->players[ii].name;
+ }
+}
+
XP_Bool
model_getPlayersLastScore( ModelCtxt* model, XP_S16 player, LastMoveInfo* lmi )
{
@@ -2458,6 +2702,7 @@ model_getPlayersLastScore( ModelCtxt* model, XP_S16 player, LastMoveInfo* lmi )
XP_S16 nEntries, which;
StackEntry entry;
XP_Bool found = XP_FALSE;
+ XP_Bool inDuplicateMode = model->vol.gi->inDuplicateMode;
XP_MEMSET( lmi, 0, sizeof(*lmi) );
XP_ASSERT( !!stack );
@@ -2466,37 +2711,46 @@ model_getPlayersLastScore( ModelCtxt* model, XP_S16 player, LastMoveInfo* lmi )
for ( which = nEntries; which >= 0; ) {
if ( stack_getNthEntry( stack, --which, &entry ) ) {
- if ( -1 == player || entry.playerNum == player ) {
+ if ( -1 == player || inDuplicateMode || entry.playerNum == player ) {
found = XP_TRUE;
break;
}
}
+ stack_freeEntry( stack, &entry );
}
-
if ( found ) { /* success? */
- XP_ASSERT( -1 == player || player == entry.playerNum );
+ XP_ASSERT( -1 == player || inDuplicateMode || player == entry.playerNum );
+
XP_LOGF( "%s: found move %d", __func__, which );
- lmi->name = model->vol.gi->players[entry.playerNum].name;
+ lmi->names[0] = model->vol.gi->players[entry.playerNum].name;
+ lmi->nWinners = 1;
lmi->moveType = entry.moveType;
+ lmi->inDuplicateMode = inDuplicateMode;
switch ( entry.moveType ) {
case MOVE_TYPE:
+ XP_ASSERT( !inDuplicateMode || entry.playerNum == DUP_PLAYER );
lmi->nTiles = entry.u.move.moveInfo.nTiles;
if ( 0 < entry.u.move.moveInfo.nTiles ) {
- scoreLastMove( model, &entry.u.move.moveInfo, nEntries - which,
- lmi );
+ scoreLastMove( model, &entry.u.move.moveInfo,
+ nEntries - which, lmi );
+ if ( inDuplicateMode ) {
+ listHighestScores( model, lmi, &entry.u.move );
+ }
}
break;
case TRADE_TYPE:
+ XP_ASSERT( !inDuplicateMode || entry.playerNum == DUP_PLAYER );
lmi->nTiles = entry.u.trade.oldTiles.nTiles;
break;
case PHONY_TYPE:
- break;
case ASSIGN_TYPE:
- // found = XP_FALSE;
+ case PAUSE_TYPE:
break;
+ default:
+ XP_ASSERT( 0 );
}
}
@@ -2597,7 +2851,7 @@ static void
assertDiffTurn( ModelCtxt* model, XP_U16 XP_UNUSED(turn),
const StackEntry* entry, void* closure )
{
- if ( 1 < model->nPlayers ) {
+ if ( 1 < model->nPlayers && ! model->vol.gi->inDuplicateMode ) {
DiffTurnState* state = (DiffTurnState*)closure;
if ( -1 != state->lastPlayerNum ) {
XP_ASSERT( state->lastPlayerNum != entry->playerNum );
@@ -2607,6 +2861,44 @@ assertDiffTurn( ModelCtxt* model, XP_U16 XP_UNUSED(turn),
state->lastMoveNum = entry->moveNum;
}
}
+
+void
+model_printTrays( const ModelCtxt* model )
+{
+ for ( XP_U16 ii = 0; ii < model->nPlayers; ++ii ) {
+ const PlayerCtxt* player = &model->players[ii];
+ XP_UCHAR buf[128];
+ XP_LOGF( "%s(): player %d: %s", __func__, ii,
+ formatTileSet( &player->trayTiles, buf, VSIZE(buf) ) );
+ }
+}
+
+void
+model_dumpSelf( const ModelCtxt* model, const XP_UCHAR* msg )
+{
+ XP_LOGF( "%s(msg=%s)", __func__, msg );
+
+ XP_UCHAR buf[256];
+ XP_U16 offset = 0;
+
+ for ( XP_U16 col = 0; col < model_numCols( model ); ++col ) {
+ offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset,
+ "%.2d ", col );
+ }
+ XP_LOGF( " %s", buf );
+
+ for ( XP_U16 row = 0; row < model_numRows( model ); ++row ) {
+ XP_UCHAR buf[256];
+ XP_U16 offset = 0;
+
+ for ( XP_U16 col = 0; col < model_numCols( model ); ++col ) {
+ Tile tile = getModelTileRaw( model, col, row );
+ offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset,
+ "%.2x ", tile );
+ }
+ XP_LOGF( "%.2d: %s", row, buf );
+ }
+}
#endif
#ifdef CPLUS
diff --git a/xwords4/common/model.h b/xwords4/common/model.h
index 588e57102..882ff72d9 100644
--- a/xwords4/common/model.h
+++ b/xwords4/common/model.h
@@ -24,6 +24,7 @@
#include "comtypes.h"
#include "dictnry.h"
#include "mempool.h"
+#include "dutil.h"
#ifdef CPLUS
extern "C" {
@@ -48,8 +49,8 @@ extern "C" {
#define TILE_PENDING_BIT 0x0100
#define PREV_MOVE_BIT 0x200
-#define CELL_OWNER_MASK 0x0C00
#define CELL_OWNER_OFFSET 10
+#define CELL_OWNER_MASK (0x0003 << CELL_OWNER_OFFSET)
#define CELL_OWNER(t) (((t)&CELL_OWNER_MASK) >> CELL_OWNER_OFFSET)
#define MAX_UNIQUE_TILES 64 /* max tile non-blank faces */
@@ -72,15 +73,17 @@ typedef struct MoveInfo {
} MoveInfo;
typedef struct _LastMoveInfo {
- const XP_UCHAR* name;
- XP_U8 moveType;
+ const XP_UCHAR* names[MAX_NUM_PLAYERS];
+ XP_U16 nWinners; /* >1 possible in duplicate case only */
XP_U16 score;
XP_U16 nTiles;
- XP_UCHAR word[MAX_COLS+1];
+ XP_UCHAR word[MAX_COLS * 2]; /* be safe */
+ XP_U8 moveType;
+ XP_Bool inDuplicateMode;
} LastMoveInfo;
typedef XP_U8 TrayTile;
-typedef struct TrayTileSet {
+typedef struct _TrayTileSet {
XP_U8 nTiles;
TrayTile tiles[MAX_TRAY_TILES];
} TrayTileSet;
@@ -140,12 +143,17 @@ void model_listPlacedBlanks( ModelCtxt* model, XP_U16 turn,
XP_Bool includePending, BlankQueue* bcp );
XP_U16 model_getCellOwner( ModelCtxt* model, XP_U16 col, XP_U16 row );
-
+void model_addNewTiles( ModelCtxt* model, XP_S16 turn,
+ const TrayTileSet* tiles );
void model_assignPlayerTiles( ModelCtxt* model, XP_S16 turn,
const TrayTileSet* tiles );
+void model_assignDupeTiles( ModelCtxt* model, const TrayTileSet* tiles );
+
Tile model_getPlayerTile( const ModelCtxt* model, XP_S16 turn, XP_S16 index );
Tile model_removePlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index );
+void model_removePlayerTiles( ModelCtxt* model, XP_S16 turn, const MoveInfo* mi );
+void model_removePlayerTiles2( ModelCtxt* model, XP_S16 turn, const TrayTileSet* tiles );
void model_addPlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index,
Tile tile );
void model_moveTileOnTray( ModelCtxt* model, XP_S16 turn, XP_S16 indexCur,
@@ -159,6 +167,7 @@ const TrayTileSet* model_getPlayerTiles( const ModelCtxt* model, XP_S16 turn );
#ifdef DEBUG
XP_UCHAR* formatTileSet( const TrayTileSet* tiles, XP_UCHAR* buf, XP_U16 len );
+void model_printTrays( const ModelCtxt* model );
#endif
void model_sortTiles( ModelCtxt* model, XP_S16 turn );
@@ -196,6 +205,15 @@ void model_getCurrentMoveTile( ModelCtxt* model, XP_S16 turn, XP_S16* index,
XP_Bool model_commitTurn( ModelCtxt* model, XP_S16 player,
TrayTileSet* newTiles );
+void model_commitDupeTurn( ModelCtxt* model, const MoveInfo* moveInfo,
+ XP_U16 nScores, XP_U16* scores,
+ TrayTileSet* newTiles );
+void model_commitDupeTrade( ModelCtxt* model, const TrayTileSet* oldTiles,
+ const TrayTileSet* newTiles );
+void model_noteDupePause( ModelCtxt* model, DupPauseType typ, XP_S16 turn,
+ const XP_UCHAR* msg );
+void model_cloneDupeTrays( ModelCtxt* model );
+
void model_commitRejectedPhony( ModelCtxt* model, XP_S16 player );
void model_makeTileTrade( ModelCtxt* model, XP_S16 player,
const TrayTileSet* oldTiles,
@@ -210,7 +228,9 @@ void model_rejectPreviousMove( ModelCtxt* model, PoolContext* pool,
void model_trayToStream( ModelCtxt* model, XP_S16 turn,
XWStreamCtxt* stream );
void model_currentMoveToStream( ModelCtxt* model, XP_S16 turn,
- XWStreamCtxt* stream);
+ XWStreamCtxt* stream );
+void model_currentMoveToMoveInfo( ModelCtxt* model, XP_S16 turn,
+ MoveInfo* moveInfo );
XP_Bool model_makeTurnFromStream( ModelCtxt* model, XP_U16 playerNum,
XWStreamCtxt* stream );
void model_makeTurnFromMoveInfo( ModelCtxt* model, XP_U16 playerNum,
@@ -218,8 +238,10 @@ void model_makeTurnFromMoveInfo( ModelCtxt* model, XP_U16 playerNum,
#ifdef DEBUG
void juggleMoveIfDebug( MoveInfo* move );
+void model_dumpSelf( const ModelCtxt* model, const XP_UCHAR* msg );
#else
# define juggleMoveIfDebug(newMove)
+# define model_dumpSelf( model, msg )
#endif
void model_resetCurrentTurn( ModelCtxt* model, XP_S16 turn );
@@ -304,9 +326,9 @@ void model_figureFinalScores( ModelCtxt* model, ScoresArray* scores,
ScoresArray* tilePenalties );
/* figureMoveScore is meant only for the engine's use */
-XP_U16 figureMoveScore( const ModelCtxt* model, XP_U16 turn, MoveInfo* mvInfo,
- EngineCtxt* engine, XWStreamCtxt* stream,
- WordNotifierInfo* notifyInfo );
+XP_U16 figureMoveScore( const ModelCtxt* model, XP_U16 turn,
+ const MoveInfo* mvInfo, EngineCtxt* engine,
+ XWStreamCtxt* stream, WordNotifierInfo* notifyInfo );
/* tap into internal WordNotifierInfo */
WordNotifierInfo* model_initWordCounter( ModelCtxt* model, XWStreamCtxt* stream );
diff --git a/xwords4/common/modelp.h b/xwords4/common/modelp.h
index 8a933372e..a25e78a11 100644
--- a/xwords4/common/modelp.h
+++ b/xwords4/common/modelp.h
@@ -92,7 +92,8 @@ void invalidateScore( ModelCtxt* model, XP_S16 player );
XP_Bool tilesInLine( ModelCtxt* model, XP_S16 turn, XP_Bool* isHorizontal );
void normalizeMoves( const ModelCtxt* model, XP_S16 turn,
XP_Bool isHorizontal, MoveInfo* moveInfo );
-void adjustScoreForUndone( ModelCtxt* model, MoveInfo* mi, XP_U16 turn );
+void normalizeMI( MoveInfo* moveInfoOut, const MoveInfo* moveInfoIn );
+void adjustScoreForUndone( ModelCtxt* model, const MoveInfo* mi, XP_U16 turn );
#ifdef CPLUS
}
#endif
diff --git a/xwords4/common/movestak.c b/xwords4/common/movestak.c
index 9f9f2130a..d49daa8e3 100644
--- a/xwords4/common/movestak.c
+++ b/xwords4/common/movestak.c
@@ -1,6 +1,6 @@
/* -*- compile-command: "cd ../linux && make -j5 MEMDEBUG=TRUE"; -*- */
/*
- * Copyright 2001-2015 by Eric House (xwords@eehouse.org). All rights
+ * Copyright 2001 - 2019 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
@@ -41,27 +41,30 @@ extern "C" {
struct StackCtxt {
VTableMgr* vtmgr;
-
XWStreamCtxt* data;
-
XWStreamPos top;
-
XWStreamPos cachedPos;
-
XP_U16 cacheNext;
XP_U16 nEntries;
XP_U16 bitsPerTile;
XP_U16 highWaterMark;
+ XP_U16 typeBits;
+ XP_U8 flags;
+
+ XP_Bool inDuplicateMode;
DIRTY_SLOT
MPSLOT
};
+#define HAVE_FLAGS_MASK ((XP_U16)0x8000)
+
void
-stack_init( StackCtxt* stack )
+stack_init( StackCtxt* stack, XP_Bool inDuplicateMode )
{
stack->nEntries = stack->highWaterMark = 0;
stack->top = START_OF_STREAM;
+ stack->inDuplicateMode = inDuplicateMode;
/* I see little point in freeing or shrinking stack->data. It'll get
shrunk to fit as soon as we serialize/deserialize anyway. */
@@ -88,13 +91,14 @@ stack_setBitsPerTile( StackCtxt* stack, XP_U16 bitsPerTile )
}
StackCtxt*
-stack_make( MPFORMAL VTableMgr* vtmgr )
+stack_make( MPFORMAL VTableMgr* vtmgr, XP_Bool inDuplicateMode )
{
StackCtxt* result = (StackCtxt*)XP_MALLOC( mpool, sizeof( *result ) );
if ( !!result ) {
XP_MEMSET( result, 0, sizeof(*result) );
MPASSIGN(result->mpool, mpool);
result->vtmgr = vtmgr;
+ result->inDuplicateMode = inDuplicateMode;
}
return result;
@@ -114,7 +118,24 @@ stack_destroy( StackCtxt* stack )
void
stack_loadFromStream( StackCtxt* stack, XWStreamCtxt* stream )
{
+ /* Problem: the moveType field is getting bigger to support
+ * DUP_MOVE_TYPE. So 3 bits are needed rather than 2. I can't use the
+ * parent stream's version since the parent stream is re-written each time
+ * the game's saved (with the new version) but the stack is not rewritten,
+ * only appended to (normally). The solution is to take advantage of the
+ * extra bits at the top of the stack's data size (nBytes below). If the
+ * first bit's set, the stream was created by code that assumes 3 bits for
+ * the moveType field.
+ */
XP_U16 nBytes = stream_getU16( stream );
+ if ( (HAVE_FLAGS_MASK & nBytes) != 0 ) {
+ stack->flags = stream_getU8( stream );
+ stack->typeBits = 3;
+ } else {
+ XP_ASSERT( 0 == stack->flags );
+ stack->typeBits = 2;
+ }
+ nBytes &= ~HAVE_FLAGS_MASK;
if ( nBytes > 0 ) {
stack->highWaterMark = stream_getU16( stream );
@@ -133,18 +154,23 @@ stack_loadFromStream( StackCtxt* stack, XWStreamCtxt* stream )
void
stack_writeToStream( const StackCtxt* stack, XWStreamCtxt* stream )
{
- XP_U16 nBytes;
+ XP_U16 nBytes = 0;
XWStreamCtxt* data = stack->data;
XWStreamPos oldPos = START_OF_STREAM;
+ /* XP_LOGF( "%s(): writing stream; hash: %X", __func__, hash ); */
+ /* XP_U32 hash = stream_getHash( data, START_OF_STREAM, XP_TRUE ); */
+
if ( !!data ) {
oldPos = stream_setPos( data, POS_READ, START_OF_STREAM );
nBytes = stream_getSize( data );
- } else {
- nBytes = 0;
}
- stream_putU16( stream, nBytes );
+ XP_ASSERT( 0 == (HAVE_FLAGS_MASK & nBytes) ); /* under 32K? I hope so */
+ stream_putU16( stream, nBytes | (stack->typeBits == 3 ? HAVE_FLAGS_MASK : 0) );
+ if ( stack->typeBits == 3 ) {
+ stream_putU8( stream, stack->flags );
+ }
if ( nBytes > 0 ) {
stream_putU16( stream, stack->highWaterMark );
@@ -166,7 +192,8 @@ stack_copy( const StackCtxt* stack )
stack->vtmgr );
stack_writeToStream( stack, stream );
- newStack = stack_make( MPPARM(stack->mpool) stack->vtmgr );
+ newStack = stack_make( MPPARM(stack->mpool) stack->vtmgr,
+ stack->inDuplicateMode );
stack_loadFromStream( newStack, stream );
stack_setBitsPerTile( newStack, stack->bitsPerTile );
stream_destroy( stream );
@@ -176,46 +203,39 @@ stack_copy( const StackCtxt* stack )
static void
pushEntryImpl( StackCtxt* stack, const StackEntry* entry )
{
- XP_U16 ii, bitsPerTile;
- XWStreamPos oldLoc;
- XP_U16 nTiles = entry->u.move.moveInfo.nTiles;
XWStreamCtxt* stream = stack->data;
+ XP_LOGF( "%s(typ=%s, player=%d)", __func__,
+ StackMoveType_2str(entry->moveType), entry->playerNum );
+
if ( !stream ) {
stream = mem_stream_make_raw( MPPARM(stack->mpool) stack->vtmgr );
stack->data = stream;
+ stack->typeBits = stack->inDuplicateMode ? 3 : 2; /* the new size */
+ XP_ASSERT( 0 == stack->flags );
}
- oldLoc = stream_setPos( stream, POS_WRITE, stack->top );
+ XWStreamPos oldLoc = stream_setPos( stream, POS_WRITE, stack->top );
- stream_putBits( stream, 2, entry->moveType );
+ stream_putBits( stream, stack->typeBits, entry->moveType );
stream_putBits( stream, 2, entry->playerNum );
switch( entry->moveType ) {
case MOVE_TYPE:
+ moveInfoToStream( stream, &entry->u.move.moveInfo, stack->bitsPerTile );
+ traySetToStream( stream, &entry->u.move.newTiles );
+ if ( stack->inDuplicateMode ) {
+ stream_putBits( stream, NPLAYERS_NBITS, entry->u.move.dup.nScores );
+ scoresToStream( stream, entry->u.move.dup.nScores, entry->u.move.dup.scores );
+ }
+ break;
case PHONY_TYPE:
-
- stream_putBits( stream, NTILES_NBITS, nTiles );
- stream_putBits( stream, 5, entry->u.move.moveInfo.commonCoord );
- stream_putBits( stream, 1, entry->u.move.moveInfo.isHorizontal );
- bitsPerTile = stack->bitsPerTile;
- XP_ASSERT( bitsPerTile == 5 || bitsPerTile == 6 );
- for ( ii = 0; ii < nTiles; ++ii ) {
- Tile tile;
- stream_putBits( stream, 5,
- entry->u.move.moveInfo.tiles[ii].varCoord );
-
- tile = entry->u.move.moveInfo.tiles[ii].tile;
- stream_putBits( stream, bitsPerTile, tile & TILE_VALUE_MASK );
- stream_putBits( stream, 1, (tile & TILE_BLANK_BIT) != 0 );
- }
- if ( entry->moveType == MOVE_TYPE ) {
- traySetToStream( stream, &entry->u.move.newTiles );
- }
+ moveInfoToStream( stream, &entry->u.phony.moveInfo, stack->bitsPerTile );
break;
case ASSIGN_TYPE:
traySetToStream( stream, &entry->u.assign.tiles );
+ XP_ASSERT( entry->playerNum == DUP_PLAYER || !stack->inDuplicateMode );
break;
case TRADE_TYPE:
@@ -226,6 +246,13 @@ pushEntryImpl( StackCtxt* stack, const StackEntry* entry )
second guy */
traySetToStream( stream, &entry->u.trade.newTiles );
break;
+ case PAUSE_TYPE:
+ stream_putBits( stream, 2, entry->u.pause.pauseType );
+ stream_putU32( stream, entry->u.pause.when );
+ stringToStream( stream, entry->u.pause.msg );
+ break;
+ default:
+ XP_ASSERT(0);
}
++stack->nEntries;
@@ -262,37 +289,22 @@ pushEntry( StackCtxt* stack, const StackEntry* entry )
static void
readEntry( const StackCtxt* stack, StackEntry* entry )
{
- XP_U16 nTiles, ii, bitsPerTile;
XWStreamCtxt* stream = stack->data;
- entry->moveType = (StackMoveType)stream_getBits( stream, 2 );
+ entry->moveType = (StackMoveType)stream_getBits( stream, stack->typeBits );
entry->playerNum = (XP_U8)stream_getBits( stream, 2 );
switch( entry->moveType ) {
-
case MOVE_TYPE:
+ moveInfoFromStream( stream, &entry->u.move.moveInfo, stack->bitsPerTile );
+ traySetFromStream( stream, &entry->u.move.newTiles );
+ if ( stack->inDuplicateMode ) {
+ entry->u.move.dup.nScores = stream_getBits( stream, NPLAYERS_NBITS );
+ scoresFromStream( stream, entry->u.move.dup.nScores, entry->u.move.dup.scores );
+ }
+ break;
case PHONY_TYPE:
- nTiles = entry->u.move.moveInfo.nTiles =
- (XP_U8)stream_getBits( stream, NTILES_NBITS );
- XP_ASSERT( nTiles <= MAX_TRAY_TILES );
- entry->u.move.moveInfo.commonCoord = (XP_U8)stream_getBits(stream, 5);
- entry->u.move.moveInfo.isHorizontal = (XP_U8)stream_getBits(stream, 1);
- bitsPerTile = stack->bitsPerTile;
- XP_ASSERT( bitsPerTile == 5 || bitsPerTile == 6 );
- for ( ii = 0; ii < nTiles; ++ii ) {
- Tile tile;
- entry->u.move.moveInfo.tiles[ii].varCoord =
- (XP_U8)stream_getBits(stream, 5);
- tile = (Tile)stream_getBits( stream, bitsPerTile );
- if ( 0 != stream_getBits( stream, 1 ) ) {
- tile |= TILE_BLANK_BIT;
- }
- entry->u.move.moveInfo.tiles[ii].tile = tile;
- }
-
- if ( entry->moveType == MOVE_TYPE ) {
- traySetFromStream( stream, &entry->u.move.newTiles );
- }
+ moveInfoFromStream( stream, &entry->u.phony.moveInfo, stack->bitsPerTile );
break;
case ASSIGN_TYPE:
@@ -305,13 +317,23 @@ readEntry( const StackCtxt* stack, StackEntry* entry )
XP_ASSERT( entry->u.trade.newTiles.nTiles
== entry->u.trade.oldTiles.nTiles );
break;
+
+ case PAUSE_TYPE:
+ entry->u.pause.pauseType = (DupPauseType)stream_getBits( stream, 2 );
+ entry->u.pause.when = stream_getU32( stream );
+ entry->u.pause.msg = stringFromStream( stack->mpool, stream );
+ break;
+
+ default:
+ XP_ASSERT(0);
}
} /* readEntry */
-void
-stack_addMove( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo,
- const TrayTileSet* newTiles )
+
+static void
+addMove( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo,
+ XP_U16 nScores, XP_U16* scores, const TrayTileSet* newTiles )
{
StackEntry move = {.playerNum = (XP_U8)turn,
.moveType = MOVE_TYPE,
@@ -320,9 +342,31 @@ stack_addMove( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo,
XP_MEMCPY( &move.u.move.moveInfo, moveInfo, sizeof(move.u.move.moveInfo));
move.u.move.newTiles = *newTiles;
+ XP_ASSERT( 0 == nScores || stack->inDuplicateMode );
+ if ( stack->inDuplicateMode ) {
+ move.u.move.dup.nScores = nScores;
+ XP_MEMCPY( &move.u.move.dup.scores[0], scores,
+ nScores * sizeof(move.u.move.dup.scores[0]) );
+ }
+
pushEntry( stack, &move );
+}
+
+void
+stack_addMove( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo,
+ const TrayTileSet* newTiles )
+{
+ addMove( stack, turn, moveInfo, 0, NULL, newTiles );
} /* stack_addMove */
+void
+stack_addDupMove( StackCtxt* stack, const MoveInfo* moveInfo,
+ XP_U16 nScores, XP_U16* scores, const TrayTileSet* newTiles )
+{
+ XP_ASSERT( stack->inDuplicateMode );
+ addMove( stack, DUP_PLAYER, moveInfo, nScores, scores, newTiles );
+}
+
void
stack_addPhony( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo )
{
@@ -336,10 +380,21 @@ stack_addPhony( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo )
pushEntry( stack, &move );
} /* stack_addPhony */
+void
+stack_addDupTrade( StackCtxt* stack, const TrayTileSet* oldTiles,
+ const TrayTileSet* newTiles )
+{
+ XP_ASSERT( stack->inDuplicateMode );
+ XP_ASSERT( oldTiles->nTiles == newTiles->nTiles );
+
+ stack_addTrade( stack, DUP_PLAYER, oldTiles, newTiles );
+}
+
void
stack_addTrade( StackCtxt* stack, XP_U16 turn,
const TrayTileSet* oldTiles, const TrayTileSet* newTiles )
{
+ XP_ASSERT( oldTiles->nTiles == newTiles->nTiles );
StackEntry move = { .playerNum = (XP_U8)turn,
.moveType = TRADE_TYPE,
};
@@ -362,6 +417,26 @@ stack_addAssign( StackCtxt* stack, XP_U16 turn, const TrayTileSet* tiles )
pushEntry( stack, &move );
} /* stack_addAssign */
+void
+stack_addPause( StackCtxt* stack, DupPauseType pauseType, XP_S16 turn,
+ XP_U32 when, const XP_UCHAR* msg )
+{
+ StackEntry move = { .moveType = PAUSE_TYPE,
+ .u.pause.pauseType = pauseType,
+ .u.pause.when = when,
+ .u.pause.msg = copyString( stack->mpool, msg ),
+ };
+
+ if ( 0 <= turn ) {
+ move.playerNum = turn; /* don't store the -1 case (pauseType==AUTOPAUSED) */
+ } else {
+ XP_ASSERT( AUTOPAUSED == pauseType );
+ }
+
+ pushEntry( stack, &move );
+ stack_freeEntry( stack, &move );
+}
+
static XP_Bool
setCacheReadyFor( StackCtxt* stack, XP_U16 nn )
{
@@ -371,6 +446,7 @@ setCacheReadyFor( StackCtxt* stack, XP_U16 nn )
for ( ii = 0; ii < nn; ++ii ) {
StackEntry dummy;
readEntry( stack, &dummy );
+ stack_freeEntry( stack, &dummy );
}
stack->cacheNext = nn;
@@ -386,7 +462,7 @@ stack_getNEntries( const StackCtxt* stack )
} /* stack_getNEntries */
XP_Bool
-stack_getNthEntry( StackCtxt* stack, XP_U16 nn, StackEntry* entry )
+stack_getNthEntry( StackCtxt* stack, const XP_U16 nn, StackEntry* entry )
{
XP_Bool found;
@@ -409,8 +485,10 @@ stack_getNthEntry( StackCtxt* stack, XP_U16 nn, StackEntry* entry )
stack->cachedPos = stream_setPos( stack->data, POS_READ, oldPos );
++stack->cacheNext;
- }
+ /* XP_LOGF( "%s(%d) (typ=%s, player=%d, num=%d)", __func__, nn, */
+ /* StackMoveType_2str(entry->moveType), entry->playerNum, entry->moveNum ); */
+ }
return found;
} /* stack_getNthEntry */
@@ -446,6 +524,18 @@ stack_redo( StackCtxt* stack, StackEntry* entry )
return canRedo;
} /* stack_redo */
+void
+stack_freeEntry( StackCtxt* stack, StackEntry* entry )
+{
+ XP_ASSERT( entry->moveType != __BOGUS );
+ switch( entry->moveType ) {
+ case PAUSE_TYPE:
+ XP_FREEP( stack->mpool, &entry->u.pause.msg );
+ break;
+ }
+ entry->moveType = __BOGUS;
+}
+
#ifdef CPLUS
}
#endif
diff --git a/xwords4/common/movestak.h b/xwords4/common/movestak.h
index 2845f628a..6da35fdeb 100644
--- a/xwords4/common/movestak.h
+++ b/xwords4/common/movestak.h
@@ -22,15 +22,21 @@
#include "comtypes.h"
#include "model.h"
+#include "dutil.h"
#include "vtabmgr.h"
#ifdef CPLUS
extern "C" {
#endif
-enum { ASSIGN_TYPE, MOVE_TYPE, TRADE_TYPE, PHONY_TYPE };
+enum { ASSIGN_TYPE, MOVE_TYPE, TRADE_TYPE, PHONY_TYPE, PAUSE_TYPE,
+ /* used for debugging, and can be changed because never stored: */
+ __BOGUS,
+};
typedef XP_U8 StackMoveType;
+#define DUP_PLAYER 0
+
typedef struct AssignRec {
TrayTileSet tiles;
} AssignRec;
@@ -43,17 +49,28 @@ typedef struct TradeRec {
typedef struct MoveRec {
MoveInfo moveInfo;
TrayTileSet newTiles;
+ struct {
+ XP_U16 nScores;
+ XP_U16 scores[MAX_NUM_PLAYERS];
+ } dup;
} MoveRec;
typedef struct PhonyRec {
MoveInfo moveInfo;
} PhonyRec;
+typedef struct _PauseRec {
+ DupPauseType pauseType;
+ XP_U32 when;
+ const XP_UCHAR* msg; /* requires stack_freeEntry() */
+} PauseRec;
+
typedef union EntryData {
AssignRec assign;
TradeRec trade;
MoveRec move;
PhonyRec phony;
+ PauseRec pause;
} EntryData;
typedef struct StackEntry {
@@ -65,10 +82,10 @@ typedef struct StackEntry {
typedef struct StackCtxt StackCtxt;
-StackCtxt* stack_make( MPFORMAL VTableMgr* vtmgr );
+StackCtxt* stack_make( MPFORMAL VTableMgr* vtmgr, XP_Bool inDuplicateMode );
void stack_destroy( StackCtxt* stack );
-void stack_init( StackCtxt* stack );
+void stack_init( StackCtxt* stack, XP_Bool inDuplicateMode );
XP_U32 stack_getHash( const StackCtxt* stack, XP_Bool correct );
void stack_setBitsPerTile( StackCtxt* stack, XP_U16 bitsPerTile );
@@ -78,19 +95,30 @@ StackCtxt* stack_copy( const StackCtxt* stack );
void stack_addMove( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo,
const TrayTileSet* newTiles );
+void stack_addDupMove( StackCtxt* stack, const MoveInfo* moveInfo,
+ XP_U16 nScores, XP_U16* scores,
+ const TrayTileSet* tiles );
void stack_addPhony( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo );
void stack_addTrade( StackCtxt* stack, XP_U16 turn,
const TrayTileSet* oldTiles,
const TrayTileSet* newTiles );
+void stack_addDupTrade( StackCtxt* stack, const TrayTileSet* oldTiles,
+ const TrayTileSet* newTiles );
+
void stack_addAssign( StackCtxt* stack, XP_U16 turn,
const TrayTileSet* tiles );
+void stack_addPause( StackCtxt* stack, DupPauseType pauseTYpe, XP_S16 turn,
+ XP_U32 when, const XP_UCHAR* msg );
+
XP_U16 stack_getNEntries( const StackCtxt* stack );
XP_Bool stack_getNthEntry( StackCtxt* stack, XP_U16 n, StackEntry* entry );
XP_Bool stack_popEntry( StackCtxt* stack, StackEntry* entry );
XP_Bool stack_redo( StackCtxt* stack, StackEntry* entry );
+
+void stack_freeEntry( StackCtxt* stack, StackEntry* entry );
#ifdef CPLUS
}
diff --git a/xwords4/common/mscore.c b/xwords4/common/mscore.c
index df6a27143..2d568767c 100644
--- a/xwords4/common/mscore.c
+++ b/xwords4/common/mscore.c
@@ -22,6 +22,7 @@
#include "util.h"
#include "engine.h"
#include "game.h"
+#include "strutils.h"
#include "LocalizedStrIncludes.h"
#ifdef CPLUS
@@ -94,7 +95,7 @@ scoreCurrentMove( ModelCtxt* model, XP_S16 turn, XWStreamCtxt* stream,
} /* scoreCurrentMove */
void
-adjustScoreForUndone( ModelCtxt* model, MoveInfo* mi, XP_U16 turn )
+adjustScoreForUndone( ModelCtxt* model, const MoveInfo* mi, XP_U16 turn )
{
XP_U16 moveScore;
PlayerCtxt* player = &model->players[turn];
@@ -102,8 +103,8 @@ adjustScoreForUndone( ModelCtxt* model, MoveInfo* mi, XP_U16 turn )
if ( mi->nTiles == 0 ) {
moveScore = 0;
} else {
- moveScore = figureMoveScore( model, turn, mi, (EngineCtxt*)NULL,
- (XWStreamCtxt*)NULL,
+ moveScore = figureMoveScore( model, turn, mi, (EngineCtxt*)NULL,
+ (XWStreamCtxt*)NULL,
(WordNotifierInfo*)NULL );
}
player->score -= moveScore;
@@ -130,14 +131,16 @@ invalidateScore( ModelCtxt* model, XP_S16 turn )
XP_Bool
getCurrentMoveScoreIfLegal( ModelCtxt* model, XP_S16 turn,
XWStreamCtxt* stream,
- WordNotifierInfo* wni, XP_S16* score )
+ WordNotifierInfo* wni, XP_S16* scoreP )
{
PlayerCtxt* player = &model->players[turn];
if ( !player->curMoveValid ) {
scoreCurrentMove( model, turn, stream, wni );
}
- *score = player->curMoveScore;
+ if ( !!scoreP ) {
+ *scoreP = player->curMoveScore;
+ }
return player->curMoveScore != ILLEGAL_MOVE_SCORE;
} /* getCurrentMoveScoreIfLegal */
@@ -277,42 +280,56 @@ tilesInLine( ModelCtxt* model, XP_S16 turn, XP_Bool* isHorizontal )
} /* tilesInLine */
void
-normalizeMoves( const ModelCtxt* model, XP_S16 turn, XP_Bool isHorizontal,
- MoveInfo* moveInfo )
+normalizeMI( MoveInfo* moveInfoOut, const MoveInfo* moveInfoIn )
{
- XP_S16 lowCol, ii, jj, thisCol; /* unsigned is a problem on palm */
- const PlayerCtxt* player = &model->players[turn];
- XP_U16 nTiles = player->nPending;
- XP_S16 lastTaken;
- short lowIndex = 0;
- const PendingTile* pt;
+ /* use scratch in case in and out are same */
+ MoveInfo tmp = *moveInfoIn;
+ // const XP_Bool isHorizontal = tmp.isHorizontal;
- moveInfo->isHorizontal = isHorizontal;
- moveInfo->nTiles = (XP_U8)nTiles;
-
- lastTaken = -1;
- for ( ii = 0; ii < nTiles; ++ii ) {
- lowCol = 100; /* high enough to always be changed */
- for ( jj = 0; jj < nTiles; ++jj ) {
- pt = &player->pendingTiles[jj];
- thisCol = isHorizontal? pt->col:pt->row;
- if (thisCol < lowCol && thisCol > lastTaken ) {
- lowCol = thisCol;
+ XP_S16 lastTaken = -1;
+ XP_U16 next = 0;
+ for ( XP_U16 ii = 0; ii < tmp.nTiles; ++ii ) {
+ XP_U16 lowest = 100; /* high enough to always be changed */
+ XP_U16 lowIndex = 100;
+ for ( XP_U16 jj = 0; jj < tmp.nTiles; ++jj ) {
+ XP_U16 cur = moveInfoIn->tiles[jj].varCoord;
+ if ( cur < lowest && cur > lastTaken ) {
+ lowest = cur;
lowIndex = jj;
}
}
- /* we've found the next to transfer (4 bytes smaller without a temp
- local ptr. */
- pt = &player->pendingTiles[lowIndex];
- lastTaken = lowCol;
- moveInfo->tiles[ii].varCoord = (XP_U8)lastTaken;
- moveInfo->tiles[ii].tile = pt->tile;
+ XP_ASSERT( lowIndex < MAX_ROWS );
+ tmp.tiles[next++] = moveInfoIn->tiles[lowIndex];
+
+ lastTaken = lowest;
}
+ XP_ASSERT( next == tmp.nTiles );
+ *moveInfoOut = tmp;
+}
+
+void
+normalizeMoves( const ModelCtxt* model, XP_S16 turn, XP_Bool isHorizontal,
+ MoveInfo* moveInfo )
+{
+ const PlayerCtxt* player = &model->players[turn];
+ const XP_U16 nTiles = player->nPending;
+
+ moveInfo->isHorizontal = isHorizontal;
+ moveInfo->nTiles = nTiles;
+
if ( 0 < nTiles ) {
- pt = &player->pendingTiles[0];
+ const PendingTile* pt = &player->pendingTiles[0];
moveInfo->commonCoord = isHorizontal? pt->row:pt->col;
+
+ for ( XP_U16 ii = 0; ii < nTiles; ++ii ) {
+ const PendingTile* pt = &player->pendingTiles[ii];
+ moveInfo->tiles[ii].tile = pt->tile;
+ moveInfo->tiles[ii].varCoord = isHorizontal? pt->col:pt->row;
+ }
+
+ normalizeMI( moveInfo, moveInfo );
}
} /* normalizeMoves */
@@ -452,7 +469,7 @@ isLegalMove( ModelCtxt* model, MoveInfo* mInfo, XP_Bool silent )
} /* isLegalMove */
XP_U16
-figureMoveScore( const ModelCtxt* model, XP_U16 turn, MoveInfo* moveInfo,
+figureMoveScore( const ModelCtxt* model, XP_U16 turn, const MoveInfo* moveInfo,
EngineCtxt* engine, XWStreamCtxt* stream,
WordNotifierInfo* notifyInfo )
{
@@ -464,7 +481,7 @@ figureMoveScore( const ModelCtxt* model, XP_U16 turn, MoveInfo* moveInfo,
short moveMultiplier = 1;
short multipliers[MAX_TRAY_TILES];
MoveInfo tmpMI;
- MoveInfoTile* tiles;
+ const MoveInfoTile* tiles;
XP_U16 nTiles = moveInfo->nTiles;
XP_ASSERT( nTiles > 0 );
@@ -573,6 +590,8 @@ scoreWord( const ModelCtxt* model, XP_U16 turn,
XP_U16 firstCoord = tiles->varCoord;
DictionaryCtxt* dict = model_getPlayerDict( model, turn );
+ assertSorted( movei );
+
if ( movei->isHorizontal ) {
row = movei->commonCoord;
incr = &col;
diff --git a/xwords4/common/nli.c b/xwords4/common/nli.c
index c65d708b9..59cd6d024 100644
--- a/xwords4/common/nli.c
+++ b/xwords4/common/nli.c
@@ -27,7 +27,8 @@
/* Don't check in other than 0 for a few releases!!! */
#ifndef NLI_VERSION
-# define NLI_VERSION 0
+// # define NLI_VERSION 0
+# define NLI_VERSION 1 /* adds inDuplicateMode */
#endif
void
@@ -41,6 +42,7 @@ nli_init( NetLaunchInfo* nli, const CurGameInfo* gi, const CommsAddrRec* addr,
nli->nPlayersT = gi->nPlayers;
nli->nPlayersH = nPlayers;
nli->forceChannel = forceChannel;
+ nli->inDuplicateMode = gi->inDuplicateMode;
CommsConnType typ;
for ( XP_U32 st = 0; addr_iter( addr, &typ, &st ); ) {
@@ -118,6 +120,7 @@ nli_saveToStream( const NetLaunchInfo* nli, XWStreamCtxt* stream )
if ( NLI_VERSION > 0 ) {
stream_putBits( stream, 1, nli->remotesAreRobots ? 1 : 0 );
+ stream_putBits( stream, 1, nli->inDuplicateMode ? 1 : 0 );
}
}
@@ -157,9 +160,13 @@ nli_makeFromStream( NetLaunchInfo* nli, XWStreamCtxt* stream )
nli->osVers = stream_getU32( stream );
}
- if ( version > 0 ) {
+ if ( version > 0 && 0 < stream_getSize( stream ) ) {
nli->remotesAreRobots = 0 != stream_getBits( stream, 1 );
- XP_LOGF( "%s(): remotesAreRobots: %d", __func__, nli->remotesAreRobots );
+ nli->inDuplicateMode = stream_getBits( stream, 1 );
+ XP_LOGF( "%s(): remotesAreRobots: %d; inDuplicateMode: %d", __func__,
+ nli->remotesAreRobots, nli->inDuplicateMode );
+ } else {
+ nli->inDuplicateMode = XP_FALSE;
}
XP_ASSERT( 0 == stream_getSize( stream ) );
diff --git a/xwords4/common/nli.h b/xwords4/common/nli.h
index c0a22ccdb..9d9a7ddde 100644
--- a/xwords4/common/nli.h
+++ b/xwords4/common/nli.h
@@ -46,6 +46,7 @@ typedef struct _InviteInfo {
XP_U8 nPlayersT;
XP_U8 nPlayersH;
XP_Bool remotesAreRobots;
+ XP_Bool inDuplicateMode;
/* Relay */
XP_UCHAR room[MAX_INVITE_LEN + 1];
diff --git a/xwords4/common/nwgamest.c b/xwords4/common/nwgamest.c
index e4b02feac..135f6f8c5 100644
--- a/xwords4/common/nwgamest.c
+++ b/xwords4/common/nwgamest.c
@@ -53,6 +53,7 @@ struct NewGameCtx {
#ifndef XWFEATURE_STANDALONE_ONLY
XP_TriEnable settingsEnabled;
#endif
+ XP_Bool duplicateEnabled;
MPSLOT
};
@@ -150,6 +151,15 @@ newg_load( NewGameCtx* ngc, const CurGameInfo* gi )
(*ngc->enableAttrProc)( closure, NG_ATTR_NPLAYERS, ngc->isNewGame?
TRI_ENAB_ENABLED : TRI_ENAB_DISABLED );
+
+ ngc->timerSeconds = gi->gameSeconds;
+ value.ng_u16 = ngc->timerSeconds;
+ (*ngc->setAttrProc)( closure, NG_ATTR_TIMER, value );
+
+ ngc->duplicateEnabled = gi->inDuplicateMode;
+ value.ng_bool = ngc->duplicateEnabled;
+ (*ngc->setAttrProc)( closure, NG_ATTR_DUPLICATE, value );
+
ngc->timerSeconds = gi->gameSeconds;
value.ng_u16 = ngc->timerSeconds;
(*ngc->setAttrProc)( closure, NG_ATTR_TIMER, value );
@@ -220,7 +230,6 @@ XP_Bool
newg_store( NewGameCtx* ngc, CurGameInfo* gi,
XP_Bool XP_UNUSED_STANDALONE(warn) )
{
- XP_U16 player;
XP_Bool consistent = checkConsistent( ngc, warn );
if ( consistent ) {
@@ -230,10 +239,15 @@ newg_store( NewGameCtx* ngc, CurGameInfo* gi,
gi->serverRole = ngc->role;
makeLocal = ngc->role != SERVER_ISSERVER;
#endif
+
gi->gameSeconds = ngc->timerSeconds;
gi->timerEnabled = gi->gameSeconds > 0;
- for ( player = 0; player < MAX_NUM_PLAYERS; ++player ) {
+ gi->inDuplicateMode = ngc->duplicateEnabled;
+ gi->gameSeconds = ngc->timerSeconds;
+ gi->timerEnabled = gi->gameSeconds > 0;
+
+ for ( XP_U16 player = 0; player < MAX_NUM_PLAYERS; ++player ) {
storePlayer( ngc, player, &gi->players[player] );
if ( makeLocal ) {
gi->players[player].isLocal = XP_TRUE;
@@ -273,6 +287,9 @@ newg_attrChanged( NewGameCtx* ngc, NewGameAttr attr, NGValue value )
case NG_ATTR_TIMER:
ngc->timerSeconds = value.ng_u16;
break;
+ case NG_ATTR_DUPLICATE:
+ ngc->duplicateEnabled = value.ng_bool;
+ break;
default:
XP_ASSERT( 0 );
}
diff --git a/xwords4/common/nwgamest.h b/xwords4/common/nwgamest.h
index 668011273..a9b129317 100644
--- a/xwords4/common/nwgamest.h
+++ b/xwords4/common/nwgamest.h
@@ -1,6 +1,7 @@
/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */
/*
- * Copyright 1997 - 2006 by Eric House (xwords@eehouse.org). All rights reserved.
+ * Copyright 1997 - 2019 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
@@ -60,6 +61,7 @@ typedef enum {
,NG_ATTR_NPLAYHEADER
,NG_ATTR_CANJUGGLE
,NG_ATTR_TIMER
+ ,NG_ATTR_DUPLICATE
} NewGameAttr;
typedef union NGValue {
diff --git a/xwords4/common/pool.c b/xwords4/common/pool.c
index a4223ddd5..6279f8c9d 100644
--- a/xwords4/common/pool.c
+++ b/xwords4/common/pool.c
@@ -144,7 +144,7 @@ getRandomTile( PoolContext* pool )
} /* getRandomTile */
void
-pool_requestTiles( PoolContext* pool, Tile* tiles, XP_U8* maxNum )
+pool_requestTiles( PoolContext* pool, Tile* tiles, XP_U16* maxNum )
{
XP_S16 numWanted = *maxNum;
XP_U16 numWritten = 0;
@@ -163,19 +163,24 @@ pool_requestTiles( PoolContext* pool, Tile* tiles, XP_U8* maxNum )
*tiles++ = t;
++numWritten;
}
- *maxNum = (XP_U8)numWritten;
+ *maxNum = numWritten;
#ifdef BLANKS_FIRST
pool->lettersLeft[pool->blankIndex] = oldCount - 1;
#endif
+
+ XP_LOGF( "%s: %d tiles left in pool", __func__, pool->numTilesLeft );
} /* pool_requestTiles */
void
pool_replaceTiles( PoolContext* pool, const TrayTileSet* tiles )
{
- XP_U16 nTiles = tiles->nTiles;
- const Tile* tilesP = tiles->tiles;
+ pool_replaceTiles2( pool, tiles->nTiles, tiles->tiles );
+}
+void
+pool_replaceTiles2( PoolContext* pool, XP_U16 nTiles, const Tile* tilesP )
+{
while ( nTiles-- ) {
Tile tile = *tilesP++; /* do I need to filter off high bits? */
@@ -238,8 +243,7 @@ pool_getNTilesLeftFor( const PoolContext* pool, Tile tile )
void
pool_initFromDict( PoolContext* pool, DictionaryCtxt* dict )
{
- XP_U16 numFaces = dict_numTileFaces( dict );
- Tile ii;
+ const XP_U16 numFaces = dict_numTileFaces( dict );
XP_FREEP( pool->mpool, &pool->lettersLeft );
@@ -248,7 +252,7 @@ pool_initFromDict( PoolContext* pool, DictionaryCtxt* dict )
numFaces * sizeof(pool->lettersLeft[0]) );
pool->numTilesLeft = 0;
- for ( ii = 0; ii < numFaces; ++ii ) {
+ for ( Tile ii = 0; ii < numFaces; ++ii ) {
XP_U16 numTiles = dict_numTiles( dict, ii );
pool->lettersLeft[ii] = (XP_U8)numTiles;
pool->numTilesLeft += numTiles;
@@ -276,5 +280,20 @@ checkTilesLeft( const PoolContext* pool )
}
XP_ASSERT( count == pool->numTilesLeft );
}
+
+void
+pool_dumpSelf( const PoolContext* pool )
+{
+ XP_UCHAR buf[256] = {0};
+ XP_U16 offset = 0;
+ for ( Tile tile = 0; tile < pool->numFaces; ++tile ) {
+ XP_U16 count = pool->lettersLeft[tile];
+ if ( count > 0 ) {
+ offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset, "%x/%d,", tile, count );
+ }
+ }
+ XP_LOGF( "%s(): {numTiles: %d, pool: %s}", __func__,
+ pool->numTilesLeft, buf );
+}
#endif
diff --git a/xwords4/common/pool.h b/xwords4/common/pool.h
index c3c8b57c4..37c9df892 100644
--- a/xwords4/common/pool.h
+++ b/xwords4/common/pool.h
@@ -25,8 +25,10 @@
#include "model.h"
void pool_requestTiles( PoolContext* pool, Tile* tiles,
- /*in out*/ XP_U8* maxNum );
+ /*in out*/ XP_U16* maxNum );
void pool_replaceTiles( PoolContext* pool, const TrayTileSet* tiles );
+void pool_replaceTiles2( PoolContext* pool, XP_U16 nTiles, const Tile* tilesP );
+
void pool_removeTiles( PoolContext* pool, const TrayTileSet* tiles );
XP_Bool pool_containsTiles( const PoolContext* pool,
const TrayTileSet* tiles );
@@ -42,4 +44,8 @@ void pool_initFromDict( PoolContext* pool, DictionaryCtxt* dict );
void pool_writeToStream( PoolContext* pool, XWStreamCtxt* stream );
PoolContext* pool_makeFromStream( MPFORMAL XWStreamCtxt* stream );
+#ifdef DEBUG
+void pool_dumpSelf( const PoolContext* pool );
+#endif
+
#endif
diff --git a/xwords4/common/scorebdp.c b/xwords4/common/scorebdp.c
index 45be5af02..63b920bc1 100644
--- a/xwords4/common/scorebdp.c
+++ b/xwords4/common/scorebdp.c
@@ -57,7 +57,6 @@ drawScoreBoard( BoardCtxt* board )
if ( draw_scoreBegin( board->draw, &board->scoreBdBounds, nPlayers,
scores.arr, nTilesInPool, dfs ) ) {
- XP_S16 curTurn = server_getCurrentTurn( board->server );
XP_U16 selPlayer = board->selPlayer;
XP_Rect playerRects[nPlayers];
XP_U16 remDim;
@@ -117,7 +116,7 @@ drawScoreBoard( BoardCtxt* board )
#endif
dsi->playerNum = ii;
dsi->totalScore = scores.arr[ii];
- dsi->isTurn = (ii == curTurn);
+ dsi->isTurn = server_isPlayersTurn( board->server, ii );
dsi->name = emptyStringIfNull(lp->name);
dsi->selected = board->trayVisState != TRAY_HIDDEN
&& ii==selPlayer;
@@ -175,7 +174,6 @@ drawScoreBoard( BoardCtxt* board )
XP_ASSERT( nPlayers <= MAX_NUM_PLAYERS );
if ( nPlayers > 0 ) {
ModelCtxt* model = board->model;
- XP_S16 curTurn = server_getCurrentTurn( board->server, NULL );
XP_U16 selPlayer = board->selPlayer;
XP_S16 nTilesInPool = server_countTilesInPool( board->server );
XP_Rect scoreRect = board->scoreBdBounds;
@@ -259,7 +257,7 @@ drawScoreBoard( BoardCtxt* board )
#endif
dp->dsi.playerNum = ii;
dp->dsi.totalScore = scores.arr[ii];
- dp->dsi.isTurn = (ii == curTurn);
+ dp->dsi.isTurn = server_isPlayersTurn( board->server, ii );
dp->dsi.name = emptyStringIfNull(lp->name);
dp->dsi.selected = board->trayVisState != TRAY_HIDDEN
&& ii==selPlayer;
@@ -341,25 +339,17 @@ drawScoreBoard( BoardCtxt* board )
} /* drawScoreBoard */
#endif
-static XP_S16
-figureSecondsLeft( BoardCtxt* board )
-{
- CurGameInfo* gi = board->gi;
- XP_U16 secondsUsed = gi->players[board->selPlayer].secondsUsed;
- XP_U16 secondsAvailable = gi->gameSeconds / gi->nPlayers;
- XP_ASSERT( gi->timerEnabled );
- return secondsAvailable - secondsUsed;
-} /* figureSecondsLeft */
-
void
-drawTimer( BoardCtxt* board )
+drawTimer( const BoardCtxt* board )
{
- if ( board->gi->timerEnabled && 0 < board->timerBounds.width
- && 0 < board->timerBounds.height ) {
- XP_S16 secondsLeft = figureSecondsLeft( board );
-
+ if ( !!board->draw && board->gi->timerEnabled ) {
+ XP_S16 secondsLeft = server_getTimerSeconds( board->server,
+ board->selPlayer );
+ XP_Bool turnDone = board->gi->inDuplicateMode
+ ? server_dupTurnDone( board->server, board->selPlayer )
+ : XP_FALSE;
draw_drawTimer( board->draw, &board->timerBounds,
- board->selPlayer, secondsLeft );
+ board->selPlayer, secondsLeft, turnDone );
}
} /* drawTimer */
diff --git a/xwords4/common/scorebdp.h b/xwords4/common/scorebdp.h
index 0cb6d89f0..0468a7d5f 100644
--- a/xwords4/common/scorebdp.h
+++ b/xwords4/common/scorebdp.h
@@ -24,7 +24,7 @@
void drawScoreBoard( BoardCtxt* board );
XP_S16 figureScoreRectTapped( const BoardCtxt* board, XP_U16 x, XP_U16 y );
-void drawTimer( BoardCtxt* board );
+void drawTimer( const BoardCtxt* board );
void penTimerFiredScore( const BoardCtxt* board );
#if defined POINTER_SUPPORT || defined KEYBOARD_NAV
diff --git a/xwords4/common/server.c b/xwords4/common/server.c
index 150e03b1a..8ac1c8e23 100644
--- a/xwords4/common/server.c
+++ b/xwords4/common/server.c
@@ -18,8 +18,6 @@
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
-/* #include */
-
#include "comtypes.h"
#include "server.h"
#include "util.h"
@@ -27,7 +25,6 @@
#include "comms.h"
#include "memstream.h"
#include "game.h"
-/* #include "board.h" */
#include "states.h"
#include "xwproto.h"
#include "util.h"
@@ -51,6 +48,12 @@ enum {
};
typedef XP_U8 GameEndReason;
+typedef enum {DUPE_STUFF_TRADES_SERVER,
+ DUPE_STUFF_MOVES_SERVER,
+ DUPE_STUFF_MOVE_CLIENT,
+ DUPE_STUFF_PAUSE,
+} DUPE_STUFF;
+
typedef struct ServerPlayer {
EngineCtxt* engine; /* each needs his own so don't interfere each other */
XP_S8 deviceIndex; /* 0 means local, -1 means unknown */
@@ -76,8 +79,11 @@ typedef struct ServerVolatiles {
CurGameInfo* gi;
TurnChangeListener turnChangeListener;
void* turnChangeData;
+ TimerChangeListener timerChangeListener;
+ void* timerChangeData;
GameOverListener gameOverListener;
void* gameOverData;
+ XP_U16 bitsPerTile;
XP_Bool showPrevMove;
XP_Bool pickTilesCalled[MAX_NUM_PLAYERS];
} ServerVolatiles;
@@ -134,6 +140,8 @@ struct ServerCtxt {
# define ROBOTWAITING(s) XP_FALSE
#endif
+# define dupTimerRunning() server_canPause(server)
+
#define NPASSES_OK(s) model_recentPassCountOk((s)->vol.model)
@@ -141,7 +149,11 @@ struct ServerCtxt {
static XP_Bool assignTilesToAll( ServerCtxt* server );
static void makePoolOnce( ServerCtxt* server );
-static void resetEngines( ServerCtxt* server );
+static XP_S8 getIndexForDevice( const ServerCtxt* server,
+ XP_PlayerAddr channelNo );
+static XP_S8 getIndexForStream( const ServerCtxt* server,
+ const XWStreamCtxt* stream );
+
static void nextTurn( ServerCtxt* server, XP_S16 nxtTurn );
static void doEndGame( ServerCtxt* server, XP_S16 quitter );
@@ -154,6 +166,25 @@ static XWStreamCtxt* mkServerStream( ServerCtxt* server );
static void fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch,
const TrayTileSet* tradedTiles,
TrayTileSet* resultTiles );
+static void finishMove( ServerCtxt* server, TrayTileSet* newTiles,
+ XP_U16 turn );
+static XP_Bool dupe_checkTurns( ServerCtxt* server );
+static void dupe_forceCommits( ServerCtxt* server );
+
+static void dupe_clearState( ServerCtxt* server );
+static XP_U16 dupe_nextTurn( const ServerCtxt* server );
+static void dupe_commitAndReportMove( ServerCtxt* server, XP_U16 winner,
+ XP_U16 nPlayers, XP_U16* scores,
+ XP_U16 nTiles );
+static XP_Bool commitMoveImpl( ServerCtxt* server, XP_U16 player,
+ TrayTileSet* newTilesP, XP_Bool forced );
+static void dupe_makeAndReportTrade( ServerCtxt* server );
+static void dupe_transmitPause( ServerCtxt* server, DupPauseType typ,
+ XP_U16 turn, const XP_UCHAR* msg,
+ XP_S16 skipDev );
+static void dupe_resetTimer( ServerCtxt* server );
+static XP_Bool setDupCheckTimer( ServerCtxt* server );
+static void sortTilesIf( ServerCtxt* server, XP_S16 turn );
#ifndef XWFEATURE_STANDALONE_ONLY
static XWStreamCtxt* messageStreamWithHeader( ServerCtxt* server,
@@ -198,21 +229,31 @@ getStateStr( XW_State st )
#if 0
//def DEBUG
static void
-logNewState( XW_State old, XW_State newst )
+logNewState( XW_State old, XW_State newst, const char* caller )
{
if ( old != newst ) {
char* oldStr = getStateStr(old);
char* newStr = getStateStr(newst);
- XP_LOGF( "state transition %s => %s", oldStr, newStr );
+ XP_LOGF( "state transition %s => %s (from %s())", oldStr, newStr, caller );
}
}
-# define SETSTATE( s, st ) { XW_State old = (s)->nv.gameState; \
- (s)->nv.gameState = (st); \
- logNewState(old, st); }
+# define SETSTATE( s, st ) { \
+ XW_State old = (s)->nv.gameState; \
+ (s)->nv.gameState = (st); \
+ logNewState( old, st, __func__); \
+ }
#else
-# define SETSTATE( s, st ) (s)->nv.gameState = (st)
+# define SETSTATE( s, st ) (s)->nv.gameState = (st)
#endif
+static XP_Bool
+inDuplicateMode( const ServerCtxt* server )
+{
+ XP_Bool result = server->vol.gi->inDuplicateMode;
+ // LOG_RETURNF( "%d", result );
+ return result;
+}
+
/*****************************************************************************
*
****************************************************************************/
@@ -690,8 +731,7 @@ static void
sendChatToClientsExcept( ServerCtxt* server, XP_U16 skip, const XP_UCHAR* msg,
XP_S8 from, XP_U32 timestamp )
{
- XP_U16 devIndex;
- for ( devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) {
+ for ( XP_U16 devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) {
if ( devIndex != skip ) {
sendChatTo( server, devIndex, msg, from, timestamp );
}
@@ -708,6 +748,24 @@ server_sendChat( ServerCtxt* server, const XP_UCHAR* msg, XP_S16 from )
sendChatToClientsExcept( server, SERVER_DEVICE, msg, from, timestamp );
}
}
+
+static XP_Bool
+receiveChat( ServerCtxt* server, XWStreamCtxt* incoming )
+{
+ XP_UCHAR* msg = stringFromStream( server->mpool, incoming );
+ XP_S16 from = 1 <= stream_getSize( incoming )
+ ? stream_getU8( incoming ) : -1;
+ XP_U32 timestamp = sizeof(timestamp) <= stream_getSize( incoming )
+ ? stream_getU32( incoming ) : 0;
+ if ( amServer( server ) ) {
+ XP_U16 sourceClientIndex = getIndexForStream( server, incoming );
+ sendChatToClientsExcept( server, sourceClientIndex, msg, from,
+ timestamp );
+ }
+ util_showChat( server->vol.util, msg, from, timestamp );
+ XP_FREE( server->mpool, msg );
+ return XP_TRUE;
+}
#endif
static void
@@ -718,14 +776,24 @@ callTurnChangeListener( const ServerCtxt* server )
}
} /* callTurnChangeListener */
+static void
+callDupTimerListener( const ServerCtxt* server, XP_S32 oldVal, XP_S32 newVal )
+{
+ if ( server->vol.timerChangeListener != NULL ) {
+ (*server->vol.timerChangeListener)( server->vol.timerChangeData,
+ server->vol.gi->gameID, oldVal, newVal );
+ } else {
+ XP_LOGF( "%s(): no listener!!", __func__ );
+ }
+}
+
#ifndef XWFEATURE_STANDALONE_ONLY
# ifdef STREAM_VERS_BIGBOARD
static void
setStreamVersion( ServerCtxt* server )
{
- XP_U16 devIndex;
XP_U8 streamVersion = CUR_STREAM_VERS;
- for ( devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) {
+ for ( XP_U16 devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) {
XP_U8 devVersion = server->nv.addresses[devIndex].streamVersion;
if ( devVersion < streamVersion ) {
streamVersion = devVersion;
@@ -816,6 +884,351 @@ handleRegistrationMsg( ServerCtxt* server, XWStreamCtxt* stream )
return success;
} /* handleRegistrationMsg */
+
+static XP_U16
+bitsPerTile( ServerCtxt* server )
+{
+ if ( 0 == server->vol.bitsPerTile ) {
+ DictionaryCtxt* dict = model_getDictionary( server->vol.model );
+ XP_U16 nFaces = dict_numTileFaces( dict );
+ server->vol.bitsPerTile = nFaces <= 32? 5 : 6;
+ }
+ return server->vol.bitsPerTile;
+}
+
+static void
+dupe_setupShowTrade( ServerCtxt* server, XP_U16 nTiles )
+{
+ if ( server->nv.showRobotScores ) {
+ XP_ASSERT( !server->nv.prevMoveStream );
+
+ XWStreamCtxt* stream = mkServerStream( server );
+
+ XP_UCHAR buf[64];
+ XP_SNPRINTF( buf, VSIZE(buf), "No moves made; traded %d tiles", nTiles );
+ stream_catString( stream, buf );
+
+ server->nv.prevMoveStream = stream;
+ server->vol.showPrevMove = XP_TRUE;
+ }
+}
+
+static void
+dupe_setupShowMove( ServerCtxt* server, XP_U16* scores )
+{
+ if ( server->nv.showRobotScores ) {
+ XP_ASSERT( !server->nv.prevMoveStream );
+
+ const CurGameInfo* gi = server->vol.gi;
+ const XP_U16 nPlayers = gi->nPlayers;
+
+ XWStreamCtxt* stream = mkServerStream( server );
+
+ XP_U16 lastMax = 0x7FFF;
+ for ( XP_U16 nDone = 0; nDone < nPlayers; ) {
+
+ /* Find the largest score we haven't already done */
+ XP_U16 thisMax = 0;
+ for ( XP_U16 ii = 0; ii < nPlayers; ++ii ) {
+ XP_U16 score = scores[ii];
+ if ( score < lastMax && score > thisMax ) {
+ thisMax = score;
+ }
+ }
+
+ /* Process everybody with that score */
+ for ( XP_U16 ii = 0; ii < nPlayers; ++ii ) {
+ if ( scores[ii] == thisMax ) {
+ ++nDone;
+ XP_UCHAR buf[64];
+ XP_SNPRINTF( buf, VSIZE(buf), "%s: %d points\n",
+ gi->players[ii].name, scores[ii] );
+ stream_catString( stream, buf );
+ }
+ }
+ lastMax = thisMax;
+ }
+
+ server->nv.prevMoveStream = stream;
+ server->vol.showPrevMove = XP_TRUE;
+ }
+}
+
+static void
+addDupeStuffMark( XWStreamCtxt* stream, DUPE_STUFF typ )
+{
+ stream_putBits( stream, 3, typ );
+}
+
+static DUPE_STUFF
+getDupeStuffMark( XWStreamCtxt* stream )
+{
+ return (DUPE_STUFF)stream_getBits( stream, 3 );
+}
+
+/* Called on server when client has sent a message giving its local players'
+ duplicate moves for a single turn. */
+static XP_Bool
+dupe_handleClientMoves( ServerCtxt* server, XWStreamCtxt* stream )
+{
+ LOG_FUNC();
+ ModelCtxt* model = server->vol.model;
+ XP_Bool success = XP_TRUE;
+
+ XP_U16 movesInMsg = (XP_U16)stream_getBits( stream, NPLAYERS_NBITS );
+ XP_LOGF( "%s(): reading %d moves", __func__, movesInMsg );
+ for ( XP_U16 ii = 0; success && ii < movesInMsg; ++ii ) {
+ XP_U16 turn = (XP_U16)stream_getBits( stream, PLAYERNUM_NBITS );
+ XP_Bool forced = (XP_Bool)stream_getBits( stream, 1 );
+
+ model_resetCurrentTurn( model, turn );
+ success = model_makeTurnFromStream( model, turn, stream );
+ XP_ASSERT( success ); /* shouldn't fail in duplicate case */
+ if ( success ) {
+ XP_ASSERT( !server->nv.dupTurnsMade[turn] ); /* firing */
+ XP_ASSERT( !server->vol.gi->players[turn].isLocal );
+ server->nv.dupTurnsMade[turn] = XP_TRUE;
+ server->nv.dupTurnsForced[turn] = forced;
+ }
+ }
+
+ if ( success ) {
+ dupe_checkTurns( server );
+ nextTurn( server, PICK_NEXT );
+ }
+
+ LOG_RETURNF( "%d", success );
+ return success;
+}
+
+static void
+updateOthersTiles( ServerCtxt* server )
+{
+ sortTilesIf( server, DUP_PLAYER );
+ model_cloneDupeTrays( server->vol.model );
+}
+
+static XP_Bool
+checkDupTimerProc( void* closure, XWTimerReason XP_why )
+{
+ XP_ASSERT( XP_why == TIMER_DUP_TIMERCHECK );
+ ServerCtxt* server = (ServerCtxt*)closure;
+ XP_ASSERT( inDuplicateMode( server ) );
+ // Don't call server_do() if the timer hasn't fired yet
+ return setDupCheckTimer( server ) || server_do( server );
+}
+
+static XP_Bool
+setDupCheckTimer( ServerCtxt* server )
+{
+ XP_Bool set = XP_FALSE;
+ XP_U32 now = dutil_getCurSeconds( server->vol.dutil );
+ if ( server->nv.dupTimerExpires > 0 && server->nv.dupTimerExpires > now ) {
+ XP_U32 diff = server->nv.dupTimerExpires - now;
+ XP_ASSERT( diff <= 0x7FFF );
+ XP_U16 whenSeconds = (XP_U16) diff;
+ util_setTimer( server->vol.util, TIMER_DUP_TIMERCHECK, whenSeconds,
+ checkDupTimerProc, server );
+ set = XP_TRUE;
+ }
+ return set;
+}
+
+static void
+setDupTimerExpires( ServerCtxt* server, XP_S32 newVal )
+{
+ XP_LOGF( "%s(%d)", __func__, newVal );
+ if ( newVal != server->nv.dupTimerExpires ) {
+ XP_S32 oldVal = server->nv.dupTimerExpires;
+ server->nv.dupTimerExpires = newVal;
+ callDupTimerListener( server, oldVal, newVal );
+ }
+}
+
+static void
+dupe_resetTimer( ServerCtxt* server )
+{
+ XP_S32 newVal = 0;
+ if ( server->vol.gi->timerEnabled && 0 < server->vol.gi->gameSeconds ) {
+ XP_U32 now = dutil_getCurSeconds( server->vol.dutil );
+ newVal = now + server->vol.gi->gameSeconds;
+ } else {
+ XP_LOGF( "%s(): doing nothing because timers disabled", __func__ );
+ }
+
+ if ( server_canUnpause( server ) ) {
+ XP_U32 now = dutil_getCurSeconds( server->vol.dutil );
+ newVal = -(newVal - now);
+ }
+ setDupTimerExpires( server, newVal );
+
+ setDupCheckTimer( server );
+}
+
+XP_S32
+server_getDupTimerExpires( const ServerCtxt* server )
+{
+ return server->nv.dupTimerExpires;
+}
+
+/* If we're in dup mode, this is 0 if no timer otherwise the number of seconds
+ left. */
+XP_S16
+server_getTimerSeconds( const ServerCtxt* server, XP_U16 turn )
+{
+ XP_S16 result;
+ if ( inDuplicateMode( server ) ) {
+ XP_S32 dupTimerExpires = server->nv.dupTimerExpires;
+ if ( dupTimerExpires <= 0 ) {
+ result = (XP_S16)-dupTimerExpires;
+ } else {
+ XP_U32 now = dutil_getCurSeconds( server->vol.dutil );
+ result = dupTimerExpires > now ? dupTimerExpires - now : 0;
+ }
+ XP_ASSERT( result >= 0 ); /* should never go negative */
+ } else {
+ CurGameInfo* gi = server->vol.gi;
+ XP_U16 secondsUsed = gi->players[turn].secondsUsed;
+ XP_U16 secondsAvailable = gi->gameSeconds / gi->nPlayers;
+ XP_ASSERT( gi->timerEnabled );
+ result = secondsAvailable - secondsUsed;
+ }
+ return result;
+}
+
+XP_Bool
+server_canPause( const ServerCtxt* server )
+{
+ XP_Bool result = inDuplicateMode( server )
+ && 0 < server_getDupTimerExpires( server );
+ /* LOG_RETURNF( "%d", result ); */
+ return result;
+}
+
+XP_Bool
+server_canUnpause( const ServerCtxt* server )
+{
+ XP_Bool result = inDuplicateMode( server )
+ && 0 > server_getDupTimerExpires( server );
+ /* LOG_RETURNF( "%d", result ); */
+ return result;
+}
+
+static void
+pauseImpl( ServerCtxt* server )
+{
+ XP_ASSERT( server_canPause( server ) );
+ /* Figure out how many seconds are left on the timer, and set timer to the
+ negative of that (since negative is the flag) */
+ XP_U32 now = dutil_getCurSeconds( server->vol.dutil );
+ setDupTimerExpires( server, -(server->nv.dupTimerExpires - now) );
+ XP_ASSERT( 0 > server->nv.dupTimerExpires );
+ XP_ASSERT( server_canUnpause( server ) );
+}
+
+void
+server_pause( ServerCtxt* server, XP_S16 turn, const XP_UCHAR* msg )
+{
+ XP_LOGF( "%s(turn=%d)", __func__, turn );
+ pauseImpl( server );
+ /* Figure out how many seconds are left on the timer, and set timer to the
+ negative of that (since negative is the flag) */
+ dupe_transmitPause( server, PAUSED, turn, msg, -1 );
+ model_noteDupePause( server->vol.model, PAUSED, turn, msg );
+ LOG_RETURN_VOID();
+}
+
+static void
+autoPause( ServerCtxt* server )
+{
+ XP_LOGF( "%s()", __func__ );
+
+ /* Reset timer: we're starting turn over */
+ dupe_resetTimer( server );
+ dupe_clearState( server );
+
+ /* Then pause us */
+ pauseImpl( server );
+
+ dupe_transmitPause( server, AUTOPAUSED, 0, NULL, -1 );
+ model_noteDupePause( server->vol.model, AUTOPAUSED, -1, NULL );
+ LOG_RETURN_VOID();
+}
+
+void
+server_unpause( ServerCtxt* server, XP_S16 turn, const XP_UCHAR* msg )
+{
+ XP_LOGF( "%s(turn=%d)", __func__, turn );
+ XP_ASSERT( server_canUnpause( server ) );
+ XP_U32 now = dutil_getCurSeconds( server->vol.dutil );
+ /* subtract because it's negative */
+ setDupTimerExpires( server, now - server->nv.dupTimerExpires );
+ XP_ASSERT( server_canPause( server ) );
+ dupe_transmitPause( server, UNPAUSED, turn, msg, -1 );
+ model_noteDupePause( server->vol.model, UNPAUSED, turn, msg );
+ LOG_RETURN_VOID();
+}
+
+/* Called on client. Unpacks DUP move data and applies it. */
+static XP_Bool
+dupe_handleServerMoves( ServerCtxt* server, XWStreamCtxt* stream )
+{
+ LOG_FUNC();
+ MoveInfo moveInfo;
+ moveInfoFromStream( stream, &moveInfo, bitsPerTile(server) );
+ TrayTileSet newTiles;
+ traySetFromStream( stream, &newTiles );
+ XP_ASSERT( newTiles.nTiles <= moveInfo.nTiles );
+ XP_ASSERT( pool_containsTiles( server->pool, &newTiles ) );
+
+ XP_U16 nScores = stream_getBits( stream, NPLAYERS_NBITS );
+ XP_U16 scores[MAX_NUM_PLAYERS];
+ XP_ASSERT( nScores <= MAX_NUM_PLAYERS );
+ scoresFromStream( stream, nScores, scores );
+
+ dupe_resetTimer( server );
+
+ pool_removeTiles( server->pool, &newTiles );
+ model_commitDupeTurn( server->vol.model, &moveInfo, nScores, scores,
+ &newTiles );
+
+ /* Need to remove the played tiles from all local trays */
+ updateOthersTiles( server );
+
+ dupe_setupShowMove( server, scores );
+
+ dupe_clearState( server );
+ nextTurn( server, PICK_NEXT );
+ LOG_RETURN_VOID();
+ return XP_TRUE;
+} /* dupe_handleServerMoves */
+
+static XP_Bool
+dupe_handleServerTrade( ServerCtxt* server, XWStreamCtxt* stream )
+{
+ TrayTileSet oldTiles, newTiles;
+ traySetFromStream( stream, &oldTiles );
+ traySetFromStream( stream, &newTiles );
+
+ ModelCtxt* model = server->vol.model;
+ model_resetCurrentTurn( model, DUP_PLAYER );
+ model_removePlayerTiles2( model, DUP_PLAYER, &oldTiles );
+ pool_replaceTiles( server->pool, &oldTiles );
+ pool_removeTiles( server->pool, &newTiles );
+
+ model_commitDupeTrade( model, &oldTiles, &newTiles );
+
+ model_addNewTiles( model, DUP_PLAYER, &newTiles );
+ updateOthersTiles( server );
+
+ dupe_resetTimer( server );
+ dupe_setupShowTrade( server, newTiles.nTiles );
+
+ dupe_clearState( server );
+ nextTurn( server, PICK_NEXT );
+ return XP_TRUE;
+}
+
#endif
/* Just for grins....trade in all the tiles that weren't used in the
@@ -906,16 +1319,16 @@ makeRobotMove( ServerCtxt* server )
#endif
XP_ASSERT( !!server_getEngineFor( server, turn ) );
searchComplete = engine_findMove( server_getEngineFor( server, turn ),
- model, turn, XP_FALSE, tileSet->tiles,
- tileSet->nTiles, XP_FALSE,
+ model, turn, XP_FALSE, XP_FALSE,
+ tileSet->tiles, tileSet->nTiles, XP_FALSE,
#ifdef XWFEATURE_BONUSALL
allTilesBonus,
#endif
#ifdef XWFEATURE_SEARCHLIMIT
NULL, XP_FALSE,
#endif
- server->vol.gi->players[turn].robotIQ,
- &canMove, &newMove );
+ gi->players[turn].robotIQ,
+ &canMove, &newMove, NULL );
}
if ( forceTrade || searchComplete ) {
const XP_UCHAR* str;
@@ -968,7 +1381,7 @@ makeRobotMove( ServerCtxt* server )
server->nv.prevMoveStream = stream;
server->nv.prevWordsStream = wordsStream;
}
- result = server_commitMove( server, NULL );
+ result = server_commitMove( server, turn, NULL );
} else {
result = XP_FALSE;
}
@@ -1051,7 +1464,9 @@ showPrevScore( ServerCtxt* server )
prevTurn = (server->nv.currentTurn + nPlayers - 1) % nPlayers;
lp = &gi->players[prevTurn];
- if ( LP_IS_LOCAL(lp) ) {
+ if ( inDuplicateMode( server ) ) {
+ str = "Duplicate turn complete. Scores:\n";
+ } else if ( LP_IS_LOCAL(lp) ) {
str = dutil_getUserString( dutil, STR_ROBOT_MOVED );
} else {
str = dutil_getUserString( dutil, STRS_REMOTE_MOVED );
@@ -1062,12 +1477,11 @@ showPrevScore( ServerCtxt* server )
stream = mkServerStream( server );
stream_catString( stream, str );
- if ( !!server->nv.prevMoveStream ) {
- XWStreamCtxt* prevStream = server->nv.prevMoveStream;
- XP_U16 len = stream_getSize( prevStream );
-
+ XWStreamCtxt* prevStream = server->nv.prevMoveStream;
+ if ( !!prevStream ) {
server->nv.prevMoveStream = NULL;
+ XP_U16 len = stream_getSize( prevStream );
stream_putBytes( stream, stream_getPtr( prevStream ), len );
stream_destroy( prevStream );
}
@@ -1095,6 +1509,7 @@ server_tilesPicked( ServerCtxt* server, XP_U16 player,
pool_removeTiles( server->pool, &newTiles );
fetchTiles( server, player, MAX_TRAY_TILES, NULL, &newTiles );
+ XP_ASSERT( !inDuplicateMode(server) );
model_assignPlayerTiles( server->vol.model, player, &newTiles );
util_requestTime( server->vol.util );
@@ -1152,6 +1567,9 @@ server_do( ServerCtxt* server )
if ( assignTilesToAll( server ) ) {
SETSTATE( server, XWSTATE_INTURN );
setTurn( server, 0 );
+ if ( inDuplicateMode( server ) ) {
+ dupe_resetTimer( server );
+ }
moreToDo = XP_TRUE;
}
}
@@ -1197,6 +1615,12 @@ server_do( ServerCtxt* server )
moreToDo = XWSTATE_NEED_SHOWSCORE != server->nv.gameState;
break;
case XWSTATE_INTURN:
+ if ( inDuplicateMode( server ) ) {
+ /* For now, anyway; makes dev easier */
+ dupe_forceCommits( server );
+ dupe_checkTurns( server );
+ }
+
if ( robotMovePending( server ) && !ROBOTWAITING(server) ) {
result = makeRobotMove( server );
/* if robot was interrupted, we need to schedule again */
@@ -1220,14 +1644,22 @@ server_do( ServerCtxt* server )
} /* server_do */
#ifndef XWFEATURE_STANDALONE_ONLY
+
static XP_S8
-getIndexForDevice( ServerCtxt* server, XP_PlayerAddr channelNo )
+getIndexForStream( const ServerCtxt* server, const XWStreamCtxt* stream )
+{
+ XP_PlayerAddr channelNo = stream_getAddress( stream );
+ return getIndexForDevice( server, channelNo );
+}
+
+static XP_S8
+getIndexForDevice( const ServerCtxt* server, XP_PlayerAddr channelNo )
{
short ii;
XP_S8 result = -1;
for ( ii = 0; ii < server->nv.nDevices; ++ii ) {
- RemoteAddress* addr = &server->nv.addresses[ii];
+ const RemoteAddress* addr = &server->nv.addresses[ii];
if ( addr->channelNo == channelNo ) {
result = ii;
break;
@@ -1339,9 +1771,8 @@ clearLocalRobots( ServerCtxt* server )
static void
sortTilesIf( ServerCtxt* server, XP_S16 turn )
{
- ModelCtxt* model = server->vol.model;
if ( server->nv.sortNewTiles ) {
- model_sortTiles( model, turn );
+ model_sortTiles( server->vol.model, turn );
}
}
@@ -1359,11 +1790,6 @@ client_readInitialMessage( ServerCtxt* server, XWStreamCtxt* stream )
/* We should never get this message a second time, but very rarely we do.
Drop it in that case. */
if ( accepted ) {
- DictionaryCtxt* newDict;
- DictionaryCtxt* curDict;
- XP_U16 nPlayers, nCols;
- XP_PlayerAddr channelNo;
- XP_U16 ii;
ModelCtxt* model = server->vol.model;
CurGameInfo* gi = server->vol.gi;
CurGameInfo localGI;
@@ -1392,9 +1818,9 @@ client_readInitialMessage( ServerCtxt* server, XWStreamCtxt* stream )
localGI.dictName = copyString( server->mpool, gi->dictName );
gi_copy( MPPARM(server->mpool) gi, &localGI );
- nCols = localGI.boardSize;
+ XP_U16 nCols = localGI.boardSize;
- newDict = util_makeEmptyDict( server->vol.util );
+ DictionaryCtxt* newDict = util_makeEmptyDict( server->vol.util );
dict_loadFromStream( newDict, stream );
#ifdef STREAM_VERS_BIGBOARD
@@ -1406,14 +1832,14 @@ client_readInitialMessage( ServerCtxt* server, XWStreamCtxt* stream )
}
#endif
- channelNo = stream_getAddress( stream );
+ XP_PlayerAddr channelNo = stream_getAddress( stream );
XP_ASSERT( channelNo != 0 );
server->nv.addresses[0].channelNo = channelNo;
XP_LOGF( "%s: assigning channelNo %x for 0", __func__, channelNo );
model_setSize( model, nCols );
- nPlayers = localGI.nPlayers;
+ XP_U16 nPlayers = localGI.nPlayers;
XP_LOGF( "%s: reading in %d players", __func__, localGI.nPlayers );
gi_disposePlayerInfo( MPPARM(server->mpool) &localGI );
@@ -1421,7 +1847,7 @@ client_readInitialMessage( ServerCtxt* server, XWStreamCtxt* stream )
gi->nPlayers = nPlayers;
model_setNPlayers( model, nPlayers );
- curDict = model_getDictionary( model );
+ DictionaryCtxt* curDict = model_getDictionary( model );
XP_ASSERT( !!newDict );
@@ -1451,19 +1877,25 @@ client_readInitialMessage( ServerCtxt* server, XWStreamCtxt* stream )
/* now read the assigned tiles for each player from the stream, and
remove them from the newly-created local pool. */
- for ( ii = 0; ii < nPlayers; ++ii ) {
- TrayTileSet tiles;
-
- traySetFromStream( stream, &tiles );
- XP_ASSERT( tiles.nTiles <= MAX_TRAY_TILES );
+ TrayTileSet tiles;
+ for ( XP_U16 ii = 0; ii < nPlayers; ++ii ) {
+ /* Pull/remove only once if duplicate-mode game */
+ if ( ii == 0 || !inDuplicateMode(server) ) {
+ traySetFromStream( stream, &tiles );
+ XP_ASSERT( tiles.nTiles <= MAX_TRAY_TILES );
+ /* remove what the server's assigned so we won't conflict
+ later. */
+ pool_removeTiles( pool, &tiles );
+ }
XP_LOGF( "%s: got %d tiles for player %d", __func__, tiles.nTiles, ii );
- model_assignPlayerTiles( model, ii, &tiles );
-
- /* remove what the server's assigned so we won't conflict
- later. */
- pool_removeTiles( pool, &tiles );
+ if ( inDuplicateMode(server ) ) {
+ model_assignDupeTiles( model, &tiles );
+ break;
+ } else {
+ model_assignPlayerTiles( model, ii, &tiles );
+ }
sortTilesIf( server, ii );
}
@@ -1475,6 +1907,7 @@ client_readInitialMessage( ServerCtxt* server, XWStreamCtxt* stream )
/* Give board a chance to redraw self with the full compliment of known
players */
setTurn( server, 0 );
+ dupe_resetTimer( server );
} else {
XP_LOGF( "%s: wanted 0; got %d", __func__,
server->nv.addresses[0].channelNo );
@@ -1519,26 +1952,19 @@ makeSendableGICopy( ServerCtxt* server, CurGameInfo* giCopy,
static void
server_sendInitialMessage( ServerCtxt* server )
{
- XP_U16 ii;
- XP_U16 deviceIndex;
ModelCtxt* model = server->vol.model;
XP_U16 nPlayers = server->vol.gi->nPlayers;
- CurGameInfo localGI;
XP_U32 gameID = server->vol.gi->gameID;
#ifdef STREAM_VERS_BIGBOARD
XP_U8 streamVersion = server->nv.streamVersion;
#endif
XP_ASSERT( server->nv.nDevices > 1 );
- for ( deviceIndex = 1; deviceIndex < server->nv.nDevices;
+ for ( XP_U16 deviceIndex = 1; deviceIndex < server->nv.nDevices;
++deviceIndex ) {
- RemoteAddress* addr = &server->nv.addresses[deviceIndex];
- XWStreamCtxt* stream = util_makeStreamFromAddr( server->vol.util,
- addr->channelNo );
- DictionaryCtxt* dict = model_getDictionary(model);
+ XWStreamCtxt* stream = messageStreamWithHeader( server, deviceIndex,
+ XWPROTO_CLIENT_SETUP );
XP_ASSERT( !!stream );
- stream_open( stream );
- writeProto( server, stream, XWPROTO_CLIENT_SETUP );
#ifdef STREAM_VERS_BIGBOARD
XP_ASSERT( 0 < streamVersion );
@@ -1550,9 +1976,11 @@ server_sendInitialMessage( ServerCtxt* server )
XP_LOGF( "putting gameID %x into msg", gameID );
stream_putU32( stream, gameID );
+ CurGameInfo localGI;
makeSendableGICopy( server, &localGI, deviceIndex );
gi_writeToStream( stream, &localGI );
+ DictionaryCtxt* dict = model_getDictionary( model );
dict_writeToStream( dict, stream );
#ifdef STREAM_VERS_BIGBOARD
if ( STREAM_VERS_DICTNAME <= streamVersion ) {
@@ -1561,8 +1989,11 @@ server_sendInitialMessage( ServerCtxt* server )
}
#endif
/* send tiles currently in tray */
- for ( ii = 0; ii < nPlayers; ++ii ) {
+ for ( XP_U16 ii = 0; ii < nPlayers; ++ii ) {
model_trayToStream( model, ii, stream );
+ if ( inDuplicateMode(server) ) {
+ break;
+ }
}
stream_destroy( stream );
@@ -1571,6 +2002,8 @@ server_sendInitialMessage( ServerCtxt* server )
/* Set after messages are built so their connID will be 0, but all
non-initial messages will have a non-0 connID. */
comms_setConnID( server->vol.comms, gameID );
+
+ dupe_resetTimer( server );
} /* server_sendInitialMessage */
#endif
@@ -1639,11 +2072,11 @@ codeToStr( XW_Proto code )
caseStr( XWPROTO_CLIENT_REQ_END_GAME );
caseStr( XWPROTO_END_GAME );
caseStr( XWPROTO_NEW_PROTO );
+ caseStr( XWPROTO_DUPE_STUFF );
}
return str;
} /* codeToStr */
-
#define PRINTCODE( intro, code ) \
XP_STATUSF( "\t%s(): %s for %s", __func__, intro, codeToStr(code) )
@@ -1658,7 +2091,7 @@ messageStreamWithHeader( ServerCtxt* server, XP_U16 devIndex, XW_Proto code )
XWStreamCtxt* stream;
XP_PlayerAddr channelNo = server->nv.addresses[devIndex].channelNo;
- PRINTCODE("making", code);
+ PRINTCODE( "making", code );
stream = util_makeStreamFromAddr( server->vol.util, channelNo );
stream_open( stream );
@@ -1667,17 +2100,6 @@ messageStreamWithHeader( ServerCtxt* server, XP_U16 devIndex, XW_Proto 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* XP_UNUSED(server),
- XWStreamCtxt* XP_UNUSED(stream) )
-{
- return XP_TRUE;
-} /* readStreamHeader */
-
static void
sendBadWordMsgs( ServerCtxt* server )
{
@@ -1715,14 +2137,13 @@ badWordMoveUndoAndTellUser( ServerCtxt* server, BadWordInfo* bwi )
EngineCtxt*
server_getEngineFor( ServerCtxt* server, XP_U16 playerNum )
{
- ServerPlayer* player;
- EngineCtxt* engine;
+ const CurGameInfo* gi = server->vol.gi;
+ XP_ASSERT( playerNum < gi->nPlayers );
- XP_ASSERT( playerNum < server->vol.gi->nPlayers );
-
- player = &server->players[playerNum];
- engine = player->engine;
- if ( !engine && server->vol.gi->players[playerNum].isLocal ) {
+ ServerPlayer* player = &server->players[playerNum];
+ EngineCtxt* engine = player->engine;
+ if ( !engine &&
+ (inDuplicateMode(server) || gi->players[playerNum].isLocal) ) {
engine = engine_make( MPPARM(server->mpool)
server->vol.util );
player->engine = engine;
@@ -1731,34 +2152,21 @@ server_getEngineFor( ServerCtxt* server, XP_U16 playerNum )
return engine;
} /* server_getEngineFor */
-#ifdef XWFEATURE_CHANGEDICT
-void
-server_resetEngines( ServerCtxt* server )
-{
- XP_U16 nPlayers = server->vol.gi->nPlayers;
- while ( 0 < nPlayers-- ) {
- server_resetEngine( server, nPlayers );
- }
-}
-#endif
-
void
server_resetEngine( ServerCtxt* server, XP_U16 playerNum )
{
ServerPlayer* player = &server->players[playerNum];
if ( !!player->engine ) {
- XP_ASSERT( player->deviceIndex == 0 );
+ XP_ASSERT( player->deviceIndex == 0 || inDuplicateMode(server) );
engine_reset( player->engine );
}
} /* server_resetEngine */
-static void
-resetEngines( ServerCtxt* server )
+void
+server_resetEngines( ServerCtxt* server )
{
- XP_U16 ii;
XP_U16 nPlayers = server->vol.gi->nPlayers;
-
- for ( ii = 0; ii < nPlayers; ++ii ) {
+ for ( XP_U16 ii = 0; ii < nPlayers; ++ii ) {
server_resetEngine( server, ii );
}
} /* resetEngines */
@@ -1799,7 +2207,6 @@ makeNotAVowel( ServerCtxt* server, Tile* newTile )
pool_replaceTiles( pool, &set );
pool_requestTiles( pool, &tile, &numGot );
-
}
} /* makeNotAVowel */
@@ -1856,13 +2263,61 @@ XP_Bool
server_askPickTiles( ServerCtxt* server, XP_U16 turn, TrayTileSet* newTiles,
XP_U16 nToPick )
{
- XP_Bool asked = newTiles == NULL && server->vol.gi->allowPickTiles;
+ /* May want to allow the host to pick tiles even in duplicate mode. Not
+ sure how that'll work! PENDING */
+ XP_Bool asked = newTiles == NULL && !inDuplicateMode(server)
+ && server->vol.gi->allowPickTiles;
if ( asked ) {
asked = informNeedPickTiles( server, XP_FALSE, turn, nToPick );
}
return asked;
}
+/* dupe_trayAllowsMoves()
+ *
+ * Assuming a model with a turn loaded (but maybe not committed), build the
+ * tile set containing the current model tray tiles PLUS the new set we're
+ * considering, and see if the engine can find moves.
+ */
+static XP_Bool
+dupe_trayAllowsMoves( ServerCtxt* server, XP_U16 turn,
+ const Tile* tiles, XP_U16 nTiles )
+{
+ ModelCtxt* model = server->vol.model;
+ XP_U16 nInTray = model_getNumTilesInTray( model, turn );
+ XP_LOGF( "%s(nTiles=%d): nInTray: %d", __func__, nTiles, nInTray );
+ XP_ASSERT( nInTray + nTiles <= MAX_TRAY_TILES ); /* fired! */
+ Tile tmpTiles[MAX_TRAY_TILES];
+ const TrayTileSet* tray = model_getPlayerTiles( model, turn );
+ XP_MEMCPY( tmpTiles, &tray->tiles[0], nInTray * sizeof(tmpTiles[0]) );
+ XP_MEMCPY( &tmpTiles[nInTray], &tiles[0], nTiles * sizeof(tmpTiles[0]) );
+
+ /* XP_LOGF( "%s(nTiles=%d)", __func__, nTiles ); */
+ EngineCtxt* engine = server_getEngineFor( server, turn );
+ XP_Bool canMove;
+ MoveInfo newMove = {0};
+ XP_U16 score = 0;
+ XP_Bool result = engine_findMove( engine, server->vol.model, turn,
+ XP_TRUE, XP_TRUE,
+ tmpTiles, nTiles + nInTray, XP_FALSE, 0,
+#ifdef XWFEATURE_SEARCHLIMIT
+ NULL, XP_FALSE,
+#endif
+ 0, /* not a robot */
+ &canMove, &newMove, &score )
+ && canMove;
+
+ if ( result ) {
+ XP_LOGF( "%s(): first move found has score of %d", __func__, score );
+ } else {
+ XP_LOGF( "%s(): no moves found for tray!!!", __func__ );
+ }
+
+ server_resetEngine( server, turn );
+
+ return result;
+}
+
/* Get tiles for one user. If picking is available, let user pick until
* cancels. Otherwise, and after cancel, pick for 'im.
*/
@@ -1870,6 +2325,7 @@ static void
fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch,
const TrayTileSet* tradedTiles, TrayTileSet* resultTiles )
{
+ XP_ASSERT( server->vol.gi->serverRole != SERVER_ISCLIENT || !inDuplicateMode(server) );
XP_Bool ask;
XP_U16 nSoFar = resultTiles->nTiles;
PoolContext* pool = server->pool;
@@ -1886,9 +2342,9 @@ fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch,
ask = XP_FALSE;
#endif
- XP_U16 nLeft = pool_getNTilesLeft( pool );
- if ( nLeft < nToFetch ) {
- nToFetch = nLeft;
+ XP_U16 nLeftInPool = pool_getNTilesLeft( pool );
+ if ( nLeftInPool < nToFetch ) {
+ nToFetch = nLeftInPool;
}
TrayTileSet oneTile = {.nTiles = 1};
@@ -1902,6 +2358,7 @@ fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch,
#ifdef FEATURE_TRAY_EDIT /* good compiler would note ask==0, but... */
/* First ask until cancelled */
while ( ask && nSoFar < nToFetch ) {
+ XP_ASSERT( !inDuplicateMode(server) );
const XP_UCHAR* texts[MAX_UNIQUE_TILES];
Tile tiles[MAX_UNIQUE_TILES];
XP_S16 chosen;
@@ -1920,7 +2377,7 @@ fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch,
TrayTileSet tiles;
tiles.nTiles = 1;
tiles.tiles[0] = resultTiles->tiles[--nSoFar];
- pool_replaceTiles( server->pool, &tiles );
+ pool_replaceTiles( pool, &tiles );
--pi.nCurTiles;
--pi.thisPick;
}
@@ -1935,12 +2392,25 @@ fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch,
}
#endif
- /* Then fetch the rest without asking */
- if ( nSoFar < nToFetch ) {
- XP_U8 nLeft = nToFetch - nSoFar;
+ /* Then fetch the rest without asking. But if we're in duplicate mode,
+ make sure the tray allows some moves (e.g. isn't all consonants when
+ the board's empty.) */
+ XP_ASSERT( nToFetch >= nSoFar );
+ XP_U16 nLeft = nToFetch - nSoFar;
+ for ( XP_U16 nBadTrays = 0; 0 < nLeft; ) {
pool_requestTiles( pool, &resultTiles->tiles[nSoFar], &nLeft );
- nSoFar += nLeft;
+
+ if ( !inDuplicateMode( server ) ) {
+ break;
+ } else if ( dupe_trayAllowsMoves( server, playerNum, &resultTiles->tiles[0],
+ nSoFar + nLeft )
+ || ++nBadTrays >= 5 ) {
+ break;
+ }
+ pool_replaceTiles2( pool, nLeft, &resultTiles->tiles[nSoFar] );
}
+
+ nSoFar += nLeft;
XP_ASSERT( nSoFar < 0x00FF );
resultTiles->nTiles = (XP_U8)nSoFar;
@@ -1953,7 +2423,7 @@ makePoolOnce( ServerCtxt* server )
XP_ASSERT( model_getDictionary(model) != NULL );
if ( server->pool == NULL ) {
server->pool = pool_make( MPPARM_NOCOMMA(server->mpool) );
- XP_STATUSF( "initing pool" );
+ XP_STATUSF( "%s(): initing pool", __func__ );
pool_initFromDict( server->pool, model_getDictionary(model));
}
}
@@ -1987,6 +2457,7 @@ assignTilesToAll( ServerCtxt* server )
XP_Bool pickingTiles = gi->serverRole == SERVER_STANDALONE
&& gi->allowPickTiles;
+ TrayTileSet newTiles;
for ( ii = 0; ii < nPlayers; ++ii ) {
if ( 0 == model_getNumTilesInTray( model, ii ) ) {
if ( pickingTiles && !LP_IS_ROBOT(&gi->players[ii])
@@ -1995,9 +2466,18 @@ assignTilesToAll( ServerCtxt* server )
allDone = XP_FALSE;
break;
}
- TrayTileSet newTiles = {0};
- fetchTiles( server, ii, numAssigned, NULL, &newTiles );
- model_assignPlayerTiles( model, ii, &newTiles );
+ if ( 0 == ii || !gi->inDuplicateMode ) {
+ newTiles.nTiles = 0;
+ fetchTiles( server, ii, numAssigned, NULL, &newTiles );
+ }
+
+ if ( gi->inDuplicateMode ) {
+ XP_ASSERT( ii == DUP_PLAYER );
+ model_assignDupeTiles( model, &newTiles );
+ break;
+ } else {
+ model_assignPlayerTiles( model, ii, &newTiles );
+ }
}
sortTilesIf( server, ii );
}
@@ -2022,18 +2502,22 @@ getPlayerTime( ServerCtxt* server, XWStreamCtxt* stream, XP_U16 turn )
static void
nextTurn( ServerCtxt* server, XP_S16 nxtTurn )
{
- XP_U16 nPlayers = server->vol.gi->nPlayers;
- XP_U16 playerTilesLeft = 0;
+ LOG_FUNC();
+ CurGameInfo* gi = server->vol.gi;
+ XP_U16 nPlayers = gi->nPlayers;
+ XP_Bool playerTilesLeft = XP_FALSE;
XP_S16 currentTurn = server->nv.currentTurn;
XP_Bool moreToDo = XP_FALSE;
if ( nxtTurn == PICK_NEXT ) {
- XP_ASSERT( server->nv.gameState == XWSTATE_INTURN );
+ XP_ASSERT( server->nv.gameState == XWSTATE_INTURN ); /* fired.... */
if ( currentTurn >= 0 ) {
- // XP_ASSERT( currentTurn >= 0 ); /* fired! */
- playerTilesLeft = model_getNumTilesTotal( server->vol.model,
- currentTurn );
- nxtTurn = (currentTurn+1) % nPlayers;
+ playerTilesLeft = tileCountsOk( server );
+ if ( inDuplicateMode(server) ) {
+ nxtTurn = dupe_nextTurn( server );
+ } else {
+ nxtTurn = (currentTurn+1) % nPlayers;
+ }
} else {
XP_LOGF( "%s(): turn == -1 so dropping", __func__ );
}
@@ -2041,21 +2525,19 @@ nextTurn( ServerCtxt* server, XP_S16 nxtTurn )
/* 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;
+ playerTilesLeft = XP_TRUE;
}
SETSTATE( server, XWSTATE_INTURN ); /* even if game over, if undoing */
- if ( (playerTilesLeft > 0) && tileCountsOk(server) && NPASSES_OK(server) ){
-
+ if ( playerTilesLeft && NPASSES_OK(server) ){
setTurn( server, 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. Yes, but be sure not to compute
another PASS move. Just don't do anything! */
- if ( server->vol.gi->serverRole != SERVER_ISCLIENT ) {
- SETSTATE( server, XWSTATE_NEEDSEND_ENDGAME );
+ if ( gi->serverRole != SERVER_ISCLIENT ) {
+ SETSTATE( server, XWSTATE_NEEDSEND_ENDGAME ); /* this is it */
moreToDo = XP_TRUE;
} else if ( currentTurn >= 0 ) {
XP_LOGF( "%s: Doing nothing; waiting for server to end game",
@@ -2075,7 +2557,7 @@ nextTurn( ServerCtxt* server, XP_S16 nxtTurn )
}
/* It's safer, if perhaps not always necessary, to do this here. */
- resetEngines( server );
+ server_resetEngines( server );
XP_ASSERT( server->nv.gameState != XWSTATE_GAMEOVER );
callTurnChangeListener( server );
@@ -2094,10 +2576,20 @@ void
server_setTurnChangeListener( ServerCtxt* server, TurnChangeListener tl,
void* data )
{
+ XP_ASSERT( !server->vol.turnChangeListener );
server->vol.turnChangeListener = tl;
server->vol.turnChangeData = data;
} /* server_setTurnChangeListener */
+void
+server_setTimerChangeListener( ServerCtxt* server, TimerChangeListener tl,
+ void* data )
+{
+ XP_ASSERT( !server->vol.timerChangeListener );
+ server->vol.timerChangeListener = tl;
+ server->vol.timerChangeData = data;
+}
+
void
server_setGameOverListener( ServerCtxt* server, GameOverListener gol,
void* data )
@@ -2151,13 +2643,12 @@ sendMoveTo( ServerCtxt* server, XP_U16 devIndex, XP_U16 turn,
XP_Bool legal, TrayTileSet* newTiles,
const 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 );
+ XWStreamCtxt* stream = messageStreamWithHeader( server, devIndex, code );
#ifdef STREAM_VERS_BIGBOARD
XP_U16 version = stream_getVersion( stream );
@@ -2218,10 +2709,9 @@ readMoveInfo( ServerCtxt* server, XWStreamCtxt* stream,
#ifdef STREAM_VERS_BIGBOARD
if ( STREAM_VERS_BIGBOARD <= stream_getVersion( stream ) ) {
XP_U32 hashReceived = stream_getU32( stream );
- success = model_hashMatches( server->vol.model, hashReceived );
- if ( !success ) {
- success = model_popToHash( server->vol.model, hashReceived, server->pool );
- }
+ success = model_hashMatches( server->vol.model, hashReceived )
+ || model_popToHash( server->vol.model, hashReceived, server->pool );
+
if ( !success ) {
XP_LOGF( "%s: hash mismatch: %X not found",__func__, hashReceived );
#ifdef DEBUG_HASHING
@@ -2235,6 +2725,7 @@ readMoveInfo( ServerCtxt* server, XWStreamCtxt* stream,
XP_U16 whoMoved = stream_getBits( stream, PLAYERNUM_NBITS );
traySetFromStream( stream, newTiles );
success = pool_containsTiles( server->pool, newTiles );
+ XP_ASSERT( success );
if ( success ) {
isTrade = stream_getBits( stream, 1 );
@@ -2266,9 +2757,7 @@ sendMoveToClientsExcept( ServerCtxt* server, XP_U16 whoMoved, XP_Bool legal,
TrayTileSet* newTiles, const TrayTileSet* tradedTiles,
XP_U16 skip )
{
- XP_U16 devIndex;
-
- for ( devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) {
+ for ( XP_U16 devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) {
if ( devIndex != skip ) {
sendMoveTo( server, devIndex, whoMoved, legal,
newTiles, tradedTiles );
@@ -2325,8 +2814,7 @@ reflectMoveAndInform( ServerCtxt* server, XWStreamCtxt* stream )
TrayTileSet newTiles;
TrayTileSet tradedTiles;
CurGameInfo* gi = server->vol.gi;
- XP_U16 sourceClientIndex =
- getIndexForDevice( server, stream_getAddress( stream ) );
+ XP_U16 sourceClientIndex = getIndexForStream( server, stream );
XWStreamCtxt* mvStream = NULL;
XWStreamCtxt* wordsStream = NULL;
@@ -2367,7 +2855,7 @@ reflectMoveAndInform( ServerCtxt* server, XWStreamCtxt* stream )
}
success = model_commitTurn( model, whoMoved, &newTiles );
- resetEngines( server );
+ server_resetEngines( server );
}
if ( success && isLegalMove ) {
@@ -2421,11 +2909,10 @@ reflectMove( ServerCtxt* server, XWStreamCtxt* stream )
XWStreamCtxt* wordsStream = NULL;
moveOk = XWSTATE_INTURN == server->nv.gameState
- && server->nv.currentTurn >= 0;
- if ( moveOk ) {
- moveOk = readMoveInfo( server, stream, &whoMoved, &isTrade, &newTiles,
- &tradedTiles, &isLegal ); /* modifies model */
- }
+ && server->nv.currentTurn >= 0
+ && readMoveInfo( server, stream, &whoMoved, &isTrade, &newTiles,
+ &tradedTiles, &isLegal ); /* modifies model */
+
if ( moveOk ) {
if ( isTrade ) {
model_makeTileTrade( model, whoMoved, &tradedTiles, &newTiles );
@@ -2446,7 +2933,7 @@ reflectMove( ServerCtxt* server, XWStreamCtxt* stream )
server->nv.prevWordsStream = wordsStream;
}
- resetEngines( server );
+ server_resetEngines( server );
if ( !isLegal ) {
XP_ASSERT( server->vol.gi->serverRole == SERVER_ISCLIENT );
@@ -2460,6 +2947,530 @@ reflectMove( ServerCtxt* server, XWStreamCtxt* stream )
} /* reflectMove */
#endif /* XWFEATURE_STANDALONE_ONLY */
+static void
+chooseMove( const ServerCtxt* server, XP_U16 nPlayers, XP_U16 scores[],
+ XP_U16* winner, XP_U16* winningNTiles )
+{
+ ModelCtxt* model = server->vol.model;
+ struct {
+ XP_U16 score;
+ XP_U16 nTiles;
+ XP_U16 player;
+ } moveData[MAX_NUM_PLAYERS];
+ XP_U16 nWinners = 0;
+
+ /* Pick the best move. "Best" means highest scoring, or in case of a score
+ tie the largest number of tiles used. If there's still a tie, pick at
+ random. :-) */
+ for ( XP_U16 player = 0; player < nPlayers; ++player ) {
+ XP_S16 score;
+ if ( !getCurrentMoveScoreIfLegal( model, player, NULL, NULL, &score ) ) {
+ score = 0;
+ }
+ scores[player] = score;
+
+ XP_U16 nTiles = score == 0 ? 0 : model_getCurrentMoveCount( model, player );
+
+ XP_Bool saveIt = nWinners == 0;
+ if ( !saveIt ) { /* not our first time through */
+ if ( score > moveData[nWinners-1].score ) { /* score wins? Keep it! */
+ saveIt = XP_TRUE;
+ nWinners = 0;
+ } else if ( score < moveData[nWinners-1].score ) { /* score too low? */
+ // score lower than best; drop it!
+ } else if ( nTiles > moveData[nWinners-1].nTiles ) {
+ saveIt = XP_TRUE;
+ nWinners = 0;
+ } else if ( nTiles < moveData[nWinners-1].nTiles ) {
+ // number of tiles lower than best; drop it!
+ } else {
+ saveIt = XP_TRUE;
+ }
+ }
+
+ if ( saveIt ) {
+ moveData[nWinners].score = score;
+ moveData[nWinners].nTiles = nTiles;
+ moveData[nWinners].player = player;
+ ++nWinners;
+ }
+
+ }
+
+ const XP_U16 winnerIndx = XP_RANDOM() % nWinners;
+ *winner = moveData[winnerIndx].player;
+ *winningNTiles = moveData[winnerIndx].nTiles;
+ /* This fires: I need the reassign-no-moves thing */
+ if ( *winningNTiles == 0 ) {
+ XP_LOGF( "%s(): no scoring move found", __func__ );
+ } else {
+ XP_LOGF( "%s(): %d wins with %d points", __func__, *winner,
+ scores[*winner] );
+ }
+}
+
+static XP_Bool
+allForced( const ServerCtxt* server )
+{
+ XP_Bool result = XP_TRUE;
+ for ( XP_U16 ii = 0; result && ii < server->vol.gi->nPlayers; ++ii ) {
+ result = server->nv.dupTurnsForced[ii];
+ }
+ LOG_RETURNF( "%d", result );
+ return result;
+}
+
+/* Called for host or standalone case when all moves for the turn are
+ present. Pick the best one and commit locally. In server case, transmit to
+ each guest device as well. */
+static void
+dupe_commitAndReport( ServerCtxt* server )
+{
+ const XP_U16 nPlayers = server->vol.gi->nPlayers;
+ XP_U16 scores[nPlayers];
+
+ XP_U16 winner;
+ XP_U16 nTiles;
+ chooseMove( server, nPlayers, scores, &winner, &nTiles );
+
+ /* If nobody can move AND there are tiles left, trade instead of recording
+ a 0. Unless we're running a timer, in which case it's most likely
+ noboby's playing, so pause the game instead. */
+ if ( 0 == scores[winner] && 0 < pool_getNTilesLeft(server->pool) ) {
+ if ( dupTimerRunning() && allForced( server ) ) {
+ autoPause( server );
+ } else {
+ dupe_makeAndReportTrade( server );
+ }
+ } else {
+ dupe_commitAndReportMove( server, winner, nPlayers, scores, nTiles );
+ }
+ dupe_clearState( server );
+} /* dupe_commitAndReport */
+
+static void
+sendStreamToDev( ServerCtxt* server, XP_U16 dev, XW_Proto code,
+ XWStreamCtxt* data )
+{
+ XWStreamCtxt* stream = messageStreamWithHeader( server, dev, code );
+ const XP_U16 dataLen = stream_getSize( data );
+ const XP_U8* dataPtr = stream_getPtr( data );
+ stream_putBytes( stream, dataPtr, dataLen );
+ stream_destroy( stream );
+}
+
+/* Called in the case where nobody was able to move, does a trade. The goal is
+ to make it more likely that folks will be able to move with the next set of
+ tiles. For now I'll put them back first so there's a chance of getting some
+ of the same back.
+*/
+static void
+dupe_makeAndReportTrade( ServerCtxt* server )
+{
+ LOG_FUNC();
+ PoolContext* pool = server->pool;
+ ModelCtxt* model = server->vol.model;
+
+ model_resetCurrentTurn( model, DUP_PLAYER );
+
+ TrayTileSet oldTiles = *model_getPlayerTiles( model, DUP_PLAYER );
+ model_removePlayerTiles2( model, DUP_PLAYER, &oldTiles );
+ pool_replaceTiles( pool, &oldTiles );
+
+ TrayTileSet newTiles = {0};
+ fetchTiles( server, DUP_PLAYER, oldTiles.nTiles, NULL, &newTiles );
+
+ model_commitDupeTrade( model, &oldTiles, &newTiles );
+
+ model_addNewTiles( model, DUP_PLAYER, &newTiles );
+ updateOthersTiles( server );
+
+ if ( server->vol.gi->serverRole == SERVER_ISSERVER ) {
+ XWStreamCtxt* tmpStream =
+ mem_stream_make_raw( MPPARM(server->mpool)
+ dutil_getVTManager(server->vol.dutil) );
+
+ addDupeStuffMark( tmpStream, DUPE_STUFF_TRADES_SERVER );
+
+ traySetToStream( tmpStream, &oldTiles );
+ traySetToStream( tmpStream, &newTiles );
+
+ /* Send it to each one */
+ for ( XP_U16 dev = 1; dev < server->nv.nDevices; ++dev ) {
+ sendStreamToDev( server, dev, XWPROTO_DUPE_STUFF, tmpStream );
+ }
+
+ stream_destroy( tmpStream );
+ }
+
+ dupe_resetTimer( server );
+
+ dupe_setupShowTrade( server, newTiles.nTiles );
+ LOG_RETURN_VOID();
+} /* dupe_makeAndReportTrade */
+
+static void
+dupe_transmitPause( ServerCtxt* server, DupPauseType typ, XP_U16 turn,
+ const XP_UCHAR* msg, XP_S16 skipDev )
+{
+ XP_LOGF( "%s(type=%d, msg=%s)", __func__, typ, msg );
+ CurGameInfo* gi = server->vol.gi;
+ if ( gi->serverRole != SERVER_STANDALONE ) {
+ XP_Bool amClient = SERVER_ISCLIENT == gi->serverRole;
+ XWStreamCtxt* tmpStream =
+ mem_stream_make_raw( MPPARM(server->mpool)
+ dutil_getVTManager(server->vol.dutil) );
+
+ addDupeStuffMark( tmpStream, DUPE_STUFF_PAUSE );
+
+ stream_putBits( tmpStream, 1, amClient );
+ stream_putBits( tmpStream, 2, typ );
+ if ( AUTOPAUSED != typ ) {
+ stream_putBits( tmpStream, PLAYERNUM_NBITS, turn );
+ }
+ stream_putU32( tmpStream, server->nv.dupTimerExpires );
+ if ( AUTOPAUSED != typ ) {
+ stringToStream( tmpStream, msg );
+ }
+
+ if ( amClient ) {
+ sendStreamToDev( server, SERVER_DEVICE, XWPROTO_DUPE_STUFF, tmpStream );
+ } else {
+ for ( XP_U16 dev = 1; dev < server->nv.nDevices; ++dev ) {
+ if ( dev != skipDev ) {
+ sendStreamToDev( server, dev, XWPROTO_DUPE_STUFF, tmpStream );
+ }
+ }
+ }
+ stream_destroy( tmpStream );
+ }
+}
+
+static XP_Bool
+dupe_receivePause( ServerCtxt* server, XWStreamCtxt* stream )
+{
+ LOG_FUNC();
+ XP_Bool isClient = (XP_Bool)stream_getBits( stream, 1 );
+ XP_Bool accept = isClient == amServer( server );
+ if ( accept ) {
+ const CurGameInfo* gi = server->vol.gi;
+ DupPauseType pauseType = (DupPauseType)stream_getBits( stream, 2 );
+ XP_S16 turn = -1;
+ if ( AUTOPAUSED != pauseType ) {
+ turn = (XP_S16)stream_getBits( stream, PLAYERNUM_NBITS );
+ XP_ASSERT( 0 <= turn );
+ } else {
+ dupe_clearState( server );
+ }
+
+ setDupTimerExpires( server, (XP_S32)stream_getU32( stream ) );
+
+ XP_UCHAR* msg = NULL;
+ if ( AUTOPAUSED != pauseType ) {
+ msg = stringFromStream( server->mpool, stream );
+ XP_LOGF( "%s(): pauseType: %d; guiltyParty: %d; msg: %s", __func__,
+ pauseType, turn, msg );
+ }
+
+ if ( amServer( server ) ) {
+ XP_U16 senderDev = getIndexForStream( server, stream );
+ dupe_transmitPause( server, pauseType, turn, msg, senderDev );
+ }
+
+ model_noteDupePause( server->vol.model, pauseType, turn, msg );
+ callTurnChangeListener( server );
+
+ const XP_UCHAR* name = NULL;
+ if ( AUTOPAUSED != pauseType ) {
+ name = gi->players[turn].name;
+ }
+ dutil_notifyPause( server->vol.dutil, gi->gameID, pauseType, turn,
+ name, msg );
+
+ XP_FREEP( server->mpool, &msg );
+ }
+ LOG_RETURNF( "%d", accept );
+ return accept;
+}
+
+static XP_Bool
+handleDupeStuff( ServerCtxt* server, XWStreamCtxt* stream )
+{
+ XP_Bool accepted;
+ XP_Bool isServer = amServer( server );
+ DUPE_STUFF typ = getDupeStuffMark( stream );
+ switch ( typ ) {
+ case DUPE_STUFF_MOVE_CLIENT:
+ accepted = isServer && dupe_handleClientMoves( server, stream );
+ break;
+ case DUPE_STUFF_MOVES_SERVER:
+ accepted = !isServer && dupe_handleServerMoves( server, stream );
+ break;
+ case DUPE_STUFF_TRADES_SERVER:
+ accepted = !isServer && dupe_handleServerTrade( server, stream );
+ break;
+ case DUPE_STUFF_PAUSE:
+ accepted = dupe_receivePause( server, stream );
+ break;
+ default:
+ XP_ASSERT(0);
+ accepted = XP_FALSE;
+ }
+ return accepted;
+}
+
+static void
+dupe_commitAndReportMove( ServerCtxt* server, XP_U16 winner,
+ XP_U16 nPlayers, XP_U16* scores,
+ XP_U16 nTiles )
+{
+ ModelCtxt* model = server->vol.model;
+
+ /* The winning move is the one we'll commit everywhere. Get it. Reset
+ everybody else then commit it there. */
+ MoveInfo moveInfo = {0};
+ model_currentMoveToMoveInfo( model, winner, &moveInfo );
+
+ TrayTileSet newTiles = {0};
+ fetchTiles( server, winner, nTiles, NULL, &newTiles );
+
+ for ( XP_U16 player = 0; player < nPlayers; ++player ) {
+ model_resetCurrentTurn( model, player );
+ }
+
+ model_commitDupeTurn( model, &moveInfo, nPlayers, scores, &newTiles );
+
+ updateOthersTiles( server );
+
+ if ( server->vol.gi->serverRole == SERVER_ISSERVER ) {
+ XWStreamCtxt* tmpStream =
+ mem_stream_make_raw( MPPARM(server->mpool)
+ dutil_getVTManager(server->vol.dutil) );
+
+ addDupeStuffMark( tmpStream, DUPE_STUFF_MOVES_SERVER );
+
+ moveInfoToStream( tmpStream, &moveInfo, bitsPerTile(server) );
+ traySetToStream( tmpStream, &newTiles );
+
+ /* Now write all the scores */
+ stream_putBits( tmpStream, NPLAYERS_NBITS, nPlayers );
+ scoresToStream( tmpStream, nPlayers, scores );
+
+ /* Send it to each one */
+ for ( XP_U16 dev = 1; dev < server->nv.nDevices; ++dev ) {
+ sendStreamToDev( server, dev, XWPROTO_DUPE_STUFF, tmpStream );
+ }
+
+ stream_destroy( tmpStream );
+ }
+
+ dupe_resetTimer( server );
+
+ dupe_setupShowMove( server, scores );
+} /* dupe_commitAndReportMove */
+
+static void
+dupe_forceCommits( ServerCtxt* server )
+{
+ if ( dupTimerRunning() ) {
+ XP_U32 now = dutil_getCurSeconds( server->vol.dutil );
+ if ( server->nv.dupTimerExpires <= now ) {
+
+ ModelCtxt* model = server->vol.model;
+ for ( XP_U16 ii = 0; ii < server->vol.gi->nPlayers; ++ii ) {
+ if ( server->vol.gi->players[ii].isLocal
+ && !server->nv.dupTurnsMade[ii] ) {
+ if ( !model_checkMoveLegal( model, ii, (XWStreamCtxt*)NULL,
+ (WordNotifierInfo*)NULL ) ) {
+ model_resetCurrentTurn( model, ii );
+ }
+ commitMoveImpl( server, ii, NULL, XP_TRUE );
+ }
+ }
+ }
+ }
+}
+
+/* Figure out whether everything we care about is done for this turn. If I'm a
+ guest, I care only about local players. If I'm a host or standalone, I care
+ about everything. */
+static void
+checkWhatsDone( const ServerCtxt* server, XP_Bool amServer,
+ XP_Bool* allDoneP, XP_Bool* allLocalsDoneP )
+{
+ XP_Bool allDone = XP_TRUE;
+ XP_Bool allLocalsDone = XP_TRUE;
+ for ( XP_U16 ii = 0;
+ (allLocalsDone || allDone) && ii < server->vol.gi->nPlayers;
+ ++ii ) {
+ XP_Bool done = server->nv.dupTurnsMade[ii];
+ XP_Bool isLocal = server->vol.gi->players[ii].isLocal;
+ if ( isLocal ) {
+ allLocalsDone = allLocalsDone & done;
+ }
+ if ( amServer || isLocal ) {
+ allDone = allDone && done;
+ }
+ }
+
+ // XP_LOGF( "%s(): allDone: %d; allLocalsDone: %d", __func__, allDone, allLocalsDone );
+ *allDoneP = allDone;
+ *allLocalsDoneP = allLocalsDone;
+}
+
+XP_Bool
+server_dupTurnDone( const ServerCtxt* server, XP_U16 turn )
+{
+ return server->vol.gi->players[turn].isLocal
+ && server->nv.dupTurnsMade[turn];
+}
+
+static XP_Bool
+dupe_checkTurns( ServerCtxt* server )
+{
+ /* If all local players have made moves, it's time to commit the moves
+ locally or notifiy the host */
+ XP_Bool allDone = XP_TRUE;
+ XP_Bool allLocalsDone = XP_TRUE;
+ XP_Bool amServer = server->vol.gi->serverRole == SERVER_ISSERVER
+ || server->vol.gi->serverRole == SERVER_STANDALONE;
+ checkWhatsDone( server, amServer, &allDone, &allLocalsDone );
+
+ XP_LOGF( "%s(): allDone: %d", __func__, allDone );
+
+ if ( allDone ) { /* Yep: commit time */
+ if ( amServer ) { /* I now have everything I need to move the
+ game foreward */
+ dupe_commitAndReport( server );
+ } else if ( ! server->nv.dupTurnsSent ) { /* I need to send info for
+ local players to host */
+ XWStreamCtxt* stream =
+ messageStreamWithHeader( server, SERVER_DEVICE,
+ XWPROTO_DUPE_STUFF );
+
+ addDupeStuffMark( stream, DUPE_STUFF_MOVE_CLIENT );
+
+ /* XP_U32 hash = model_getHash( server->vol.model ); */
+ /* stream_putU32( stream, hash ); */
+
+ XP_U16 localCount = gi_countLocalPlayers( server->vol.gi, XP_FALSE );
+ XP_LOGF( "%s(): writing %d moves", __func__, localCount );
+ stream_putBits( stream, NPLAYERS_NBITS, localCount );
+ for ( XP_U16 ii = 0; ii < server->vol.gi->nPlayers; ++ii ) {
+ if ( server->vol.gi->players[ii].isLocal ) {
+ stream_putBits( stream, PLAYERNUM_NBITS, ii );
+ stream_putBits( stream, 1, server->nv.dupTurnsForced[ii] );
+ model_currentMoveToStream( server->vol.model, ii, stream );
+ XP_LOGF( "%s(): wrote move %d ", __func__, ii );
+ }
+ }
+
+ stream_destroy( stream ); /* sends it */
+ server->nv.dupTurnsSent = XP_TRUE;
+ }
+ }
+ return allDone;
+} /* dupe_checkTurns */
+
+static void
+dupe_postStatus( const ServerCtxt* server, XP_Bool allDone )
+{
+ /* Standalone case: say nothing here. Should be self evident what's
+ up.*/
+ /* If I'm a client and it's NOT a local turn, tell user that his
+ turn's been sent off and he has to wait.
+ *
+ * If I'm a server, tell user how many of the expected moves have not
+ * yet been received. If all have been, say nothing.
+ */
+
+ XP_UCHAR buf[256] = {0};
+ XP_Bool amHost = XP_FALSE;
+ switch ( server->vol.gi->serverRole ) {
+ case SERVER_STANDALONE:
+ /* do nothing */
+ break;
+ case SERVER_ISCLIENT:
+ if ( allDone ) {
+ const XP_UCHAR* fmt = dutil_getUserString( server->vol.dutil,
+ STR_DUP_CLIENT_SENT );
+ XP_SNPRINTF( buf, VSIZE(buf), "%s", fmt );
+ }
+ break;
+ case SERVER_ISSERVER:
+ amHost = XP_TRUE;
+ if ( !allDone ) {
+ XP_U16 nHere = 0;
+ for ( XP_U16 ii = 0; ii < server->vol.gi->nPlayers; ++ii ) {
+ if ( server->nv.dupTurnsMade[ii] ) {
+ ++nHere;
+ }
+ }
+ const XP_UCHAR* fmt = dutil_getUserString( server->vol.dutil,
+ STRDD_DUP_HOST_RECEIVED );
+ XP_SNPRINTF( buf, VSIZE(buf), fmt, nHere, server->vol.gi->nPlayers );
+ }
+ }
+
+ if ( !!buf[0] ) {
+ XP_LOGF( "%s(): msg=%s", __func__, buf );
+ util_notifyDupStatus( server->vol.util, amHost, buf );
+ }
+}
+
+/* Called on client only? */
+static void
+dupe_storeTurn( ServerCtxt* server, XP_U16 turn, XP_Bool forced )
+{
+ XP_ASSERT( !server->nv.dupTurnsMade[turn] );
+ XP_ASSERT( server->vol.gi->players[turn].isLocal ); /* not if I'm the host! */
+ server->nv.dupTurnsMade[turn] = XP_TRUE;
+ server->nv.dupTurnsForced[turn] = forced;
+
+ XP_Bool allDone = dupe_checkTurns( server );
+ dupe_postStatus( server, allDone );
+ nextTurn( server, PICK_NEXT );
+
+ XP_LOGF( "%s(): player %d now has %d tiles", __func__, turn,
+ model_getNumTilesInTray( server->vol.model, turn ) );
+}
+
+static void
+dupe_clearState( ServerCtxt* server )
+{
+ for ( XP_U16 ii = 0; ii < server->vol.gi->nPlayers; ++ii ) {
+ server->nv.dupTurnsMade[ii] = XP_FALSE;
+ server->nv.dupTurnsForced[ii] = XP_FALSE;
+ }
+ server->nv.dupTurnsSent = XP_FALSE;
+}
+
+/* Make it the "turn" of the next local player who hasn't yet submitted a
+ turn. If all have, make it a non-local player's turn. */
+static XP_U16
+dupe_nextTurn( const ServerCtxt* server )
+{
+ CurGameInfo* gi = server->vol.gi;
+ XP_S16 result = -1;
+ XP_U16 nextNonLocal = DUP_PLAYER;
+ for ( XP_U16 ii = 0; ii < gi->nPlayers; ++ii ) {
+ if ( !server->nv.dupTurnsMade[ii] ) {
+ if ( gi->players[ii].isLocal ) {
+ result = ii;
+ break;
+ } else {
+ nextNonLocal = ii;
+ }
+ }
+ }
+
+ if ( -1 == result ) {
+ result = nextNonLocal;
+ }
+
+ return result;
+}
+
/* 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
@@ -2478,16 +3489,16 @@ reflectMove( ServerCtxt* server, XWStreamCtxt* stream )
* 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, TrayTileSet* newTilesP )
+static XP_Bool
+commitMoveImpl( ServerCtxt* server, XP_U16 player, TrayTileSet* newTilesP,
+ XP_Bool forced )
{
- XP_S16 turn = server->nv.currentTurn;
ModelCtxt* model = server->vol.model;
CurGameInfo* gi = server->vol.gi;
+ XP_Bool inDupeMode = inDuplicateMode(server);
+ XP_ASSERT( server->nv.currentTurn == player || inDupeMode );
+ XP_S16 turn = player;
TrayTileSet newTiles = {0};
- XP_U16 nTilesMoved;
- XP_Bool isLegalMove = XP_TRUE;
- XP_Bool isClient = gi->serverRole == SERVER_ISCLIENT;
if ( !!newTilesP ) {
newTiles = *newTilesP;
@@ -2504,27 +3515,51 @@ server_commitMove( ServerCtxt* server, TrayTileSet* newTilesP )
if client, send to server. */
XP_ASSERT( turn >= 0 );
- pool_removeTiles( server->pool, &newTiles );
+ if ( inDupeMode ) {
+ dupe_storeTurn( server, turn, forced );
+ } else {
+ finishMove( server, &newTiles, turn );
+ }
+
+ return XP_TRUE;
+}
+
+XP_Bool
+server_commitMove( ServerCtxt* server, XP_U16 player, TrayTileSet* newTilesP )
+{
+ return commitMoveImpl( server, player, newTilesP, XP_FALSE );
+}
+
+static void
+finishMove( ServerCtxt* server, TrayTileSet* newTiles, XP_U16 turn )
+{
+ LOG_FUNC();
+ ModelCtxt* model = server->vol.model;
+ CurGameInfo* gi = server->vol.gi;
+
+ pool_removeTiles( server->pool, newTiles );
server->vol.pickTilesCalled[turn] = XP_FALSE;
- nTilesMoved = model_getCurrentMoveCount( model, turn );
- fetchTiles( server, turn, nTilesMoved, NULL, &newTiles );
+ XP_U16 nTilesMoved = model_getCurrentMoveCount( model, turn );
+ fetchTiles( server, turn, nTilesMoved, NULL, newTiles );
+ XP_Bool isClient = gi->serverRole == SERVER_ISCLIENT;
+ XP_Bool isLegalMove = XP_TRUE;
#ifndef XWFEATURE_STANDALONE_ONLY
if ( isClient ) {
/* just send to server */
- sendMoveTo( server, SERVER_DEVICE, turn, XP_TRUE, &newTiles,
+ sendMoveTo( server, SERVER_DEVICE, turn, XP_TRUE, newTiles,
(TrayTileSet*)NULL );
} else {
isLegalMove = checkMoveAllowed( server, turn );
- sendMoveToClientsExcept( server, turn, isLegalMove, &newTiles,
+ sendMoveToClientsExcept( server, turn, isLegalMove, newTiles,
(TrayTileSet*)NULL, SERVER_DEVICE );
}
#else
isLegalMove = checkMoveAllowed( server, turn );
#endif
- model_commitTurn( model, turn, &newTiles );
+ model_commitTurn( model, turn, newTiles );
sortTilesIf( server, turn );
if ( !isLegalMove && !isClient ) {
@@ -2544,12 +3579,12 @@ server_commitMove( ServerCtxt* server, TrayTileSet* newTilesP )
} else {
nextTurn( server, PICK_NEXT );
}
-
XP_LOGF( "%s(): player %d now has %d tiles", __func__, turn,
model_getNumTilesInTray( model, turn ) );
+} /* finishMove */
- return XP_TRUE;
-} /* server_commitMove */
+/* return XP_TRUE; */
+/* } /\* server_commitMove *\/ */
XP_Bool
server_commitTrade( ServerCtxt* server, const TrayTileSet* oldTiles,
@@ -2583,7 +3618,7 @@ server_commitTrade( ServerCtxt* server, const TrayTileSet* oldTiles,
} /* server_commitTrade */
XP_S16
-server_getCurrentTurn( ServerCtxt* server, XP_Bool* isLocal )
+server_getCurrentTurn( const ServerCtxt* server, XP_Bool* isLocal )
{
XP_S16 turn = server->nv.currentTurn;
if ( NULL != isLocal && turn >= 0 ) {
@@ -2593,7 +3628,25 @@ server_getCurrentTurn( ServerCtxt* server, XP_Bool* isLocal )
} /* server_getCurrentTurn */
XP_Bool
-server_getGameIsOver( ServerCtxt* server )
+server_isPlayersTurn( const ServerCtxt* server, XP_U16 turn )
+{
+ XP_Bool result = XP_FALSE;
+
+ if ( inDuplicateMode(server) ) {
+ if ( server->vol.gi->players[turn].isLocal
+ && ! server->nv.dupTurnsMade[turn] ) {
+ result = XP_TRUE;
+ }
+ } else {
+ result = turn == server_getCurrentTurn( server, NULL );
+ }
+
+ // XP_LOGF( "%s(%d) => %d", __func__, turn, result );
+ return result;
+}
+
+XP_Bool
+server_getGameIsOver( const ServerCtxt* server )
{
return server->nv.gameState == XWSTATE_GAMEOVER;
} /* server_getGameIsOver */
@@ -2721,7 +3774,8 @@ server_endGame( ServerCtxt* server )
} /* server_endGame */
/* If game is about to end because one player's out of tiles, we don't want to
- * keep trying to move */
+ * keep trying to move. Note that in duplicate mode if ANY player has tiles
+ * the answer's yes. */
static XP_Bool
tileCountsOk( const ServerCtxt* server )
{
@@ -2729,18 +3783,24 @@ tileCountsOk( const ServerCtxt* server )
if ( maybeOver ) {
ModelCtxt* model = server->vol.model;
XP_U16 nPlayers = server->vol.gi->nPlayers;
- XP_Bool zeroFound = XP_FALSE;
+ XP_Bool inDupMode = inDuplicateMode( server );
+ XP_Bool zeroFound = inDupMode;
- while ( nPlayers-- ) {
- XP_U16 count = model_getNumTilesTotal( model, nPlayers );
- if ( count == 0 ) {
+ for ( XP_U16 player = 0; player < nPlayers; ++player ) {
+ XP_U16 count = model_getNumTilesTotal( model, player );
+ if ( inDupMode && count > 0 ) {
+ zeroFound = XP_FALSE;
+ break;
+ } else if ( !inDupMode && count == 0 ) {
zeroFound = XP_TRUE;
break;
}
}
maybeOver = zeroFound;
}
- return !maybeOver;
+ XP_Bool result = !maybeOver;
+ // LOG_RETURNF( "%d", result );
+ return result;
} /* tileCountsOk */
static void
@@ -2748,7 +3808,11 @@ setTurn( ServerCtxt* server, XP_S16 turn )
{
XP_ASSERT( -1 == turn
|| (!amServer(server) || (0 == server->nv.pendingRegistrations)));
- if ( server->nv.currentTurn != turn || 1 == server->vol.gi->nPlayers ) {
+ XP_Bool inDupMode = inDuplicateMode( server );
+ if ( inDupMode || server->nv.currentTurn != turn || 1 == server->vol.gi->nPlayers ) {
+ if ( DUP_PLAYER == turn && inDupMode ) {
+ turn = dupe_nextTurn( server );
+ }
server->nv.currentTurn = turn;
server->nv.lastMoveTime = dutil_getCurSeconds( server->vol.dutil );
callTurnChangeListener( server );
@@ -2841,8 +3905,7 @@ reflectUndos( ServerCtxt* server, XWStreamCtxt* stream, XW_Proto code )
sortTilesIf( server, turn );
if ( code == XWPROTO_UNDO_INFO_CLIENT ) { /* need to inform */
- XP_U16 sourceClientIndex =
- getIndexForDevice( server, stream_getAddress( stream ) );
+ XP_U16 sourceClientIndex = getIndexForStream( server, stream );
sendUndoToClientsExcept( server, sourceClientIndex, nUndone,
lastUndone );
@@ -2955,7 +4018,8 @@ server_receiveMessage( ServerCtxt* server, XWStreamCtxt* incoming )
const XW_Proto code = readProto( server, incoming );
XP_LOGF( "%s(code=%s)", __func__, codeToStr(code) );
- if ( code == XWPROTO_DEVICE_REGISTRATION ) {
+ switch ( code ) {
+ case XWPROTO_DEVICE_REGISTRATION:
accepted = isServer;
if ( accepted ) {
/* This message is special: doesn't have the header that's possible
@@ -2964,104 +4028,84 @@ server_receiveMessage( ServerCtxt* server, XWStreamCtxt* incoming )
XP_LOGF( "%s: somebody's registering!!!", __func__ );
accepted = handleRegistrationMsg( server, incoming );
}
- } else if ( code == XWPROTO_CLIENT_SETUP ) {
+ break;
+ case XWPROTO_CLIENT_SETUP:
accepted = !isServer;
if ( accepted ) {
XP_STATUSF( "client got XWPROTO_CLIENT_SETUP" );
accepted = client_readInitialMessage( server, incoming );
}
+ break;
#ifdef XWFEATURE_CHAT
- } else if ( code == XWPROTO_CHAT ) {
- XP_UCHAR* msg = stringFromStream( server->mpool, incoming );
- XP_S16 from = 1 <= stream_getSize( incoming )
- ? stream_getU8( incoming ) : -1;
- XP_U32 timestamp = sizeof(timestamp) <= stream_getSize( incoming )
- ? stream_getU32( incoming ) : 0;
- if ( isServer ) {
- XP_U16 sourceClientIndex =
- getIndexForDevice( server, stream_getAddress( incoming ) );
- sendChatToClientsExcept( server, sourceClientIndex, msg, from,
- timestamp );
- }
- util_showChat( server->vol.util, msg, from, timestamp );
- XP_FREE( server->mpool, msg );
- accepted = XP_TRUE;
+ case XWPROTO_CHAT:
+ accepted = receiveChat( server, incoming );
+ break;
#endif
- } else if ( readStreamHeader( server, incoming ) ) {
+ case XWPROTO_MOVEMADE_INFO_CLIENT: /* client is reporting a move */
+ if ( XWSTATE_INTURN == server->nv.gameState ) {
+ accepted = reflectMoveAndInform( server, incoming );
+ } else {
+ XP_LOGF( "%s(): bad state: %s", __func__, getStateStr( server->nv.gameState ) );
+ }
+ break;
+
+ case XWPROTO_MOVEMADE_INFO_SERVER: /* server telling me about a move */
+ if ( isServer ) {
+ XP_LOGF( "%s(): %s received by server!", __func__, codeToStr(code) );
+ accepted = XP_FALSE;
+ } else {
+ accepted = reflectMove( server, incoming );
+ }
+ if ( accepted ) {
+ nextTurn( server, PICK_NEXT );
+ }
+ break;
+
+ case XWPROTO_UNDO_INFO_CLIENT:
+ case XWPROTO_UNDO_INFO_SERVER:
+ accepted = reflectUndos( server, incoming, code );
+ /* nextTurn is called by reflectUndos */
+ break;
+
+ case XWPROTO_BADWORD_INFO:
+ accepted = handleIllegalWord( server, incoming );
+ if ( accepted && server->nv.gameState != XWSTATE_GAMEOVER ) {
+ nextTurn( server, PICK_NEXT );
+ }
+ break;
+
+ case XWPROTO_MOVE_CONFIRM:
+ accepted = handleMoveOk( server, incoming );
+ break;
+
+ case XWPROTO_CLIENT_REQ_END_GAME: {
XP_S8 quitter;
- switch( code ) {
-/* case XWPROTO_MOVEMADE_INFO: */
-/* accepted = client_reflectMoveMade( server, incoming ); */
-/* if ( accepted ) { */
-/* nextTurn( server ); */
-/* } */
-/* break; */
-/* case XWPROTO_TRADEMADE_INFO: */
-/* accepted = client_reflectTradeMade( server, incoming ); */
-/* if ( accepted ) { */
-/* nextTurn( server ); */
-/* } */
-/* break; */
-/* case XWPROTO_CLIENT_MOVE_INFO: */
-/* accepted = handleClientMoved( server, incoming ); */
-/* break; */
-/* case XWPROTO_CLIENT_TRADE_INFO: */
-/* accepted = handleClientTraded( server, incoming ); */
-/* break; */
-
- case XWPROTO_MOVEMADE_INFO_CLIENT: /* client is reporting a move */
- accepted = (XWSTATE_INTURN == server->nv.gameState)
- && reflectMoveAndInform( server, incoming );
- break;
-
- case XWPROTO_MOVEMADE_INFO_SERVER: /* server telling me about a move */
- accepted = !isServer && reflectMove( server, incoming );
- if ( accepted ) {
- nextTurn( server, PICK_NEXT );
- }
- break;
-
- case XWPROTO_UNDO_INFO_CLIENT:
- case XWPROTO_UNDO_INFO_SERVER:
- accepted = reflectUndos( server, incoming, code );
- /* nextTurn is called by reflectUndos */
- break;
-
- case XWPROTO_BADWORD_INFO:
- accepted = handleIllegalWord( server, incoming );
- if ( accepted && server->nv.gameState != XWSTATE_GAMEOVER ) {
- nextTurn( server, PICK_NEXT );
- }
- break;
-
- case XWPROTO_MOVE_CONFIRM:
- accepted = handleMoveOk( server, incoming );
- break;
-
- case XWPROTO_CLIENT_REQ_END_GAME:
- getQuitter( server, incoming, &quitter );
- endGameInternal( server, END_REASON_USER_REQUEST, quitter );
- accepted = XP_TRUE;
- break;
- case XWPROTO_END_GAME:
- getQuitter( server, incoming, &quitter );
- doEndGame( server, quitter );
- accepted = XP_TRUE;
- break;
- default:
- XP_WARNF( "%s: Unknown code on incoming message: %d\n",
- __func__, code );
- break;
- } /* switch */
+ getQuitter( server, incoming, &quitter );
+ endGameInternal( server, END_REASON_USER_REQUEST, quitter );
+ accepted = XP_TRUE;
}
+ break;
+ case XWPROTO_END_GAME: {
+ XP_S8 quitter;
+ getQuitter( server, incoming, &quitter );
+ doEndGame( server, quitter );
+ accepted = XP_TRUE;
+ }
+ break;
+ case XWPROTO_DUPE_STUFF:
+ accepted = handleDupeStuff( server, incoming );
+ break;
+ default:
+ XP_WARNF( "%s: Unknown code on incoming message: %d\n",
+ __func__, code );
+ XP_ASSERT( 0 );
+ break;
+ } /* switch */
XP_ASSERT( isServer == amServer( server ) ); /* caching value is ok? */
stream_close( incoming );
- if ( !accepted ) {
- XP_LOGF( "%s(): failure processing code %s", __func__, codeToStr(code) );
- // XP_ASSERT( 0 );
- }
- XP_LOGF( "%s(%s) => %d", __func__, codeToStr(code), accepted );
+
+ XP_LOGF( "%s() => %d (code=%s)", __func__, accepted, codeToStr(code) );
return accepted;
} /* server_receiveMessage */
#endif
@@ -3293,7 +4337,6 @@ server_writeFinalScores( ServerCtxt* server, XWStreamCtxt* stream )
XP_UCHAR buf[128];
XP_S16 thisScore = IMPOSSIBLY_LOW_SCORE;
XP_S16 thisIndex = -1;
- XP_UCHAR tmpbuf[48];
XP_U16 ii, placeKey = 0;
XP_Bool firstDone;
@@ -3335,12 +4378,15 @@ server_writeFinalScores( ServerCtxt* server, XWStreamCtxt* stream )
}
}
- firstDone = model_getNumTilesTotal( model, thisIndex) == 0;
- XP_SNPRINTF( tmpbuf, sizeof(tmpbuf),
- (firstDone? addString:subString),
- firstDone?
- tilePenalties.arr[thisIndex]:
- -tilePenalties.arr[thisIndex] );
+ XP_UCHAR tmpbuf[48] = {0};
+ if ( !inDuplicateMode( server ) ) {
+ firstDone = model_getNumTilesTotal( model, thisIndex) == 0;
+ XP_SNPRINTF( tmpbuf, sizeof(tmpbuf),
+ (firstDone? addString:subString),
+ firstDone?
+ tilePenalties.arr[thisIndex]:
+ -tilePenalties.arr[thisIndex] );
+ }
const XP_UCHAR* name = emptyStringIfNull(gi->players[thisIndex].name);
if ( 0 == placeKey ) {
@@ -3350,16 +4396,18 @@ server_writeFinalScores( ServerCtxt* server, XWStreamCtxt* stream )
name, scores.arr[thisIndex] );
} else {
const XP_UCHAR* fmt = dutil_getUserString( server->vol.dutil,
- placeKey );
+ placeKey );
XP_SNPRINTF( buf, sizeof(buf), fmt, name,
scores.arr[thisIndex] );
}
- XP_UCHAR buf2[64];
- XP_SNPRINTF( buf2, sizeof(buf2), XP_CR " (%d %s%s)",
- model_getPlayerScore( model, thisIndex ),
- tmpbuf, timeStr );
- XP_STRCAT( buf, buf2 );
+ if ( !inDuplicateMode( server ) ) {
+ XP_UCHAR buf2[64];
+ XP_SNPRINTF( buf2, sizeof(buf2), XP_CR " (%d %s%s)",
+ model_getPlayerScore( model, thisIndex ),
+ tmpbuf, timeStr );
+ XP_STRCAT( buf, buf2 );
+ }
if ( 1 < place ) {
stream_catString( stream, XP_CR );
diff --git a/xwords4/common/server.h b/xwords4/common/server.h
index 5813a7f89..af608746c 100644
--- a/xwords4/common/server.h
+++ b/xwords4/common/server.h
@@ -30,17 +30,6 @@
extern "C" {
#endif
-/* typedef struct ServerCtxt ServerCtxt; */
-
-/* typedef struct ServerVtable { */
-
-/* void (*m_registerPlayer)( ServerCtxt* server, XP_U16 playerNum, */
-/* XP_PlayerSocket socket ); */
-
-/* void (*m_getTileValueInfo)( ServerCtxt* server, void* valueBuf ); */
-
-/* } ServerVtable; */
-
ServerCtxt* server_make( MPFORMAL ModelCtxt* model, CommsCtxt* comms,
XW_UtilCtxt* util );
@@ -59,6 +48,11 @@ typedef void (*TurnChangeListener)( void* data );
void server_setTurnChangeListener( ServerCtxt* server, TurnChangeListener tl,
void* data );
+typedef void (*TimerChangeListener)( void* data, XP_U32 gameID,
+ XP_S32 oldVal, XP_S32 newVal );
+void server_setTimerChangeListener( ServerCtxt* server, TimerChangeListener tl,
+ void* data );
+
typedef void (*GameOverListener)( void* data, XP_S16 quitter );
void server_setGameOverListener( ServerCtxt* server, GameOverListener gol,
void* data );
@@ -80,8 +74,17 @@ XP_U16 server_secondsUsedBy( ServerCtxt* server, XP_U16 playerNum );
XP_Bool server_handleUndo( ServerCtxt* server, XP_U16 limit );
/* signed because negative number means nobody's turn yet */
-XP_S16 server_getCurrentTurn( ServerCtxt* server, XP_Bool* isLocal );
-XP_Bool server_getGameIsOver( ServerCtxt* server );
+XP_S16 server_getCurrentTurn( const ServerCtxt* server, XP_Bool* isLocal );
+XP_Bool server_isPlayersTurn( const ServerCtxt* server, XP_U16 turn );
+XP_Bool server_getGameIsOver( const ServerCtxt* server );
+XP_S32 server_getDupTimerExpires( const ServerCtxt* server );
+XP_S16 server_getTimerSeconds( const ServerCtxt* server, XP_U16 turn );
+XP_Bool server_dupTurnDone( const ServerCtxt* server, XP_U16 turn );
+XP_Bool server_canPause( const ServerCtxt* server );
+XP_Bool server_canUnpause( const ServerCtxt* server );
+void server_pause( ServerCtxt* server, XP_S16 turn, const XP_UCHAR* msg );
+void server_unpause( ServerCtxt* server, XP_S16 turn, const XP_UCHAR* msg );
+
/* return bitvector marking players still not arrived in networked game */
XP_U16 server_getMissingPlayers( const ServerCtxt* server );
XP_U32 server_getLastMoveTime( const ServerCtxt* server );
@@ -97,7 +100,8 @@ XP_U16 server_getPendingRegs( const ServerCtxt* server );
XP_Bool server_do( ServerCtxt* server );
-XP_Bool server_commitMove( ServerCtxt* server, TrayTileSet* newTiles );
+XP_Bool server_commitMove( ServerCtxt* server, XP_U16 player,
+ TrayTileSet* newTiles );
XP_Bool server_commitTrade( ServerCtxt* server, const TrayTileSet* oldTiles,
TrayTileSet* newTiles );
diff --git a/xwords4/common/smsproto.c b/xwords4/common/smsproto.c
index 226b49a89..d1f3e5598 100644
--- a/xwords4/common/smsproto.c
+++ b/xwords4/common/smsproto.c
@@ -672,12 +672,12 @@ restorePartials( SMSProto* state )
XP_UCHAR phone[32];
(void)stringFromStreamHere( stream, phone, VSIZE(phone) );
int nMsgIDs = stream_getU8( stream );
- XP_LOGF( "%s(): got %d message records for phone %s", __func__,
- nMsgIDs, phone );
+ /* XP_LOGF( "%s(): got %d message records for phone %s", __func__, */
+ /* nMsgIDs, phone ); */
for ( int jj = 0; jj < nMsgIDs; ++jj ) {
XP_U16 msgID = stream_getU16( stream );
int count = stream_getU8( stream );
- XP_LOGF( "%s(): got %d records for msgID %d", __func__, count, msgID );
+ /* XP_LOGF( "%s(): got %d records for msgID %d", __func__, count, msgID ); */
for ( int kk = 0; kk < count; ++kk ) {
int len = stream_getU8( stream );
if ( 0 < len ) {
diff --git a/xwords4/common/states.h b/xwords4/common/states.h
index fa7e048d6..3adc1778e 100644
--- a/xwords4/common/states.h
+++ b/xwords4/common/states.h
@@ -21,18 +21,18 @@
#define _STATES_H_
enum {
- XWSTATE_NONE,
- XWSTATE_BEGIN,
+ XWSTATE_NONE, /* 0 */
+ XWSTATE_BEGIN, /* 1 */
__UNUSED1, /* was XWSTATE_POOL_INITED */
XWSTATE_NEED_SHOWSCORE, /* client-only */
- __XWSTATE_WAITING_ALL_REG, /* unused */
+ __XWSTATE_WAITING_ALL_REG, /* 4 (unused) */
XWSTATE_RECEIVED_ALL_REG, /* includes waiting for dict from server */
- XWSTATE_NEEDSEND_BADWORD_INFO,
+ XWSTATE_NEEDSEND_BADWORD_INFO, /* 6 */
XWSTATE_MOVE_CONFIRM_WAIT, /* client's waiting to hear back */
XWSTATE_MOVE_CONFIRM_MUSTSEND,/* server should tell client asap */
- XWSTATE_NEEDSEND_ENDGAME,
- XWSTATE_INTURN,
- XWSTATE_GAMEOVER,
+ XWSTATE_NEEDSEND_ENDGAME, /* 9 */
+ XWSTATE_INTURN, /* 10 */
+ XWSTATE_GAMEOVER, /* 11 */
XWSTATE_LAST /* for asserts only :-) */
};
diff --git a/xwords4/common/strutils.c b/xwords4/common/strutils.c
index 8ae1a3ed5..66e465f86 100644
--- a/xwords4/common/strutils.c
+++ b/xwords4/common/strutils.c
@@ -28,13 +28,13 @@ extern "C" {
#endif
XP_U16
-bitsForMax( XP_U32 n )
+bitsForMax( XP_U32 nn )
{
XP_U16 result = 0;
- XP_ASSERT( n > 0 );
+ XP_ASSERT( nn > 0 );
- while ( n != 0 ) {
- n >>= 1;
+ while ( nn != 0 ) {
+ nn >>= 1;
++result;
}
@@ -65,6 +65,37 @@ tilesFromStream( XWStreamCtxt* stream, Tile* tiles, XP_U16 nTiles )
}
} /* tilesFromStream */
+void
+scoresToStream( XWStreamCtxt* stream, XP_U16 nScores, const XP_U16* scores )
+{
+ if ( 0 < nScores ) {
+ XP_U16 maxScore = 1; /* 0 will confuse bitsForMax */
+ for ( XP_U16 ii = 0; ii < nScores; ++ii ) {
+ XP_U16 score = scores[ii];
+ if ( score > maxScore ) {
+ maxScore = score;
+ }
+ }
+
+ XP_U16 bits = bitsForMax( maxScore );
+ stream_putBits( stream, 4, bits );
+ for ( XP_U16 ii = 0; ii < nScores; ++ii ) {
+ stream_putBits( stream, bits, scores[ii] );
+ }
+ }
+}
+
+void
+scoresFromStream( XWStreamCtxt* stream, XP_U16 nScores, XP_U16* scores )
+{
+ if ( 0 < nScores ) {
+ XP_U16 bits = (XP_U16)stream_getBits( stream, 4 );
+ for ( XP_U16 ii = 0; ii < nScores; ++ii ) {
+ scores[ii] = stream_getBits( stream, bits );
+ }
+ }
+}
+
void
traySetFromStream( XWStreamCtxt* stream, TrayTileSet* ts )
{
@@ -73,6 +104,69 @@ traySetFromStream( XWStreamCtxt* stream, TrayTileSet* ts )
ts->nTiles = (XP_U8)nTiles;
} /* traySetFromStream */
+#ifdef DEBUG
+void
+assertSorted( const MoveInfo* mi )
+{
+ for ( XP_U16 ii = 1; ii < mi->nTiles; ++ii ) {
+ XP_ASSERT( mi->tiles[ii-1].varCoord < mi->tiles[ii].varCoord );
+ }
+}
+#endif
+
+void
+moveInfoToStream( XWStreamCtxt* stream, const MoveInfo* mi, XP_U16 bitsPerTile )
+{
+#ifdef DEBUG
+ /* XP_UCHAR buf[64] = {0}; */
+ /* XP_U16 offset = 0; */
+#endif
+ assertSorted( mi );
+
+ stream_putBits( stream, NTILES_NBITS, mi->nTiles );
+ stream_putBits( stream, NUMCOLS_NBITS_5, mi->commonCoord );
+ stream_putBits( stream, 1, mi->isHorizontal );
+
+ XP_ASSERT( bitsPerTile == 5 || bitsPerTile == 6 );
+ for ( XP_U16 ii = 0; ii < mi->nTiles; ++ii ) {
+ stream_putBits( stream, NUMCOLS_NBITS_5, mi->tiles[ii].varCoord );
+
+ Tile tile = mi->tiles[ii].tile;
+#ifdef DEBUG
+ /* offset += XP_SNPRINTF( &buf[offset], VSIZE(buf)-offset, "%x,", tile ); */
+#endif
+ stream_putBits( stream, bitsPerTile, tile & TILE_VALUE_MASK );
+ stream_putBits( stream, 1, (tile & TILE_BLANK_BIT) != 0 );
+ }
+ // XP_LOGF( "%s(): tiles: %s", __func__, buf );
+}
+
+void
+moveInfoFromStream( XWStreamCtxt* stream, MoveInfo* mi, XP_U16 bitsPerTile )
+{
+#ifdef DEBUG
+ /* XP_UCHAR buf[64] = {0}; */
+ /* XP_U16 offset = 0; */
+#endif
+ mi->nTiles = stream_getBits( stream, NTILES_NBITS );
+ XP_ASSERT( mi->nTiles <= MAX_TRAY_TILES );
+ mi->commonCoord = stream_getBits( stream, NUMCOLS_NBITS_5 );
+ mi->isHorizontal = stream_getBits( stream, 1 );
+ for ( XP_U16 ii = 0; ii < mi->nTiles; ++ii ) {
+ mi->tiles[ii].varCoord = stream_getBits( stream, NUMCOLS_NBITS_5 );
+ Tile tile = stream_getBits( stream, bitsPerTile );
+ if ( 0 != stream_getBits( stream, 1 ) ) {
+ tile |= TILE_BLANK_BIT;
+ }
+ mi->tiles[ii].tile = tile;
+#ifdef DEBUG
+ /* offset += XP_SNPRINTF( &buf[offset], VSIZE(buf)-offset, "%x,", tile ); */
+#endif
+ }
+ assertSorted( mi );
+ // XP_LOGF( "%s(): tiles: %s", __func__, buf );
+}
+
void
removeTile( TrayTileSet* tiles, XP_U16 index )
{
@@ -465,6 +559,7 @@ smsToBin( XP_U8* out, XP_U16* outlenp, const XP_UCHAR* sms, XP_U16 smslen )
void
log_hex( const XP_U8* memp, XP_U16 len, const char* tag )
{
+ XP_LOGF( "%s(len=%d[0x%x])", __func__, len, len );
const char* hex = "0123456789ABCDEF";
XP_U16 ii, jj;
XP_U16 offset = 0;
diff --git a/xwords4/common/strutils.h b/xwords4/common/strutils.h
index 631fa8c15..5d80939f0 100644
--- a/xwords4/common/strutils.h
+++ b/xwords4/common/strutils.h
@@ -37,6 +37,14 @@ void traySetFromStream( XWStreamCtxt* stream, TrayTileSet* ts );
void sortTiles( TrayTileSet* dest, const TrayTileSet* src, XP_U16 skip );
void removeTile( TrayTileSet* tiles, XP_U16 index );
+void scoresToStream( XWStreamCtxt* stream, XP_U16 nScores, const XP_U16* scores );
+void scoresFromStream( XWStreamCtxt* stream, XP_U16 nScores, XP_U16* scores );
+
+void moveInfoToStream( XWStreamCtxt* stream, const MoveInfo* mi,
+ XP_U16 bitsPerTile );
+void moveInfoFromStream( XWStreamCtxt* stream, MoveInfo* mi,
+ XP_U16 bitsPerTile );
+
XP_S32 signedFromStream( XWStreamCtxt* stream, XP_U16 nBits );
void signedToStream( XWStreamCtxt* stream, XP_U16 nBits, XP_S32 num );
@@ -101,9 +109,11 @@ XP_Bool smsToBin( XP_U8* out, XP_U16* outlen, const XP_UCHAR* in, XP_U16 inlen )
#endif
#ifdef DEBUG
+void assertSorted( const MoveInfo* mi );
void log_hex( const XP_U8* memp, XP_U16 len, const char* tag );
# define LOG_HEX(m,l,t) log_hex((const XP_U8*)(m),(l),(t))
#else
+# define assertSorted(mi)
# define LOG_HEX(m,l,t)
#endif
diff --git a/xwords4/common/tray.c b/xwords4/common/tray.c
index 730b2bbf5..473aae5f8 100644
--- a/xwords4/common/tray.c
+++ b/xwords4/common/tray.c
@@ -271,7 +271,6 @@ drawTray( BoardCtxt* board )
board->trayInvalBits = trayInvalBits;
}
}
-
} /* drawTray */
const XP_UCHAR*
@@ -339,7 +338,7 @@ drawPendingScore( BoardCtxt* board, XP_S16 turnScore, XP_Bool hasCursor )
/* Draw the pending score down in the last tray's rect */
if ( countTilesToShow( board ) < MAX_TRAY_TILES ) {
XP_U16 selPlayer = board->selPlayer;
- XP_S16 curTurn = server_getCurrentTurn( board->server, NULL );
+ XP_Bool curTurn = server_isPlayersTurn( board->server, selPlayer );
XP_Rect lastTileR;
figureTrayTileRect( board, MAX_TRAY_TILES-1, &lastTileR );
diff --git a/xwords4/common/util.h b/xwords4/common/util.h
index ef771cb65..0ba262005 100644
--- a/xwords4/common/util.h
+++ b/xwords4/common/util.h
@@ -116,6 +116,8 @@ typedef struct UtilVtable {
#ifdef XWFEATURE_TURNCHANGENOTIFY
void (*m_util_turnChanged)(XW_UtilCtxt* uc, XP_S16 newTurn);
#endif
+ void (*m_util_notifyDupStatus)( XW_UtilCtxt* uc, XP_Bool amHost,
+ const XP_UCHAR* msg );
void (*m_util_informMove)( XW_UtilCtxt* uc, XP_S16 turn,
XWStreamCtxt* expl, XWStreamCtxt* words );
void (*m_util_informUndo)( XW_UtilCtxt* uc );
@@ -146,6 +148,14 @@ typedef struct UtilVtable {
void (*m_util_remSelected)(XW_UtilCtxt* uc);
+ void (*m_util_timerSelected)(XW_UtilCtxt* uc, XP_Bool inDuplicateMode,
+ XP_Bool canPause);
+
+ void (*m_util_formatPauseHistory)( XW_UtilCtxt* uc, XWStreamCtxt* stream,
+ DupPauseType typ, XP_S16 turn,
+ XP_U32 secsPrev, XP_U32 secsCur,
+ const XP_UCHAR* msg );
+
#ifndef XWFEATURE_MINIWIN
void (*m_util_bonusSquareHeld)( XW_UtilCtxt* uc, XWBonusType bonus );
void (*m_util_playerScoreHeld)( XW_UtilCtxt* uc, XP_U16 player );
@@ -229,6 +239,8 @@ struct XW_UtilCtxt {
# define util_turnChanged( uc, t )
#endif
+#define util_notifyDupStatus(uc, h, m) \
+ (uc)->vtable->m_util_notifyDupStatus( (uc), (h), (m) )
#define util_informMove(uc,t,e,w) \
(uc)->vtable->m_util_informMove( (uc), (t), (e), (w))
#define util_informUndo(uc) \
@@ -267,6 +279,13 @@ struct XW_UtilCtxt {
#define util_remSelected( uc ) \
(uc)->vtable->m_util_remSelected((uc))
+#define util_timerSelected( uc, dm, cp ) \
+ (uc)->vtable->m_util_timerSelected((uc), (dm), (cp))
+
+#define util_formatPauseHistory( uc, s, typ, turn, secsPrev, secsCur, msg ) \
+ (uc)->vtable->m_util_formatPauseHistory( (uc), (s), (typ), (turn), \
+ (secsPrev), (secsCur), (msg) )
+
#ifndef XWFEATURE_MINIWIN
# define util_bonusSquareHeld( uc, b ) \
(uc)->vtable->m_util_bonusSquareHeld( (uc), (b) )
diff --git a/xwords4/common/xwproto.h b/xwords4/common/xwproto.h
index 6361cd157..cd5584cad 100644
--- a/xwords4/common/xwproto.h
+++ b/xwords4/common/xwproto.h
@@ -20,9 +20,6 @@
#ifndef _XWPROTO_H_
#define _XWPROTO_H_
-
-
-
typedef enum {
XWPROTO_ERROR = 0 /* illegal value */
,XWPROTO_CHAT /* broadcast text message for display */
@@ -46,6 +43,8 @@ typedef enum {
,XWPROTO_END_GAME /* server says to end game */
,XWPROTO_NEW_PROTO
+
+ ,XWPROTO_DUPE_STUFF /* used for all duplicate-mode messages */
} XW_Proto;
#define XWPROTO_NBITS 4
diff --git a/xwords4/common/xwstream.h b/xwords4/common/xwstream.h
index a6a2efb66..61fb750b8 100644
--- a/xwords4/common/xwstream.h
+++ b/xwords4/common/xwstream.h
@@ -78,7 +78,7 @@ typedef struct StreamCtxVTable {
DBG_LINE_FILE_FORMAL );
void (*m_stream_getFromStream)( XWStreamCtxt* dctx, XWStreamCtxt* src,
- XP_U16 nBytes );
+ XP_U16 nBytes );
XWStreamPos (*m_stream_getPos)( const XWStreamCtxt* dctx, PosWhich which );
XWStreamPos (*m_stream_setPos)( XWStreamCtxt* dctx, PosWhich which,
diff --git a/xwords4/linux/LocalizedStrIncludes.h b/xwords4/linux/LocalizedStrIncludes.h
index 1ba334416..773884322 100644
--- a/xwords4/linux/LocalizedStrIncludes.h
+++ b/xwords4/linux/LocalizedStrIncludes.h
@@ -27,6 +27,7 @@ enum {
STRD_REMAINING_TILES_ADD,
STRD_UNUSED_TILES_SUB,
STR_COMMIT_CONFIRM,
+ STR_SUBMIT_CONFIRM,
STRD_TURN_SCORE,
STR_BONUS_ALL,
STR_NONLOCAL_NAME,
@@ -57,6 +58,9 @@ enum {
STRSD_WINNER,
STRDSD_PLACER,
+ STR_DUP_CLIENT_SENT,
+ STRDD_DUP_HOST_RECEIVED,
+
STR_LAST
};
diff --git a/xwords4/linux/Makefile b/xwords4/linux/Makefile
index 366e698b0..9c1cb511c 100644
--- a/xwords4/linux/Makefile
+++ b/xwords4/linux/Makefile
@@ -23,7 +23,7 @@ DEFINES = -DMEM_DEBUG -DDEBUG -DENABLE_LOGGING -DNUMBER_KEY_AS_INDEX
DEFINES += -DCOMMS_CHECKSUM
DEFINES += -DLOG_COMMS_MSGNOS
CFLAGS += -g $(GPROFFLAG) -Wall -Wunused-parameter -Wcast-align -Werror -O0
-# DEFINES += -DDEBUG_HASHING
+DEFINES += -DDEBUG_HASHING
CFLAGS += -DDEBUG_TS -rdynamic
PLATFORM = obj_linux_memdbg
else
diff --git a/xwords4/linux/cursesboard.c b/xwords4/linux/cursesboard.c
index eb12cf51f..be654df24 100644
--- a/xwords4/linux/cursesboard.c
+++ b/xwords4/linux/cursesboard.c
@@ -557,7 +557,7 @@ initNoDraw( CursesBoardState* cbState, sqlite3_int64 rowid,
initMenus( result );
- if ( linuxOpenGame( cGlobals, &result->procs, gi, addr ) ) {
+ if ( linuxOpenGame( cGlobals, &result->procs, addr ) ) {
result = ref( result );
} else {
disposeBoard( result );
@@ -847,6 +847,14 @@ curses_util_informMove( XW_UtilCtxt* uc, XP_S16 XP_UNUSED(turn),
free( question );
}
+static void
+curses_util_notifyDupStatus( XW_UtilCtxt* XP_UNUSED(uc),
+ XP_Bool amHost,
+ const XP_UCHAR* msg )
+{
+ XP_LOGF( "%s(amHost=%d, msg=%s)", __func__, amHost, msg );
+}
+
static void
curses_util_informUndo( XW_UtilCtxt* XP_UNUSED(uc))
{
@@ -1017,6 +1025,14 @@ curses_util_remSelected( XW_UtilCtxt* uc )
free( text );
}
+static void
+curses_util_timerSelected( XW_UtilCtxt* XP_UNUSED(uc), XP_Bool inDuplicateMode,
+ XP_Bool canPause )
+{
+ XP_LOGF( "%s(inDuplicateMode=%d, canPause=%d)", __func__, inDuplicateMode,
+ canPause );
+}
+
static void
curses_util_bonusSquareHeld( XW_UtilCtxt* uc, XWBonusType bonus )
{
@@ -1100,6 +1116,7 @@ setupCursesUtilCallbacks( CursesBoardGlobals* bGlobals, XW_UtilCtxt* util )
#ifdef XWFEATURE_TURNCHANGENOTIFY
SET_PROC(turnChanged);
#endif
+ SET_PROC(notifyDupStatus);
SET_PROC(informMove);
SET_PROC(informUndo);
SET_PROC(informNetDict);
@@ -1114,6 +1131,7 @@ setupCursesUtilCallbacks( CursesBoardGlobals* bGlobals, XW_UtilCtxt* util )
SET_PROC(altKeyDown); /* ?? */
SET_PROC(notifyIllegalWords);
SET_PROC(remSelected);
+ SET_PROC(timerSelected);
#ifndef XWFEATURE_MINIWIN
SET_PROC(bonusSquareHeld);
SET_PROC(playerScoreHeld);
diff --git a/xwords4/linux/cursesdraw.c b/xwords4/linux/cursesdraw.c
index 48b7bbcc7..bce70196f 100644
--- a/xwords4/linux/cursesdraw.c
+++ b/xwords4/linux/cursesdraw.c
@@ -313,7 +313,7 @@ curses_draw_score_drawPlayer( DrawCtx* p_dctx, const XP_Rect* rInner,
static void
curses_draw_score_pendingScore( DrawCtx* p_dctx, const XP_Rect* rect,
XP_S16 score, XP_U16 XP_UNUSED(playerNum),
- XP_S16 XP_UNUSED(curTurn),
+ XP_Bool XP_UNUSED(curTurn),
CellFlags XP_UNUSED(flags) )
{
CursesDrawCtx* dctx = (CursesDrawCtx*)p_dctx;
diff --git a/xwords4/linux/cursesmain.c b/xwords4/linux/cursesmain.c
index 1697c77b6..361ba29e9 100644
--- a/xwords4/linux/cursesmain.c
+++ b/xwords4/linux/cursesmain.c
@@ -543,19 +543,14 @@ handleRootKeyHide( CursesAppGlobals* globals )
static void
SIGWINCH_handler( int signal )
{
- int height, width;
-
assert( signal == SIGWINCH );
endwin();
-/* (*globals.drawMenu)( &globals ); */
-
+ int height, width;
getmaxyx( stdscr, height, width );
XP_LOGF( "%s:, getmaxyx()->w:%d; h:%d", __func__, width, height );
wresize( g_globals.mainWin, height-MENU_WINDOW_HEIGHT, width );
-
- // board_draw( g_globals.cGlobals.game.board );
} /* SIGWINCH_handler */
static void
@@ -853,7 +848,200 @@ initClientSocket( CursesAppGlobals* globals, char* serverName )
} /* initClientSocket */
#endif
+/* <<<<<<< HEAD */
+/* static void */
+/* curses_util_informNeedPassword( XW_UtilCtxt* XP_UNUSED(uc), */
+/* XP_U16 XP_UNUSED_DBG(playerNum), */
+/* const XP_UCHAR* XP_UNUSED_DBG(name) ) */
+/* { */
+/* XP_WARNF( "curses_util_informNeedPassword(num=%d, name=%s", playerNum, name ); */
+/* } /\* curses_util_askPassword *\/ */
+/* static void */
+/* curses_util_yOffsetChange( XW_UtilCtxt* XP_UNUSED(uc), */
+/* XP_U16 XP_UNUSED(maxOffset), */
+/* XP_U16 XP_UNUSED(oldOffset), XP_U16 XP_UNUSED(newOffset) ) */
+/* { */
+/* /\* if ( oldOffset != newOffset ) { *\/ */
+/* /\* XP_WARNF( "curses_util_yOffsetChange(%d,%d,%d) not implemented", *\/ */
+/* /\* maxOffset, oldOffset, newOffset ); *\/ */
+/* /\* } *\/ */
+/* } /\* curses_util_yOffsetChange *\/ */
+
+/* #ifdef XWFEATURE_TURNCHANGENOTIFY */
+/* static void */
+/* curses_util_turnChanged( XW_UtilCtxt* XP_UNUSED(uc), XP_S16 XP_UNUSED_DBG(newTurn) ) */
+/* { */
+/* XP_LOGF( "%s(turn=%d)", __func__, newTurn ); */
+/* } */
+/* #endif */
+
+/* static void */
+/* curses_util_notifyDupStatus( XW_UtilCtxt* XP_UNUSED(uc), */
+/* XP_Bool amHost, */
+/* const XP_UCHAR* msg ) */
+/* { */
+/* XP_LOGF( "%s(amHost=%d, msg=%s)", __func__, amHost, msg ); */
+/* } */
+
+/* static void */
+/* curses_util_notifyIllegalWords( XW_UtilCtxt* XP_UNUSED(uc), */
+/* BadWordInfo* XP_UNUSED(bwi), */
+/* XP_U16 XP_UNUSED(player), */
+/* XP_Bool XP_UNUSED(turnLost) ) */
+/* { */
+/* XP_WARNF( "curses_util_notifyIllegalWords not implemented" ); */
+/* } /\* curses_util_notifyIllegalWord *\/ */
+
+/* static void */
+/* curses_util_remSelected( XW_UtilCtxt* uc ) */
+/* { */
+/* CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure; */
+/* XWStreamCtxt* stream; */
+/* XP_UCHAR* text; */
+
+/* stream = mem_stream_make_raw( MPPARM(globals->cGlobals.util->mpool) */
+/* globals->cGlobals.params->vtMgr ); */
+/* board_formatRemainingTiles( globals->cGlobals.game.board, stream ); */
+
+/* text = strFromStream( stream ); */
+
+/* const char* buttons[] = { "Ok" }; */
+/* (void)cursesask( globals, text, VSIZE(buttons), buttons ); */
+
+/* free( text ); */
+/* } */
+
+/* #ifndef XWFEATURE_STANDALONE_ONLY */
+/* static XWStreamCtxt* */
+/* curses_util_makeStreamFromAddr(XW_UtilCtxt* uc, XP_PlayerAddr channelNo ) */
+/* { */
+/* CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure; */
+/* LaunchParams* params = globals->cGlobals.params; */
+
+/* XWStreamCtxt* stream = mem_stream_make( MPPARM(uc->mpool) params->vtMgr, */
+/* &globals->cGlobals, channelNo, */
+/* sendOnClose ); */
+/* return stream; */
+/* } /\* curses_util_makeStreamFromAddr *\/ */
+/* #endif */
+
+/* #ifdef XWFEATURE_CHAT */
+/* static void */
+/* curses_util_showChat( XW_UtilCtxt* uc, */
+/* const XP_UCHAR* const XP_UNUSED_DBG(msg), */
+/* XP_S16 XP_UNUSED_DBG(from), XP_U32 XP_UNUSED(timestamp) ) */
+/* { */
+/* CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure; */
+/* globals->nChatsSent = 0; */
+/* # ifdef DEBUG */
+/* const XP_UCHAR* name = ""; */
+/* if ( 0 <= from ) { */
+/* CommonGlobals* cGlobals = &globals->cGlobals; */
+/* name = cGlobals->gi->players[from].name; */
+/* } */
+/* XP_LOGF( "%s: got \"%s\" from %s", __func__, msg, name ); */
+/* # endif */
+/* } */
+/* #endif */
+
+/* static void */
+/* setupCursesUtilCallbacks( CursesAppGlobals* globals, XW_UtilCtxt* util ) */
+/* { */
+/* util->vtable->m_util_userError = curses_util_userError; */
+
+/* util->vtable->m_util_informNeedPassword = curses_util_informNeedPassword; */
+/* util->vtable->m_util_yOffsetChange = curses_util_yOffsetChange; */
+/* #ifdef XWFEATURE_TURNCHANGENOTIFY */
+/* util->vtable->m_util_turnChanged = curses_util_turnChanged; */
+/* #endif */
+/* util->vtable->m_util_notifyDupStatus = curses_util_notifyDupStatus; */
+/* util->vtable->m_util_notifyIllegalWords = curses_util_notifyIllegalWords; */
+/* util->vtable->m_util_remSelected = curses_util_remSelected; */
+/* #ifndef XWFEATURE_STANDALONE_ONLY */
+/* util->vtable->m_util_makeStreamFromAddr = curses_util_makeStreamFromAddr; */
+/* #endif */
+/* #ifdef XWFEATURE_CHAT */
+/* util->vtable->m_util_showChat = curses_util_showChat; */
+/* #endif */
+
+/* util->vtable->m_util_notifyMove = curses_util_notifyMove; */
+/* util->vtable->m_util_notifyTrade = curses_util_notifyTrade; */
+/* util->vtable->m_util_notifyPickTileBlank = curses_util_notifyPickTileBlank; */
+/* util->vtable->m_util_informNeedPickTiles = curses_util_informNeedPickTiles; */
+/* util->vtable->m_util_trayHiddenChange = curses_util_trayHiddenChange; */
+/* util->vtable->m_util_informMove = curses_util_informMove; */
+/* util->vtable->m_util_informUndo = curses_util_informUndo; */
+/* util->vtable->m_util_notifyGameOver = curses_util_notifyGameOver; */
+/* util->vtable->m_util_informNetDict = curses_util_informNetDict; */
+/* util->vtable->m_util_setIsServer = curses_util_setIsServer; */
+
+/* #ifdef XWFEATURE_HILITECELL */
+/* util->vtable->m_util_hiliteCell = curses_util_hiliteCell; */
+/* #endif */
+/* util->vtable->m_util_engineProgressCallback = */
+/* curses_util_engineProgressCallback; */
+
+/* util->vtable->m_util_setTimer = curses_util_setTimer; */
+/* util->vtable->m_util_clearTimer = curses_util_clearTimer; */
+/* util->vtable->m_util_requestTime = curses_util_requestTime; */
+
+/* util->closure = globals; */
+/* } /\* setupCursesUtilCallbacks *\/ */
+
+/* static CursesMenuHandler */
+/* getHandlerForKey( const MenuList* list, char ch ) */
+/* { */
+/* CursesMenuHandler handler = NULL; */
+/* while ( list->handler != NULL ) { */
+/* if ( list->key == ch ) { */
+/* handler = list->handler; */
+/* break; */
+/* } */
+/* ++list; */
+/* } */
+/* return handler; */
+/* } */
+
+/* static XP_Bool */
+/* handleKeyEvent( CursesAppGlobals* globals, const MenuList* list, char ch ) */
+/* { */
+/* CursesMenuHandler handler = getHandlerForKey( list, ch ); */
+/* XP_Bool result = XP_FALSE; */
+/* if ( !!handler ) { */
+/* result = (*handler)(globals); */
+/* } */
+/* return result; */
+/* } /\* handleKeyEvent *\/ */
+
+/* static XP_Bool */
+/* passKeyToBoard( CursesAppGlobals* globals, char ch ) */
+/* { */
+/* XP_Bool handled = ch >= 'a' && ch <= 'z'; */
+/* if ( handled ) { */
+/* ch += 'A' - 'a'; */
+/* globals->doDraw = board_handleKey( globals->cGlobals.game.board, */
+/* ch, NULL ); */
+/* } */
+/* return handled; */
+/* } /\* passKeyToBoard *\/ */
+
+/* static void */
+/* positionSizeStuff( CursesAppGlobals* globals, int width, int height ) */
+/* { */
+/* CommonGlobals* cGlobals = &globals->cGlobals; */
+/* BoardCtxt* board = cGlobals->game.board; */
+/* #ifdef COMMON_LAYOUT */
+
+/* BoardDims dims; */
+/* board_figureLayout( board, cGlobals->gi, */
+/* 0, 0, width, height, 100, */
+/* 150, 200, /\* percents *\/ */
+/* width*75/100, 2, 1, */
+/* XP_FALSE, &dims ); */
+/* board_applyLayout( board, &dims ); */
+/* ======= */
+/* >>>>>>> android_branch */
/* static const MenuList* */
/* getHandlerForKey( const MenuList* list, char ch ) */
diff --git a/xwords4/linux/gamesdb.c b/xwords4/linux/gamesdb.c
index 3c0b6d4c1..798904981 100644
--- a/xwords4/linux/gamesdb.c
+++ b/xwords4/linux/gamesdb.c
@@ -217,6 +217,7 @@ summarize( CommonGlobals* cGlobals )
XP_Bool isLocal = -1;
XP_S16 turn = server_getCurrentTurn( game->server, &isLocal );
XP_U32 lastMoveTime = server_getLastMoveTime( game->server );
+ XP_U32 dupTimerExpires = server_getDupTimerExpires( game->server );
XP_U16 seed = 0;
XP_S16 nMissing = 0;
XP_U16 nPending = 0;
@@ -289,12 +290,13 @@ summarize( CommonGlobals* cGlobals )
const char* fmt = "UPDATE games "
" SET room='%s', ended=%d, turn=%d, local=%d, ntotal=%d, "
" nmissing=%d, nmoves=%d, seed=%d, dictlang=%d, gameid=%d, connvia='%s', "
- " relayid='%s', lastMoveTime=%d, scores='%s', nPending=%d, role=%d"
+ " relayid='%s', lastMoveTime=%d, dupTimerExpires=%d, scores='%s', "
+ " nPending=%d, role=%d"
" WHERE rowid=%lld";
XP_UCHAR buf[2*256];
snprintf( buf, sizeof(buf), fmt, room, gameOver?1:0, turn, isLocal?1:0,
nTotal, nMissing, nMoves, seed, dictLang, gameID, connvia, relayID, lastMoveTime,
- scoresStr, nPending, gi->serverRole, cGlobals->rowid );
+ dupTimerExpires, scoresStr, nPending, gi->serverRole, cGlobals->rowid );
XP_LOGF( "query: %s", buf );
sqlite3_stmt* stmt = NULL;
int result = sqlite3_prepare_v2( cGlobals->params->pDb, buf, -1, &stmt, NULL );
@@ -398,7 +400,7 @@ getGameInfo( sqlite3* pDb, sqlite3_int64 rowid, GameInfo* gib )
{
XP_Bool success = XP_FALSE;
const char* fmt = "SELECT room, ended, turn, local, nmoves, ntotal, nmissing, "
- "dictlang, seed, connvia, gameid, lastMoveTime, relayid, scores, nPending, role, snap "
+ "dictlang, seed, connvia, gameid, lastMoveTime, dupTimerExpires, relayid, scores, nPending, role, snap "
"FROM games WHERE rowid = %lld";
XP_UCHAR query[256];
snprintf( query, sizeof(query), fmt, rowid );
@@ -425,6 +427,7 @@ getGameInfo( sqlite3* pDb, sqlite3_int64 rowid, GameInfo* gib )
getColumnText( ppStmt, col++, gib->conn, &len );
gib->gameID = sqlite3_column_int( ppStmt, col++ );
gib->lastMoveTime = sqlite3_column_int( ppStmt, col++ );
+ gib->dupTimerExpires = sqlite3_column_int( ppStmt, col++ );
len = sizeof(gib->relayID);
getColumnText( ppStmt, col++, gib->relayID, &len );
len = sizeof(gib->scores);
diff --git a/xwords4/linux/gamesdb.h b/xwords4/linux/gamesdb.h
index fa6a04745..53e650dba 100644
--- a/xwords4/linux/gamesdb.h
+++ b/xwords4/linux/gamesdb.h
@@ -48,6 +48,7 @@ typedef struct _GameInfo {
XP_U16 seed;
XP_U16 nPending;
XP_U32 lastMoveTime;
+ XP_U32 dupTimerExpires;
XP_U16 role;
} GameInfo;
diff --git a/xwords4/linux/gtkboard.c b/xwords4/linux/gtkboard.c
index 7891907c2..44e2210d3 100644
--- a/xwords4/linux/gtkboard.c
+++ b/xwords4/linux/gtkboard.c
@@ -87,7 +87,7 @@ static void gtkShowFinalScores( const GtkGameGlobals* globals,
static void send_invites( CommonGlobals* cGlobals, XP_U16 nPlayers,
XP_U32 relayDevID, const XP_UCHAR* relayID,
const CommsAddrRec* addrs );
-
+static void cancelTimers( GtkGameGlobals* globals );
#define GTK_TRAY_HT_ROWS 3
@@ -584,18 +584,19 @@ createOrLoadObjects( GtkGameGlobals* globals )
TransportProcs procs;
setTransportProcs( &procs, globals );
- linuxOpenGame( cGlobals, &procs, NULL, NULL );
+ if ( linuxOpenGame( cGlobals, &procs, NULL ) ) {
- if ( !params->fileName && !!params->dbName ) {
- XP_UCHAR buf[64];
- snprintf( buf, sizeof(buf), "%s / %lld", params->dbName,
- cGlobals->rowid );
- gtk_window_set_title( GTK_WINDOW(globals->window), buf );
+ if ( !params->fileName && !!params->dbName ) {
+ XP_UCHAR buf[64];
+ snprintf( buf, sizeof(buf), "%s / %lld", params->dbName,
+ cGlobals->rowid );
+ gtk_window_set_title( GTK_WINDOW(globals->window), buf );
+ }
+
+
+ addDropChecks( globals );
+ disenable_buttons( globals );
}
-
-
- addDropChecks( globals );
- disenable_buttons( globals );
} /* createOrLoadObjects */
/* Create a new backing pixmap of the appropriate size and set up contxt to
@@ -693,6 +694,8 @@ cleanup( GtkGameGlobals* globals )
g_source_remove( globals->idleID );
}
+ cancelTimers( globals );
+
#ifdef XWFEATURE_BLUETOOTH
linux_bt_close( cGlobals );
#endif
@@ -1155,6 +1158,9 @@ disenable_buttons( GtkGameGlobals* globals )
#ifdef XWFEATURE_CHAT
gtk_widget_set_sensitive( globals->chat_button, gsi.canChat );
#endif
+
+ gtk_widget_set_sensitive( globals->pause_button, gsi.canPause );
+ gtk_widget_set_sensitive( globals->unpause_button, gsi.canUnpause );
}
static gboolean
@@ -1318,6 +1324,20 @@ handle_chat_button( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals )
}
#endif
+static void
+handle_pause_button( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals )
+{
+ board_pause( globals->cGlobals.game.board, "whatever" );
+ disenable_buttons( globals );
+}
+
+static void
+handle_unpause_button( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals )
+{
+ board_unpause( globals->cGlobals.game.board, "whatever" );
+ disenable_buttons( globals );
+}
+
static void
scroll_value_changed( GtkAdjustment *adj, GtkGameGlobals* globals )
{
@@ -1721,6 +1741,14 @@ gtkShowFinalScores( const GtkGameGlobals* globals, XP_Bool ignoreTimeout )
}
} /* gtkShowFinalScores */
+static void
+gtk_util_notifyDupStatus( XW_UtilCtxt* uc, XP_Bool XP_UNUSED(amHost),
+ const XP_UCHAR* msg )
+{
+ GtkGameGlobals* globals = (GtkGameGlobals*)uc->closure;
+ (void)gtkask( globals->window, msg, GTK_BUTTONS_OK, NULL );
+}
+
static void
gtk_util_informMove( XW_UtilCtxt* uc, XP_S16 XP_UNUSED(turn),
XWStreamCtxt* expl, XWStreamCtxt* words )
@@ -1843,6 +1871,15 @@ cancelTimer( GtkGameGlobals* globals, XWTimerReason why )
}
} /* cancelTimer */
+static void
+cancelTimers( GtkGameGlobals* globals )
+{
+ /* There is no 0. */
+ for ( XWTimerReason why = 1; why < NUM_TIMERS_PLUS_ONE; ++why ) {
+ cancelTimer( globals, why );
+ }
+}
+
static gint
pen_timer_func( gpointer data )
{
@@ -1867,6 +1904,18 @@ score_timer_func( gpointer data )
return XP_FALSE;
} /* score_timer_func */
+static gint
+dup_timer_func( gpointer data )
+{
+ GtkGameGlobals* globals = (GtkGameGlobals*)data;
+
+ if ( linuxFireTimer( &globals->cGlobals, TIMER_DUP_TIMERCHECK ) ) {
+ board_draw( globals->cGlobals.game.board );
+ }
+
+ return XP_FALSE;
+} /* score_timer_func */
+
#ifndef XWFEATURE_STANDALONE_ONLY
static gint
comms_timer_func( gpointer data )
@@ -1905,27 +1954,37 @@ gtk_util_setTimer( XW_UtilCtxt* uc, XWTimerReason why,
cancelTimer( globals, why );
- if ( why == TIMER_PENDOWN ) {
+ switch( why ) {
+ case TIMER_PENDOWN:
if ( 0 != globals->timerSources[why-1] ) {
g_source_remove( globals->timerSources[why-1] );
}
newSrc = g_timeout_add( 1000, pen_timer_func, globals );
- } else if ( why == TIMER_TIMERTICK ) {
+ break;
+ case TIMER_TIMERTICK:
/* one second */
globals->scoreTimerInterval = 100 * 10000;
(void)gettimeofday( &globals->scoreTv, NULL );
newSrc = g_timeout_add( 1000, score_timer_func, globals );
+ break;
+
+ case TIMER_DUP_TIMERCHECK:
+ newSrc = g_timeout_add( 1000 * when, dup_timer_func, globals );
+ break;
+
#ifndef XWFEATURE_STANDALONE_ONLY
- } else if ( why == TIMER_COMMS ) {
+ case TIMER_COMMS:
newSrc = g_timeout_add( 1000 * when, comms_timer_func, globals );
+ break;
#endif
#ifdef XWFEATURE_SLOW_ROBOT
- } else if ( why == TIMER_SLOWROBOT ) {
+ case TIMER_SLOWROBOT:
newSrc = g_timeout_add( 1000 * when, slowrob_timer_func, globals );
+ break;
#endif
- } else {
+ default:
XP_ASSERT( 0 );
}
@@ -2030,6 +2089,20 @@ gtk_util_remSelected( XW_UtilCtxt* uc )
free( text );
}
+static void
+gtk_util_timerSelected( XW_UtilCtxt* uc, XP_Bool inDuplicateMode,
+ XP_Bool canPause )
+{
+ if ( inDuplicateMode ) {
+ GtkGameGlobals* globals = (GtkGameGlobals*)uc->closure;
+ if ( canPause ) {
+ handle_pause_button( NULL, globals );
+ } else {
+ handle_unpause_button( NULL, globals );
+ }
+ }
+}
+
#ifndef XWFEATURE_STANDALONE_ONLY
static XWStreamCtxt*
gtk_util_makeStreamFromAddr(XW_UtilCtxt* uc, XP_PlayerAddr channelNo )
@@ -2200,79 +2273,66 @@ makeShowButtonFromBitmap( void* closure, const gchar* filename,
return button;
} /* makeShowButtonFromBitmap */
+static GtkWidget*
+addVBarButton( GtkGameGlobals* globals, const gchar* icon, const gchar* title,
+ GCallback func, GtkWidget* vbox )
+{
+ GtkWidget* button = makeShowButtonFromBitmap( globals, icon, title,
+ G_CALLBACK(func) );
+ gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
+ return button;
+}
+
static GtkWidget*
makeVerticalBar( GtkGameGlobals* globals, GtkWidget* XP_UNUSED(window) )
{
- GtkWidget* vbox;
- GtkWidget* button;
+ GtkWidget* vbox = gtk_button_box_new( GTK_ORIENTATION_VERTICAL );
- vbox = gtk_button_box_new( GTK_ORIENTATION_VERTICAL );
+ globals->flip_button = addVBarButton( globals, "../flip.xpm", "f",
+ G_CALLBACK(handle_flip_button),
+ vbox );
- button = makeShowButtonFromBitmap( globals, "../flip.xpm", "f",
- G_CALLBACK(handle_flip_button) );
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
- globals->flip_button = button;
+ addVBarButton( globals, "../value.xpm", "v",
+ G_CALLBACK(handle_value_button), vbox );
+ globals->prevhint_button
+ = addVBarButton( globals, "../hint.xpm", "?-", G_CALLBACK(handle_prevhint_button), vbox );
+ globals->nexthint_button
+ = addVBarButton( globals, "../hint.xpm", "?+", G_CALLBACK(handle_nexthint_button), vbox );
+ addVBarButton( globals, "../hintNum.xpm", "n", G_CALLBACK(handle_nhint_button), vbox );
- button = makeShowButtonFromBitmap( globals, "../value.xpm", "v",
- G_CALLBACK(handle_value_button) );
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
-
- button = makeShowButtonFromBitmap( globals, "../hint.xpm", "?-",
- G_CALLBACK(handle_prevhint_button) );
- globals->prevhint_button = button;
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
- button = makeShowButtonFromBitmap( globals, "../hint.xpm", "?+",
- G_CALLBACK(handle_nexthint_button) );
- globals->nexthint_button = button;
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
-
- button = makeShowButtonFromBitmap( globals, "../hintNum.xpm", "n",
- G_CALLBACK(handle_nhint_button) );
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
-
- button = makeShowButtonFromBitmap( globals, "../colors.xpm", "c",
- G_CALLBACK(handle_colors_button) );
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
+ addVBarButton( globals, "../colors.xpm", "c", G_CALLBACK(handle_colors_button), vbox );
/* undo and redo buttons */
- button = makeShowButtonFromBitmap( globals, "../undo.xpm", "U",
- G_CALLBACK(handle_undo_button) );
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
- button = makeShowButtonFromBitmap( globals, "../redo.xpm", "R",
- G_CALLBACK(handle_redo_button) );
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
+ addVBarButton( globals, "../undo.xpm", "U", G_CALLBACK(handle_undo_button), vbox );
- button = makeShowButtonFromBitmap( globals, "", "u/r",
- G_CALLBACK(handle_toggle_undo) );
- globals->toggle_undo_button = button;
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
+ addVBarButton( globals, "../redo.xpm", "R", G_CALLBACK(handle_redo_button), vbox );
+
+ globals->toggle_undo_button
+ = addVBarButton( globals, "", "u/r", G_CALLBACK(handle_toggle_undo), vbox );
/* the four buttons that on palm are beside the tray */
- button = makeShowButtonFromBitmap( globals, "../juggle.xpm", "j",
- G_CALLBACK(handle_juggle_button) );
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
+ addVBarButton( globals, "../juggle.xpm", "j", G_CALLBACK(handle_juggle_button), vbox );
+
+ addVBarButton( globals, "../trade.xpm", "t", G_CALLBACK(handle_trade_button), vbox );
+
+ addVBarButton( globals, "../done.xpm", "d", G_CALLBACK(handle_done_button), vbox );
+
+ globals->zoomin_button
+ = addVBarButton( globals, "../done.xpm", "+", G_CALLBACK(handle_zoomin_button), vbox );
+
+ globals->zoomout_button
+ = addVBarButton( globals, "../done.xpm", "-", G_CALLBACK(handle_zoomout_button), vbox );
- button = makeShowButtonFromBitmap( globals, "../trade.xpm", "t",
- G_CALLBACK(handle_trade_button) );
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
- button = makeShowButtonFromBitmap( globals, "../done.xpm", "d",
- G_CALLBACK(handle_done_button) );
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
- button = makeShowButtonFromBitmap( globals, "../done.xpm", "+",
- G_CALLBACK(handle_zoomin_button) );
- globals->zoomin_button = button;
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
- button = makeShowButtonFromBitmap( globals, "../done.xpm", "-",
- G_CALLBACK(handle_zoomout_button) );
- globals->zoomout_button = button;
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
#ifdef XWFEATURE_CHAT
- button = makeShowButtonFromBitmap( globals, "", "chat",
- G_CALLBACK(handle_chat_button) );
- globals->chat_button = button;
- gtk_box_pack_start( GTK_BOX(vbox), button, FALSE, TRUE, 0 );
+ globals->chat_button = addVBarButton( globals, "", "Chat",
+ G_CALLBACK(handle_chat_button), vbox );
#endif
+ globals->pause_button = addVBarButton( globals, "", "Pause",
+ G_CALLBACK(handle_pause_button), vbox );
+ globals->unpause_button = addVBarButton( globals, "", "Unpause",
+ G_CALLBACK(handle_unpause_button), vbox );
+
gtk_widget_show( vbox );
return vbox;
} /* makeVerticalBar */
@@ -2307,7 +2367,9 @@ static void
setupGtkUtilCallbacks( GtkGameGlobals* globals, XW_UtilCtxt* util )
{
util->closure = globals;
+
#define SET_PROC(NAM) util->vtable->m_util_##NAM = gtk_util_##NAM
+
SET_PROC(userError);
SET_PROC(notifyMove);
SET_PROC(notifyTrade);
@@ -2319,11 +2381,11 @@ setupGtkUtilCallbacks( GtkGameGlobals* globals, XW_UtilCtxt* util )
#ifdef XWFEATURE_TURNCHANGENOTIFY
SET_PROC(turnChanged);
#endif
+ SET_PROC(notifyDupStatus);
SET_PROC(informMove);
SET_PROC(informUndo);
SET_PROC(notifyGameOver);
SET_PROC(informNetDict);
- /* SET_PROC(setIsServer); */
#ifdef XWFEATURE_HILITECELL
SET_PROC(hiliteCell);
#endif
@@ -2334,6 +2396,7 @@ setupGtkUtilCallbacks( GtkGameGlobals* globals, XW_UtilCtxt* util )
SET_PROC(requestTime);
SET_PROC(notifyIllegalWords);
SET_PROC(remSelected);
+ SET_PROC(timerSelected);
#ifndef XWFEATURE_STANDALONE_ONLY
SET_PROC(makeStreamFromAddr);
#endif
@@ -2351,7 +2414,9 @@ setupGtkUtilCallbacks( GtkGameGlobals* globals, XW_UtilCtxt* util )
#ifdef XWFEATURE_BOARDWORDS
SET_PROC(cellSquareHeld);
#endif
+
#undef SET_PROC
+
assertAllCallbacksSet( util );
} /* setupGtkUtilCallbacks */
diff --git a/xwords4/linux/gtkboard.h b/xwords4/linux/gtkboard.h
index da1851f80..bd9785ac6 100644
--- a/xwords4/linux/gtkboard.h
+++ b/xwords4/linux/gtkboard.h
@@ -115,6 +115,8 @@ typedef struct GtkGameGlobals {
#ifdef XWFEATURE_CHAT
GtkWidget* chat_button;
#endif
+ GtkWidget* pause_button;
+ GtkWidget* unpause_button;
GtkWidget* countLabel;
EngineCtxt* engine;
diff --git a/xwords4/linux/gtkdraw.c b/xwords4/linux/gtkdraw.c
index db8473bc4..c5d3ecb0b 100644
--- a/xwords4/linux/gtkdraw.c
+++ b/xwords4/linux/gtkdraw.c
@@ -1207,8 +1207,8 @@ gtk_draw_measureScoreText( DrawCtx* p_dctx, const XP_Rect* bounds,
static void
gtk_draw_score_pendingScore( DrawCtx* p_dctx, const XP_Rect* rect,
- XP_S16 score, XP_U16 playerNum,
- XP_S16 curTurn, CellFlags flags )
+ XP_S16 score, XP_U16 XP_UNUSED(playerNum),
+ XP_Bool curTurn, CellFlags flags )
{
GtkDrawCtx* dctx = (GtkDrawCtx*)p_dctx;
XP_UCHAR buf[5];
@@ -1236,7 +1236,7 @@ gtk_draw_score_pendingScore( DrawCtx* p_dctx, const XP_Rect* rect,
}
ht = localR.height >> 2;
- txtColor = (playerNum == curTurn) ? &dctx->black : &dctx->grey;
+ txtColor = curTurn ? &dctx->black : &dctx->grey;
draw_string_at( dctx, NULL, "Pts:", ht,
&localR, XP_GTK_JUST_TOPLEFT, txtColor, cursor );
draw_string_at( dctx, NULL, buf, ht,
@@ -1247,34 +1247,35 @@ gtk_draw_score_pendingScore( DrawCtx* p_dctx, const XP_Rect* rect,
static void
gtkFormatTimerText( XP_UCHAR* buf, XP_U16 bufLen, XP_S16 secondsLeft )
{
- XP_U16 minutes, seconds;
-
if ( secondsLeft < 0 ) {
*buf++ = '-';
--bufLen;
secondsLeft *= -1;
}
- minutes = secondsLeft / 60;
- seconds = secondsLeft % 60;
+ XP_U16 minutes = secondsLeft / 60;
+ XP_U16 seconds = secondsLeft % 60;
XP_SNPRINTF( buf, bufLen, "% 1d:%02d", minutes, seconds );
} /* gtkFormatTimerText */
static void
gtk_draw_drawTimer( DrawCtx* p_dctx, const XP_Rect* rInner,
- XP_U16 playerNum, XP_S16 secondsLeft )
+ XP_U16 playerNum, XP_S16 secondsLeft,
+ XP_Bool localTurnDone )
{
GtkDrawCtx* dctx = (GtkDrawCtx*)p_dctx;
XP_Bool hadCairo = haveCairo( dctx );
if ( hadCairo || initCairo( dctx ) ) {
- XP_UCHAR buf[10];
+ gtkEraseRect( dctx, rInner );
+ GdkRGBA* color = localTurnDone ? &dctx->grey
+ : &dctx->playerColors[playerNum];
+
+ XP_UCHAR buf[10];
gtkFormatTimerText( buf, VSIZE(buf), secondsLeft );
- gtkEraseRect( dctx, rInner );
draw_string_at( dctx, NULL, buf, rInner->height-1,
- rInner, XP_GTK_JUST_CENTER,
- &dctx->playerColors[playerNum], NULL );
+ rInner, XP_GTK_JUST_CENTER, color, NULL );
if ( !hadCairo ) {
destroyCairo( dctx );
}
diff --git a/xwords4/linux/gtkmain.c b/xwords4/linux/gtkmain.c
index f81494bed..9f322d254 100644
--- a/xwords4/linux/gtkmain.c
+++ b/xwords4/linux/gtkmain.c
@@ -81,7 +81,7 @@ findOpenGame( const GtkAppGlobals* apg, sqlite3_int64 rowid )
enum { ROW_ITEM, ROW_THUMB, NAME_ITEM, ROOM_ITEM, GAMEID_ITEM, SEED_ITEM, ROLE_ITEM,
CONN_ITEM, RELAYID_ITEM, OVER_ITEM, TURN_ITEM, LOCAL_ITEM, NMOVES_ITEM, NTOTAL_ITEM,
- MISSING_ITEM, LASTTURN_ITEM, N_ITEMS };
+ MISSING_ITEM, LASTTURN_ITEM, DUPTIMER_ITEM, N_ITEMS };
static void
foreachProc( GtkTreeModel* model, GtkTreePath* XP_UNUSED(path),
@@ -180,6 +180,7 @@ init_games_list( GtkAppGlobals* apg )
addTextColumn( list, "NTotal", NTOTAL_ITEM );
addTextColumn( list, "NMissing", MISSING_ITEM );
addTextColumn( list, "LastTurn", LASTTURN_ITEM );
+ addTextColumn( list, "DupTimerFires", DUPTIMER_ITEM );
GtkListStore* store = gtk_list_store_new( N_ITEMS,
G_TYPE_INT64, /* ROW_ITEM */
@@ -197,7 +198,8 @@ init_games_list( GtkAppGlobals* apg )
G_TYPE_INT, /* NMOVES_ITEM */
G_TYPE_INT, /* NTOTAL_ITEM */
G_TYPE_INT, /* MISSING_ITEM */
- G_TYPE_STRING /* LASTTURN_ITEM */
+ G_TYPE_STRING, /* LASTTURN_ITEM */
+ G_TYPE_STRING /* DUPTIMER_ITEM */
);
gtk_tree_view_set_model( GTK_TREE_VIEW(list), GTK_TREE_MODEL(store) );
g_object_unref( store );
@@ -243,6 +245,8 @@ add_to_list( GtkWidget* list, sqlite3_int64 rowid, XP_Bool isNew,
GTimeVal timeval = { tv_sec: gib->lastMoveTime, tv_usec: 0 };
gchar* timeStr = g_time_val_to_iso8601( &timeval );
+ GTimeVal timerVal = { tv_sec: gib->dupTimerExpires, tv_usec: 0 };
+ gchar* timerStr = g_time_val_to_iso8601( &timerVal );
gtk_list_store_set( store, &iter,
ROW_ITEM, rowid,
ROW_THUMB, gib->snap,
@@ -260,6 +264,7 @@ add_to_list( GtkWidget* list, sqlite3_int64 rowid, XP_Bool isNew,
NTOTAL_ITEM, gib->nTotal,
MISSING_ITEM, gib->nMissing,
LASTTURN_ITEM, timeStr,
+ DUPTIMER_ITEM, timerStr,
-1 );
g_free( timeStr );
XP_LOGF( "DONE adding" );
@@ -675,9 +680,12 @@ gameFromInvite( GtkAppGlobals* apg, const NetLaunchInfo* invite,
gi_copy( MPPARM(params->mpool) &gi, ¶ms->pgi );
gi_setNPlayers( &gi, invite->nPlayersT, invite->nPlayersH );
+ ensureLocalPlayerNames( params, &gi );
+
gi.gameID = invite->gameID;
gi.dictLang = invite->lang;
gi.forceChannel = invite->forceChannel;
+ gi.inDuplicateMode = invite->inDuplicateMode;
gi.serverRole = SERVER_ISCLIENT; /* recipient of invitation is client */
replaceStringIfDifferent( params->mpool, &gi.dictName, invite->dict );
@@ -710,7 +718,7 @@ relayInviteReceived( void* closure, NetLaunchInfo* invite )
getRowsForGameID( apg->cag.params->pDb, gameID, rowids, &nRowIDs );
if ( 0 < nRowIDs ) {
- gtktell( apg->window, "Duplicate invite rejected" );
+ gtktell( apg->window, "Duplicate invite rejected (FIXME!!!)" );
} else {
gameFromInvite( apg, invite, NULL );
}
diff --git a/xwords4/linux/gtknewgame.c b/xwords4/linux/gtknewgame.c
index c72adbfb2..5506b3946 100644
--- a/xwords4/linux/gtknewgame.c
+++ b/xwords4/linux/gtknewgame.c
@@ -62,6 +62,7 @@ typedef struct GtkNewGameState {
GtkWidget* nPlayersLabel;
GtkWidget* juggleButton;
GtkWidget* timerField;
+ GtkWidget* duplicateCheck;
} GtkNewGameState;
static void
@@ -257,6 +258,24 @@ addTimerWidget( GtkNewGameState* state, GtkWidget* parent )
gtk_box_pack_start( GTK_BOX(hbox), state->timerField, FALSE, TRUE, 0 );
}
+static void
+handle_duplicate_toggled( GtkWidget* item, GtkNewGameState* state )
+{
+ NGValue value = { .ng_bool = gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON(item) ) };
+ newg_attrChanged( state->newGameCtxt, NG_ATTR_DUPLICATE, value );
+}
+
+static void
+addDuplicateCheckbox( GtkNewGameState* state, GtkWidget* parent )
+{
+ GtkWidget* duplicateCheck = gtk_check_button_new_with_label( "Duplicate mode" );
+ state->duplicateCheck = duplicateCheck;
+ g_signal_connect( duplicateCheck, "toggled",
+ (GCallback)handle_duplicate_toggled, state );
+ gtk_widget_show( duplicateCheck );
+ gtk_box_pack_start( GTK_BOX(parent), duplicateCheck, FALSE, TRUE, 0 );
+}
+
static GtkWidget*
makeNewGameDialog( GtkNewGameState* state )
{
@@ -437,6 +456,7 @@ makeNewGameDialog( GtkNewGameState* state )
gtk_box_pack_start( GTK_BOX(vbox), hbox, FALSE, TRUE, 0 );
addTimerWidget( state, vbox );
+ addDuplicateCheckbox( state, vbox );
/* buttons at the bottom */
hbox = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
@@ -623,6 +643,10 @@ gtk_newgame_attr_set( void* closure, NewGameAttr attr, NGValue value )
gtk_label_set_text( GTK_LABEL(state->timerField), buf );
}
break;
+ case NG_ATTR_DUPLICATE:
+ gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON(state->duplicateCheck),
+ value.ng_bool );
+ break;
default:
XP_ASSERT(0);
break;
diff --git a/xwords4/linux/lindutil.c b/xwords4/linux/lindutil.c
index 922a2028f..8c7eafa21 100644
--- a/xwords4/linux/lindutil.c
+++ b/xwords4/linux/lindutil.c
@@ -57,6 +57,22 @@ static XP_UCHAR* linux_dutil_md5sum( XW_DUtilCtxt* duc, const XP_U8* ptr,
XP_U16 len );
#endif
+static void
+linux_dutil_notifyPause( XW_DUtilCtxt* XP_UNUSED(duc), XP_U32 gameID,
+ DupPauseType pauseTyp, XP_U16 pauser,
+ const XP_UCHAR* name, const XP_UCHAR* msg )
+{
+ XP_LOGF( "%s(id=%d, turn=%d, name=%s, typ=%d, %s)", __func__, gameID, pauser,
+ name, pauseTyp, msg );
+}
+
+static void
+linux_dutil_onDupTimerChanged( XW_DUtilCtxt* XP_UNUSED(duc), XP_U32 gameID,
+ XP_U32 oldVal, XP_U32 newVal )
+{
+ XP_LOGF( "%s(id=%d, oldVal=%d, newVal=%d)", __func__, gameID, oldVal, newVal );
+}
+
XW_DUtilCtxt*
dutils_init( MPFORMAL VTableMgr* vtMgr, void* closure )
{
@@ -88,6 +104,9 @@ dutils_init( MPFORMAL VTableMgr* vtMgr, void* closure )
SET_PROC(md5sum);
#endif
+ SET_PROC(notifyPause);
+ SET_PROC(onDupTimerChanged);
+
# undef SET_PROC
MPASSIGN( result->mpool, mpool );
@@ -175,6 +194,11 @@ linux_dutil_getUserString( XW_DUtilCtxt* XP_UNUSED(uc), XP_U16 code )
return "[Winner] %s: %d";
case STRDSD_PLACER:
return "[#%d] %s: %d";
+ case STR_DUP_CLIENT_SENT:
+ return "This device has sent its moves to the host. When all players "
+ "have sent their moves it will be your turn again.";
+ case STRDD_DUP_HOST_RECEIVED:
+ return "%d of %d players have reported their moves.";
default:
return (XP_UCHAR*)"unknown code to linux_util_getUserString";
diff --git a/xwords4/linux/linuxmain.c b/xwords4/linux/linuxmain.c
index 96f871034..07c5eec1e 100644
--- a/xwords4/linux/linuxmain.c
+++ b/xwords4/linux/linuxmain.c
@@ -139,6 +139,18 @@ tryConnectToServer( CommonGlobals* cGlobals )
stream );
}
+void
+ensureLocalPlayerNames( LaunchParams* params, CurGameInfo* gi )
+{
+ for ( int ii = 0; ii < gi->nPlayers; ++ii ) {
+ LocalPlayer* lp = &gi->players[ii];
+ if ( lp->isLocal && !lp->name ) {
+ replaceStringIfDifferent( params->mpool,
+ &lp->name, "LPlayer" );
+ }
+ }
+}
+
static bool
canMakeFromGI( const CurGameInfo* gi )
{
@@ -150,7 +162,7 @@ canMakeFromGI( const CurGameInfo* gi )
bool allHaveDicts = true;
for ( int ii = 0; result && ii < gi->nPlayers; ++ii ) {
const LocalPlayer* lp = &gi->players[ii];
- result = !lp->isLocal || '\0' != lp->name[0];
+ result = !lp->isLocal || (!!lp->name && '\0' != lp->name[0]);
if ( allHaveDicts ) {
allHaveDicts = !!lp->dictName;
}
@@ -164,7 +176,7 @@ canMakeFromGI( const CurGameInfo* gi )
bool
linuxOpenGame( CommonGlobals* cGlobals, const TransportProcs* procs,
- const CurGameInfo* gi, const CommsAddrRec* addrP )
+ const CommsAddrRec* addrP )
{
LOG_FUNC();
XWStreamCtxt* stream = NULL;
@@ -203,103 +215,98 @@ linuxOpenGame( CommonGlobals* cGlobals, const TransportProcs* procs,
stream_destroy( stream );
}
- if ( !opened ) {
- if ( !gi ) {
- gi = cGlobals->gi;
- }
- if ( canMakeFromGI( gi ) ) {
- opened = XP_TRUE;
+ if ( !opened && canMakeFromGI( cGlobals->gi ) ) {
+ opened = XP_TRUE;
#ifdef XWFEATURE_RELAY
- /* if ( addr.conType == COMMS_CONN_RELAY ) { */
- /* XP_ASSERT( !!params->connInfo.relay.relayName ); */
- /* globals->cGlobals.defaultServerName */
- /* = params->connInfo.relay.relayName; */
- /* } */
+ /* if ( addr.conType == COMMS_CONN_RELAY ) { */
+ /* XP_ASSERT( !!params->connInfo.relay.relayName ); */
+ /* globals->cGlobals.defaultServerName */
+ /* = params->connInfo.relay.relayName; */
+ /* } */
#endif
- game_makeNewGame( MEMPOOL &cGlobals->game, cGlobals->gi,
- cGlobals->util, cGlobals->draw,
- &cGlobals->cp, procs
+ game_makeNewGame( MEMPOOL &cGlobals->game, cGlobals->gi,
+ cGlobals->util, cGlobals->draw,
+ &cGlobals->cp, procs
#ifdef SET_GAMESEED
- , params->gameSeed
+ , params->gameSeed
#endif
- );
+ );
- CommsAddrRec addr = !!addrP ? *addrP : cGlobals->addr;
- // addr.conType = params->conType;
- CommsConnType typ;
- for ( XP_U32 st = 0; addr_iter( &addr, &typ, &st ); ) {
- if ( params->commsDisableds[typ][0] ) {
- comms_setAddrDisabled( cGlobals->game.comms, typ, XP_FALSE, XP_TRUE );
- }
- if ( params->commsDisableds[typ][1] ) {
- comms_setAddrDisabled( cGlobals->game.comms, typ, XP_TRUE, XP_TRUE );
- }
- switch( typ ) {
+ CommsAddrRec addr = !!addrP ? *addrP : cGlobals->addr;
+ // addr.conType = params->conType;
+ CommsConnType typ;
+ for ( XP_U32 st = 0; addr_iter( &addr, &typ, &st ); ) {
+ if ( params->commsDisableds[typ][0] ) {
+ comms_setAddrDisabled( cGlobals->game.comms, typ, XP_FALSE, XP_TRUE );
+ }
+ if ( params->commsDisableds[typ][1] ) {
+ comms_setAddrDisabled( cGlobals->game.comms, typ, XP_TRUE, XP_TRUE );
+ }
+ switch( typ ) {
#ifdef XWFEATURE_RELAY
- case COMMS_CONN_RELAY:
- /* addr.u.ip_relay.ipAddr = 0; */
- /* addr.u.ip_relay.port = params->connInfo.relay.defaultSendPort; */
- /* addr.u.ip_relay.seeksPublicRoom = params->connInfo.relay.seeksPublicRoom; */
- /* addr.u.ip_relay.advertiseRoom = params->connInfo.relay.advertiseRoom; */
- /* XP_STRNCPY( addr.u.ip_relay.hostName, params->connInfo.relay.relayName, */
- /* sizeof(addr.u.ip_relay.hostName) - 1 ); */
- /* XP_STRNCPY( addr.u.ip_relay.invite, params->connInfo.relay.invite, */
- /* sizeof(addr.u.ip_relay.invite) - 1 ); */
- break;
+ case COMMS_CONN_RELAY:
+ /* addr.u.ip_relay.ipAddr = 0; */
+ /* addr.u.ip_relay.port = params->connInfo.relay.defaultSendPort; */
+ /* addr.u.ip_relay.seeksPublicRoom = params->connInfo.relay.seeksPublicRoom; */
+ /* addr.u.ip_relay.advertiseRoom = params->connInfo.relay.advertiseRoom; */
+ /* XP_STRNCPY( addr.u.ip_relay.hostName, params->connInfo.relay.relayName, */
+ /* sizeof(addr.u.ip_relay.hostName) - 1 ); */
+ /* XP_STRNCPY( addr.u.ip_relay.invite, params->connInfo.relay.invite, */
+ /* sizeof(addr.u.ip_relay.invite) - 1 ); */
+ break;
#endif
#ifdef XWFEATURE_BLUETOOTH
- case COMMS_CONN_BT:
- XP_ASSERT( sizeof(addr.u.bt.btAddr)
- >= sizeof(params->connInfo.bt.hostAddr));
- XP_MEMCPY( &addr.u.bt.btAddr, ¶ms->connInfo.bt.hostAddr,
- sizeof(params->connInfo.bt.hostAddr) );
- break;
+ case COMMS_CONN_BT:
+ XP_ASSERT( sizeof(addr.u.bt.btAddr)
+ >= sizeof(params->connInfo.bt.hostAddr));
+ XP_MEMCPY( &addr.u.bt.btAddr, ¶ms->connInfo.bt.hostAddr,
+ sizeof(params->connInfo.bt.hostAddr) );
+ break;
#endif
#ifdef XWFEATURE_IP_DIRECT
- case COMMS_CONN_IP_DIRECT:
- XP_STRNCPY( addr.u.ip.hostName_ip, params->connInfo.ip.hostName,
- sizeof(addr.u.ip.hostName_ip) - 1 );
- addr.u.ip.port_ip = params->connInfo.ip.port;
- break;
+ case COMMS_CONN_IP_DIRECT:
+ XP_STRNCPY( addr.u.ip.hostName_ip, params->connInfo.ip.hostName,
+ sizeof(addr.u.ip.hostName_ip) - 1 );
+ addr.u.ip.port_ip = params->connInfo.ip.port;
+ break;
#endif
#ifdef XWFEATURE_SMS
- case COMMS_CONN_SMS:
- XP_LOGF( "%s(): SMS is on at least", __func__ );
- /* No! Don't overwrite what may be a return address with local
- stuff */
- /* XP_STRNCPY( addr.u.sms.phone, params->connInfo.sms.phone, */
- /* sizeof(addr.u.sms.phone) - 1 ); */
- /* addr.u.sms.port = params->connInfo.sms.port; */
- break;
+ case COMMS_CONN_SMS:
+ XP_LOGF( "%s(): SMS is on at least", __func__ );
+ /* No! Don't overwrite what may be a return address with local
+ stuff */
+ /* XP_STRNCPY( addr.u.sms.phone, params->connInfo.sms.phone, */
+ /* sizeof(addr.u.sms.phone) - 1 ); */
+ /* addr.u.sms.port = params->connInfo.sms.port; */
+ break;
#endif
- default:
- break;
- }
+ default:
+ break;
}
+ }
- model_setDictionary( cGlobals->game.model, cGlobals->dict );
- setSquareBonuses( cGlobals );
- model_setPlayerDicts( cGlobals->game.model, &cGlobals->dicts );
+ model_setDictionary( cGlobals->game.model, cGlobals->dict );
+ setSquareBonuses( cGlobals );
+ model_setPlayerDicts( cGlobals->game.model, &cGlobals->dicts );
- /* Need to save in order to have a valid selRow for the first send */
- linuxSaveGame( cGlobals );
+ /* Need to save in order to have a valid selRow for the first send */
+ linuxSaveGame( cGlobals );
#ifndef XWFEATURE_STANDALONE_ONLY
- /* This may trigger network activity */
- if ( !!cGlobals->game.comms ) {
- comms_setAddr( cGlobals->game.comms, &addr );
- }
+ /* This may trigger network activity */
+ if ( !!cGlobals->game.comms ) {
+ comms_setAddr( cGlobals->game.comms, &addr );
+ }
#endif
#ifdef XWFEATURE_SEARCHLIMIT
- cGlobals->gi->allowHintRect = params->allowHintRect;
+ cGlobals->gi->allowHintRect = params->allowHintRect;
#endif
- if ( params->needsNewGame ) {
- XP_ASSERT(0);
- // new_game_impl( globals, XP_FALSE );
- }
+ if ( params->needsNewGame ) {
+ XP_ASSERT(0);
+ // new_game_impl( globals, XP_FALSE );
}
}
@@ -816,6 +823,7 @@ typedef enum {
,CMD_NOCLOSESTDIN
,CMD_QUITAFTER
,CMD_BOARDSIZE
+ ,CMD_DUP_MODE
,CMD_HIDEVALUES
,CMD_SKIPCONFIRM
,CMD_VERTICALSCORE
@@ -940,6 +948,7 @@ static CmdInfoRec CmdInfoRecs[] = {
,{ CMD_NOCLOSESTDIN, false, "no-close-stdin", "do not close stdin on start" }
,{ CMD_QUITAFTER, true, "quit-after", "exit seconds after game's done" }
,{ CMD_BOARDSIZE, true, "board-size", "board is by cells" }
+ ,{ CMD_DUP_MODE, false, "duplicate-mode", "play in duplicate mode" }
,{ CMD_HIDEVALUES, false, "hide-values", "show letters, not nums, on tiles" }
,{ CMD_SKIPCONFIRM, false, "skip-confirm", "don't confirm before commit" }
,{ CMD_VERTICALSCORE, false, "vertical", "scoreboard is vertical" }
@@ -2143,14 +2152,37 @@ listDicts( const LaunchParams *params )
return result;
}
+static void
+linux_util_formatPauseHistory( XW_UtilCtxt* XP_UNUSED(uc), XWStreamCtxt* stream,
+ DupPauseType typ, XP_S16 turn,
+ XP_U32 whenPrev, XP_U32 whenCur, const XP_UCHAR* msg )
+{
+ XP_UCHAR buf[128];
+ if ( UNPAUSED == typ ) {
+ XP_SNPRINTF( buf, VSIZE(buf), "Game unpaused by player %d after %d seconds; msg: %s",
+ turn, whenCur - whenPrev, msg );
+ } else {
+ if ( AUTOPAUSED == typ ) {
+ XP_SNPRINTF( buf, VSIZE(buf), "%s", "Game auto-paused" );
+ } else {
+ XP_SNPRINTF( buf, VSIZE(buf), "Game paused by player %d; msg: %s",
+ turn, msg );
+ }
+ }
+ stream_catString( stream, buf );
+}
+
void
setupLinuxUtilCallbacks( XW_UtilCtxt* util )
{
+#define SET_PROC(NAM) util->vtable->m_util_##NAM = linux_util_##NAM
#ifndef XWFEATURE_STANDALONE_ONLY
- util->vtable->m_util_informMissing = linux_util_informMissing;
- util->vtable->m_util_addrChange = linux_util_addrChange;
- util->vtable->m_util_setIsServer = linux_util_setIsServer;
+ SET_PROC(informMissing);
+ SET_PROC(addrChange);
+ SET_PROC(setIsServer);
#endif
+ SET_PROC(formatPauseHistory);
+#undef SET_PROC
}
void
@@ -2605,6 +2637,9 @@ main( int argc, char** argv )
case CMD_BOARDSIZE:
mainParams.pgi.boardSize = atoi(optarg);
break;
+ case CMD_DUP_MODE:
+ mainParams.pgi.inDuplicateMode = XP_TRUE;
+ break;
#ifdef XWFEATURE_BLUETOOTH
case CMD_BTADDR:
addr_addType( &mainParams.addr, COMMS_CONN_BT );
diff --git a/xwords4/linux/linuxmain.h b/xwords4/linux/linuxmain.h
index 71a5b184c..2b919b59f 100644
--- a/xwords4/linux/linuxmain.h
+++ b/xwords4/linux/linuxmain.h
@@ -115,8 +115,9 @@ XP_Bool parseSMSParams( LaunchParams* params, gchar** myPhone, XP_U16* myPort );
unsigned int makeRandomInt();
bool linuxOpenGame( CommonGlobals* cGlobals, const TransportProcs* procs,
- const CurGameInfo* gi, const CommsAddrRec* addrP );
+ const CommsAddrRec* addrP );
void tryConnectToServer( CommonGlobals* cGlobals );
+void ensureLocalPlayerNames( LaunchParams* params, CurGameInfo* gi );
/* void initParams( LaunchParams* params ); */
/* void freeParams( LaunchParams* params ); */
diff --git a/xwords4/linux/linuxutl.c b/xwords4/linux/linuxutl.c
index abf6c7bd0..3a9ff9ddb 100644
--- a/xwords4/linux/linuxutl.c
+++ b/xwords4/linux/linuxutl.c
@@ -439,7 +439,8 @@ linux_getErrString( UtilErrID id, XP_Bool* silent )
void
formatLMI( const LastMoveInfo* lmi, XP_UCHAR* buf, XP_U16 len )
{
- const XP_UCHAR* name = lmi->name;
+ const XP_Bool inDuplicateMode = lmi->inDuplicateMode;
+ const XP_UCHAR* name = inDuplicateMode ? "all" : lmi->names[0];
switch( lmi->moveType ) {
case ASSIGN_TYPE:
XP_SNPRINTF( buf, len, "Tiles assigned to %s", name );
@@ -447,16 +448,23 @@ formatLMI( const LastMoveInfo* lmi, XP_UCHAR* buf, XP_U16 len )
case MOVE_TYPE:
if ( 0 == lmi->nTiles ) {
XP_SNPRINTF( buf, len, "%s passed", name );
- } else {
- XP_SNPRINTF( buf, len, "%s played %s for %d points", name,
+ } else if ( !inDuplicateMode || 1 == lmi->nWinners ) {
+ XP_SNPRINTF( buf, len, "%s played %s for %d points", lmi->names[0],
lmi->word, lmi->score );
+ } else {
+ XP_SNPRINTF( buf, len, "%d players tied for %d points. %s was played",
+ lmi->nWinners, lmi->score, lmi->word );
}
break;
case TRADE_TYPE:
- XP_SNPRINTF( buf, len, "%s traded %d tiles",
- name, lmi->nTiles );
+ if ( inDuplicateMode ) {
+ XP_SNPRINTF( buf, len, "%d tiles were exchanged", lmi->nTiles );
+ } else {
+ XP_SNPRINTF( buf, len, "%s traded %d tiles", name, lmi->nTiles );
+ }
break;
case PHONY_TYPE:
+ XP_ASSERT( !inDuplicateMode );
XP_SNPRINTF( buf, len, "%s lost a turn", name );
break;
default:
diff --git a/xwords4/linux/scripts/discon_ok2.py b/xwords4/linux/scripts/discon_ok2.py
index 8a0e75061..b4c186b7f 100755
--- a/xwords4/linux/scripts/discon_ok2.py
+++ b/xwords4/linux/scripts/discon_ok2.py
@@ -145,7 +145,9 @@ def player_params(args, NLOCALS, NPLAYERS, NAME_INDX):
PARAMS = []
while NLOCALS > 0 or NREMOTES > 0:
if 0 == random.randint(0, 2) and 0 < NLOCALS:
- PARAMS += ['--robot', g_NAMES[NAME_INDX], '--robot-iq', str(random.randint(1,100))]
+ PARAMS += ['--robot', g_NAMES[NAME_INDX]]
+ if not args.IQS_SAME:
+ PARAMS += ['--robot-iq', str(random.randint(1,100))]
NLOCALS -= 1
NAME_INDX += 1
elif 0 < NREMOTES:
@@ -158,12 +160,15 @@ def logReaderStub(dev): dev.logReaderMain()
class Device():
sHasLDevIDMap = {}
sConnNamePat = re.compile('.*got_connect_cmd: connName: "([^"]+)".*$')
- sGameOverPat = re.compile('.*\[unused tiles\].*')
- sTilesLeftPoolPat = re.compile('.*pool_removeTiles: (\d+) tiles left in pool')
+ sGameOverPat = re.compile('^\[(\#\d|Winner)\] (.*): (\d+)')
+ sTilesLeftPoolPat = re.compile('.*pool_r.*Tiles: (\d+) tiles left in pool')
sTilesLeftTrayPat = re.compile('.*player \d+ now has (\d+) tiles')
sRelayIDPat = re.compile('.*UPDATE games.*seed=(\d+),.*relayid=\'([^\']+)\'.*')
+ sScoresDup = []
+ sScoresReg = []
- def __init__(self, args, game, indx, params, room, peers, db, log, script, nInGame):
+ def __init__(self, args, game, indx, params, room, peers, db,
+ log, script, nInGame, inDupMode):
self.game = game
self.indx = indx
self.args = args
@@ -175,6 +180,7 @@ class Device():
self.logPath = log
self.script = script
self.nInGame = nInGame
+ self.inDupMode = inDupMode
# runtime stuff; init now
self.app = args.APP_OLD
self.proc = None
@@ -204,8 +210,8 @@ class Device():
def logReaderMain(self):
assert self and self.proc
+ # print('logReaderMain called; opening:', self.logPath)
stdout, stderr = self.proc.communicate()
- # print('logReaderMain called; opening:', self.logPath, 'flag:', flag)
nLines = 0
with open(self.logPath, 'a') as log:
for line in stderr.splitlines():
@@ -217,7 +223,13 @@ class Device():
# check for game over
if not self.gameOver:
match = Device.sGameOverPat.match(line)
- if match: self.gameOver = True
+ if match:
+ self.gameOver = True
+ score = int(match.group(3))
+ if self.inDupMode:
+ Device.sScoresDup.append(score)
+ else:
+ Device.sScoresReg.append(score)
# Check every line for tiles left in pool
match = Device.sTilesLeftPoolPat.match(line)
@@ -398,6 +410,7 @@ def build_cmds(args):
# make one in three games public
PUBLIC = []
if random.randint(0, 3) == 0: PUBLIC = ['--make-public', '--join-public']
+ useDupeMode = random.randint(0, 100) < args.DUP_PCT
DEV = 0
for NLOCALS in LOCALS:
DEV += 1
@@ -412,7 +425,7 @@ def build_cmds(args):
# We SHOULD support having both SMS and relay working...
if args.ADD_RELAY:
PARAMS += [ '--relay-port', args.PORT, '--room', ROOM, '--host', args.HOST]
- if random.randint(0,100) % 100 < g_UDP_PCT_START:
+ if random.randint(0, 100) < g_UDP_PCT_START:
PARAMS += ['--use-udp']
if args.ADD_SMS:
PARAMS += [ '--sms-number', PHONE_BASE + str(DEV - 1) ]
@@ -458,7 +471,10 @@ def build_cmds(args):
# print('PARAMS:', PARAMS)
- dev = Device(args, GAME, COUNTER, PARAMS, ROOM, peers, DB, LOG, SCRIPT, len(LOCALS))
+ if useDupeMode: PARAMS += ['--duplicate-mode']
+
+ dev = Device(args, GAME, COUNTER, PARAMS, ROOM, peers,
+ DB, LOG, SCRIPT, len(LOCALS), useDupeMode)
peers.add(dev)
dev.update_ldevid()
devs.append(dev)
@@ -470,8 +486,10 @@ def summarizeTileCounts(devs, endTime, state):
global gDeadLaunches
shouldGoOn = True
data = [dev.getTilesCount() for dev in devs]
+ dupModeFlags = [dev.inDupMode for dev in devs]
nDevs = len(data)
- totalTiles = 0
+ totalTilesStd = 0
+ totalTilesDup = 0
colWidth = max(2, len(str(nDevs)))
headWidth = 0
fmtData = [{'head' : 'dev', },
@@ -484,17 +502,23 @@ def summarizeTileCounts(devs, endTime, state):
# Group devices by game
games = []
+ joinChars = []
prev = -1
- for datum in data:
+ for datum, inDupMode in zip(data, dupModeFlags):
gameNo = datum['game']
if gameNo != prev:
games.append([])
+ if inDupMode: joinChars.append('.')
+ else: joinChars.append('+')
prev = gameNo
games[-1].append('{:0{width}d}'.format(datum['index'], width=colWidth))
- fmtData[0]['data'] = ['+'.join(game) for game in games]
+
+ fmtData[0]['data'] = []
+ for game, joinChar in zip(games, joinChars):
+ fmtData[0]['data'].append( joinChar.join(game) )
nLaunches = gDeadLaunches
- for datum in data:
+ for datum, inDupMode in zip(data, dupModeFlags):
launchCount = datum['launchCount']
nLaunches += launchCount
fmtData[1]['data'].append('{:{width}d}'.format(launchCount, width=colWidth))
@@ -510,12 +534,14 @@ def summarizeTileCounts(devs, endTime, state):
txt = '{:+{width}d}'.format(nTilesTray, width=colWidth-1)
else:
txt = '{:{width}d}'.format(nTilesPool, width=colWidth)
- totalTiles += int(nTilesPool)
+ if inDupMode: totalTilesDup += int(nTilesPool)
+ else: totalTilesStd += int(nTilesPool)
fmtData[2]['data'].append(txt)
print('')
- print('devs left: {}; bag tiles left: {}; total launches: {}; {}/{}'
- .format(nDevs, totalTiles, nLaunches, datetime.datetime.now(), endTime ))
+ print('devs left: {}; bag tiles left: {} (std: {}, dup: {}); total launches: {}; {}/{}'
+ .format(nDevs, totalTilesStd + totalTilesDup, totalTilesStd, totalTilesDup,
+ nLaunches, datetime.datetime.now(), endTime ))
fmt = '{head:>%d} {data}' % headWidth
for datum in fmtData: datum['data'] = ' '.join(datum['data'])
for datum in fmtData:
@@ -621,6 +647,14 @@ def run_cmds(args, devs):
# echo $2
# }
+def log_scores( devs ):
+ if len(Device.sScoresReg) > 0:
+ print( "average score for regular games:",
+ sum(Device.sScoresReg) // len(Device.sScoresReg) )
+ if len(Device.sScoresDup) > 0:
+ print( "average score for dup games:",
+ sum(Device.sScoresDup) // len(Device.sScoresDup) )
+
def mkParser():
parser = argparse.ArgumentParser()
parser.add_argument('--send-chat', dest = 'SEND_CHAT', type = str, default = None,
@@ -644,6 +678,10 @@ def mkParser():
parser.add_argument('--dup-packets', dest = 'DUP_PACKETS', default = False, help = 'send all packet twice')
parser.add_argument('--use-gtk', dest = 'USE_GTK', default = False, action = 'store_true',
help = 'run games using gtk instead of ncurses')
+
+ parser.add_argument('--duplicate-pct', dest = 'DUP_PCT', default = 0, type = int,
+ help = 'this fraction played in duplicate mode')
+
# #
# # echo " [--clean-start] \\" >&2
parser.add_argument('--game-dict', dest = 'DICTS', action = 'append', default = [])
@@ -656,6 +694,9 @@ def mkParser():
parser.add_argument('--max-devs', dest = 'MAXDEVS', type = int, default = 4,
help = 'No game will have more devices than this')
+ parser.add_argument('--robots-all-same-iq', dest = 'IQS_SAME', default = False,
+ action = 'store_true', help = 'give all robots the same IQ')
+
parser.add_argument('--min-run', dest = 'MINRUN', type = int, default = 2,
help = 'Keep each run alive at least this many seconds')
# # echo " [--new-app &2
@@ -898,6 +939,7 @@ def main():
nDevs = len(devs)
run_cmds(args, devs)
print('{} finished; took {} for {} devices'.format(sys.argv[0], datetime.datetime.now() - startTime, nDevs))
+ log_scores( devs )
##############################################################################
if __name__ == '__main__':
diff --git a/xwords4/linux/scripts/run-curses-sms.sh b/xwords4/linux/scripts/run-curses-sms.sh
new file mode 100755
index 000000000..ce28e86f1
--- /dev/null
+++ b/xwords4/linux/scripts/run-curses-sms.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+usage() {
+ [ $# -ge 1 ] && echo "ERROR: $1"
+ echo "usage: %0 "
+ echo "create one of two apps differentiated with instance"
+ echo "typically called .e.g $0 Eric, then use the invitE menuitem to create second game"
+ exit 1
+}
+
+SELF=''
+
+while [ $# -gt 0 ]; do
+ case $1 in
+ --help|-h)
+ usage
+ ;;
+ *)
+ [ $# -eq 1 ] || usage "requires one parameter"
+ SELF=$1
+ ;;
+ esac
+ shift
+done
+
+[ -n "$SELF" ] || usage "param is not optional"
+
+./obj_linux_memdbg/xwords --curses --db ${SELF}.db --name "$SELF" --remote-player \
+ --sms-number "$SELF" --invitee-sms-number "$SELF" \
+ --server --game-dict dict.xwd \
+ 2>"$SELF"_log.txt \
+