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 9e46cf204..5087fe729 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 @@ -65,6 +65,7 @@ public enum DlgID { GAMES_LIST_NEWGAME, CHANGE_CONN, GAMES_LIST_NAME_REMATCH, + GAMES_LIST_GET_RO, ASK_DUP_PAUSE, CHOOSE_TILES, SHOW_TILES, 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 7e6e0adb4..ba4f0b113 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 @@ -52,6 +52,7 @@ import org.eehouse.android.xw4.jni.UtilCtxt; import org.eehouse.android.xw4.jni.UtilCtxtImpl; import org.eehouse.android.xw4.jni.XwJNI; import org.eehouse.android.xw4.jni.XwJNI.GamePtr; +import org.eehouse.android.xw4.jni.XwJNI.RematchOrder; import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.Utils.ISOCode; @@ -72,6 +73,14 @@ public class GameUtils { void onResendDone( Context context, int numSent ); } + interface NeedRematchOrder { + // Return null if unable to produce it immediately. Implementation may + // want to start a query at the same time and from its + // ok-button-handler call makeRematch() again with a different + // implementation that simply returns a cached RematchOrder + RematchOrder getRematchOrder(); + } + private static Integer s_minScreen; // Used to determine whether to resend all messages on networking coming // back up. The length of the array determines the number of times in the @@ -574,7 +583,8 @@ public class GameUtils { } public static long makeRematch( Context context, long srcRowid, - long groupID, String gameName ) + long groupID, String gameName, + NeedRematchOrder nro ) { long rowid = DBUtils.ROWID_NOTFOUND; try ( GameLock lock = GameLock.tryLockRO( srcRowid ) ) { @@ -582,13 +592,22 @@ public class GameUtils { CurGameInfo gi = new CurGameInfo( context ); try ( GamePtr gamePtr = loadMakeGame( context, gi, lock ) ) { if ( null != gamePtr ) { - UtilCtxt util = new UtilCtxtImpl( context ); - CommonPrefs cp = CommonPrefs.get(context); - try ( GamePtr gamePtrNew = XwJNI - .game_makeRematch( gamePtr, util, cp, gameName ) ) { - if ( null != gamePtrNew ) { - rowid = saveNewGame1( context, gamePtrNew, - groupID, gameName ); + RematchOrder ro = RematchOrder.RO_SAME; + if ( XwJNI.server_canOfferRematch( gamePtr ) ) { + ro = XWPrefs.getDefaultRematchOrder( context ); + if ( null == ro ) { + ro = nro.getRematchOrder(); + } + } + if ( null != ro ) { + UtilCtxt util = new UtilCtxtImpl( context ); + CommonPrefs cp = CommonPrefs.get( context ); + try ( GamePtr gamePtrNew = XwJNI + .game_makeRematch( gamePtr, util, cp, gameName, ro ) ) { + if ( null != gamePtrNew ) { + rowid = saveNewGame1( context, gamePtrNew, + groupID, gameName ); + } } } } 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 239b01e90..694f3abf9 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 @@ -67,6 +67,7 @@ import org.eehouse.android.xw4.jni.CurGameInfo; import org.eehouse.android.xw4.jni.GameSummary; import org.eehouse.android.xw4.jni.LastMoveInfo; import org.eehouse.android.xw4.jni.XwJNI; +import org.eehouse.android.xw4.jni.XwJNI.RematchOrder; import org.eehouse.android.xw4.loc.LocUtils; import static org.eehouse.android.xw4.DBUtils.ROWID_NOTFOUND; @@ -874,6 +875,12 @@ public class GamesListDelegate extends ListDelegateBase } break; + case GAMES_LIST_GET_RO: { + NRO nro = (NRO)params[0]; + dialog = mkRematchConfigDlg( nro ); + } + break; + case GAMES_LIST_NAME_REMATCH: { final LinearLayout view = (LinearLayout) LocUtils.inflate( m_activity, R.layout.msg_label_and_edit ); @@ -1577,6 +1584,30 @@ public class GamesListDelegate extends ListDelegateBase return handled || super.onDismissed( action, params ); } + private Dialog mkRematchConfigDlg( NRO nro ) + { + final RematchConfigView view = (RematchConfigView) + LocUtils.inflate( m_activity, R.layout.rematch_config ); + + int iconResID = nro.isSolo() + ? R.drawable.ic_sologame : R.drawable.ic_multigame; + AlertDialog.Builder ab = makeAlertBuilder() + .setView( view ) + .setIcon( iconResID ) + .setTitle( R.string.button_rematch ) + .setNegativeButton( android.R.string.cancel, null ) + .setPositiveButton( android.R.string.ok, + new OnClickListener() { + @Override + public void onClick( DialogInterface dlg, int ii ) { + RematchOrder ro = view.onOkClicked(); + nro.rerun( ro ); + } + } ) + ; + return ab.create(); + } + private Dialog mkLoadStoreDlg( final Uri uri ) { final BackupConfigView view = (BackupConfigView) @@ -2358,6 +2389,7 @@ public class GamesListDelegate extends ListDelegateBase button.setVisibility( View.VISIBLE ); final boolean solo = isSolos[ii]; button.setOnClickListener( new View.OnClickListener() { + @Override public void onClick( View view ) { curThis().handleNewGameButton( solo ); } @@ -2710,7 +2742,52 @@ public class GamesListDelegate extends ListDelegateBase } } + private class NRO implements Serializable, GameUtils.NeedRematchOrder { + private RematchOrder mChosenOrder = null; + private Bundle mExtras; + private String mGameName; + private CommsConnTypeSet mAddrs; + + NRO( Bundle extras, String gameName, CommsConnTypeSet addrs ) + { + mExtras = extras; + mGameName = gameName; + mAddrs = addrs; + } + + @Override + public RematchOrder getRematchOrder() + { + RematchOrder result = mChosenOrder; + if ( null == result ) { + showDialogFragment( DlgID.GAMES_LIST_GET_RO, this ); + } + return result; + } + + boolean isSolo() { return mExtras.getBoolean( REMATCH_IS_SOLO, true ); } + + void rerun( RematchOrder ro ) + { + mChosenOrder = ro; + m_rematchExtras = mExtras; + runOnUiThread( new Runnable() { + @Override + public void run() { + rematchWithNameAndPerm( mGameName, mAddrs, NRO.this ); + } + } ); + } + } // class NRO + private void rematchWithNameAndPerm( String gameName, CommsConnTypeSet addrs ) + { + NRO nro = new NRO( m_rematchExtras, gameName, addrs ); + rematchWithNameAndPerm( gameName, addrs, nro ); + } + + private void rematchWithNameAndPerm( String gameName, CommsConnTypeSet addrs, + NRO nro ) { if ( null != gameName && 0 < gameName.length() ) { Bundle extras = m_rematchExtras; @@ -2720,17 +2797,19 @@ public class GamesListDelegate extends ListDelegateBase DBUtils.GROUPID_UNSPEC ); long newid = GameUtils.makeRematch( m_activity, srcRowID, - groupID, gameName ); + groupID, gameName, nro ); - if ( extras.getBoolean( REMATCH_DELAFTER_EXTRA, false ) ) { - String name = DBUtils.getName( m_activity, srcRowID ); - makeConfirmThenBuilder( Action.LAUNCH_AFTER_DEL, - R.string.confirm_del_after_rematch_fmt, - name ) - .setParams( newid, srcRowID ) - .show(); - } else { - launchGame( newid ); + if ( DBUtils.ROWID_NOTFOUND != newid ) { + if ( extras.getBoolean( REMATCH_DELAFTER_EXTRA, false ) ) { + String name = DBUtils.getName( m_activity, srcRowID ); + makeConfirmThenBuilder( Action.LAUNCH_AFTER_DEL, + R.string.confirm_del_after_rematch_fmt, + name ) + .setParams( newid, srcRowID ) + .show(); + } else { + launchGame( newid ); + } } } m_rematchExtras = null; diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RematchConfigView.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RematchConfigView.java new file mode 100644 index 000000000..bea3e6a6c --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RematchConfigView.java @@ -0,0 +1,78 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2020 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.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.AdapterView; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import java.util.HashMap; +import java.util.Map; + +import org.eehouse.android.xw4.jni.XwJNI.RematchOrder; +import org.eehouse.android.xw4.loc.LocUtils; + +public class RematchConfigView extends LinearLayout +{ + private Context mContext; + private RadioGroup mGroup; + Map mRos = new HashMap<>(); + + public RematchConfigView( Context cx, AttributeSet as ) + { + super( cx, as ); + mContext = cx; + } + + @Override + protected void onFinishInflate() + { + mGroup = (RadioGroup)findViewById( R.id.group ); + for ( RematchOrder ro : RematchOrder.values() ) { + RadioButton button = new RadioButton( mContext ); + button.setText( LocUtils.getString( mContext, ro.getStrID() ) ); + mGroup.addView( button ); + mRos.put( button.getId(), ro ); + if ( 1 == mRos.size() ) { + button.setChecked( true ); + } + } + } + + public RematchOrder onOkClicked() + { + int id = mGroup.getCheckedRadioButtonId(); + RematchOrder ro = mRos.get(id); + + // Save it if default button checked + CheckBox check = (CheckBox)findViewById( R.id.make_default ); + if ( check.isChecked() ) { + XWPrefs.setDefaultRematchOrder( mContext, ro ); + } + + return ro; + } +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java index 68c77b8c9..967471645 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java @@ -32,6 +32,8 @@ import org.json.JSONObject; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet; +import org.eehouse.android.xw4.jni.XwJNI.RematchOrder; + public class XWPrefs { private static final String TAG = XWPrefs.class.getSimpleName(); @@ -328,6 +330,30 @@ public class XWPrefs { return groupID; } + public static void setDefaultRematchOrder( Context context, RematchOrder ro ) + { + String storedStr = null == ro ? "" : context.getString( ro.getStrID() ); + setPrefsString( context, R.string.key_rematch_order, storedStr ); + } + + public static RematchOrder getDefaultRematchOrder( Context context ) + { + String storedStr = getPrefsString( context, R.string.key_rematch_order ); + + // Let's try to get this from the enum... + RematchOrder ro = null; + for ( RematchOrder one: RematchOrder.values() ) { + int strID = one.getStrID(); + String str = context.getString( strID ); + if ( str.equals( storedStr ) ) { + ro = one; + break; + } + } + + return ro; + } + public static void setDefaultNewGameGroup( Context context, long val ) { Assert.assertTrue( DBUtils.GROUPID_UNSPEC != val ); 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 eb8f06ef8..d109e8366 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 @@ -31,6 +31,7 @@ import org.eehouse.android.xw4.DbgUtils; import org.eehouse.android.xw4.Log; import org.eehouse.android.xw4.NetLaunchInfo; import org.eehouse.android.xw4.Quarantine; +import org.eehouse.android.xw4.R; import org.eehouse.android.xw4.Utils.ISOCode; import org.eehouse.android.xw4.Utils; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; @@ -336,11 +337,24 @@ public class XwJNI { return gamePtr; } + // Keep in sync with server.h + public enum RematchOrder { + RO_SAME(R.string.ro_same), + RO_LOW_SCORE_FIRST(R.string.ro_low_score_first), + RO_HIGH_SCORE_FIRST(R.string.ro_high_score_first), + RO_JUGGLE(R.string.ro_juggle), + ; + private int mStrID; + private RematchOrder(int str) { mStrID = str; } + public int getStrID() { return mStrID; } + }; + public static GamePtr game_makeRematch( GamePtr gamePtr, UtilCtxt util, - CommonPrefs cp, String gameName ) + CommonPrefs cp, String gameName, + RematchOrder ro ) { GamePtr gamePtrNew = initGameJNI( 0 ); - if ( !game_makeRematch( gamePtr, gamePtrNew, util, cp, gameName ) ) { + if ( !game_makeRematch( gamePtr, gamePtrNew, util, cp, gameName, ro ) ) { gamePtrNew.release(); gamePtrNew = null; } @@ -384,7 +398,7 @@ public class XwJNI { private static native boolean game_makeRematch( GamePtr gamePtr, GamePtr gamePtrNew, UtilCtxt util, CommonPrefs cp, - String gameName ); + String gameName, RematchOrder ro ); private static native boolean game_makeFromInvite( GamePtr gamePtr, NetLaunchInfo nli, UtilCtxt util, @@ -536,6 +550,7 @@ public class XwJNI { public static native boolean server_getGameIsConnected( GamePtr gamePtr ); public static native String server_writeFinalScores( GamePtr gamePtr ); public static native boolean server_initClientConnection( GamePtr gamePtr ); + public static native boolean server_canOfferRematch( GamePtr gamePtr ); public static native void server_endGame( GamePtr gamePtr ); // hybrid to save work diff --git a/xwords4/android/app/src/main/res/layout/rematch_config.xml b/xwords4/android/app/src/main/res/layout/rematch_config.xml new file mode 100644 index 000000000..266e770f3 --- /dev/null +++ b/xwords4/android/app/src/main/res/layout/rematch_config.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + 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 bf7e92862..2bae1f285 100644 --- a/xwords4/android/app/src/main/res/values/common_rsrc.xml +++ b/xwords4/android/app/src/main/res/values/common_rsrc.xml @@ -83,6 +83,7 @@ key_robot_name key_default_robodict key_default_phonies2 + key_rematch_order key_default_timerenabled key_notify_sound key_disable_mqtt @@ -222,6 +223,14 @@ @string/phonies_block + + @string/ro_no_default + @string/ro_same + @string/ro_low_score_first + @string/ro_high_score_first + @string/ro_juggle + + @string/robot_smartest @string/robot_smarter diff --git a/xwords4/android/app/src/main/res/values/tmpstrings.xml b/xwords4/android/app/src/main/res/values/tmpstrings.xml index 8f5432a97..fe63beb17 100644 --- a/xwords4/android/app/src/main/res/values/tmpstrings.xml +++ b/xwords4/android/app/src/main/res/values/tmpstrings.xml @@ -5,4 +5,12 @@ All scores: %1$s + Choose how to order players in the new game + Rematched Players Order + Ask each time + Same as this game + Low scorer goes first + High scorer goes first + Randomly + diff --git a/xwords4/android/app/src/main/res/xml/prefs_dflts.xml b/xwords4/android/app/src/main/res/xml/prefs_dflts.xml index 411968f82..a05dcb3c4 100644 --- a/xwords4/android/app/src/main/res/xml/prefs_dflts.xml +++ b/xwords4/android/app/src/main/res/xml/prefs_dflts.xml @@ -16,6 +16,13 @@ android:title="@string/title_addrs_pref" /> + + GetStringUTFChars( env, jGameName, NULL ); + RematchOrder ro = jEnumToInt( env, jRo ); success = game_makeRematch( &oldState->game, env, globals->util, &cp, - (TransportProcs*)NULL, &state->game, gameName ); + (TransportProcs*)NULL, &state->game, + gameName, ro ); (*env)->ReleaseStringUTFChars( env, jGameName, gameName ); if ( success ) { @@ -2200,6 +2202,19 @@ Java_org_eehouse_android_xw4_jni_XwJNI_server_1initClientConnection return result; } +JNIEXPORT jboolean JNICALL +Java_org_eehouse_android_xw4_jni_XwJNI_server_1canOfferRematch +( JNIEnv* env, jclass C, GamePtrType gamePtr ) +{ + jboolean result; + XWJNI_START_GLOBALS(gamePtr); + XP_Bool canOffer; + XP_Bool canRematch= server_canRematch( state->game.server, &canOffer ); + result = canRematch && canOffer; + XWJNI_END(); + return result; +} + JNIEXPORT void JNICALL Java_org_eehouse_android_xw4_jni_XwJNI_comms_1start ( JNIEnv* env, jclass C, GamePtrType gamePtr ) diff --git a/xwords4/common/comms.c b/xwords4/common/comms.c index c1e993703..3da279c7a 100644 --- a/xwords4/common/comms.c +++ b/xwords4/common/comms.c @@ -239,6 +239,7 @@ struct CommsCtxt { #define FLAG_HARVEST_DONE 1 #define FLAG_QUASHED 2 + #define QUASHED(COMMS) (0 != ((COMMS)->flags & FLAG_QUASHED)) #if defined XWFEATURE_IP_DIRECT || defined XWFEATURE_DIRECTIP @@ -655,7 +656,7 @@ comms_setConnID( CommsCtxt* comms, XP_U32 connID, XP_U16 streamVersion ) XP_ASSERT( 0 == comms->streamVersion || streamVersion == comms->streamVersion ); comms->streamVersion = streamVersion; - XP_LOGFF( "set connID (gameID) to %x, streamVersion to 0x%X", + XP_LOGFF( "set connID (gameID) to %X, streamVersion to 0x%X", connID, streamVersion ); THREAD_CHECK_END(); } /* comms_setConnID */ @@ -724,6 +725,7 @@ addrFromStreamOne( CommsAddrRec* addrP, XWStreamCtxt* stream, CommsConnType typ void addrFromStream( CommsAddrRec* addrP, XWStreamCtxt* stream ) { + XP_MEMSET( addrP, 0, sizeof(*addrP) ); XP_U8 tmp = stream_getU8( stream ); XP_U16 version = stream_getVersion( stream ); XP_ASSERT( 0 < version ); @@ -1381,6 +1383,41 @@ comms_getChannelAddr( const CommsCtxt* comms, XP_PlayerAddr channelNo, XP_ASSERT( found ); } +static XP_Bool +addrs_same( const CommsAddrRec* addr1, const CommsAddrRec* addr2 ) +{ + /* Empty addresses are the same only if both are empty */ + XP_Bool same = addr1->_conTypes == 0 && addr2->_conTypes == 0; + + CommsConnType typ; + for ( XP_U32 st = 0; !same && addr_iter( addr1, &typ, &st ); ) { + if ( addr_hasType( addr2, typ ) ) { + switch ( typ ) { + case COMMS_CONN_MQTT: + same = addr1->u.mqtt.devID == addr2->u.mqtt.devID; + break; + case COMMS_CONN_SMS: + same = addr1->u.sms.port == addr2->u.sms.port + && 0 == XP_STRCMP(addr1->u.sms.phone, addr2->u.sms.phone ); + break; + default: + XP_LOGFF( "ignoring %s", ConnType2Str(typ) ); + } + } + } + + return same; +} + +XP_Bool +comms_addrsAreSame( const CommsCtxt* XP_UNUSED(comms), + const CommsAddrRec* addr1, + const CommsAddrRec* addr2 ) +{ + XP_Bool result = addrs_same( addr1, addr2 ); + return result; +} + typedef struct _NonAcks { int count; } NonAcks; @@ -3604,16 +3641,16 @@ static void logAddrComms( const CommsCtxt* comms, const CommsAddrRec* addr, const char* caller ) { - logAddr( MPPARM(comms->mpool) comms->dutil, addr, caller ); + logAddr( comms->dutil, addr, caller ); } void -logAddr( MPFORMAL XW_DUtilCtxt* dutil, const CommsAddrRec* addr, +logAddr( XW_DUtilCtxt* dutil, const CommsAddrRec* addr, const char* caller ) { if ( !!addr ) { char buf[128]; - XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(mpool) + XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(dutil->mpool) dutil_getVTManager(dutil)); if ( !!caller ) { snprintf( buf, sizeof(buf), "called on %p from %s:\n", @@ -3872,6 +3909,14 @@ types_hasType( XP_U16 conTypes, CommsConnType typ ) return hasType; } +XP_Bool +addr_isEmpty( const CommsAddrRec* addr ) +{ + CommsConnType typ; + XP_U32 st = 0; + return !addr_iter( addr, &typ, &st ); +} + CommsConnType addr_getType( const CommsAddrRec* addr ) { diff --git a/xwords4/common/comms.h b/xwords4/common/comms.h index 3ca977ed8..02cc22353 100644 --- a/xwords4/common/comms.h +++ b/xwords4/common/comms.h @@ -219,6 +219,8 @@ XP_S16 comms_resendAll( CommsCtxt* comms, XWEnv xwe, CommsConnType filter, XP_U16 comms_getChannelSeed( CommsCtxt* comms ); void comms_getChannelAddr( const CommsCtxt* comms, XP_PlayerAddr channelNo, CommsAddrRec* addr ); +XP_Bool comms_addrsAreSame( const CommsCtxt* comms, const CommsAddrRec* addr1, + const CommsAddrRec* addr2 ); #ifdef XWFEATURE_COMMSACK void comms_ackAny( CommsCtxt* comms, XWEnv xwe ); @@ -256,6 +258,7 @@ void comms_gameJoined( CommsCtxt* comms, const XP_UCHAR* connname, XWHostID hid XP_Bool augmentAddr( CommsAddrRec* addr, const CommsAddrRec* newer, XP_Bool isNewer ); +XP_Bool addr_isEmpty( const CommsAddrRec* addr ); CommsConnType addr_getType( const CommsAddrRec* addr ); void addr_setType( CommsAddrRec* addr, CommsConnType type ); void addr_addType( CommsAddrRec* addr, CommsConnType type ); @@ -283,7 +286,7 @@ void comms_setAddrDisabled( CommsCtxt* comms, CommsConnType typ, XP_Bool send, XP_Bool enabled ); XP_Bool comms_getAddrDisabled( const CommsCtxt* comms, CommsConnType typ, XP_Bool send ); -void logAddr( MPFORMAL XW_DUtilCtxt* dutil, const CommsAddrRec* addr, +void logAddr( XW_DUtilCtxt* dutil, const CommsAddrRec* addr, const char* caller ); # else diff --git a/xwords4/common/comtypes.h b/xwords4/common/comtypes.h index 66d342245..0663b1c6e 100644 --- a/xwords4/common/comtypes.h +++ b/xwords4/common/comtypes.h @@ -48,6 +48,7 @@ #define MAX_COLS MAX_ROWS #define MIN_COLS 11 +#define STREAM_VERS_REMATCHORDER 0x25 #define STREAM_VERS_REMATCHADDRS 0x24 #define STREAM_VERS_MSGSTREAMVERS 0x23 #define STREAM_VERS_NORELAY 0x22 @@ -99,7 +100,7 @@ #define STREAM_VERS_405 0x01 /* search for FIX_NEXT_VERSION_CHANGE next time this is changed */ -#define CUR_STREAM_VERS STREAM_VERS_REMATCHADDRS +#define CUR_STREAM_VERS STREAM_VERS_REMATCHORDER typedef struct XP_Rect { XP_S16 left; diff --git a/xwords4/common/game.c b/xwords4/common/game.c index 58c0af60e..08157c6cd 100644 --- a/xwords4/common/game.c +++ b/xwords4/common/game.c @@ -237,14 +237,14 @@ game_makeNewGame( MPFORMAL XWEnv xwe, XWGame* game, CurGameInfo* gi, XP_Bool game_makeRematch( const XWGame* oldGame, XWEnv xwe, XW_UtilCtxt* newUtil, const CommonPrefs* newCp, const TransportProcs* procs, - XWGame* newGame, const XP_UCHAR* newName ) + XWGame* newGame, const XP_UCHAR* newName, RematchOrder ro ) { XP_Bool success = XP_FALSE; - XP_LOGFF( "(newName=%s)", newName ); + XP_LOGFF( "(newName=%s, ro=%s)", newName, RO2Str(ro) ); - RematchAddrs ra; + RematchInfo* rip; if ( server_getRematchInfo( oldGame->server, newUtil, - makeGameID( newUtil ), &ra ) ) { + makeGameID( newUtil ), ro, &rip ) ) { CommsAddrRec* selfAddrP = NULL; CommsAddrRec selfAddr; if ( !!oldGame->comms ) { @@ -255,20 +255,31 @@ game_makeRematch( const XWGame* oldGame, XWEnv xwe, XW_UtilCtxt* newUtil, if ( game_makeNewGame( MPPARM(newUtil->mpool) xwe, newGame, newUtil->gameInfo, selfAddrP, (CommsAddrRec*)NULL, newUtil, (DrawCtx*)NULL, newCp, procs ) ) { + if ( !!newGame->comms ) { + server_setRematchOrder( newGame->server, rip ); - const CurGameInfo* newGI = newUtil->gameInfo; - for ( int ii = 0; ii < ra.nAddrs; ++ii ) { - NetLaunchInfo nli; - /* hard-code one player per device -- for now */ - nli_init( &nli, newGI, selfAddrP, 1, ii + 1 ); - if ( !!newName ) { - nli_setGameName( &nli, newName ); + const CurGameInfo* newGI = newUtil->gameInfo; + for ( int ii = 0; ; ++ii ) { + CommsAddrRec guestAddr; + XP_U16 nPlayersH; + if ( !server_ri_getAddr( rip, ii, &guestAddr, &nPlayersH ) ) { + break; + } + XP_ASSERT( !comms_addrsAreSame( newGame->comms, &guestAddr, + &selfAddr ) ); + + NetLaunchInfo nli; + nli_init( &nli, newGI, selfAddrP, nPlayersH, ii + 1 ); + if ( !!newName ) { + nli_setGameName( &nli, newName ); + } + LOGNLI( &nli ); + comms_invite( newGame->comms, xwe, &nli, &guestAddr, XP_TRUE ); } - LOGNLI( &nli ); - comms_invite( newGame->comms, xwe, &nli, &ra.addrs[ii], XP_TRUE ); } success = XP_TRUE; } + server_disposeRematchInfo( oldGame->server, &rip ); } LOG_RETURNF( "%s", boolToStr(success) ); return success; @@ -518,8 +529,7 @@ game_getState( const XWGame* game, XWEnv xwe, GameStateInfo* gsi ) gsi->canTrade = board_canTrade( board, xwe ); gsi->nPendingMessages = !!game->comms ? comms_countPendingPackets(game->comms, NULL) : 0; - - gsi->canRematch = server_canRematch( server ); + gsi->canRematch = server_canRematch( server, NULL ); gsi->canPause = server_canPause( server ); gsi->canUnpause = server_canUnpause( server ); } diff --git a/xwords4/common/game.h b/xwords4/common/game.h index a2b87493c..8db4d2515 100644 --- a/xwords4/common/game.h +++ b/xwords4/common/game.h @@ -86,9 +86,11 @@ XP_Bool game_makeNewGame( MPFORMAL XWEnv xwe, XWGame* game, CurGameInfo* gi, ,XP_U16 gameSeed #endif ); + XP_Bool game_makeRematch( const XWGame* game, XWEnv xwe, XW_UtilCtxt* util, const CommonPrefs* cp, const TransportProcs* procs, - XWGame* newGame, const XP_UCHAR* newName ); + XWGame* newGame, const XP_UCHAR* newName, + RematchOrder ro ); void game_changeDict( MPFORMAL XWGame* game, XWEnv xwe, CurGameInfo* gi, DictionaryCtxt* dict ); diff --git a/xwords4/common/model.h b/xwords4/common/model.h index 1058d56f4..abf246aa1 100644 --- a/xwords4/common/model.h +++ b/xwords4/common/model.h @@ -275,7 +275,7 @@ typedef struct WordNotifierInfo { XP_Bool getCurrentMoveScoreIfLegal( ModelCtxt* model, XWEnv xwe, XP_S16 turn, XWStreamCtxt* stream, WordNotifierInfo* wni, XP_S16* score ); -XP_S16 model_getPlayerScore( ModelCtxt* model, XP_S16 player ); +XP_S16 model_getPlayerScore( const ModelCtxt* model, XP_S16 player ); XP_Bool model_getPlayersLastScore( ModelCtxt* model, XWEnv xwe, XP_S16 player, LastMoveInfo* info ); @@ -299,9 +299,12 @@ XP_Bool model_checkMoveLegal( ModelCtxt* model, XWEnv xwe, XP_S16 player, WordNotifierInfo* notifyInfo ); typedef struct _ScoresArray { XP_S16 arr[MAX_NUM_PLAYERS]; } ScoresArray; -void model_figureFinalScores( ModelCtxt* model, ScoresArray* scores, +void model_figureFinalScores( const ModelCtxt* model, ScoresArray* scores, ScoresArray* tilePenalties ); +void model_getCurScores( const ModelCtxt* model, ScoresArray* scores, + XP_Bool gameOver ); + /* figureMoveScore is meant only for the engine's use */ XP_U16 figureMoveScore( const ModelCtxt* model, XWEnv xwe, XP_U16 turn, const MoveInfo* mvInfo, EngineCtxt* engine, diff --git a/xwords4/common/mscore.c b/xwords4/common/mscore.c index 23e2d0c8b..04f4901f8 100644 --- a/xwords4/common/mscore.c +++ b/xwords4/common/mscore.c @@ -145,7 +145,7 @@ getCurrentMoveScoreIfLegal( ModelCtxt* model, XWEnv xwe, XP_S16 turn, } /* getCurrentMoveScoreIfLegal */ XP_S16 -model_getPlayerScore( ModelCtxt* model, XP_S16 player ) +model_getPlayerScore( const ModelCtxt* model, XP_S16 player ) { return model->players[player].score; } /* model_getPlayerScore */ @@ -155,7 +155,7 @@ model_getPlayerScore( ModelCtxt* model, XP_S16 player ) * player. */ void -model_figureFinalScores( ModelCtxt* model, ScoresArray* finalScoresP, +model_figureFinalScores( const ModelCtxt* model, ScoresArray* finalScoresP, ScoresArray* tilePenaltiesP ) { XP_S16 ii, jj; @@ -164,7 +164,7 @@ model_figureFinalScores( ModelCtxt* model, ScoresArray* finalScoresP, XP_U16 nPlayers = model->nPlayers; XP_S16 firstDoneIndex = -1; /* not set unless FIRST_DONE_BONUS is set */ const TrayTileSet* tray; - PlayerCtxt* player; + const PlayerCtxt* player; const DictionaryCtxt* dict = model_getDictionary( model ); CurGameInfo* gi = model->vol.gi; @@ -217,6 +217,20 @@ model_figureFinalScores( ModelCtxt* model, ScoresArray* finalScoresP, } } /* model_figureFinalScores */ +void +model_getCurScores( const ModelCtxt* model, ScoresArray* scores, + XP_Bool gameOver ) +{ + if ( gameOver ) { + model_figureFinalScores( model, scores, NULL ); + } else { + int nPlayers = model->vol.gi->nPlayers; + for ( int ii = 0; ii < nPlayers; ++ii ) { + scores->arr[ii] = model_getPlayerScore( model, ii ); + } + } +} + typedef struct _BlockCheckState { ModelCtxt* model; XWStreamCtxt* stream; diff --git a/xwords4/common/nlityp.h b/xwords4/common/nlityp.h index 9635feac8..426a3ab37 100644 --- a/xwords4/common/nlityp.h +++ b/xwords4/common/nlityp.h @@ -33,8 +33,8 @@ typedef enum {OSType_NONE, OSType_LINUX, OSType_ANDROID, } XP_OSType; typedef struct _NetLaunchInfo { XP_U16 _conTypes; - XP_UCHAR gameName[MAX_GAME_NAME_LEN]; - XP_UCHAR dict[MAX_DICT_NAME_LEN]; + XP_UCHAR gameName[MAX_GAME_NAME_LEN+1]; + XP_UCHAR dict[MAX_DICT_NAME_LEN+1]; XP_UCHAR isoCodeStr[MAX_ISO_CODE_LEN+1]; XP_U8 forceChannel; XP_U8 nPlayersT; @@ -42,6 +42,9 @@ typedef struct _NetLaunchInfo { XP_Bool remotesAreRobots; XP_Bool inDuplicateMode; + XP_U32 gameID; + XP_UCHAR inviteID[32]; /* still used? */ + /* Relay */ XP_UCHAR room[MAX_INVITE_LEN + 1]; XP_U32 devID; /* not used on android; remove?? */ @@ -53,12 +56,9 @@ typedef struct _NetLaunchInfo { // SMS XP_UCHAR phone[32]; XP_Bool isGSM; - XP_OSType osType; + XP_OSType osType; /* used? */ XP_U32 osVers; - XP_U32 gameID; - XP_UCHAR inviteID[32]; - /* MQTT */ XP_UCHAR mqttDevID[17]; } NetLaunchInfo; diff --git a/xwords4/common/scorebdp.c b/xwords4/common/scorebdp.c index 0a88e8bae..1059b02f8 100644 --- a/xwords4/common/scorebdp.c +++ b/xwords4/common/scorebdp.c @@ -200,16 +200,10 @@ drawScoreBoard( BoardCtxt* board, XWEnv xwe ) #endif /* Get the scores from the model or by calculating them based on the end-of-game state. */ - if ( board->gameOver ) { - model_figureFinalScores( model, &scores, NULL ); - } else { - for ( ii = 0; ii < nPlayers; ++ii ) { - scores.arr[ii] = model_getPlayerScore( model, ii ); - } - } + model_getCurScores( model, &scores, board->gameOver ); - if ( draw_scoreBegin( board->draw, xwe, &board->scoreBdBounds, nPlayers, - scores.arr, nTilesInPool, + if ( draw_scoreBegin( board->draw, xwe, &board->scoreBdBounds, + nPlayers, scores.arr, nTilesInPool, dfsFor( board, OBJ_SCORE ) ) ) { XP_U16 totalDim = 0; /* not counting rem */ XP_U16 gotPct; diff --git a/xwords4/common/server.c b/xwords4/common/server.c index a763904e9..4e286c801 100644 --- a/xwords4/common/server.c +++ b/xwords4/common/server.c @@ -117,7 +117,11 @@ typedef struct ServerVolatiles { XP_Bool pickTilesCalled[MAX_NUM_PLAYERS]; } ServerVolatiles; -typedef struct ServerNonvolatiles { +#define MASK_IS_FROM_REMATCH (1<<0) +#define MASK_HAVE_RIP_INFO (1<<1) + +typedef struct _ServerNonvolatiles { + XP_U32 flags; /* */ XP_U32 lastMoveTime; /* seconds of last turn change */ XP_S32 dupTimerExpires; XP_U8 nDevices; @@ -145,8 +149,14 @@ typedef struct ServerNonvolatiles { XWStreamCtxt* prevWordsStream; /* On guests only, stores addresses of other clients for rematch use*/ - XP_U16 rematchAddrsLen; - XP_U8* rematchAddrs; + struct { + /* clients store this */ + XP_U16 addrsLen; + XP_U8* addrs; + + /* rematch-created hosts store this */ + RematchInfo* order; /* rematched host only */ + } rematch; XP_Bool dupTurnsMade[MAX_NUM_PLAYERS]; XP_Bool dupTurnsForced[MAX_NUM_PLAYERS]; @@ -171,6 +181,21 @@ struct ServerCtxt { MPSLOT }; +/* RematchInfo: used to store remote addresses and the order within an + eventual game where players coming from those addresses are meant to + live. Not all addresses (esp. the local host's) are meant to be here. + Local players' indices are -1 +*/ + +struct RematchInfo { + XP_U16 nPlayers; /* how many of users array are there */ + XP_S8 addrIndices[MAX_NUM_PLAYERS]; /* indices into addrs */ + XP_U16 nAddrs; /* needn't be serialized; may not be needed */ + CommsAddrRec addrs[MAX_NUM_PLAYERS]; +}; + +#define RIP_LOCAL_INDX -1 + #ifdef XWFEATURE_SLOW_ROBOT # define ROBOTWAITING(s) (s)->robotWaiting #else @@ -240,6 +265,25 @@ static void writeProto( const ServerCtxt* server, XWStreamCtxt* stream, XW_Proto proto ); static void readGuestAddrs( ServerCtxt* server, XWStreamCtxt* stream ); +static void ri_fromStream( RematchInfo* rip, XWStreamCtxt* stream, + const ServerCtxt* server ); +static void ri_toStream( XWStreamCtxt* stream, const RematchInfo* rip, + const ServerCtxt* server ); +static void ri_addAddrAt( RematchInfo* rip, const ServerCtxt* server, + const CommsAddrRec* addr, XP_U16 playerIndex ); +static void ri_addHostAddrs( RematchInfo* rip, const ServerCtxt* server ); +static void ri_addLocal( RematchInfo* rip ); + +#ifdef DEBUG +static void assertRI( const RematchInfo* rip, const CurGameInfo* gi ); +static void log_ri( const ServerCtxt* server, const RematchInfo* rip, + const char* caller, int line ); +# define LOG_RI(RIP) log_ri(server, (RIP), __func__, __LINE__ ) +#else +# define LOG_RI(rip) +# define assertRI(r, s) +#endif + #endif #define PICK_NEXT -1 @@ -306,14 +350,13 @@ inDuplicateMode( const ServerCtxt* server ) static void syncPlayers( ServerCtxt* server ) { - XP_U16 ii; - CurGameInfo* gi = server->vol.gi; - LocalPlayer* lp = gi->players; - ServerPlayer* player = server->srvPlyrs; - for ( ii = 0; ii < gi->nPlayers; ++ii, ++lp, ++player ) { + const CurGameInfo* gi = server->vol.gi; + for ( int ii = 0; ii < gi->nPlayers; ++ii ) { + const LocalPlayer* lp = &gi->players[ii]; if ( !lp->isLocal/* && !lp->name */ ) { ++server->nv.pendingRegistrations; } + ServerPlayer* player = &server->srvPlyrs[ii]; player->deviceIndex = lp->isLocal? HOST_DEVICE : UNKNOWN_DEVICE; } } @@ -384,6 +427,11 @@ getNV( XWStreamCtxt* stream, ServerNonvolatiles* nv, XP_U16 nPlayers ) XP_U16 ii; XP_U16 version = stream_getVersion( stream ); + XP_ASSERT( 0 == nv->flags ); + if ( STREAM_VERS_REMATCHORDER <= version ) { + nv->flags = stream_getU32VL( stream ); + } + if ( STREAM_VERS_DICTNAME <= version ) { nv->lastMoveTime = stream_getU32( stream ); } @@ -441,6 +489,7 @@ getNV( XWStreamCtxt* stream, ServerNonvolatiles* nv, XP_U16 nPlayers ) static void putNV( XWStreamCtxt* stream, const ServerNonvolatiles* nv, XP_U16 nPlayers ) { + stream_putU32VL( stream, nv->flags ); stream_putU32( stream, nv->lastMoveTime ); stream_putU32( stream, nv->dupTimerExpires ); @@ -551,16 +600,15 @@ server_makeFromStream( MPFORMAL XWEnv xwe, XWStreamCtxt* stream, ModelCtxt* mode { ServerCtxt* server; XP_U16 version = stream_getVersion( stream ); - short ii; - server = server_make( MPPARM(mpool) xwe, model, comms, util ); /* BAE */ + server = server_make( MPPARM(mpool) xwe, model, comms, util ); getNV( stream, &server->nv, nPlayers ); if ( stream_getBits(stream, 1) != 0 ) { server->pool = pool_makeFromStream( MPPARM(mpool) stream ); } - for ( ii = 0; ii < nPlayers; ++ii ) { + for ( int ii = 0; ii < nPlayers; ++ii ) { ServerPlayer* player = &server->srvPlyrs[ii]; player->deviceIndex = stream_getU8( stream ); @@ -591,6 +639,12 @@ server_makeFromStream( MPFORMAL XWEnv xwe, XWStreamCtxt* stream, ModelCtxt* mode readGuestAddrs( server, stream ); } + if ( 0 != (server->nv.flags & MASK_HAVE_RIP_INFO) ) { + struct RematchInfo ri; + ri_fromStream( &ri, stream, server ); + server_setRematchOrder( server, &ri ); + } + /* Hack alert: recovering from an apparent bug that leaves the game thinking it's a client but being in the host-only XWSTATE_BEGIN state. */ @@ -634,9 +688,14 @@ server_writeToStream( const ServerCtxt* server, XWStreamCtxt* stream ) if ( server->vol.gi->serverRole == SERVER_ISCLIENT && 2 < nPlayers ) { - XP_U16 len = server->nv.rematchAddrsLen; + XP_U16 len = server->nv.rematch.addrsLen; stream_putU32VL( stream, len ); - stream_putBytes( stream, server->nv.rematchAddrs, len ); + stream_putBytes( stream, server->nv.rematch.addrs, len ); + } + + if ( 0 != (server->nv.flags & MASK_HAVE_RIP_INFO) ) { + XP_ASSERT( !!server->nv.rematch.order ); + ri_toStream( stream, server->nv.rematch.order, server ); } } /* server_writeToStream */ @@ -680,7 +739,8 @@ cleanupServer( ServerCtxt* server ) stream_destroy( server->nv.prevWordsStream ); } - XP_FREEP( server->mpool, &server->nv.rematchAddrs ); + XP_FREEP( server->mpool, &server->nv.rematch.addrs ); + XP_FREEP( server->mpool, &server->nv.rematch.order ); XP_MEMSET( &server->nv, 0, sizeof(server->nv) ); } /* cleanupServer */ @@ -801,32 +861,93 @@ readMQTTDevID( ServerCtxt* server, XWStreamCtxt* stream ) } } +/* Build a RematchInfo from the perspective of the guest we're sending it to, + with the addresses of all the devices not it. Rather than include my + address, which the guest knows already, add an empty address as a + placeholder. Guest will replace it if needed. */ +static void +buildGuestRI( const ServerCtxt* server, XP_U16 guestIndex, RematchInfo* rip ) +{ + XP_MEMSET( rip, 0, sizeof(*rip) ); + + const CurGameInfo* gi = server->vol.gi; + for ( int ii = 0; ii < gi->nPlayers; ++ii ) { + const LocalPlayer* lp = &gi->players[ii]; + if ( lp->isLocal ) { /* that's me, the host */ + CommsAddrRec addr = {0}; + ri_addAddrAt( rip, server, &addr, ii ); + } else { + XP_S8 deviceIndex = server->srvPlyrs[ii].deviceIndex; + if ( guestIndex == deviceIndex ) { + ri_addLocal( rip ); + } else { + XP_PlayerAddr channelNo + = server->nv.addresses[deviceIndex].channelNo; + CommsAddrRec addr; + comms_getChannelAddr( server->vol.comms, channelNo, &addr ); + ri_addAddrAt( rip, server, &addr, ii ); + } + } + } + LOG_RI(rip); +} + +static void +loadRemoteRI( const ServerCtxt* server, const CurGameInfo* gi, RematchInfo* rip ) +{ + XWStreamCtxt* tmpStream = mkServerStream( server ); + stream_setVersion( tmpStream, server->nv.streamVersion ); + stream_putBytes( tmpStream, server->nv.rematch.addrs, server->nv.rematch.addrsLen ); + + ri_fromStream( rip, tmpStream, server ); + stream_destroy( tmpStream ); + + /* Now find the unaddressed host and add its address */ + XP_ASSERT( rip->nPlayers == gi->nPlayers ); + + ri_addHostAddrs( rip, server ); + + LOG_RI( rip ); +} + static void addGuestAddrsIf( const ServerCtxt* server, XP_U16 sendee, XWStreamCtxt* stream ) { XP_LOGFF("(sendee: %d)", sendee ); XP_ASSERT( amHost( server ) ); - if ( STREAM_VERS_REMATCHADDRS <= stream_getVersion(stream) + XP_U16 version = stream_getVersion( stream ); + if ( STREAM_VERS_REMATCHADDRS <= version /* Not needed for two-device games */ - && 2 < server->nv.nDevices - /* no two-player devices? */ - && server->nv.nDevices == server->vol.gi->nPlayers ) { + && 2 < server->nv.nDevices ) { XWStreamCtxt* tmpStream = mkServerStream( server ); - stream_setVersion( tmpStream, stream_getVersion( stream ) ); + stream_setVersion( tmpStream, version ); + XP_Bool skipIt = XP_FALSE; - for ( XP_U16 devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) { - if ( devIndex == sendee ) { - continue; + if ( STREAM_VERS_REMATCHORDER <= version ) { + RematchInfo ri; + buildGuestRI( server, sendee, &ri ); + ri_toStream( tmpStream, &ri, server ); + + /* Old verion requires no two-player devices */ + } else if ( server->nv.nDevices == server->vol.gi->nPlayers ) { + for ( XP_U16 devIndex = 1; devIndex < server->nv.nDevices; ++devIndex ) { + if ( devIndex == sendee ) { + continue; + } + XP_PlayerAddr channelNo + = server->nv.addresses[devIndex].channelNo; + CommsAddrRec addr; + comms_getChannelAddr( server->vol.comms, channelNo, &addr ); + addrToStream( tmpStream, &addr ); } - XP_PlayerAddr channelNo - = server->nv.addresses[devIndex].channelNo; - CommsAddrRec addr; - comms_getChannelAddr( server->vol.comms, channelNo, &addr ); - addrToStream( tmpStream, &addr ); + } else { + skipIt = XP_TRUE; + } + if ( !skipIt ) { + XP_U16 len = stream_getSize( tmpStream ); + stream_putU32VL( stream, len ); + stream_putBytes( stream, stream_getPtr(tmpStream), len ); } - XP_U16 len = stream_getSize( tmpStream ); - stream_putU32VL( stream, len ); - stream_putBytes( stream, stream_getPtr(tmpStream), len ); stream_destroy( tmpStream ); } } @@ -837,23 +958,24 @@ readGuestAddrs( ServerCtxt* server, XWStreamCtxt* stream ) XP_U16 version = stream_getVersion( stream ); XP_LOGFF( "version: %X", version ); if ( STREAM_VERS_REMATCHADDRS <= version && 0 < stream_getSize(stream) ) { - XP_U16 len = server->nv.rematchAddrsLen = stream_getU32VL( stream ); - XP_LOGFF( "rematchAddrsLen: %d", server->nv.rematchAddrsLen ); + XP_U16 len = server->nv.rematch.addrsLen = stream_getU32VL( stream ); + XP_LOGFF( "rematch.addrsLen: %d", server->nv.rematch.addrsLen ); if ( 0 < len ) { - XP_ASSERT( !server->nv.rematchAddrs ); - server->nv.rematchAddrs = XP_MALLOC( server->mpool, len ); - stream_getBytes( stream, server->nv.rematchAddrs, len ); - XP_LOGFF( "loaded %d bytes of rematchAddrs", len ); + XP_ASSERT( !server->nv.rematch.addrs ); + server->nv.rematch.addrs = XP_MALLOC( server->mpool, len ); + stream_getBytes( stream, server->nv.rematch.addrs, len ); + XP_LOGFF( "loaded %d bytes of rematch.addrs", len ); } #ifdef DEBUG XWStreamCtxt* tmpStream = mkServerStream( server ); stream_setVersion( tmpStream, stream_getVersion( stream ) ); - stream_putBytes( tmpStream, server->nv.rematchAddrs, server->nv.rematchAddrsLen ); + stream_putBytes( tmpStream, server->nv.rematch.addrs, + server->nv.rematch.addrsLen ); while ( 0 < stream_getSize(tmpStream) ) { CommsAddrRec addr = {0}; addrFromStream( &addr, tmpStream ); XP_LOGFF( "got an address" ); - logAddr( MPPARM(server->mpool) server->vol.dutil, &addr, __func__ ); + logAddr( server->vol.dutil, &addr, __func__ ); } stream_destroy( tmpStream ); #endif @@ -1098,6 +1220,9 @@ handleRegistrationMsg( ServerCtxt* server, XWEnv xwe, XWStreamCtxt* stream ) setStreamVersion( server ); checkResizeBoard( server ); (void)assignTilesToAll( server, xwe ); + /* We won't need this any more */ + XP_FREEP( server->mpool, &server->nv.rematch.order ); + server->nv.flags &= ~MASK_HAVE_RIP_INFO; SETSTATE( server, XWSTATE_RECEIVED_ALL_REG ); } informMissing( server, xwe ); @@ -1892,13 +2017,12 @@ getIndexForStream( const ServerCtxt* server, const XWStreamCtxt* stream ) static XP_S8 getIndexForDevice( const ServerCtxt* server, XP_PlayerAddr channelNo ) { - short ii; XP_S8 result = -1; - for ( ii = 0; ii < server->nv.nDevices; ++ii ) { + for ( int ii = 0; ii < server->nv.nDevices; ++ii ) { const RemoteAddress* addr = &server->nv.addresses[ii]; if ( addr->channelNo == channelNo ) { - result = ii; + result = (XP_S8)ii; break; } } @@ -1907,54 +2031,91 @@ getIndexForDevice( const ServerCtxt* server, XP_PlayerAddr channelNo ) return result; } /* getIndexForDevice */ -static LocalPlayer* -findFirstPending( ServerCtxt* server, ServerPlayer** playerP ) +static XP_Bool +findFirstPending( ServerCtxt* server, ServerPlayer** spp, + LocalPlayer** lpp ) { - LocalPlayer* lp; + /* We want to find the local player and srvPlyrs slot for this + connection. There's a srvPlyrs slot for each client that will + register. For each we find in use, skip a non-local slot in the gi. */ CurGameInfo* gi = server->vol.gi; - XP_U16 nPlayers = gi->nPlayers; - XP_U16 nPending = server->nv.pendingRegistrations; - - XP_ASSERT( nPlayers > 0 ); - lp = gi->players + nPlayers; - - while ( --lp >= gi->players ) { - --nPlayers; + XP_Bool success = XP_FALSE; + for ( int ii = 0; !success && ii < gi->nPlayers; ++ii ) { + LocalPlayer* lp = &gi->players[ii]; if ( !lp->isLocal ) { - if ( --nPending == 0 ) { - break; + ServerPlayer* sp = &server->srvPlyrs[ii]; + XP_ASSERT( HOST_DEVICE != sp->deviceIndex ); + if ( UNKNOWN_DEVICE == sp->deviceIndex ) { + success = XP_TRUE; + *lpp = lp; + *spp = sp; } } } - if ( lp < gi->players ) { /* did we find a slot? */ - XP_LOGFF( "no slot found for player; duplicate packet?" ); - lp = NULL; - } else { - *playerP = server->srvPlyrs + nPlayers; - } - return lp; + return success; } /* findFirstPending */ +static XP_Bool +findOrderedSlot( ServerCtxt* server, XWStreamCtxt* stream, + ServerPlayer** spp, LocalPlayer** lpp ) +{ + LOG_FUNC(); + CurGameInfo* gi = server->vol.gi; + XP_PlayerAddr channelNo = stream_getAddress( stream ); + CommsAddrRec guestAddr; + const CommsCtxt* comms = server->vol.comms; + comms_getChannelAddr( comms, channelNo, &guestAddr ); + + const RematchInfo* rip = server->nv.rematch.order; + LOG_RI(rip); + + XP_Bool success = XP_FALSE; + + /* We have an incoming player with an address. We want to find the first + open slot (a srvPlyrs entry where deviceIndex is -1) where the + corresponding entry in the RematchInfo points to the same address. + */ + + for ( int ii = 0; !success && ii < gi->nPlayers; ++ii ) { + ServerPlayer* sp = &server->srvPlyrs[ii]; + if ( UNKNOWN_DEVICE == sp->deviceIndex ) { + int addrIndx = rip->addrIndices[ii]; + if ( comms_addrsAreSame( comms, &guestAddr, &rip->addrs[addrIndx] ) ) { + *spp = sp; + *lpp = &gi->players[ii]; + XP_ASSERT( !(*lpp)->isLocal ); + success = XP_TRUE; + } + } + } + + LOG_RETURNF( "%s", boolToStr(success) ); + return success; +} + static XP_S8 registerRemotePlayer( ServerCtxt* server, XWEnv xwe, XWStreamCtxt* stream ) { XP_S8 deviceIndex = -1; - XP_PlayerAddr channelNo; - XP_U16 nameLen; - LocalPlayer* lp; - ServerPlayer* player = (ServerPlayer*)NULL; /* The player must already be there with a null name, or it's an error. Take the first empty slot. */ XP_ASSERT( server->nv.pendingRegistrations > 0 ); /* find the slot to use */ - lp = findFirstPending( server, &player ); - if ( NULL != lp ) { + ServerPlayer* sp; + LocalPlayer* lp; + XP_Bool success; + if ( server_isFromRematch( server ) ) { + success = findOrderedSlot( server, stream, &sp, &lp ); + } else { + success = findFirstPending( server, &sp, &lp ); + } + if ( success ) { /* get data from stream */ lp->robotIQ = 1 == stream_getBits( stream, 1 )? 1 : 0; - nameLen = stream_getBits( stream, NAME_LEN_NBITS ); + XP_U16 nameLen = stream_getBits( stream, NAME_LEN_NBITS ); XP_UCHAR name[nameLen + 1]; stream_getBytes( stream, name, nameLen ); name[nameLen] = '\0'; @@ -1962,7 +2123,7 @@ registerRemotePlayer( ServerCtxt* server, XWEnv xwe, XWStreamCtxt* stream ) replaceStringIfDifferent( server->mpool, &lp->name, name ); - channelNo = stream_getAddress( stream ); + XP_PlayerAddr channelNo = stream_getAddress( stream ); deviceIndex = getIndexForDevice( server, channelNo ); --server->nv.pendingRegistrations; @@ -1983,7 +2144,7 @@ registerRemotePlayer( ServerCtxt* server, XWEnv xwe, XWStreamCtxt* stream ) XP_LOGFF( "deviceIndex already set" ); } - player->deviceIndex = deviceIndex; + sp->deviceIndex = deviceIndex; informMissing( server, xwe ); } @@ -2016,8 +2177,8 @@ client_readInitialMessage( ServerCtxt* server, XWEnv xwe, XWStreamCtxt* stream ) if ( accepted ) { ModelCtxt* model = server->vol.model; CommsCtxt* comms = server->vol.comms; - CurGameInfo* gi = server->vol.gi; - XP_U32 gameID; + CurGameInfo* gi = server->vol.gi; /* we'll overwrite this */ + XP_U32 gameID = 0; PoolContext* pool; #ifdef STREAM_VERS_BIGBOARD XP_UCHAR rmtDictName[128]; @@ -2033,16 +2194,23 @@ client_readInitialMessage( ServerCtxt* server, XWEnv xwe, XWStreamCtxt* stream ) } // XP_ASSERT( streamVersion <= CUR_STREAM_VERS ); /* else do what? */ - gameID = stream_getU32( stream ); - XP_LOGFF( "read gameID of %x/%d; calling comms_setConnID (replacing %d)", - gameID, gameID, server->vol.gi->gameID ); - server->vol.gi->gameID = gameID; - comms_setConnID( comms, gameID, streamVersion ); - + /* Get rid of this!!!! It's in the damned gi that's read next */ + gameID = streamVersion < STREAM_VERS_REMATCHORDER + ? stream_getU32( stream ) : 0; CurGameInfo localGI = {0}; gi_readFromStream( MPPARM(server->mpool) stream, &localGI ); + XP_ASSERT( gameID == 0 || gameID == localGI.gameID ); + gameID = localGI.gameID; localGI.serverRole = SERVER_ISCLIENT; + /* never seems to replace anything -- gi is already correct on guests + apparently. How? Will have come in with invitation, of course. */ + XP_LOGFF( "read gameID of %X/%d; calling comms_setConnID (replacing %X)", + gameID, gameID, server->vol.gi->gameID ); + XP_ASSERT( server->vol.gi->gameID == gameID ); + server->vol.gi->gameID = gameID; + comms_setConnID( comms, gameID, streamVersion ); + XP_ASSERT( !localGI.dictName ); localGI.dictName = copyString( server->mpool, gi->dictName ); gi_copy( MPPARM(server->mpool) gi, &localGI ); @@ -2151,18 +2319,13 @@ static void makeSendableGICopy( ServerCtxt* server, CurGameInfo* giCopy, XP_U16 deviceIndex ) { - XP_U16 nPlayers; - LocalPlayer* clientPl; - XP_U16 ii; - XP_MEMCPY( giCopy, server->vol.gi, sizeof(*giCopy) ); - nPlayers = giCopy->nPlayers; + for ( int ii = 0; ii < giCopy->nPlayers; ++ii ) { + LocalPlayer* lp = &giCopy->players[ii]; - for ( clientPl = giCopy->players, ii = 0; - ii < nPlayers; ++clientPl, ++ii ) { /* adjust isLocal to client's perspective */ - clientPl->isLocal = server->srvPlyrs[ii].deviceIndex == deviceIndex; + lp->isLocal = server->srvPlyrs[ii].deviceIndex == deviceIndex; } giCopy->forceChannel = deviceIndex; @@ -2170,6 +2333,7 @@ makeSendableGICopy( ServerCtxt* server, CurGameInfo* giCopy, giCopy->forceChannel ); giCopy->dictName = (XP_UCHAR*)NULL; /* so we don't sent the bytes */ + LOGGI( giCopy, "after" ); } /* makeSendableGICopy */ static void @@ -2196,8 +2360,9 @@ sendInitialMessage( ServerCtxt* server, XWEnv xwe ) stream_putU8( stream, CUR_STREAM_VERS ); #endif - XP_LOGFF( "putting gameID %x into msg", gameID ); - stream_putU32( stream, gameID ); + if ( streamVersion < STREAM_VERS_REMATCHORDER ) { + stream_putU32( stream, gameID ); + } CurGameInfo localGI; makeSendableGICopy( server, &localGI, deviceIndex ); @@ -2306,6 +2471,23 @@ codeToStr( XW_Proto code ) return str; } /* codeToStr */ +const XP_UCHAR* +RO2Str( RematchOrder ro ) +{ + const char* str = (char*)NULL; + switch( ro ) { + caseStr(RO_SAME); + caseStr(RO_LOW_SCORE_FIRST); + caseStr(RO_HIGH_SCORE_FIRST); + caseStr(RO_JUGGLE); +#ifdef XWFEATURE_RO_BYNAME + caseStr(RO_BY_NAME); +#endif + caseStr(RO_NUM_ROS); /* should never print!!! */ + } + return str; +} + #define PRINTCODE( intro, code ) \ XP_STATUSF( "\t%s(): %s for %s", __func__, intro, codeToStr(code) ) @@ -3975,12 +4157,13 @@ server_getOpenChannel( const ServerCtxt* server, XP_U16* channel ) } XP_Bool -server_canRematch( const ServerCtxt* server ) +server_canRematch( const ServerCtxt* server, XP_Bool* canOrderP ) { /* XP_LOGFF( "nDevices: %d; nPlayers: %d", */ /* server->nv.nDevices, server->vol.gi->nPlayers ); */ const CurGameInfo* gi = server->vol.gi; XP_Bool result; + XP_Bool canOrder = XP_TRUE; switch ( gi->serverRole ) { case SERVER_STANDALONE: result = XP_TRUE; /* can always rematch a local game */ @@ -3991,24 +4174,196 @@ server_canRematch( const ServerCtxt* server ) && server->nv.nDevices == server->vol.gi->nPlayers; break; case SERVER_ISCLIENT: - result = 2 == gi->nPlayers - ? XP_TRUE /* We always have server address */ - : 0 < server->nv.rematchAddrsLen; + if ( 2 == gi->nPlayers ) { + result = XP_TRUE; + } else { + result = 0 < server->nv.rematch.addrsLen; + canOrder = STREAM_VERS_REMATCHORDER <= server->nv.streamVersion; + } break; } + + if ( !!canOrderP ) { + *canOrderP = canOrder; + } + /* LOG_RETURNF( "%s", boolToStr(result) ); */ return result; } +/* Modify the RematchInfo data to be consistent with the order we'll enforce + as invitees join the new game. + */ +static void +sortBySame( const ServerCtxt* server, int newOrder[] ) +{ + const CurGameInfo* gi = server->vol.gi; + for ( int ii = 0; ii < gi->nPlayers; ++ii ) { + newOrder[ii] = ii; + } +} + +static void +sortByScoreLow( const ServerCtxt* server, int newOrder[] ) +{ + const CurGameInfo* gi = server->vol.gi; + + ScoresArray scores = {0}; + model_getCurScores( server->vol.model, &scores, server_getGameIsOver(server) ); + + int mask = 0; /* mark values already consumed */ + for ( int resultIndx = 0; resultIndx < gi->nPlayers; ++resultIndx ) { + int lowest = 10000; + int newPosn = -1; + for ( int jj = 0; jj < gi->nPlayers; ++jj ) { + if ( 0 != (mask & (1 << jj)) ) { + continue; + } else if ( scores.arr[jj] < lowest ) { + lowest = scores.arr[jj]; + newPosn = jj; + } + } + if ( newPosn == -1 ) { + break; + } else { + mask |= 1 << newPosn; + newOrder[resultIndx] = newPosn; + /* XP_LOGFF( "result[%d] = %d (for score %d)", resultIndx, newPosn, */ + /* lowest ); */ + } + } +} + +static void +sortByScoreHigh( const ServerCtxt* server, int newOrder[] ) +{ + sortByScoreLow( server, newOrder ); + + const CurGameInfo* gi = server->vol.gi; + for ( int ii = 0, jj = gi->nPlayers - 1; ii < jj; ++ii, --jj ) { + int tmp = newOrder[ii]; + newOrder[ii] = newOrder[jj]; + newOrder[jj] = tmp; + } +} + +static void +sortByRandom( const ServerCtxt* server, int newOrder[] ) +{ + const CurGameInfo* gi = server->vol.gi; + int src[gi->nPlayers]; + for ( int ii = 0; ii < gi->nPlayers; ++ii ) { + src[ii] = ii; + } + for ( int ii = 0; ii < gi->nPlayers; ++ii ) { + int nLeft = gi->nPlayers - ii; + int indx = XP_RANDOM() % nLeft; + newOrder[ii] = src[indx]; + XP_LOGFF( "set result[%d] to %d", ii, newOrder[ii] ); + /* now swap the last down */ + src[indx] = src[nLeft-1]; + } +} + +#ifdef XWFEATURE_RO_BYNAME +static void +sortByName( const ServerCtxt* server, int newOrder[] ) +{ + const CurGameInfo* gi = server->vol.gi; + int mask = 0; /* mark values already consumed */ + for ( int ii = 0; ii < gi->nPlayers; ++ii ) { + /* find the lowest not already used */ + int lowest = -1; + for ( int jj = 0; jj < gi->nPlayers; ++jj ) { + if ( 0 != (mask & (1 << jj)) ) { + continue; + } else if ( lowest == -1 ) { + lowest = jj; + } else if ( 0 < XP_STRCMP( gi->players[lowest].name, + gi->players[jj].name ) ) { + lowest = jj; + } + } + XP_ASSERT( lowest != -1 ); + mask |= 1 << lowest; + newOrder[ii] = lowest; + } +} +#endif + +typedef void (*OrderProc)(const ServerCtxt* server, int newOrder[]); + +static XP_Bool +setPlayerOrder( const ServerCtxt* server, RematchOrder ro, + CurGameInfo* gi, RematchInfo* rip ) +{ + // XP_LOGFF( "(ro=%s)", RO2Str(ri->ro) ); + LOGGI( gi, "start" ); + OrderProc proc = NULL; + switch ( ro ) { + case RO_SAME: + proc = sortBySame; + // sortBySame( server, newOrder ); + break; + case RO_LOW_SCORE_FIRST: + proc = sortByScoreLow; + break; + case RO_HIGH_SCORE_FIRST: + proc = sortByScoreHigh; + break; + case RO_JUGGLE: + proc = sortByRandom; + break; +#ifdef XWFEATURE_RO_BYNAME + case RO_BY_NAME: + proc = sortByName; + break; +#endif + case RO_NUM_ROS: + default: + XP_ASSERT(0); break; + } + + XP_ASSERT( !!proc ); + int newOrder[gi->nPlayers]; + XP_MEMSET( newOrder, 0, sizeof(newOrder) ); + XP_Bool success = !!proc; + if ( success ) { + (*proc)( server, newOrder ); + /* We have gi and rip that express an ordering of players. And we have + a new order into which to move them. Just walk and swap the current + with the right one above it. */ + + CurGameInfo srcGi = *gi; + RematchInfo srcRi; + if ( !!rip ) { + srcRi = *rip; + } + + for ( int ii = 0; ii < gi->nPlayers; ++ii ) { + gi->players[ii] = srcGi.players[newOrder[ii]]; + if ( !!rip ) { + rip->addrIndices[ii] = srcRi.addrIndices[newOrder[ii]]; + } + } + + LOGGI( gi, "end" ); + if ( !!rip ) { + LOG_RI( rip ); + } + } + return success; +} + XP_Bool server_getRematchInfo( const ServerCtxt* server, XW_UtilCtxt* newUtil, - XP_U32 gameID, RematchAddrs* ra ) + XP_U32 gameID, RematchOrder ro, RematchInfo** ripp ) { - LOG_FUNC(); + XP_LOGFF( "(ro=%s)", RO2Str(ro) ); + XP_Bool success = server_canRematch( server, NULL ); const CommsCtxt* comms = server->vol.comms; - XP_Bool success = server_canRematch( server ); if ( success ) { - XP_MEMSET( ra, 0, sizeof(*ra) ); + RematchInfo ri = {0}; CurGameInfo* newGI = newUtil->gameInfo; gi_disposePlayerInfo( MPPARM(newUtil->mpool) newGI ); @@ -4020,49 +4375,306 @@ server_getRematchInfo( const ServerCtxt* server, XW_UtilCtxt* newUtil, } LOGGI( newUtil->gameInfo, "ready to invite" ); - /* Now build the address list */ + /* Now build the address list. Simple cases are STANDALONE, when I'm + the host, or when there are only two devices/players. If I'm guest + and there is another guest, I count on the host having sent rematch + info, and *that* info has an old and a new format. Sheesh. */ + XP_Bool canOrder = XP_TRUE; if ( !comms ) { /* no addressing to do!! */ - } else if ( amHost( server ) ) { - /* skip 0; it's me */ - for ( int ii = 1; ii < server->nv.nDevices; ++ii ) { - XP_PlayerAddr channelNo = server->nv.addresses[ii].channelNo; - comms_getChannelAddr( comms, channelNo, &ra->addrs[ra->nAddrs] ); - ++ra->nAddrs; + } else if ( amHost( server ) || 2 == newGI->nPlayers ) { + for ( int ii = 0; ii < newGI->nPlayers; ++ii ) { + if ( newGI->players[ii].isLocal ) { + ri_addLocal( &ri ); + } else { + CommsAddrRec addr; + if ( amHost(server) ) { + XP_S8 deviceIndex = server->srvPlyrs[ii].deviceIndex; + XP_ASSERT( deviceIndex != RIP_LOCAL_INDX ); + XP_PlayerAddr channelNo = + server->nv.addresses[deviceIndex].channelNo; + comms_getChannelAddr( comms, channelNo, &addr ); + } else { + comms_getHostAddr( comms, &addr ); + } + ri_addAddrAt( &ri, server, &addr, ii ); + } + } + } else if ( !!server->nv.rematch.addrs ) { + XP_U16 streamVersion = server->nv.streamVersion; + if ( STREAM_VERS_REMATCHORDER <= streamVersion ) { + loadRemoteRI( server, newGI, &ri ); + + } else { + /* I don't have complete info yet. So let's go through the gi, + assigning an address to all non-local players. We'll use + the host address first, then the rest we have. If we don't + have the right number of everything, we fail. Note: we + should not have given the user a choice in rematch ordering + here!!!*/ + canOrder = newGI->nPlayers <= 2; + XP_ASSERT( !canOrder ); + canOrder = XP_FALSE; + + CommsAddrRec addrs[MAX_NUM_PLAYERS]; + int nAddrs = 0; + comms_getHostAddr( comms, &addrs[nAddrs++] ); + if ( !!server->nv.rematch.addrs ) { + XWStreamCtxt* stream = mkServerStream( server ); + stream_setVersion( stream, server->nv.streamVersion ); + stream_putBytes( stream, server->nv.rematch.addrs, + server->nv.rematch.addrsLen ); + while ( 0 < stream_getSize( stream ) ) { + XP_ASSERT( nAddrs < VSIZE(addrs) ); + addrFromStream( &addrs[nAddrs++], stream ); + } + stream_destroy( stream ); + } + + int nextRemote = 0; + for ( int ii = 0; success && ii < newGI->nPlayers; ++ii ) { + if ( newGI->players[ii].isLocal ) { + ri_addLocal( &ri ); + } else if ( nextRemote < nAddrs ) { + ri_addAddrAt( &ri, server, &addrs[nextRemote++], ii ); + } else { + XP_LOGFF( "ERROR: not enough addresses for all remote players" ); + success = XP_FALSE; + } + } + if ( success ) { + success = nextRemote == nAddrs-1; + } } } else { - /* first, server's address */ - comms_getHostAddr( comms, &ra->addrs[ra->nAddrs++] ); - /* then, any other guests we've been told about */ - if ( !!server->nv.rematchAddrs ) { - XWStreamCtxt* stream = mkServerStream( server ); - stream_setVersion( stream, server->nv.streamVersion ); - stream_putBytes( stream, server->nv.rematchAddrs, - server->nv.rematchAddrsLen ); - while ( 0 < stream_getSize( stream ) - && ra->nAddrs < VSIZE(ra->addrs) ) { - addrFromStream( &ra->addrs[ra->nAddrs++], stream ); - } - stream_destroy( stream ); + XP_ASSERT( 0 ); /* should not have returned TRUE from server_canRematch(); */ + success = XP_FALSE; + } + + if ( success && canOrder ) { + if ( !!comms ) { + assertRI( &ri, newGI ); + } + success = setPlayerOrder( server, ro, newGI, !!comms ? &ri : NULL ); + } + + if ( success && !!comms ) { + LOG_RI( &ri ); + assertRI( &ri, newGI ); + XP_ASSERT( success ); + *ripp = XP_MALLOC(server->mpool, sizeof(**ripp)); + **ripp = ri; + } else { + *ripp = NULL; + } + XP_ASSERT( success ); + } + + LOG_RETURNF( "%s", boolToStr(success) ); + /* Until I'm testing edge cases, this will fail because I did something + * wrong, and I need to know that immediately. + */ + XP_ASSERT( success ); + return success; +} /* server_getRematchInfo */ + +void +server_disposeRematchInfo( ServerCtxt* server, RematchInfo** ripp ) +{ + XP_LOGFF( "(%p)", *ripp ); + if ( !!*ripp ) { + LOG_RI( *ripp ); + } + XP_FREEP( server->mpool, ripp ); +} + +XP_Bool +server_ri_getAddr( const RematchInfo* rip, XP_U16 nth, + CommsAddrRec* addr, XP_U16* nPlayersH ) +{ + const CommsAddrRec* rec = &rip->addrs[nth]; + XP_Bool success = !addr_isEmpty( rec ); + + if ( success ) { + XP_U16 count = 0; + for ( int ii = 0; ii < rip->nPlayers; ++ii ) { + if ( rip->addrIndices[ii] == nth ) { + ++count; + } + } + success = 0 < count; + if ( success ) { + *nPlayersH = count; + *addr = *rec; + } + } + + return success; +} + +/* Record the desired order, which is already set in the RematchInfo passed + in, so we can enforce it as clients register. */ +void +server_setRematchOrder( ServerCtxt* server, const RematchInfo* rip ) +{ + if ( amHost( server ) ) { /* standalones can call without harm.... */ + XP_ASSERT( !!rip ); + XP_ASSERT( !server->nv.rematch.order ); + server->nv.rematch.order = XP_MALLOC( server->mpool, sizeof(*rip) ); + *server->nv.rematch.order = *rip; + server->nv.flags |= MASK_HAVE_RIP_INFO + MASK_IS_FROM_REMATCH; + } +} + +XP_Bool +server_isFromRematch( const ServerCtxt* server ) +{ + return 0 != (server->nv.flags & MASK_IS_FROM_REMATCH); +} + +#ifdef DEBUG +static void +log_ri( const ServerCtxt* server, const RematchInfo* rip, + const char* caller, int line ) +{ + XP_LOGFF( "called from line %d of %s() with ptr %p", line, caller, rip ); + if ( !!rip ) { + char buf[64] = {0}; + int offset = 0; + int maxIndx = -1; + for ( int ii = 0; ii < rip->nPlayers; ++ii ) { + XP_S8 indx = rip->addrIndices[ii]; + offset += XP_SNPRINTF( buf+offset, VSIZE(buf)-offset, "%d, ", indx ); + if ( indx > maxIndx ) { + maxIndx = indx; + } + } + XP_LOGFF( "%d players (and %d addrs): [%s]", rip->nPlayers, rip->nAddrs, buf ); + + for ( int ii = 0; ii < rip->nAddrs; ++ii ) { + XP_SNPRINTF( buf, VSIZE(buf), "[%d of %d]: %s from %s", + ii, rip->nAddrs, __func__, caller ); + logAddr( server->vol.dutil, &rip->addrs[ii], __func__ ); + } + } +} +#endif + +static void +ri_toStream( XWStreamCtxt* stream, const RematchInfo* rip, + const ServerCtxt* server ) +{ + LOG_RI(rip); + XP_U16 nPlayers = !!rip ? rip->nPlayers : 0; + for ( int ii = 0; ii < nPlayers; ++ii ) { + XP_S8 indx = rip->addrIndices[ii]; + if ( RIP_LOCAL_INDX != indx ) { + stream_putBits( stream, PLAYERNUM_NBITS, indx ); + } + } + + for ( int ii = 0; ii < rip->nAddrs; ++ii ) { + addrToStream( stream, &rip->addrs[ii] ); + } +} + +static void +ri_fromStream( RematchInfo* rip, XWStreamCtxt* stream, + const ServerCtxt* server ) +{ + const CurGameInfo* gi = server->vol.gi; + XP_MEMSET( rip, 0, sizeof(*rip) ); + rip->nPlayers = gi->nPlayers; + + for ( int ii = 0; ii < gi->nPlayers; ++ii ) { + if ( gi->players[ii].isLocal ) { + rip->addrIndices[ii] = RIP_LOCAL_INDX; + } else { + XP_U16 indx = stream_getBits( stream, PLAYERNUM_NBITS ); + rip->addrIndices[ii] = indx; + if ( indx > rip->nAddrs ) { + rip->nAddrs = indx; } } } -#ifdef DEBUG - if ( success ) { - for ( int ii = 0; ii < ra->nAddrs; ++ii ) { - XP_LOGFF( "addr %d of %d: ", ii, ra->nAddrs ); - logAddr( MPPARM(server->mpool) server->vol.dutil, - &ra->addrs[ii], NULL ); + ++rip->nAddrs; /* it's count now, not index */ + for ( int ii = 0; ii < rip->nAddrs; ++ii ) { + addrFromStream( &rip->addrs[ii], stream ); + } + + LOG_RI(rip); + XP_ASSERT( 0 < rip->nPlayers ); +} + +/* Given an address, insert it if it's new, or point to an existing copy + otherwise */ +static void +ri_addAddrAt( RematchInfo* rip, const ServerCtxt* server, + const CommsAddrRec* addr, const XP_U16 player ) +{ + const CommsCtxt* comms = server->vol.comms; + XP_S8 newIndex = RIP_LOCAL_INDX; + for ( int ii = 0; ii < player; ++ii ) { + int index = rip->addrIndices[ii]; + if ( index != RIP_LOCAL_INDX && + comms_addrsAreSame( comms, addr, &rip->addrs[index] ) ) { + newIndex = index; + break; } } -#endif - XP_ASSERT( !success || !comms || server->vol.gi->nPlayers == ra->nAddrs + 1 ); - LOG_RETURNF( "%s", boolToStr(success) ); - return success; + // didn't find it? + if ( RIP_LOCAL_INDX == newIndex ) { + newIndex = rip->nAddrs; + rip->addrs[newIndex] = *addr; + ++rip->nAddrs; + } + + rip->addrIndices[player] = newIndex; + XP_ASSERT( rip->nPlayers == player ); + ++rip->nPlayers; } +static void +ri_addHostAddrs( RematchInfo* rip, const ServerCtxt* server ) +{ + for ( int ii = 0; ii < rip->nAddrs; ++ii ) { + if ( addr_isEmpty( &rip->addrs[ii] ) ) { + comms_getHostAddr( server->vol.comms, &rip->addrs[ii] ); + } + } +} + +static void +ri_addLocal( RematchInfo* rip ) +{ + rip->addrIndices[rip->nPlayers++] = RIP_LOCAL_INDX; +} + +#ifdef DEBUG +static void +assertRI( const RematchInfo* rip, const CurGameInfo* gi ) +{ + /* Local players should not be represented */ + XP_ASSERT( gi && rip ); + XP_ASSERT( gi->nPlayers == rip->nPlayers ); + XP_U16 mask = 0; + for ( int ii = 0; ii < gi->nPlayers; ++ii ) { + XP_Bool isLocal = gi->players[ii].isLocal; + XP_ASSERT( isLocal == (rip->addrIndices[ii] == RIP_LOCAL_INDX) ); + if ( !isLocal ) { + mask |= 1 << rip->addrIndices[ii]; + } + } + XP_ASSERT( countBits(mask) == rip->nAddrs ); + + for ( int ii = 0; ii < rip->nAddrs; ++ii ) { + XP_ASSERT( !addr_isEmpty( &rip->addrs[ii] ) ); + } +} +#endif + XP_U32 server_getLastMoveTime( const ServerCtxt* server ) { diff --git a/xwords4/common/server.h b/xwords4/common/server.h index 2c6bb2325..267a99390 100644 --- a/xwords4/common/server.h +++ b/xwords4/common/server.h @@ -88,7 +88,7 @@ XP_S16 server_getTimerSeconds( const ServerCtxt* server, XWEnv xwe, 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 ); -XP_Bool server_canRematch( const ServerCtxt* server ); +XP_Bool server_canRematch( const ServerCtxt* server, XP_Bool* canOrder ); void server_pause( ServerCtxt* server, XWEnv xwe, XP_S16 turn, const XP_UCHAR* msg ); void server_unpause( ServerCtxt* server, XWEnv xwe, XP_S16 turn, const XP_UCHAR* msg ); @@ -146,18 +146,46 @@ XP_U16 server_figureFinishBonus( const ServerCtxt* server, XP_U16 turn ); XP_Bool server_getIsHost( const ServerCtxt* server ); #endif -typedef struct _RematchAddrs { - CommsAddrRec addrs[MAX_NUM_PLAYERS]; - XP_U16 nAddrs; -} RematchAddrs; +typedef enum { + RO_SAME, /* preserve the parent game's order */ + RO_LOW_SCORE_FIRST, /* lowest scorer in parent goes first, etc */ + RO_HIGH_SCORE_FIRST, /* highest scorer in parent goes first, etc */ + RO_JUGGLE, /* rearrange randomly */ +#ifdef XWFEATURE_RO_BYNAME + RO_BY_NAME, /* alphabetical -- for testing only! :-) */ +#endif + RO_NUM_ROS, +} RematchOrder; + +#ifdef DEBUG +const XP_UCHAR* RO2Str(RematchOrder ro); +#endif + + +/* Info about remote addresses that lets us determine an order for invited + players as they arrive. It stores the addresses of all remote devices, and + for each a mask of which players will come from that address. + + No need for a count: once we find a playersMask == 0 we're done +*/ /* Sets up newUtil->gameInfo correctly, and returns with a set of addresses to which invitation should be sent. But: meant to be called only from game.c anyway. */ +typedef struct RematchInfo RematchInfo; XP_Bool server_getRematchInfo( const ServerCtxt* server, XW_UtilCtxt* newUtil, - XP_U32 gameID, RematchAddrs* ra ); + XP_U32 gameID, RematchOrder ro, RematchInfo** ripp ); +void server_disposeRematchInfo( ServerCtxt* server, RematchInfo** rip ); +XP_Bool server_ri_getAddr( const RematchInfo* ri, XP_U16 nth, + CommsAddrRec* addr, XP_U16* nPlayersH ); +/* Pass in the info the server will need to hang onto until all invitees have + registered, at which point it can set and communicate the player order for + the game. To be called only from game.c! */ +void server_setRematchOrder( ServerCtxt* server, const RematchInfo* ri ); + +XP_Bool server_isFromRematch( const ServerCtxt* server ); #ifdef CPLUS } diff --git a/xwords4/common/strutils.c b/xwords4/common/strutils.c index b8237eb55..045ac0ee9 100644 --- a/xwords4/common/strutils.c +++ b/xwords4/common/strutils.c @@ -518,6 +518,17 @@ randIntArray( XP_U16* rnums, XP_U16 count ) return changed; } /* randIntArray */ +XP_U16 +countBits( XP_U32 mask ) +{ + XP_U16 result = 0; + while ( 0 != mask ) { + ++result; + mask &= mask - 1; + } + return result; +} + #ifdef XWFEATURE_BASE64 /* base-64 encode binary data as a message legal for SMS. See * http://www.ietf.org/rfc/rfc2045.txt for the algorithm. glib uses this and diff --git a/xwords4/common/strutils.h b/xwords4/common/strutils.h index 3e9088f23..4e74b2f07 100644 --- a/xwords4/common/strutils.h +++ b/xwords4/common/strutils.h @@ -122,6 +122,8 @@ XP_UCHAR* emptyStringIfNull( XP_UCHAR* str ); /* Produce an array of ints 0..count-1, juggled */ XP_Bool randIntArray( XP_U16* rnums, XP_U16 count ); +XP_U16 countBits( XP_U32 mask ); + #ifdef XWFEATURE_BASE64 void binToSms( XP_UCHAR* out, XP_U16* outlen, const XP_U8* in, XP_U16 inlen ); XP_Bool smsToBin( XP_U8* out, XP_U16* outlen, const XP_UCHAR* in, XP_U16 inlen ); diff --git a/xwords4/linux/curgamlistwin.c b/xwords4/linux/curgamlistwin.c index eb85352b7..17d12d1cf 100644 --- a/xwords4/linux/curgamlistwin.c +++ b/xwords4/linux/curgamlistwin.c @@ -186,17 +186,6 @@ adjustCurSel( CursGameList* cgl ) cgl_draw( cgl ); } -static int -countBits( int bits ) -{ - int result = 0; - while ( 0 != bits ) { - ++result; - bits &= bits - 1; - } - return result; -} - void cgl_draw( CursGameList* cgl ) { diff --git a/xwords4/linux/cursesboard.c b/xwords4/linux/cursesboard.c index 3d166f092..91c59fb0f 100644 --- a/xwords4/linux/cursesboard.c +++ b/xwords4/linux/cursesboard.c @@ -135,7 +135,8 @@ static void relay_requestJoin_curses( void* closure, const XP_UCHAR* devID, XP_U16 nPlayersTotal, XP_U16 seed, XP_U16 lang ); #endif -static XP_Bool rematch_and_save( CursesBoardGlobals* bGlobals, XP_U32* newGameIDP ); +static XP_Bool rematch_and_save( CursesBoardGlobals* bGlobals, RematchOrder ro, + XP_U32* newGameIDP ); static void disposeBoard( CursesBoardGlobals* bGlobals ); static void initCP( CommonGlobals* cGlobals ); static CursesBoardGlobals* commonInit( CursesBoardState* cbState, @@ -697,16 +698,17 @@ cb_addInvite( CursesBoardState* cbState, XP_U32 gameID, XP_U16 forceChannel, NetLaunchInfo nli; nli_init( &nli, cGlobals->gi, &selfAddr, 1, forceChannel ); - nli.remotesAreRobots = XP_TRUE; comms_invite( comms, NULL_XWE, &nli, destAddr, XP_TRUE ); } XP_Bool -cb_makeRematch( CursesBoardState* cbState, XP_U32 gameID, XP_U32* newGameIDP ) +cb_makeRematch( CursesBoardState* cbState, XP_U32 gameID, RematchOrder ro, + XP_U32* newGameIDP ) { - CursesBoardGlobals* bGlobals = findOrOpenForGameID( cbState, gameID, NULL, NULL ); - XP_Bool success = rematch_and_save( bGlobals, newGameIDP ); + CursesBoardGlobals* bGlobals = findOrOpenForGameID( cbState, gameID, + NULL, NULL ); + XP_Bool success = rematch_and_save( bGlobals, ro, newGameIDP ); return success; } @@ -727,9 +729,11 @@ cb_makeMoveIf( CursesBoardState* cbState, XP_U32 gameID ) XP_FALSE, #endif XP_FALSE, &ignored ); - if ( success ) { - success = board_commitTurn( board, NULL_XWE, XP_TRUE, XP_TRUE, NULL ); + if ( !success ) { + XP_LOGFF( "unable to find hint; so PASSing" ); } + success = board_commitTurn( board, NULL_XWE, XP_TRUE, XP_TRUE, + NULL ); } } LOG_RETURNF( "%s", boolToStr(success) ); @@ -994,7 +998,7 @@ curses_util_informUndo( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe) ) } static void -rematch_and_save_once( CursesBoardGlobals* bGlobals ) +rematch_and_save_once( CursesBoardGlobals* bGlobals, RematchOrder ro ) { LOG_FUNC(); CommonGlobals* cGlobals = &bGlobals->cGlobals; @@ -1006,7 +1010,7 @@ rematch_and_save_once( CursesBoardGlobals* bGlobals ) && 0 != alreadyDone ) { XP_LOGFF( "already rematched game %X", cGlobals->gi->gameID ); } else { - if ( rematch_and_save( bGlobals, NULL ) ) { + if ( rematch_and_save( bGlobals, ro, NULL ) ) { gdb_storeInt( cGlobals->params->pDb, key, 1 ); } } @@ -1014,7 +1018,8 @@ rematch_and_save_once( CursesBoardGlobals* bGlobals ) } static XP_Bool -rematch_and_save( CursesBoardGlobals* bGlobals, XP_U32* newGameIDP ) +rematch_and_save( CursesBoardGlobals* bGlobals, RematchOrder ro, + XP_U32* newGameIDP ) { LOG_FUNC(); CommonGlobals* cGlobals = &bGlobals->cGlobals; @@ -1025,7 +1030,7 @@ rematch_and_save( CursesBoardGlobals* bGlobals, XP_U32* newGameIDP ) XP_Bool success = game_makeRematch( &bGlobals->cGlobals.game, NULL_XWE, bGlobalsNew->cGlobals.util, &cGlobals->cp, &bGlobalsNew->cGlobals.procs, - &bGlobalsNew->cGlobals.game, "newName" ); + &bGlobalsNew->cGlobals.game, "newName", ro ); if ( success ) { if ( !!newGameIDP ) { *newGameIDP = bGlobalsNew->cGlobals.gi->gameID; @@ -1038,13 +1043,13 @@ rematch_and_save( CursesBoardGlobals* bGlobals, XP_U32* newGameIDP ) } static void -curses_util_notifyGameOver( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe), XP_S16 quitter ) +curses_util_notifyGameOver( XW_UtilCtxt* uc, XWEnv xwe, XP_S16 quitter ) { LOG_FUNC(); CursesBoardGlobals* bGlobals = (CursesBoardGlobals*)uc->closure; CommonGlobals* cGlobals = &bGlobals->cGlobals; LaunchParams* params = cGlobals->params; - board_draw( cGlobals->game.board, NULL_XWE ); + board_draw( cGlobals->game.board, xwe ); /* game belongs in cGlobals... */ if ( params->printHistory ) { @@ -1057,14 +1062,15 @@ curses_util_notifyGameOver( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe), XP_S16 quitte sleep( params->quitAfter ); handleQuit( bGlobals->cbState->aGlobals, 0 ); } else if ( params->undoWhenDone ) { - server_handleUndo( cGlobals->game.server, NULL_XWE, 0 ); + server_handleUndo( cGlobals->game.server, xwe, 0 ); } else if ( !params->skipGameOver && !!bGlobals->boardWin ) { /* This is modal. Don't show if quitting */ cursesShowFinalScores( bGlobals ); } if ( params->rematchOnDone ) { - rematch_and_save_once( bGlobals ); + XP_ASSERT( !!params->rematchOrder ); + rematch_and_save_once( bGlobals, roFromStr(params->rematchOrder) ); } } /* curses_util_notifyGameOver */ diff --git a/xwords4/linux/cursesboard.h b/xwords4/linux/cursesboard.h index 4c630191d..c82f4e46f 100644 --- a/xwords4/linux/cursesboard.h +++ b/xwords4/linux/cursesboard.h @@ -53,7 +53,8 @@ void cb_feedGame( CursesBoardState* cbState, XP_U32 gameID, const XP_U8* buf, XP_U16 len, const CommsAddrRec* from ); void cb_addInvite( CursesBoardState* cbState, XP_U32 gameID, XP_U16 forceChannel, const CommsAddrRec* destAddr ); -XP_Bool cb_makeRematch( CursesBoardState* cbState, XP_U32 gameID, XP_U32* newGameID ); +XP_Bool cb_makeRematch( CursesBoardState* cbState, XP_U32 gameID, + RematchOrder ro, XP_U32* newGameID ); XP_Bool cb_makeMoveIf( CursesBoardState* cbState, XP_U32 gameID ); const CommonGlobals* cb_getForGameID( CursesBoardState* cbState, XP_U32 gameID ); diff --git a/xwords4/linux/cursesmain.c b/xwords4/linux/cursesmain.c index 7156f1d59..44a11a70e 100644 --- a/xwords4/linux/cursesmain.c +++ b/xwords4/linux/cursesmain.c @@ -1530,7 +1530,6 @@ makeGameFromArgs( CursesAppGlobals* aGlobals, cJSON* args ) params->localName ); for ( int ii = 0; ii < gi.nPlayers; ++ii ) { gi.players[ii].isLocal = ii == hostPosn; - gi.players[ii].robotIQ = 1; } tmp = cJSON_GetObjectItem( args, "dict" ); @@ -1601,8 +1600,11 @@ rematchFromArgs( CursesAppGlobals* aGlobals, cJSON* args ) XP_U32 gameID = gidFromObject( args ); + cJSON* tmp = cJSON_GetObjectItem( args, "rematchOrder" ); + RematchOrder ro = roFromStr( tmp->valuestring ); + XP_U32 newGameID = 0; - if ( cb_makeRematch( aGlobals->cbState, gameID, &newGameID ) ) { + if ( cb_makeRematch( aGlobals->cbState, gameID, ro, &newGameID ) ) { result = newGameID; } return result; diff --git a/xwords4/linux/gtkask.c b/xwords4/linux/gtkask.c index 2da903ad4..cf5047d52 100644 --- a/xwords4/linux/gtkask.c +++ b/xwords4/linux/gtkask.c @@ -75,6 +75,26 @@ gtkask_timeout( GtkWidget* parent, const gchar* message, LOG_RETURNF( "%d", response ); return response; -} /* gtkask */ +} /* gtkask_timeout */ + +bool +gtkask_radios( GtkWidget* parent, const gchar *message, + const AskPair* buttxts, int* chosen ) +{ + gint askResponse = gtkask_timeout( parent, message, GTK_BUTTONS_CANCEL, buttxts, 0 ); + bool result = askResponse != GTK_RESPONSE_CANCEL; + if ( result ) { + for ( int ii = 0; ; ++ii ) { + if ( !buttxts[ii].txt ) { + XP_ASSERT(0); + break; + } else if ( askResponse == buttxts[ii].result ) { + *chosen = ii; + break; + } + } + } + return result; +} #endif diff --git a/xwords4/linux/gtkask.h b/xwords4/linux/gtkask.h index 20756b5c5..c3338c71e 100644 --- a/xwords4/linux/gtkask.h +++ b/xwords4/linux/gtkask.h @@ -1,6 +1,6 @@ /* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */ /* - * Copyright 2000 by Eric House (xwords@eehouse.org). All rights reserved. + * Copyright 2000-2023 by Eric House (xwords@eehouse.org). All rights reserved. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -40,5 +40,11 @@ gint gtkask_timeout( GtkWidget* parent, const gchar *message, GtkButtonsType buttons, const AskPair* buttxts, uint32_t timeoutMS ); +/* Put up buttxts as radio buttons/single choice with and OK button to confirm + and a cancel. That's later; for now just call gtkask() with a ton of + buttons. */ +bool gtkask_radios( GtkWidget* parent, const gchar *message, + const AskPair* buttxts, int* chosen ); + #endif #endif /* PLATFORM_GTK */ diff --git a/xwords4/linux/gtkboard.c b/xwords4/linux/gtkboard.c index 768a38369..675cea93a 100644 --- a/xwords4/linux/gtkboard.c +++ b/xwords4/linux/gtkboard.c @@ -1095,8 +1095,12 @@ makeMenus( GtkGameGlobals* globals ) static void disenable_buttons( GtkGameGlobals* globals ) { - XP_U16 nPending = server_getPendingRegs( globals->cGlobals.game.server ); - if ( !globals->invite_button && 0 < nPending && !!globals->buttons_hbox ) { + XWGame* game = &globals->cGlobals.game; + XP_U16 nPending = server_getPendingRegs( game->server ); + if ( !globals->invite_button + && 0 < nPending + && !server_isFromRematch( game->server ) + && !!globals->buttons_hbox ) { globals->invite_button = addButton( globals->buttons_hbox, "Invite", G_CALLBACK(handle_invite_button), globals ); @@ -1584,7 +1588,7 @@ ask_tiles( gpointer data ) } return 0; -} +} /* ask_tiles */ static void gtk_util_informNeedPickTiles( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe), diff --git a/xwords4/linux/gtkdraw.c b/xwords4/linux/gtkdraw.c index 42bf9e5f3..ac5e196e4 100644 --- a/xwords4/linux/gtkdraw.c +++ b/xwords4/linux/gtkdraw.c @@ -1083,7 +1083,7 @@ formatScoreText( PangoLayout* layout, XP_UCHAR* buf, XP_U16 bufLen, { XP_S16 score = dsi->totalScore; XP_U16 nTilesLeft = dsi->nTilesLeft; - XP_Bool isTurn = dsi->isTurn; + XP_Bool isTurn = XP_TRUE; // dsi->isTurn; XP_S16 maxWidth = bounds->width; XP_UCHAR numBuf[16]; int width, height; diff --git a/xwords4/linux/gtkmain.c b/xwords4/linux/gtkmain.c index 803fc7313..94d0bacc5 100644 --- a/xwords4/linux/gtkmain.c +++ b/xwords4/linux/gtkmain.c @@ -363,6 +363,32 @@ handle_open_button( GtkWidget* XP_UNUSED(widget), void* closure ) void make_rematch( GtkAppGlobals* apg, const CommonGlobals* cGlobals ) { + XP_Bool canOffer; + XP_Bool canRematch = server_canRematch( cGlobals->game.server, &canOffer ); + XP_ASSERT( canRematch ); + + RematchOrder ro = RO_SAME; + if ( canOffer ) { + const AskPair buttons[] = { + {"Juggle", RO_JUGGLE}, + {"Low score first", RO_LOW_SCORE_FIRST}, + {"High score first", RO_HIGH_SCORE_FIRST}, +#ifdef XWFEATURE_RO_BYNAME + { "Alphabetical", RO_BY_NAME }, +#endif + { "Keep existing", RO_SAME }, + { NULL, 0 } + }; + + gint response; + if ( gtkask_radios( apg->window, "rematch? choose new order", + buttons, &response ) ) { + ro = buttons[response].result; + } else { + goto exit; + } + } + LaunchParams* params = apg->cag.params; GtkGameGlobals* newGlobals = calloc( 1, sizeof(*newGlobals) ); initBoardGlobalsGtk( newGlobals, params, NULL ); @@ -373,13 +399,15 @@ make_rematch( GtkAppGlobals* apg, const CommonGlobals* cGlobals ) snprintf( buf, VSIZE(buf), "Game %lX", XP_RANDOM() % 256 ); game_makeRematch( &cGlobals->game, NULL_XWE, util, cp, &newGlobals->cGlobals.procs, - &newGlobals->cGlobals.game, buf ); + &newGlobals->cGlobals.game, buf, ro ); linuxSaveGame( &newGlobals->cGlobals ); sqlite3_int64 rowid = newGlobals->cGlobals.rowid; freeGlobals( newGlobals ); open_row( apg, rowid, XP_TRUE ); + exit: + return; } /* make_rematch */ static void diff --git a/xwords4/linux/gtknewgame.c b/xwords4/linux/gtknewgame.c index 2ee05307e..3b1c59994 100644 --- a/xwords4/linux/gtknewgame.c +++ b/xwords4/linux/gtknewgame.c @@ -409,7 +409,7 @@ makeNewGameDialog( GtkNewGameState* state ) GtkWidget* hbox; #ifndef XWFEATURE_STANDALONE_ONLY GtkWidget* roleCombo; - char* roles[] = { "Standalone", "Host", "Guest" }; + char* roles[] = { "Standalone", "Host" }; #endif dialog = gtk_dialog_new(); @@ -641,8 +641,13 @@ gtk_newgame_col_set( void* closure, XP_U16 player, NewGameColumn col, gchar buf[32]; cp = !!value.ng_cp ? value.ng_cp : ""; if ( NG_COL_NAME == col && '\0' == cp[0] ) { - sprintf( buf, "Linuser %d", 1 + player ); - cp = buf; + LaunchParams* params = state->globals->cGlobals.params; + if ( !!params->localName ) { + cp = params->localName; + } else { + sprintf( buf, "Linuser %d", 1 + player ); + cp = buf; + } } gtk_entry_set_text( GTK_ENTRY(widget), cp ); break; diff --git a/xwords4/linux/linuxmain.c b/xwords4/linux/linuxmain.c index 3ff0561da..4406e31c2 100644 --- a/xwords4/linux/linuxmain.c +++ b/xwords4/linux/linuxmain.c @@ -2533,6 +2533,32 @@ makeSelfAddress( CommsAddrRec* selfAddr, const LaunchParams* params ) } } +RematchOrder +roFromStr(const char* rematchOrder ) +{ + RematchOrder result; + struct { + char* str; + RematchOrder ro; + } vals [] = { + { "same", RO_SAME }, + { "low_score_first", RO_LOW_SCORE_FIRST }, + { "high_score_first", RO_HIGH_SCORE_FIRST }, + { "juggle", RO_JUGGLE }, +#ifdef XWFEATURE_RO_BYNAME + { "by_name", RO_BY_NAME }, +#endif + }; + for ( int ii = 0; ii < VSIZE(vals); ++ii ) { + if ( 0 == strcmp( rematchOrder, vals[ii].str ) ) { + result = vals[ii].ro; + break; + } + } + XP_LOGFF( "(%s) => %d", rematchOrder, result ); + return result; +} + static void writeStatus( const char* statusSocket, const char* dbName ) { diff --git a/xwords4/linux/linuxmain.h b/xwords4/linux/linuxmain.h index 03cc76842..e177a3403 100644 --- a/xwords4/linux/linuxmain.h +++ b/xwords4/linux/linuxmain.h @@ -117,6 +117,8 @@ void tryConnectToServer( CommonGlobals* cGlobals ); void ensureLocalPlayerNames( LaunchParams* params, CurGameInfo* gi ); void cancelTimers( CommonGlobals* cGlobals ); +RematchOrder roFromStr(const char* rematchOrder ); + /* void initParams( LaunchParams* params ); */ /* void freeParams( LaunchParams* params ); */ diff --git a/xwords4/linux/main.h b/xwords4/linux/main.h index ebe504b8b..45a8da429 100644 --- a/xwords4/linux/main.h +++ b/xwords4/linux/main.h @@ -1,6 +1,6 @@ /* -*- compile-command: "make MEMDEBUG=TRUE -j3"; -*- */ /* - * Copyright 2001 - 2020 by Eric House (xwords@eehouse.org). All rights + * Copyright 2001 - 2023 by Eric House (xwords@eehouse.org). All rights * reserved. * * This program is free software; you can redistribute it and/or @@ -157,6 +157,8 @@ typedef struct _LaunchParams { const XP_UCHAR* iterTestPatStr; #endif + const char* rematchOrder; + struct { void (*quit)(void* params); } cmdProcs; diff --git a/xwords4/linux/scripts/netGamesTest.py b/xwords4/linux/scripts/netGamesTest.py index 013c10417..4cf09e2f0 100755 --- a/xwords4/linux/scripts/netGamesTest.py +++ b/xwords4/linux/scripts/netGamesTest.py @@ -4,6 +4,8 @@ import argparse, datetime, json, os, random, shutil, signal, \ socket, struct, subprocess, sys, threading, time g_NAMES = ['Brynn', 'Ariela', 'Kati', 'Eric'] +# These must correspond to what the linux app is looking for in roFromStr() +g_ROS = ['same', 'low_score_first', 'high_score_first', 'juggle', 'by_name',] gDone = False gGamesMade = 0 g_LOGFILE = None @@ -292,7 +294,9 @@ class Device(): # way. But how I figure out the other players differs. def rematch(self, game): gid = game.gid - newGid = self._sendWaitReply('rematch', gid=gid).get('newGid') + rematchOrder = self.figureRematchOrder() + newGid = self._sendWaitReply('rematch', gid=gid, rematchOrder=rematchOrder) \ + .get('newGid') if newGid: guests = Device.playersIn(gid) guests.remove(self.host) @@ -326,6 +330,11 @@ class Device(): self.guestGames.append(GuestGameInfo(self, gid, rematchLevel)) self.launchIfNot() + def figureRematchOrder(self): + ro = self.args.REMATCH_ORDER + if not ro: ro = random.choice(g_ROS) + return ro + # Return true only if all games I host are finished on all games. # But: what about games I don't host? For now, let's make it all # games! @@ -671,6 +680,8 @@ def mkParser(): parser.add_argument('--rematch-level', dest = 'REMATCH_LEVEL', type = int, default = 0, help = 'rematch games down to this ancestry/depth') + parser.add_argument('--rematch-order', dest = 'REMATCH_ORDER', type = str, default = None, + help = 'order rematched games one of these ways: {}'.format(g_ROS)) # envpat = 'DISCON_COREPAT' # parser.add_argument('--core-pat', dest = 'CORE_PAT', default = os.environ.get(envpat), diff --git a/xwords4/linux/scripts/playnum.sh b/xwords4/linux/scripts/playnum.sh deleted file mode 100755 index a153a3370..000000000 --- a/xwords4/linux/scripts/playnum.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# I use this thing this way: playnum.sh 10 2>&1 | ./wordlens.pl - -NUM=$1 -echo "NUM=$NUM" - -while :; do - - ../linux/xwords -u -s -r Eric -d ../linux/dicts/OSPD2to15.xwd -q -i - - NUM=$(( NUM - 1 )); - - if (( $NUM <= 0 )); then exit 0; fi -done \ No newline at end of file