mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2025-02-05 20:45:49 +01:00
Add option to choose how rematch-game players will be ordered
When rematching, some users have a convention that e.g. lowest scoring player in the "parent" game goes first. So allow that, providing the choice on each rematch until a default has been chosen. Support changing that default in a new prefs setting. The place I chose to enforce the order was on the host as invitees are registering and being assigned slots. But by then there's no longer any connection to the game that was rematched, e.g. to use its scores. So during the rematched game creation process I create and store with the new game the necessary ordering information. For the 3-and-4 device case, it was also necessary to tweak the information about other guests that the host sends guests (added during earlier work on rematching.)
This commit is contained in:
parent
2936869b45
commit
1181e908dc
39 changed files with 1354 additions and 253 deletions
|
@ -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,
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Integer, RematchOrder> 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;
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
|
|
|
@ -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
|
||||
|
|
32
xwords4/android/app/src/main/res/layout/rematch_config.xml
Normal file
32
xwords4/android/app/src/main/res/layout/rematch_config.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.eehouse.android.xw4.RematchConfigView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp"
|
||||
>
|
||||
|
||||
<TextView android:id="@+id/explanation"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/expl_rematch_order"
|
||||
/>
|
||||
|
||||
<RadioGroup android:id="@+id/group"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
/>
|
||||
|
||||
<CheckBox android:id="@+id/make_default"
|
||||
android:text="@string/dicts_item_select"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginLeft="12dp"
|
||||
/>
|
||||
|
||||
</org.eehouse.android.xw4.RematchConfigView>
|
|
@ -83,6 +83,7 @@
|
|||
<string name="key_robot_name">key_robot_name</string>
|
||||
<string name="key_default_robodict">key_default_robodict</string>
|
||||
<string name="key_default_phonies">key_default_phonies2</string>
|
||||
<string name="key_rematch_order">key_rematch_order</string>
|
||||
<string name="key_default_timerenabled">key_default_timerenabled</string>
|
||||
<string name="key_notify_sound">key_notify_sound</string>
|
||||
<string name="key_disable_mqtt">key_disable_mqtt</string>
|
||||
|
@ -222,6 +223,14 @@
|
|||
<item>@string/phonies_block</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="ro_names">
|
||||
<item>@string/ro_no_default</item>
|
||||
<item>@string/ro_same</item>
|
||||
<item>@string/ro_low_score_first</item>
|
||||
<item>@string/ro_high_score_first</item>
|
||||
<item>@string/ro_juggle</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="robot_levels">
|
||||
<item>@string/robot_smartest</item>
|
||||
<item>@string/robot_smarter</item>
|
||||
|
|
|
@ -5,4 +5,12 @@
|
|||
|
||||
<string name="dup_allscores_fmt">All scores: %1$s</string>
|
||||
|
||||
<string name="expl_rematch_order">Choose how to order players in the new game</string>
|
||||
<string name="title_rematch_order">Rematched Players Order</string>
|
||||
<string name="ro_no_default">Ask each time</string>
|
||||
<string name="ro_same">Same as this game</string>
|
||||
<string name="ro_low_score_first">Low scorer goes first</string>
|
||||
<string name="ro_high_score_first">High scorer goes first</string>
|
||||
<string name="ro_juggle">Randomly</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -16,6 +16,13 @@
|
|||
android:title="@string/title_addrs_pref"
|
||||
/>
|
||||
|
||||
<org.eehouse.android.xw4.XWListPreference
|
||||
android:key="@string/key_rematch_order"
|
||||
android:title="@string/title_rematch_order"
|
||||
android:entries="@array/ro_names"
|
||||
android:entryValues="@array/ro_names"
|
||||
/>
|
||||
|
||||
<Preference app:title="@string/prefs_dicts"
|
||||
app:summary="@string/prefs_dicts_summary"
|
||||
app:fragment="org.eehouse.android.xw4.gen.PrefsWrappers$prefs_dflts_dicts"
|
||||
|
|
|
@ -1423,7 +1423,7 @@ initGameGlobals( JNIEnv* env, JNIState* state, jobject jutil, jobject jprocs )
|
|||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_eehouse_android_xw4_jni_XwJNI_game_1makeRematch
|
||||
( JNIEnv* env, jclass C, GamePtrType gamePtr, GamePtrType gamePtrNew,
|
||||
jobject jutil, jobject jcp, jstring jGameName )
|
||||
jobject jutil, jobject jcp, jstring jGameName, jobject jRo )
|
||||
{
|
||||
jboolean success = false;
|
||||
XWJNI_START_GLOBALS(gamePtr);
|
||||
|
@ -1437,8 +1437,10 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1makeRematch
|
|||
loadCommonPrefs( env, &cp, jcp );
|
||||
|
||||
const char* gameName = (*env)->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 )
|
||||
|
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
@ -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 ); */
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
Loading…
Add table
Reference in a new issue