Squashed commit of the following:

commit 6ed0462040d6fd45bf43c431df5db4702630da4e
Author: Eric House <eehouse@eehouse.org>
Date:   Thu Jan 30 15:55:29 2020 -0800

    script meant to be run in UserLAnd on android

commit 07b603f4abde00f1cb4face7f27d996fed1b0258
Author: Eric House <eehouse@eehouse.org>
Date:   Thu Jan 30 15:01:00 2020 -0800

    fix gtk invite scenario

    required --name param, apparently.

commit 13201c4cedbf6564516ae850956b2f3155def0b1
Merge: a301491fb 752e3ff3e
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jan 30 12:38:09 2020 -0800

    Merge branch 'android_branch' into duplicate_mode

commit a301491fb5d7b2e09f4b932b16c0a82d1429cb64
Merge: 5401a4bbe d6cf1d2f9
Author: Eric House <xwords@eehouse.org>
Date:   Sat Jan 11 20:45:26 2020 -0800

    Merge branch 'android_branch' into duplicate_mode

commit 5401a4bbe54cdad6adbfa8f9ba8a1c0112ff4dbe
Merge: a92275173 9406297c0
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jan 10 09:54:30 2020 -0800

    Merge branch 'android_branch' into duplicate_mode

commit a922751730be87ae1ea7f6b2ee4263e8b14faabf
Author: Eric House <xwords@eehouse.org>
Date:   Sun Dec 29 16:39:51 2019 -0800

    move game list item pending counter below "dup"

commit 53ca75a56ba9ecde917a5c8c91deaf0afdb04ea0
Merge: 658e4ef92 15239d4a6
Author: Eric House <xwords@eehouse.org>
Date:   Sun Dec 29 16:34:07 2019 -0800

    Merge branch 'android_branch' into duplicate_mode

commit 658e4ef92277d0a1b5c7840883edfab55251d99f
Author: Eric House <xwords@eehouse.org>
Date:   Tue Dec 10 20:33:24 2019 -0800

    move BT UUID into build.gradle

    And add a new UUID for the duplicate build so it doesn't try to talk to
    the xw4d variant when both are installed.

commit a3141c86204bcc134a6fea1e8e6d1cedc743ee13
Author: Eric House <xwords@eehouse.org>
Date:   Thu Nov 21 18:52:11 2019 -0800

    modify icon for easier identification

commit db23f45ac075e8bc50c56b7fb9808a7e270f5625
Author: Eric House <xwords@eehouse.org>
Date:   Thu Nov 21 18:38:18 2019 -0800

    change name of key

    I want to force the apps already out there to generate a correct
    key. This has the lowest impact.

commit 91bc3f6ac406b2fda3426bb363c8e59c3973b75f
Author: Eric House <xwords@eehouse.org>
Date:   Thu Nov 21 18:25:08 2019 -0800

    use the right generated file

    Duh. Used the wrong app/project so FCM couldn't work for the new Dup app
    variant. Now it does.

commit d4ef6f12681bae4437e907e47fbdf1b35bf10d98
Author: Eric House <xwords@eehouse.org>
Date:   Mon Nov 18 09:26:46 2019 -0800

    add comms_getPending(), and refactor to implement

commit 60870e43373a8b73f3541529031791e1457aaa15
Author: Eric House <eehouse@eehouse.org>
Date:   Wed Nov 13 12:30:07 2019 -0800

    first cut at python version of dawg2dict

    Perl version doesn't work and I don't remember enough of the language to
    fix it.

commit b9547d1ec49f1d9d95b3b76b2bcc6ef0d088c531
Author: Eric House <eehouse@eehouse.org>
Date:   Wed Nov 13 12:30:57 2019 -0800

    fix comment

commit 442d219aeab0acfd3713e065f3c46cf0b5d40012
Author: Eric House <eehouse@eehouse.org>
Date:   Tue Nov 12 09:08:08 2019 -0800

    check for undefined variable

commit 247415e18d0b764d3cdb52e9eedeac4b183428c5
Merge: 94c2a7e15 bd3b3e75e
Author: Eric House <eehouse@eehouse.org>
Date:   Tue Nov 19 20:12:18 2019 -0800

    Merge branch 'duplicate_mode' of ssh://maidu2/home/eehouse/src/git/repos/xwords into duplicate_mode

commit 94c2a7e151eeb81bb29589db120a08ffdb5e78fb
Merge: 514e33900 a161abc1b
Author: Eric House <eehouse@eehouse.org>
Date:   Tue Nov 5 21:13:23 2019 -0800

    Merge remote-tracking branch 'tmp-eeh/duplicate_mode' into duplicate_mode

commit a161abc1b2c1c0e8ce76ac71e9feed9d567a3a73
Author: Eric House <xwords@eehouse.org>
Date:   Tue Nov 5 15:56:26 2019 -0800

    make a new CrossDup variant

    It's too much trouble having versions of CrossDbg that aren't compatible.

commit 514e33900fad2695dac89371995ea197a3c9fa4b
Merge: 2d5ec2dfc c69ec5250
Author: Eric House <eehouse@eehouse.org>
Date:   Tue Nov 5 08:43:52 2019 -0800

    Merge branch 'android_branch' into duplicate_mode

commit 2d5ec2dfcdf2788baee2fadd69c8cbb7ac3cafd8
Author: Eric House <eehouse@eehouse.org>
Date:   Sun Nov 3 12:34:48 2019 +0000

    log some hashing stuff (to share while debugging)

commit 564165ed2aef2cd2e893a1552dfb94ca61734f0d
Author: Eric House <eehouse@eehouse.org>
Date:   Sun Nov 3 11:23:40 2019 +0000

    remove unnecessary logging

commit 0111db1951e40ff039cd276d1a77dc31d380ce32
Author: Eric House <eehouse@eehouse.org>
Date:   Sun Nov 3 11:22:27 2019 +0000

    fix log_hex format alignment

commit c7e92680c2624c2d45bade42fddb3334348ce8fc
Author: Eric House <eehouse@eehouse.org>
Date:   Sun Nov 3 10:23:17 2019 +0000

    toward making non-dupe stack hashes match

    Changing the format meant that old builds and new couldn't play non-dupe
    games together, which clearly sucks. There's still a problem, but this
    is part of the fix.

commit fb185eca355840423b561e3237a6529ade409850
Merge: a6df962b1 37ac29e65
Author: Eric House <eehouse@eehouse.org>
Date:   Sun Nov 3 09:43:57 2019 +0000

    Merge branch 'android_branch' into duplicate_mode

commit a6df962b1046ff191453a617afbeee272ce3dc66
Author: Eric House <eehouse@eehouse.org>
Date:   Thu Oct 24 21:28:35 2019 +0200

    cleanup (not dup-specific)

commit f8e31d20ae5399db98589540fff432dc053ec633
Author: Eric House <eehouse@eehouse.org>
Date:   Tue Oct 22 13:38:37 2019 +0200

    use https now to fix broken updating

commit 0add4e050235e9aed5e4f841497c02b80113dbdd
Author: Eric House <eehouse@eehouse.org>
Date:   Tue Sep 10 16:39:19 2019 +0300

    let cells be taller than wide too (take 2)

commit 897d4453702a4c820fb99e60ca0be1bca44d0f42
Author: Eric House <eehouse@eehouse.org>
Date:   Tue Sep 10 16:30:14 2019 +0300

    cleanup (not dup-specific)

commit 2973ffb908fc8a966082870dae348cdc74e646c7
Author: Eric House <eehouse@eehouse.org>
Date:   Thu Aug 29 23:08:36 2019 +0300

    assert that valid message is handled

    I suspect that messages are being dropped (in server) after being
    recorded (in comms), causing the game to stall forever. So add an
    assertion that it's not happening. Haven't seen it yet in a few hundred
    games, but it'll be nice to be more confident.

commit ae6aca71245c934ffe1cb8ba4dc12a0bcbfd1dda
Author: Eric House <eehouse@eehouse.org>
Date:   Mon Aug 26 18:35:12 2019 +0300

    remove x86 from release builds

    I either remove it or add the 64-bit abi. Since it's only there to run
    in the emulator let's just leave it in for DEBUG builds. If I need to
    run a release build in the emulator I can add it back.

commit 4d96f05f2ffea6ca3e9b7dd1776c81fbc0a1350b
Author: Eric House <eehouse@eehouse.org>
Date:   Mon Aug 26 18:14:01 2019 +0300

    arm means two ABIs now

commit f00e8c1258ed62fcf74c7f4db856fb8366d3f348
Author: Eric House <eehouse@eehouse.org>
Date:   Mon Aug 26 18:13:40 2019 +0300

    fix release build warnings

commit 86a7de8c493eb57012531e4953bfde8de38bbb97
Author: Eric House <eehouse@eehouse.org>
Date:   Mon Aug 26 08:23:52 2019 +0300

    up API version to 28 (required soon)

commit 3382823b254cd7f2861362a895a7db483e253781
Author: Eric House <xwords@eehouse.org>
Date:   Sun Jul 21 12:08:33 2019 -0700

    wip: add clear 'X' to [un]pause msg editor

commit f0c0663b85bdcb6f36f50464a06ab5fb6041e738
Author: Eric House <xwords@eehouse.org>
Date:   Sat Jul 13 10:11:17 2019 -0700

    wip: stop using IMPORTANCE_HIGH: too obnoxious

commit 280838b680d10f0e5d280257e21c2eab66dbcec9
Author: Eric House <xwords@eehouse.org>
Date:   Sat Jul 13 10:10:53 2019 -0700

    wip: move misplaced assert

commit de004c956f2804264320adf4e197ed7274092f44
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 12 12:06:10 2019 -0700

    wip: store the damned playerNum already

    And remove mistaken minus-sign (typo?)

commit 17434fcb0c35f5e9489a6b01fd40e4908d4547e6
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 12 11:47:27 2019 -0700

    wip: use rowid for notification id where possible

commit 5b20ba2f8c4d17118a709cdec556ea38b3f4199d
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 12 10:53:50 2019 -0700

    wip: be smarter about looking for timer changes

    Add a dutil callback for when timer value changes. In it, pass the new
    value to DupeModeTimer. In that, don't bother servicing until the saved
    value matches the stored/expected value (because there's otherwise yet
    another save coming.)

commit e98d4154224e2013389962d22476bc571d721aa5
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 12 10:48:59 2019 -0700

    wip: add unique notification id generator

    I'm tired of trying to keep rowid-based notifications from trampling
    each other. Now they can be unique per-channel.

commit fd94a0f0d23b4658c93bfb7793e658387bcbbcce
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jul 11 23:55:54 2019 -0700

    wip: auto-pause when nobody moves

    If moves all come in 0 and the timer's running, assume nobody's actually
    playing and pause the game. While at it added [un]pause to game history
    and made history entries include malloc()'d message, requiring a
    free(). Lots of changes, with at least one glitch that games sometimes
    hang after the autopause; and gameslist shows it as nobody's turn though
    when opened the game knows it's a player's turn.

commit dfb6d2c2574fa63d416c7c800e7dbe30b0925143
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jul 8 20:40:11 2019 -0700

    wip: cleanup/refactor

commit d4a37e5a03e874a4d1e43e72183eb73a8c0f70f5
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jul 8 20:23:58 2019 -0700

    wip: run dupemodetimer more than once

    Failure to reset a variable meant I never processed more than one timer
    firing. Oops.

commit a19979ae7f71da4ba8d478c9a5a5ec4bc70b4c96
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 5 20:03:57 2019 -0700

    wip: zero timer value when move's been made

    Used to actually set the timer value to 0 when move was made, and
    DupeModeTimer expected that. So zero the stored value before returning
    it IFF move's been made already.

commit 7c52ce2a6a08db3b4e2a1bc32f71b073227f23ce
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 5 19:48:29 2019 -0700

    wip: add pause button to notification

    Make it easier to discover pausibility by adding pause button to
    always-on notification. Implemented by opening the game and then within
    it the Pause confirm alert, which required passing a boolean Extra
    through from the launch intent to the one passed into BoardDelegate.

commit 9f82c2f2a3d710f6ad52b1b4f570625e2c19e277
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 5 14:31:43 2019 -0700

    wip: fix to redraw timer when only turnDone changes

commit dee10cf35521018805621e3e2b4b58f2c2dee686
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 5 13:36:06 2019 -0700

    wip: fix compile

commit b1e55307ccd8a849afddc27f9214eea738eb25d7
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 5 11:43:13 2019 -0700

    wip: show timer when paused, and dim when turn done

commit 289080dcfa19e36ea0aa6bd204b61f81872f2b38
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jul 1 21:01:09 2019 -0700

    wip: save separate excuses for pause and unpause

commit 0d427df8f45cc97e385886058b0b456a4e4ca495
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jul 1 21:00:20 2019 -0700

    wip: invoke pause stuff via tap on timer

commit 6ef72472426c61573249e17a4842247114ecd432
Author: Eric House <xwords@eehouse.org>
Date:   Sun Jun 30 20:39:17 2019 -0700

    wip: pass name with pause info for better notification

commit 429baf6edb41a6821103a499b88406de9eb13673
Author: Eric House <xwords@eehouse.org>
Date:   Sat Jun 29 11:50:40 2019 -0700

    wip: improve pause/unpause confirm dialog

commit 9a302b137d56966513393abb4ca3169274ad72e3
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 21:45:17 2019 -0700

    wip: pass just the message. Let client package it.

commit 6c9025e347a1fb97b90778db6563cd22a5aaa2b8
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 21:41:24 2019 -0700

    wip: trigger notifications etc on pause change

commit dabe79c58c1a6ba1c098688dd2b6b764c543e98b
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 21:18:13 2019 -0700

    wip: missing files

commit 352584858fa106b2094e9a393a9b3959a90511b9
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 21:15:58 2019 -0700

    wip: pass explanatory message with [un]pause

commit a99976713b5f35ee61fc9629fb2df9ac11af9098
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 15:28:55 2019 -0700

    set dbname in build.gradle

commit c0b3be342ad9d89b43ba00d77ae91ad09f8ff211
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 10:00:25 2019 -0700

    fix crash on initial install

    uninitialized variable

commit 3656f1d938c61ebf9ac780905f1a084cb4b024f1
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 27 13:35:30 2019 -0700

    wip: fix too-frequent dupe-game checking

    Do it all on a a background thread, and never process a game that was
    added for processing while being processed. Fixes re-opening because we
    just changed it, but may miss changes that matter.

commit a977b8fa51a8e8f9a469bc6fbf305364a29ef8ac
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 27 11:10:14 2019 -0700

    wip: progress on timers

    Mostly fixing DupeModeTimer to do less on main thread.

commit cb4cdc1c8221dfc94e9c9ef889d5d1b099b44049
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 27 07:14:13 2019 -0700

    use macro for callbacks (as elsewhere)

commit 997de250c8d8e0a2de0b949258b06b8998bc1086
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 22:34:34 2019 -0700

    wip: transmit [un]pause to other devices

commit 970fc8c9dbf3883211c1934f3f999149532609a8
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 17:15:55 2019 -0700

    wip: more timer tweaks

commit b67b836028216345ef8d6d157bdaf9f59f698df4
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 15:01:04 2019 -0700

    wip: fix assignment to wrong variable

commit 48cafcec1471e064413ade48ed79b51f1e7fb922
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 14:41:50 2019 -0700

    wip: wire up [un]pause menus in Android board

commit 0476f1b21f3bd30e3ae970dc85a72a7eac67d5d9
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 11:09:19 2019 -0700

    wip: pause/unpause without communication

commit 71815b2f17f0ca354eb6dd4dd12f65d309723db1
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 10:56:09 2019 -0700

    wip: start adding [un]pause support

commit bc4f04c724f51668ace3db17703aa1446fad7405
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 10:07:24 2019 -0700

    wip: fix to work when timer not set

commit 2b271abbb487aa8c5fa7f792d898c72f5d69e270
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 09:53:29 2019 -0700

    wip: show tiles face-up in history for dup-mode

    No point in hiding them when everybody has the same.

commit 945b8527847af20ca955fb313b7bf49887e4e012
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 09:47:19 2019 -0700

    wip: don't draw paused (0:00) timer in duplicate mode

    Required passing in a new boolean

commit 6229fb88302d6da8d432a743955f3dc54d871bda
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 25 19:16:40 2019 -0700

    wip: adding timers

    interpret the existing timer value as per-move, counting down to 0. When the timer fires, commit and send moves for all local players.
    On Android, show notifications for games where a timer's running and nothing's been committed yet. Very rough, and somehow broke the
    python test script for duplicate games.

commit 7892c42c69d2985b39c9fd50053fad3d5f03ba5a
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 25 16:40:14 2019 -0700

    fix crash closing app when timers enabled

commit 1fbf78931b57ff4d92381a3b7f7e39770cb96ae0
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 24 12:23:33 2019 -0700

    assert thread is same for map and unmap

    It'll be a while before I'm comfortable moving this to main branch

commit 046b2e598ae8368eed6e68b5fbaeba69d0416b96
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 24 12:15:40 2019 -0700

    add refcount on thread->env mapping

    Just as a safeguard to ensure I'm not removing something I'll want
    later. There appears to be no attempt at this point to use a stacked
    protocol -- attempts to add what's already there are just dropped -- so
    actual refcounting isn't working now. But if I double-remove I want to know.

commit 5c44e3cd2df87be904db6a7689c59be4fcf2696b
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 24 09:32:36 2019 -0700

    fix breakage from backport and rebase

commit 4476bf10f222af90547e935d7eabd69fff0be427
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 24 09:13:17 2019 -0700

    add timer to gtk's new-game dialog

commit fb50963e4314f85e8013717e4435671cb66b91df
Author: Eric House <eehouse@eehouse.org>
Date:   Sat Jun 22 20:29:26 2019 -0700

    tweak text

commit 4d7fc28b0e0cc4c0e2c877401eb170a4764059f5
Author: Eric House <eehouse@eehouse.org>
Date:   Sat Jun 22 08:56:27 2019 -0700

    make all config spinners use same styles

    One had a label. How all do, and styles make them the same.

commit 5f064b31be8d8dfff0ada1d34acf2792ac6c721d
Author: Eric House <eehouse@eehouse.org>
Date:   Sat Jun 22 08:23:35 2019 -0700

    make label-plus-timer-setting single-line

commit 38a271578dda87a025acd28c065a9748d88e7246
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 14 16:32:57 2019 -0700

    wip: specify smaller size to avoid wrap on some phones

    No idea if this will be enough. Or if just saying "one-line" would work
    better.

commit 8e71bfbc011747b7738fd12e437fd73da7a7b01c
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 14 15:08:51 2019 -0700

    wip: fix false dup-mode tag in games list

    The layouts are reused, so it needs to be hidden for the non-dupe case
    in case it represented a dup-mode game before and the tag was revealed.

commit 2186d72e04df901b53a6d578c24a945b01245f52
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 14 14:50:15 2019 -0700

    fix listing of Hex language (not dup-specific)

commit a9afebea7493438681e3ebcc366f38542ecf4691
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 14 11:48:07 2019 -0700

    wip: fix div-by-zero when all games are duplicate

commit 1925ff93cd1019f4daa6c9b86222dbf9f763d876
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 14 10:30:45 2019 -0700

    wip: store scores in as many bits as largest requires

    breaks storage format!

commit 8fb2678e8e937990ad63569b39c5c12a54a9089b
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 13 20:26:50 2019 -0700

    wip: reduce scores correctly in dup-mode after undo

commit d1d72ce840eef5a1f9c0e3118660a9498e790848
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 13 18:26:15 2019 -0700

    wip: fix tiles not being sorted after undo

    I'm doing this in the model where there's no setting to prevent
    sorting. Figure undo's a violent enough change nobody will care.

commit a8a36e8dfa1f61805932cabf607cf6f01b18a2d3
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 13 17:49:00 2019 -0700

    wip: inval tray when copying from DUP player's

    Fix failure to correctly draw current tray contents after replacement
    based on winning dup. move.

commit 8b2f877f01cf954392841ee3e1eb8b75ce05b646
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 13 17:13:17 2019 -0700

    wip: mark gameslist items with "dup" if in dup mode

commit 11f680fcce14a0303f50d685dde741b2d57241be
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 13 10:55:40 2019 -0700

    wip: correct move count display in dup-mode case

commit 84bced651a5765ce5eacef90d653e1807a5e5a1b
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 11 15:29:30 2019 -0700

    wip: fix crashes using word-lookup

    Assertions about dup mode and pending plays being legal words were wrong
    given how commitTurn() was used building a temp model inside
    model_listWordsThrough().

commit 66b39ca29616dd5f7ae948185b6d16f2d207e45b
Author: Eric House <xwords@eehouse.org>
Date:   Sun Jun 9 14:43:28 2019 -0700

    print average scores at end of run

    Should really be done separately by number of players, as non-dup games
    with more players will have lower scores as each has fewer turns.

commit 3f722d974eaa77883f169974bca18e14b89dd829
Author: Eric House <xwords@eehouse.org>
Date:   Sun Jun 9 10:30:26 2019 -0700

    wip: notifiy turn change listeners in DUP case

    Fixes occasional failure to update Exipration display in game list items
    when a move's made.

commit 8fd13b8fc5040db0c5b631e5f97a31db08d7d053
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 22:16:23 2019 -0700

    wip: don't cache variable locally

    For some reason it's not set on K's phone. This is safer: what's always
    worked, and it's late....

commit 6b67c4a59ca42a28d263f22ef334fe0d40fbab80
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 19:07:11 2019 -0700

    wip: move status strings into resources

commit 6779c31f21e26abcbf0364322db1021ad3d82985
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 18:43:56 2019 -0700

    wip: add status alerts for dup state transitions

    So users know what's up, add not-again alerts when server gets moves but
    can't yet act and when client sends moves.

commit 88770a9dbbe80d2d6364cd8402f4cc2e2f2ec367
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 13:03:54 2019 -0700

    wip: create local copy of inDuplicateMode var

commit d157f114b1e1fb91da015bb6238c70c61b58b154
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 12:38:00 2019 -0700

    wip: fix misplaced assert

commit 2f7230472f846ca29c791c9799a070415271e940
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 12:08:39 2019 -0700

    wip: fix non-dup-mode scores and enable undo

commit 23a11e65e218762408315bbba1ef3d0b0f6808f1
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 11:16:05 2019 -0700

    wip: list all players in a tie

commit 250bc52a044b89f8c39f2bf9a29cd0ce58b2d1a5
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 10:58:40 2019 -0700

    wip: better last-move explanations

    on android and linux. Required passing an array through the JNI which in
    turn encouraged some refactoring.

commit 9cddab3be0164028a30997b5528bfc3334488dae
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 10:22:21 2019 -0700

    wip: use only one ASSIGN in duplicate mode

    They're otherwise all the same, and the other types all have
    playerNum == DUP_PLAYER, so let's be consistent.

commit df50f81db9eace30884613ef4d9ec1c10904f1d9
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 08:47:59 2019 -0700

    wip: call assertSorted() before scoring

    the place that was the original problem

commit ccb026b82ebe600a697f36f67ccab250ac49b32c
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 08:43:31 2019 -0700

    wip: fix so MoveInfo tiles always in order

    Display of last-played move didn't work for duplicate case because
    scoring assumes sorted tiles. So always sort, and assert sorted when
    writing to and reading from stream.

commit a0967512f60354afd15ef6877f48bcc891a1d6d2
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 08:58:44 2019 -0700

    wip: script progress shows dup- vs non-dup-mode

commit 947594afb0b1c681743fbe274d58ded302f1414a
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 08:18:19 2019 -0700

    wip: allow mix of dup- and non-dup games in test script

commit a433cb230008a6cea8236c56a3d04c97a7702a80
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 08:03:35 2019 -0700

    wip: work around misunderstood NPE (not dup-only)

commit a54a015983890338772de5043bb242aa8ebeb202
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 08:03:14 2019 -0700

    wip: cleanup and add debug/logging util

commit 28c13b678f36c4c4678b1467e68daf3297db0464
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 07:58:14 2019 -0700

    wip: don't exit engine when checking tray in dup mode

    Android tends to abort the search because it has UI events
    unprocessed. I'm hoping it's generally so quick that this is ok. If not,
    I'll need to be more clever, e.g. adding an option to end the search
    after finding only one move rather than the best move.

commit c807cb91d25a789fd3ce0e739ad864b6e84631e6
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 07:47:04 2019 -0700

    wip: fix broken compile

commit 960dacdd5d7882f0b9cca9b5ad3e884bb04b9873
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 07:18:24 2019 -0700

    wip: fix pool having too many tiles when engine busy

    Bad loop structure meant I exited only after replacing in pool tiles I
    was actually keeping.

commit 8ff5a5b27d319b55730688dc6e4bfd9a7eef59db
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 19:31:34 2019 -0700

    wip: don't include number of players figuring pass count

    otherwise we wait too long for duplicate mode game to end

commit 19677f2bb6a2eabb80d99d55087621f483cd3e07
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 19:30:41 2019 -0700

    wip: log tile sets in and out

commit 9a0993ec30cb109c2171f141341f3e926ff801f9
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 19:29:32 2019 -0700

    wip: no longer generate trades randomly

commit 2fc37b1f94c966669119e9f7b31ce8fa2d2971e0
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 19:19:06 2019 -0700

    wip: figure bits-per-tile correctly

commit 295579d06a512d0a0012e725f42994e4b78436fa
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 08:52:49 2019 -0700

    wip: flag dup-mode games and nuke all on demand

    Add a bit to a summaries table field so dup-mode games can be
    identified. Add disabled code to nuke them all without attempting to
    open, something that still needs doing on occasion.

commit ddb73d9ccf66fd516ff10394cddb8f99956d99d5
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 3 17:39:21 2019 -0700

    wip: get rid of new move types

    It's enough to know whether we're in duplicate mode at runtime, as
    confirmed by the assertions from the last commit.

commit 70918077c13eebc1651a607da326eaba97fc6813
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 3 12:10:35 2019 -0700

    wip: assert new move types are redundant

    Looks like I don't need them: I can know whether I'm in duplicate mode
    or not and interpret them appropriately. This commit is to check; next
    will remove the types.

commit de677d7276be4a78e1819920403a93fe8ca36a13
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 3 11:23:38 2019 -0700

    wip: don't dup-trade when no tiles left in pool

    and don't count dup-trade as a pass when deciding whether to end game.

commit 6914822fcb1256afb84933c0937eeecd0d906aa6
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 3 11:00:38 2019 -0700

    wip: trade when nobody can move

    Add a new move type, duplicate-trade. Generate and transmit one of those
    when none of the submitted moves scores any points. And for testing,
    randomly do it in DEBUG builds occasionally (should likely be a param
    passed by the test script instead.) Still generates a very few
    assertions run from the script, but worth snapshotting still.

commit 683fb31f2288b3767d1b8495c9ed71030556b018
Author: Eric House <xwords@eehouse.org>
Date:   Sun Jun 2 10:09:26 2019 -0700

    add prefs settings to unhide dup mode and for new-game default

    For now, it should be hard to stumble onto it, so add a debug-only
    setting so that all options are hidden by default. And so if you are
    using it it's easier to use, add a new-game default setting (itself
    hidden at first via the above.) Eventually I suspect it'll be easier to
    find for French-localized devices.

commit e7816d7587b582311a79e253c55e916f32c84c4d
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 29 10:03:13 2019 -0700

    cleanup

commit 78518d2af7c7787d46025ddee85401bdc75d7be7
Author: Eric House <xwords@eehouse.org>
Date:   Tue May 28 06:54:17 2019 -0700

    fix obscure NPE (not duplicate-specific)

commit d4f9bb2be51bd57fe87e1d5157e9e6eb408df528
Author: Eric House <xwords@eehouse.org>
Date:   Tue May 28 06:44:57 2019 -0700

    wip: fix NPE setting title too early

commit d02a2bf9b27544b8102025f599754d03797addb7
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 20:12:14 2019 -0700

    wip: fix crashes and stalls script found

    I was using a non-local player's model as scratch then failing to fully
    update it before loading the winning tray in.

commit 4e611150248cb4b1d20281e73e68b0f44a6fca1e
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 14:38:12 2019 -0700

    wip: add strings where test script can find them for dup games

commit 0685d64f9c7e17735bb651722b99f645aefe8ce8
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 14:36:44 2019 -0700

    wip: fix assert when no tiles in move

commit e02436fd7135b7c9fb799f1f8a1c91e0d7abb5ea
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 08:55:03 2019 -0700

    wip: mark duplicate games a bit

    Add "(dup.)" to game title and new string to the commit confirm alert.

commit ef32d09205d126338046f45aec34d44853523a9f
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 08:51:39 2019 -0700

    wip: fix tray-check bugs

    Needed to run the engine against the tray of the winning move, even if
    it wasn't local. Engines haven't existed for non-local players before so
    some assertions weren't happy.

commit f0a03d41602037150c9f9872a7b6a5e1cd44e038
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 08:11:13 2019 -0700

    fix to compile on Android

commit c442c92ab18e021743636e09956230fc78eb45fc
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 06:53:55 2019 -0700

    fix breakage from too-eager refactoring

commit eff9e3aee484d8c2159b6c59d5fd4a734dc5a77f
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 26 19:51:21 2019 -0700

    wip: refetch new tiles if current tray doesn't allow any moves

commit 47be80c68382dc1ac7c6a648f82e597bd0dde0df
Author: Eric House <xwords@eehouse.org>
Date:   Sat May 25 13:34:24 2019 -0700

    wip: add logging trying to catch stall

commit ccdb3a9d21d0d0a7de2fd3588dcb64754f747ad0
Author: Eric House <xwords@eehouse.org>
Date:   Fri May 24 06:54:04 2019 -0700

    wip: include dupMode in BT and SMS invitations

    Add it to the json and url read/write code so other invitation types
    also work.

commit 959895981505b9a5e58d9212e76bfb7c6f491f34
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 23 20:34:33 2019 -0700

    add option to have all robots at full smartness

    as a test of duplicate, makes all scores the same

commit bd520de6f83d7dd718cd5a50fe16d5414ce63654
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 23 20:33:55 2019 -0700

    wip: fix method-name parens assert printf

commit d8196e9fc252d488b2d42527a734ace9926517cd
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 23 20:33:37 2019 -0700

    wip: remove some logging

commit 4fa394f342b000d7ee44b42a513ea97f9f2d414e
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 22 19:43:52 2019 -0700

    wip: add ability to run test app in duplicate mode

    Doesn't come close to passing :-(

commit cb920e34ee9e3ab3000c89370d722a6a48abfdf5
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 22 16:47:20 2019 -0700

    wip: report scores when move over (sorted by score)

    Still need to made the strings localizable.

commit c3e72140a1966f53b165ff43d8b9e59951e65c94
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 22 13:57:45 2019 -0700

    wip: fix problems opening old-version move stacks

    Since the stack's not rewritten each time it's saved (moves stay in the
    stream) the versioning scheme can't work. Instead use an extra bit to
    store whether the stack uses the new format requiring an extra bit for
    moveType.

commit b635c176820916b344657e9146fb28b998adb9f5
Author: Eric House <xwords@eehouse.org>
Date:   Tue May 21 18:48:31 2019 -0700

    wip: fix end-of-game message and initial turn assignment

commit 8bccd4e482fd48307ed22f5d9bc36966667f0084
Author: Eric House <xwords@eehouse.org>
Date:   Tue May 21 18:10:48 2019 -0700

    wip: UI doesn't need to always show the same turn

    Figure what "turn" it is based on what candidate moves have been
    reported locally. Fixes Android not showing games as needing attention
    on all devices.

commit 909ce7ed7ab13317cbc51288ed71e97a4571414c
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 20 21:37:22 2019 -0700

    wip cleanup

commit 6c5f4847a463a4d2022384677402d83ee1f87d1a
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 20 20:26:08 2019 -0700

    wip: split method

commit 4d489591eb6fc6233d413cc906f84fe3e099b701
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 20 19:49:33 2019 -0700

    wip: update scoreboard and end game correctly

commit 4c3aaa741bb903c8c1c74c4156ea626c4a0d3986
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 19 21:15:56 2019 -0700

    wip: don't allow trade in dup mode

commit ea7b3495080fafe91722d2ce51299bf49e3119de
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 19 21:09:30 2019 -0700

    wip: get building/limping along on Android

    parity with GTK, basically.

commit 72d493588d518868955296fd59033fd6dc763268
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 19 17:57:45 2019 -0700

    wip: break point tie based on word length

commit 08101b4e468efdd2b4b17f85a1492526993be978
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 19 17:12:28 2019 -0700

    wip: fix networked games

commit d83094bfe750560b868d1915d4375a65120817da
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 19 16:28:46 2019 -0700

    wip: fix problems updating model

    Simplest test case now works: two-player one-device game in duplicate
    mode can make a bunch of turns. Save/restore of game works. All on gtk;
    Android likely doesn't build.

commit 8f7a00f1eecba537061fc8e22f3eb44591152cfa
Author: Eric House <xwords@eehouse.org>
Date:   Sat May 18 10:38:37 2019 -0700

    wip: tweak commit of dup move

commit b3212792311e69b2253832aedb052b6cc3c86eb9
Author: Eric House <xwords@eehouse.org>
Date:   Sat May 18 10:23:08 2019 -0700

    wip: correctly update board with dup move

    and fix crash when move stack created with wrong version

commit 449f6f6ff4402ddc9581822ecbde9bb51120db85
Author: Eric House <xwords@eehouse.org>
Date:   Sat May 18 09:10:31 2019 -0700

    wip: update scores correctly when loading stack

commit 0c83965fe8fba7c7aea7407ce6dfb37bc0add39f
Author: Eric House <xwords@eehouse.org>
Date:   Sat May 18 08:40:29 2019 -0700

    wip: add dupe-mode move to move stack

    Add new move entry type including a move and new tiles plus the scores
    for all players. Reflect move and tiles when loading from file. Next:
    figure out what to do with the scores.

commit 78edef74a2c19e4d513fea9da0c6ede4cc146b87
Author: Eric House <xwords@eehouse.org>
Date:   Fri May 17 10:20:18 2019 -0700

    wip: identify highest-scoring word

commit ac7c0b0fb543576e2121dbbeaf17bb3a13cb19c6
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 16 21:34:04 2019 -0700

    wip: transmit client dupe-mode moves to server

    and fix failure to get dupe flag into server on client side

commit de85dfaa2f484a1a02cd6997a7e732daa28de16b
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 16 19:54:41 2019 -0700

    wip: invitation works to assign tiles

    Include in nli sent across that we're in DUPLICATE mode. When sending
    and receiving assigned tiles in that mode, send only one set and remove
    from pool only once. But: games don't always connect, which might or not
    be due to these changes. Next: handle commit by sending the move but not
    a new tray.

commit dcb9e8f14d14749f81c63e08eed8418719555fb8
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 16 19:46:25 2019 -0700

    wip: remove excessive logging

commit 67c62d84f1dfe2bc11463316697a0c62d449e429
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 16 08:15:20 2019 -0700

    wip: accumulate local turns

    Track whether players have committed turns yet. Once all have
    notice (but do nothing yet).

commit d8dcb43dbc396c0f8fdb9a890b958f168137c2ad
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 15 22:24:04 2019 -0700

    wip scoreboard shows it can be multiple players' turns

    modify how scoreboard is drawn. next: let players commit until all are
    done

commit 97ecc7bd81bac2a0832dfa01df3d7130547a1829
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 15 20:05:25 2019 -0700

    wip: add duplicate checkbox and assign same tiles to all

    next: make it everybody's turn until it's nobody's. This is all in
    standalone mode for now.

commit bd3b3e75ef6a0bbf4de20cbdc1aaabb9adae2a9c
Author: Eric House <xwords@eehouse.org>
Date:   Sun Jul 21 12:08:33 2019 -0700

    wip: add clear 'X' to [un]pause msg editor

commit a0999d6af44d126fb450b220b49a743a3c7e439c
Author: Eric House <xwords@eehouse.org>
Date:   Sat Jul 13 10:11:17 2019 -0700

    wip: stop using IMPORTANCE_HIGH: too obnoxious

commit 3a7fd5758ffdaabf4b867f423db2d5558d1f10c0
Author: Eric House <xwords@eehouse.org>
Date:   Sat Jul 13 10:10:53 2019 -0700

    wip: move misplaced assert

commit d769855259daf340446e8c69b8091eec1cbc5d73
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 12 12:06:10 2019 -0700

    wip: store the damned playerNum already

    And remove mistaken minus-sign (typo?)

commit 9622a575863393668ac91a6233e5962ca413c8f8
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 12 11:47:27 2019 -0700

    wip: use rowid for notification id where possible

commit 699b76db2683f7ee9c96adb6d00e9ad623275fba
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 12 10:53:50 2019 -0700

    wip: be smarter about looking for timer changes

    Add a dutil callback for when timer value changes. In it, pass the new
    value to DupeModeTimer. In that, don't bother servicing until the saved
    value matches the stored/expected value (because there's otherwise yet
    another save coming.)

commit 25d3bc0a78557957c1cc4f1c727e581491bc969c
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 12 10:48:59 2019 -0700

    wip: add unique notification id generator

    I'm tired of trying to keep rowid-based notifications from trampling
    each other. Now they can be unique per-channel.

commit 5b2f692adaddf1cfe58f073fa4ea4e39f31b7785
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jul 11 23:55:54 2019 -0700

    wip: auto-pause when nobody moves

    If moves all come in 0 and the timer's running, assume nobody's actually
    playing and pause the game. While at it added [un]pause to game history
    and made history entries include malloc()'d message, requiring a
    free(). Lots of changes, with at least one glitch that games sometimes
    hang after the autopause; and gameslist shows it as nobody's turn though
    when opened the game knows it's a player's turn.

commit 1db4b1a3221f077cb797cc8e1c9a0468a9bc7f1f
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jul 8 20:40:11 2019 -0700

    wip: cleanup/refactor

commit 873dbf3e7c46dbe9bf0cd7163788f9c0f35b32ea
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jul 8 20:23:58 2019 -0700

    wip: run dupemodetimer more than once

    Failure to reset a variable meant I never processed more than one timer
    firing. Oops.

commit 0f13c4b95caa23ef19eae9454d034d5a881a7e20
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 5 20:03:57 2019 -0700

    wip: zero timer value when move's been made

    Used to actually set the timer value to 0 when move was made, and
    DupeModeTimer expected that. So zero the stored value before returning
    it IFF move's been made already.

commit 83761f9228828fa0ff51bd7595059912e76414bc
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 5 19:48:29 2019 -0700

    wip: add pause button to notification

    Make it easier to discover pausibility by adding pause button to
    always-on notification. Implemented by opening the game and then within
    it the Pause confirm alert, which required passing a boolean Extra
    through from the launch intent to the one passed into BoardDelegate.

commit 6e543978b1ab132b8f95ae756301555f6267b037
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 5 14:31:43 2019 -0700

    wip: fix to redraw timer when only turnDone changes

commit b3c79de07e9eeb2bbaed04ff9a46b91fb927fe3f
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 5 13:36:06 2019 -0700

    wip: fix compile

commit d293864cff6d20197703ccadaa9f75b91e634004
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jul 5 11:43:13 2019 -0700

    wip: show timer when paused, and dim when turn done

commit 9c25d5a79e39fad12054be0be3f3d33a569270a9
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jul 1 21:01:09 2019 -0700

    wip: save separate excuses for pause and unpause

commit 9dfc00f4ff462a949ac453680ce160d436da430e
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jul 1 21:00:20 2019 -0700

    wip: invoke pause stuff via tap on timer

commit e80d443cf9583ab388a5b077dbb37d0ce024e2c1
Author: Eric House <xwords@eehouse.org>
Date:   Sun Jun 30 20:39:17 2019 -0700

    wip: pass name with pause info for better notification

commit 511b319643482ef459b793cc37c6fefd585c18c1
Author: Eric House <xwords@eehouse.org>
Date:   Sat Jun 29 11:50:40 2019 -0700

    wip: improve pause/unpause confirm dialog

commit e63fc004c830f96ad87d29d26de55f9429a098e1
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 21:45:17 2019 -0700

    wip: pass just the message. Let client package it.

commit dc79c7698a3b810bb94c4c6b732aa1e470953887
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 21:41:24 2019 -0700

    wip: trigger notifications etc on pause change

commit 6654053dd9640b1c79830cfd2ccff6ee117d8bd6
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 21:18:13 2019 -0700

    wip: missing files

commit 9579766643a0b939b0ebccd6b94fc1cace3f1a1a
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 21:15:58 2019 -0700

    wip: pass explanatory message with [un]pause

commit 1b91ad47910e4f4e0e2bba06d72097e4e83013a5
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 15:28:55 2019 -0700

    set dbname in build.gradle

commit c6c8ace0404f4a39214f294d3a85634d0db2080c
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 28 10:00:25 2019 -0700

    fix crash on initial install

    uninitialized variable

commit b0bcec10a896532d3439b4045e3406c1f361477b
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 27 13:35:30 2019 -0700

    wip: fix too-frequent dupe-game checking

    Do it all on a a background thread, and never process a game that was
    added for processing while being processed. Fixes re-opening because we
    just changed it, but may miss changes that matter.

commit 5f327d2eeabd8761691439c77db74f45514a0bf9
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 27 11:10:14 2019 -0700

    wip: progress on timers

    Mostly fixing DupeModeTimer to do less on main thread.

commit 847773f19b803626e56e4ee44a5f77ba225a487b
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 27 07:14:13 2019 -0700

    use macro for callbacks (as elsewhere)

commit ff6a7430db4bb01e77b1f32efde801872995d709
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 22:34:34 2019 -0700

    wip: transmit [un]pause to other devices

commit e8138431307763ffee8c8cba49cc00f6c36b3fa8
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 17:15:55 2019 -0700

    wip: more timer tweaks

commit 7431f5b4ddb16ff3b646409f8a0c62c43d864810
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 15:01:04 2019 -0700

    wip: fix assignment to wrong variable

commit fc703b6072d2dd7066e5bc9f6708b5b5d9aa11f9
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 14:41:50 2019 -0700

    wip: wire up [un]pause menus in Android board

commit 0e53fbfcbe5bf8e4a1253e45a12b9b5a5fe1e5ad
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 11:09:19 2019 -0700

    wip: pause/unpause without communication

commit 571200bebedb7df5f7193996a0db46ba6b05d1b6
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 10:56:09 2019 -0700

    wip: start adding [un]pause support

commit dbe25a2a09beae6241cd59b322f29e532094a7ae
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 10:07:24 2019 -0700

    wip: fix to work when timer not set

commit 6a58736eae4948c4245f7154e9e54a53ab2a39e9
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 09:53:29 2019 -0700

    wip: show tiles face-up in history for dup-mode

    No point in hiding them when everybody has the same.

commit 0b80a9ddfdcefdade0cf882cd4cd5b21ee4a5825
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 26 09:47:19 2019 -0700

    wip: don't draw paused (0:00) timer in duplicate mode

    Required passing in a new boolean

commit 1e94137e670bef7ecda614d3b513c6c065245704
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 25 19:16:40 2019 -0700

    wip: adding timers

    interpret the existing timer value as per-move, counting down to 0. When the timer fires, commit and send moves for all local players.
    On Android, show notifications for games where a timer's running and nothing's been committed yet. Very rough, and somehow broke the
    python test script for duplicate games.

commit 61b81b820afe4e7c30234d0c6de428fcc41e2a70
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 25 16:40:14 2019 -0700

    fix crash closing app when timers enabled

commit d456444e33bdcf5b151e250a8afd536f52f0d362
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 24 12:23:33 2019 -0700

    assert thread is same for map and unmap

    It'll be a while before I'm comfortable moving this to main branch

commit c35de13cbaeb001d743bbfa00f6a95c16eb3a6fc
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 24 12:15:40 2019 -0700

    add refcount on thread->env mapping

    Just as a safeguard to ensure I'm not removing something I'll want
    later. There appears to be no attempt at this point to use a stacked
    protocol -- attempts to add what's already there are just dropped -- so
    actual refcounting isn't working now. But if I double-remove I want to know.

commit a43b811c3343c1b393c707197a59dc3c7c52d7ac
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 24 09:32:36 2019 -0700

    fix breakage from backport and rebase

commit 5dd2cf2a6209a6902ad7697b8831dfb998af42a8
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 24 09:13:17 2019 -0700

    add timer to gtk's new-game dialog

commit 6fbb0522d65d11752da8170511e78775fc5c16ea
Author: Eric House <eehouse@eehouse.org>
Date:   Sat Jun 22 20:29:26 2019 -0700

    tweak text

commit 9a4bf15f39e50eca71b489a771294b06787eba88
Author: Eric House <eehouse@eehouse.org>
Date:   Sat Jun 22 08:56:27 2019 -0700

    make all config spinners use same styles

    One had a label. How all do, and styles make them the same.

commit c9bf3b9d66cf4bf57fd8ab2a8311165a22ccfc5d
Author: Eric House <eehouse@eehouse.org>
Date:   Sat Jun 22 08:23:35 2019 -0700

    make label-plus-timer-setting single-line

commit ab47ad09612da9b5cad6e450ffedb3c51357431a
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 14 16:32:57 2019 -0700

    wip: specify smaller size to avoid wrap on some phones

    No idea if this will be enough. Or if just saying "one-line" would work
    better.

commit 18f60f13c11c5c87bb1cb89a34430bce4372f937
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 14 15:08:51 2019 -0700

    wip: fix false dup-mode tag in games list

    The layouts are reused, so it needs to be hidden for the non-dupe case
    in case it represented a dup-mode game before and the tag was revealed.

commit 4c77db51f3ab618375d1b4b709d4ce5492ce7e5f
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 14 14:50:15 2019 -0700

    fix listing of Hex language (not dup-specific)

commit df2006279f5203b28fae46f2cae5a99f83e80652
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 14 11:48:07 2019 -0700

    wip: fix div-by-zero when all games are duplicate

commit a13d2c630b0b5a78048663b77051e29855ad012d
Author: Eric House <xwords@eehouse.org>
Date:   Fri Jun 14 10:30:45 2019 -0700

    wip: store scores in as many bits as largest requires

    breaks storage format!

commit e1acb19d7de8b9f65f5727cd7b415dc6a05e5a1e
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 13 20:26:50 2019 -0700

    wip: reduce scores correctly in dup-mode after undo

commit c8783625b72b2469013db4ffbe33ce6f2bc129c0
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 13 18:26:15 2019 -0700

    wip: fix tiles not being sorted after undo

    I'm doing this in the model where there's no setting to prevent
    sorting. Figure undo's a violent enough change nobody will care.

commit 2a5f76e86a357194c46d7f41bf64fd8d9c238b62
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 13 17:49:00 2019 -0700

    wip: inval tray when copying from DUP player's

    Fix failure to correctly draw current tray contents after replacement
    based on winning dup. move.

commit d93ae745a011b22dd1970039577537a0b0162103
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 13 17:13:17 2019 -0700

    wip: mark gameslist items with "dup" if in dup mode

commit 5ab01db8227f16641f18b2dbe09decc5f1a10476
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 13 10:55:40 2019 -0700

    wip: correct move count display in dup-mode case

commit bcff165e45eb0d347303b581a882118a0c1efbf8
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 11 15:29:30 2019 -0700

    wip: fix crashes using word-lookup

    Assertions about dup mode and pending plays being legal words were wrong
    given how commitTurn() was used building a temp model inside
    model_listWordsThrough().

commit a3eb0723987b6abf36f5bfa9b1536ee13fd12ccc
Author: Eric House <xwords@eehouse.org>
Date:   Sun Jun 9 14:43:28 2019 -0700

    print average scores at end of run

    Should really be done separately by number of players, as non-dup games
    with more players will have lower scores as each has fewer turns.

commit f764362e435d644ee1014e22aba7075452c54558
Author: Eric House <xwords@eehouse.org>
Date:   Sun Jun 9 10:30:26 2019 -0700

    wip: notifiy turn change listeners in DUP case

    Fixes occasional failure to update Exipration display in game list items
    when a move's made.

commit 4cb91d72259d7b3190205cc93974ad1052c8d883
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 22:16:23 2019 -0700

    wip: don't cache variable locally

    For some reason it's not set on K's phone. This is safer: what's always
    worked, and it's late....

commit d78fee9a43cf0f6019f6a9494edc4a0573f6317d
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 19:07:11 2019 -0700

    wip: move status strings into resources

commit f943a8ca280da3dce0b843a14fbed307814347e0
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 18:43:56 2019 -0700

    wip: add status alerts for dup state transitions

    So users know what's up, add not-again alerts when server gets moves but
    can't yet act and when client sends moves.

commit 61bc587d656b47b194306ae9c6f10dc6a50878c7
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 13:03:54 2019 -0700

    wip: create local copy of inDuplicateMode var

commit 5b37bb2cd45ca281f410487d63a328eb80b518d9
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 12:38:00 2019 -0700

    wip: fix misplaced assert

commit dddcd5c073f4ee55d2367728381ef400faa6bc0e
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 12:08:39 2019 -0700

    wip: fix non-dup-mode scores and enable undo

commit 09848e3a85635238df9ebc7b0f2c480c4f2db02a
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 11:16:05 2019 -0700

    wip: list all players in a tie

commit 9d87087924fec2d0262ecc1ee2d3bb7d82d3c4c2
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 10:58:40 2019 -0700

    wip: better last-move explanations

    on android and linux. Required passing an array through the JNI which in
    turn encouraged some refactoring.

commit 57b8c65de433cd694d978726257dda07d5918cef
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 10:22:21 2019 -0700

    wip: use only one ASSIGN in duplicate mode

    They're otherwise all the same, and the other types all have
    playerNum == DUP_PLAYER, so let's be consistent.

commit 0d50b662a1290a5e58ee6229e1f9ad41de1eb5f3
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 08:47:59 2019 -0700

    wip: call assertSorted() before scoring

    the place that was the original problem

commit 78fc93e19658eb015ebb546e6bc3190a470bc2e5
Author: Eric House <xwords@eehouse.org>
Date:   Thu Jun 6 08:43:31 2019 -0700

    wip: fix so MoveInfo tiles always in order

    Display of last-played move didn't work for duplicate case because
    scoring assumes sorted tiles. So always sort, and assert sorted when
    writing to and reading from stream.

commit 882c7ff9d60a5d5c79c6c35b54d9d84d19912d4e
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 08:58:44 2019 -0700

    wip: script progress shows dup- vs non-dup-mode

commit 7b4bf585d4bdc3d77d858289f9c043552a41d027
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 08:18:19 2019 -0700

    wip: allow mix of dup- and non-dup games in test script

commit 874b2bafc2b466f2bd2086a239942a350b93f1dd
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 08:03:35 2019 -0700

    wip: work around misunderstood NPE (not dup-only)

commit 185a6d374fc49a955bbea90e086fd1570d023627
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 08:03:14 2019 -0700

    wip: cleanup and add debug/logging util

commit a2a8dfcbd30d53bdac170e057605a683f3f30323
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 07:58:14 2019 -0700

    wip: don't exit engine when checking tray in dup mode

    Android tends to abort the search because it has UI events
    unprocessed. I'm hoping it's generally so quick that this is ok. If not,
    I'll need to be more clever, e.g. adding an option to end the search
    after finding only one move rather than the best move.

commit 44c7528995327c9653dc69f60986f18442e232de
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 07:47:04 2019 -0700

    wip: fix broken compile

commit 1bbc1607914993565c86d8b86da78b490e08d3f4
Author: Eric House <xwords@eehouse.org>
Date:   Wed Jun 5 07:18:24 2019 -0700

    wip: fix pool having too many tiles when engine busy

    Bad loop structure meant I exited only after replacing in pool tiles I
    was actually keeping.

commit 021f5afc8d4a871d42aa619be02b80fab7e2415c
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 19:31:34 2019 -0700

    wip: don't include number of players figuring pass count

    otherwise we wait too long for duplicate mode game to end

commit 1294b3bb0a4272e0bfb87f81ef5f9fe957d78b2e
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 19:30:41 2019 -0700

    wip: log tile sets in and out

commit a01c50405e1edcfba4bb2fc89f4df0801226f5d5
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 19:29:32 2019 -0700

    wip: no longer generate trades randomly

commit aeae0164c3c7e940430d72c000e2a66baf5f1dd5
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 19:19:06 2019 -0700

    wip: figure bits-per-tile correctly

commit cb3bb8d55b9cae577941ab8707a91566f42a14f9
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 09:12:52 2019 -0700

    log service stalls that don't trigger notification

    I'm seeing RelayService never get scheduled, and need to see something
    in the logs.

commit a8e0b43d004b2556ab4b22ab52be0ad7e3ddb45d
Author: Eric House <xwords@eehouse.org>
Date:   Tue Jun 4 08:52:49 2019 -0700

    wip: flag dup-mode games and nuke all on demand

    Add a bit to a summaries table field so dup-mode games can be
    identified. Add disabled code to nuke them all without attempting to
    open, something that still needs doing on occasion.

commit 4e4bcd5d4590d0b429405a6a0a5de10f1a29689d
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 3 17:39:21 2019 -0700

    wip: get rid of new move types

    It's enough to know whether we're in duplicate mode at runtime, as
    confirmed by the assertions from the last commit.

commit 7343ba3de1941a2b9d8fcd196506c9f62acbfe91
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 3 12:10:35 2019 -0700

    wip: assert new move types are redundant

    Looks like I don't need them: I can know whether I'm in duplicate mode
    or not and interpret them appropriately. This commit is to check; next
    will remove the types.

commit 07ae965094c720ebb71b2dd6c5e9a6facf854888
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 3 11:23:38 2019 -0700

    wip: don't dup-trade when no tiles left in pool

    and don't count dup-trade as a pass when deciding whether to end game.

commit b8cbfd0149f9c8e9854ae15dddc8328832f7d8a0
Author: Eric House <xwords@eehouse.org>
Date:   Mon Jun 3 11:00:38 2019 -0700

    wip: trade when nobody can move

    Add a new move type, duplicate-trade. Generate and transmit one of those
    when none of the submitted moves scores any points. And for testing,
    randomly do it in DEBUG builds occasionally (should likely be a param
    passed by the test script instead.) Still generates a very few
    assertions run from the script, but worth snapshotting still.

commit 41f0258619a1f3ab72abd3d908d0b6cf2f999978
Author: Eric House <xwords@eehouse.org>
Date:   Sun Jun 2 10:09:26 2019 -0700

    add prefs settings to unhide dup mode and for new-game default

    For now, it should be hard to stumble onto it, so add a debug-only
    setting so that all options are hidden by default. And so if you are
    using it it's easier to use, add a new-game default setting (itself
    hidden at first via the above.) Eventually I suspect it'll be easier to
    find for French-localized devices.

commit e152b407005e837eec4fe1f6ad552f55c157d877
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 29 10:03:13 2019 -0700

    cleanup

commit 4439d8c3ea786b64dde554a25c62379bbea8347d
Author: Eric House <xwords@eehouse.org>
Date:   Tue May 28 06:54:17 2019 -0700

    fix obscure NPE (not duplicate-specific)

commit 4c5b1fc25d74f0ab40072f0d1987234e78b05569
Author: Eric House <xwords@eehouse.org>
Date:   Tue May 28 06:44:57 2019 -0700

    wip: fix NPE setting title too early

commit cc06a3df70157e3fd4c5b7f12d3adc8cfb87aa1c
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 20:12:14 2019 -0700

    wip: fix crashes and stalls script found

    I was using a non-local player's model as scratch then failing to fully
    update it before loading the winning tray in.

commit 7010cd60af70965ac106072ca2eed91536c746d5
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 14:38:12 2019 -0700

    wip: add strings where test script can find them for dup games

commit 1349d300b74168e2cae3c564fcbcec6fb28264c3
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 14:36:44 2019 -0700

    wip: fix assert when no tiles in move

commit 8a789ef9c32d4d90ef7f054dc8d863902edc34eb
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 08:55:03 2019 -0700

    wip: mark duplicate games a bit

    Add "(dup.)" to game title and new string to the commit confirm alert.

commit 4ac09cf8daf8752106d1edf9a90e0f12fe3bc80a
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 08:51:39 2019 -0700

    wip: fix tray-check bugs

    Needed to run the engine against the tray of the winning move, even if
    it wasn't local. Engines haven't existed for non-local players before so
    some assertions weren't happy.

commit 99abae4d19383afe25a9fbe57960ec0f8530f49e
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 08:11:13 2019 -0700

    fix to compile on Android

commit 22a58978dd1a6d851a5f92557928d5418d42a57a
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 27 06:53:55 2019 -0700

    fix breakage from too-eager refactoring

commit d2f1a8d7c2c90e94bc00b170d69faa6f9b3e69e1
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 26 19:51:21 2019 -0700

    wip: refetch new tiles if current tray doesn't allow any moves

commit c4c916c9f96231d9a2fd085df1aef8b54237ab4f
Author: Eric House <xwords@eehouse.org>
Date:   Sat May 25 13:34:24 2019 -0700

    wip: add logging trying to catch stall

commit 8355236bdc5b34e5a58594dfec982cf0cd3daa8a
Author: Eric House <xwords@eehouse.org>
Date:   Fri May 24 06:54:04 2019 -0700

    wip: include dupMode in BT and SMS invitations

    Add it to the json and url read/write code so other invitation types
    also work.

commit b8fb1d4c89285e0516aa55f830954dac16dbe7cc
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 23 20:34:33 2019 -0700

    add option to have all robots at full smartness

    as a test of duplicate, makes all scores the same

commit e653ad39284c5ba071e74a521a91d5bb21fbe320
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 23 20:33:55 2019 -0700

    wip: fix method-name parens assert printf

commit fca3c06ff3007c3095128b27040d87e82eddf79b
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 23 20:33:37 2019 -0700

    wip: remove some logging

commit 1a89b105d34ba0af2048b7693a3fdbeda7783070
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 22 19:43:52 2019 -0700

    wip: add ability to run test app in duplicate mode

    Doesn't come close to passing :-(

commit 308baf2a8b2cbdc1212cff17811ee6f750c67099
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 22 16:47:20 2019 -0700

    wip: report scores when move over (sorted by score)

    Still need to made the strings localizable.

commit 2db4bdceaba990f34dd32afb16f9d4bd04fd8cdc
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 22 13:57:45 2019 -0700

    wip: fix problems opening old-version move stacks

    Since the stack's not rewritten each time it's saved (moves stay in the
    stream) the versioning scheme can't work. Instead use an extra bit to
    store whether the stack uses the new format requiring an extra bit for
    moveType.

commit 1df8e1754e35b784be2070c8416c6f5038fa65a7
Author: Eric House <xwords@eehouse.org>
Date:   Tue May 21 18:48:31 2019 -0700

    wip: fix end-of-game message and initial turn assignment

commit 4548435b36e9f9165b66832b93707a0b7e00f876
Author: Eric House <xwords@eehouse.org>
Date:   Tue May 21 18:10:48 2019 -0700

    wip: UI doesn't need to always show the same turn

    Figure what "turn" it is based on what candidate moves have been
    reported locally. Fixes Android not showing games as needing attention
    on all devices.

commit 4a93f4b4528eabcef41bdfd67517bba4d1479407
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 20 21:37:22 2019 -0700

    wip cleanup

commit f8184783bc9a8914310c39c54084e5fe492694a7
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 20 20:26:08 2019 -0700

    wip: split method

commit 0d7d68737843017ecbc08dba87d881dabf7098ff
Author: Eric House <xwords@eehouse.org>
Date:   Mon May 20 19:49:33 2019 -0700

    wip: update scoreboard and end game correctly

commit d5d95424314b2e18c95f0a43eaa1759b3a7c6b5a
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 19 21:15:56 2019 -0700

    wip: don't allow trade in dup mode

commit 8b102c0a8e5c0b39cc5b29aea16a5985a0e402c6
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 19 21:09:30 2019 -0700

    wip: get building/limping along on Android

    parity with GTK, basically.

commit 16cfc6b0bb1bb01ee801ccec2116eac94e16fdab
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 19 17:57:45 2019 -0700

    wip: break point tie based on word length

commit 66dce87d98dce21d7616cc79c32ee88e8fbbb537
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 19 17:12:28 2019 -0700

    wip: fix networked games

commit 33ba891082a9d59a90115357194841f8fa0532f8
Author: Eric House <xwords@eehouse.org>
Date:   Sun May 19 16:28:46 2019 -0700

    wip: fix problems updating model

    Simplest test case now works: two-player one-device game in duplicate
    mode can make a bunch of turns. Save/restore of game works. All on gtk;
    Android likely doesn't build.

commit 33080c1014e40af0858a1d89168ffd52d1efd400
Author: Eric House <xwords@eehouse.org>
Date:   Sat May 18 10:38:37 2019 -0700

    wip: tweak commit of dup move

commit 65f06a216441624f811d1abb2e31955fb979b5c5
Author: Eric House <xwords@eehouse.org>
Date:   Sat May 18 10:23:08 2019 -0700

    wip: correctly update board with dup move

    and fix crash when move stack created with wrong version

commit 4be132686b64cf6c49e541ad7df76bb7b753b3ce
Author: Eric House <xwords@eehouse.org>
Date:   Sat May 18 09:10:31 2019 -0700

    wip: update scores correctly when loading stack

commit 107f7b2a6708c1129f8fd67dc2deecb9e19a0606
Author: Eric House <xwords@eehouse.org>
Date:   Sat May 18 08:40:29 2019 -0700

    wip: add dupe-mode move to move stack

    Add new move entry type including a move and new tiles plus the scores
    for all players. Reflect move and tiles when loading from file. Next:
    figure out what to do with the scores.

commit 6c05fe87d3ddae3e74edf0621df40dd083c9bebb
Author: Eric House <xwords@eehouse.org>
Date:   Fri May 17 10:20:18 2019 -0700

    wip: identify highest-scoring word

commit 4e498f754d637a15c63ec8498bad35c58f348a45
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 16 21:34:04 2019 -0700

    wip: transmit client dupe-mode moves to server

    and fix failure to get dupe flag into server on client side

commit 225b0f579d0c9aae85f439739d686b8afda7b6e3
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 16 19:54:41 2019 -0700

    wip: invitation works to assign tiles

    Include in nli sent across that we're in DUPLICATE mode. When sending
    and receiving assigned tiles in that mode, send only one set and remove
    from pool only once. But: games don't always connect, which might or not
    be due to these changes. Next: handle commit by sending the move but not
    a new tray.

commit 7e7b5d65e2fd4ccaf6657d88c9c9e1c59adc8c4f
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 16 19:46:25 2019 -0700

    wip: remove excessive logging

commit 8afc11dc54c5aa145066729594fb741cf9e51c2c
Author: Eric House <xwords@eehouse.org>
Date:   Thu May 16 08:15:20 2019 -0700

    wip: accumulate local turns

    Track whether players have committed turns yet. Once all have
    notice (but do nothing yet).

commit d7697fba6f7bffc37d2ee09d9b397c1bb04cdf4b
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 15 22:24:04 2019 -0700

    wip scoreboard shows it can be multiple players' turns

    modify how scoreboard is drawn. next: let players commit until all are
    done

commit eaa551d1d587fba90bb6cf960b4321949d173ef5
Author: Eric House <xwords@eehouse.org>
Date:   Wed May 15 20:05:25 2019 -0700

    wip: add duplicate checkbox and assign same tiles to all

    next: make it everybody's turn until it's nobody's. This is all in
    standalone mode for now.
This commit is contained in:
Eric House 2020-01-30 16:00:56 -08:00
parent 752e3ff3e8
commit 1d35b7e7e0
120 changed files with 5062 additions and 1164 deletions

View file

@ -4,9 +4,28 @@ def VERSION_NAME = '4.4.154'
def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY")
def BUILD_INFO_NAME = "build-info.txt"
// Not all variants use the same BT_UUID. Those with the same talk to
// each other
def XW_UUID = '"7be0d084-ff89-4d6d-9c78-594773a6f963"' // from comms.h
def XWD_UUID = '"b079b640-35fe-11e5-a432-0002a5d5c51b"' // from comms.h
def XWDUP_UUID = '"92602f84-1bc5-11ea-978f-2e728ce88125"'
def BT_UUIDS = [
'xw4fdroidDebug' : XW_UUID,
'xw4fdroidRelease' : XW_UUID,
'xw4NoSMSDebug' : XW_UUID,
'xw4NoSMSRelease' : XW_UUID,
'xw4SMSDebug' : XW_UUID,
'xw4SMSRelease' : XW_UUID,
'xw4dupDebug' : XWDUP_UUID,
'xw4dupRelease' : XWDUP_UUID,
'xw4dupNoSMSDebug' : XWDUP_UUID,
'xw4dupNoSMSRelease' : XWDUP_UUID,
]
// AID must start with F (first 4 bits) and be from 5 to 16 bytes long
def NFC_AID_XW4 = "FC8FF510B360"
def NFC_AID_XW4d = "FDDA0A3EB5E5"
def NFC_AID_XW4dup = "F8960736B33C"
boolean forFDroid = hasProperty('forFDroid')
@ -79,7 +98,7 @@ android {
buildConfigField "boolean", "UDP_ENABLED", "true"
buildConfigField "boolean", "REPORT_LOCKS", "false"
buildConfigField "boolean", "LOG_LIFECYLE", "false"
buildConfigField "boolean", "MOVE_VIA_NFC", "false"
buildConfigField "String", "KEY_FCMID", "\"FBMService_fcmid\""
buildConfigField "boolean", "ATTACH_SUPPORTED", "false"
}
@ -111,38 +130,37 @@ android {
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4"
}
xw4d {
xw4dup {
dimension "variant"
buildConfigField "String", "DB_NAME", "\"xwddb\""
applicationId "org.eehouse.android.xw4dbg"
buildConfigField "String", "DB_NAME", "\"xwddb\"";
applicationId "org.eehouse.android.xw4dup"
manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ]
resValue "string", "app_name", "CrossDbg"
resValue "string", "app_name", "CrossDup"
resValue "string", "nbs_port", "3345"
buildConfigField "boolean", "WIDIR_ENABLED", "true"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "true"
buildConfigField "String", "VARIANT_NAME", "\"Dev/Debug\""
buildConfigField "int", "VARIANT_CODE", "3"
buildConfigField "boolean", "REPORT_LOCKS", "true"
buildConfigField "boolean", "MOVE_VIA_NFC", "true"
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4d}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4d"
buildConfigField "String", "KEY_FCMID", "\"FBMService_fcmid1\""
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4dup}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4dup"
}
xw4dNoSMS {
xw4dupNoSMS {
dimension "variant"
applicationId "org.eehouse.android.xw4dup"
buildConfigField "String", "DB_NAME", "\"xwddb\""
applicationId "org.eehouse.android.xw4dbg"
manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ]
resValue "string", "app_name", "CrossDbg"
resValue "string", "app_name", "CrossDup"
resValue "string", "nbs_port", "3345"
buildConfigField "boolean", "WIDIR_ENABLED", "true"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "true"
buildConfigField "String", "VARIANT_NAME", "\"Dev/Debug NoSMS\""
buildConfigField "int", "VARIANT_CODE", "4"
buildConfigField "boolean", "REPORT_LOCKS", "true"
buildConfigField "boolean", "MOVE_VIA_NFC", "true"
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4d}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4d"
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4dup}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4dup"
}
xw4SMS {
@ -223,20 +241,20 @@ android {
jniLibs.srcDir "../libs-xw4NoSMSDebug"
}
}
xw4d {
xw4dup {
release {
jniLibs.srcDir "../libs-xw4dRelease"
jniLibs.srcDir "../libs-xw4dupRelease"
}
debug {
jniLibs.srcDir "../libs-xw4dDebug"
jniLibs.srcDir "../libs-xw4dupDebug"
}
}
xw4dNoSMS {
xw4dupNoSMS {
release {
jniLibs.srcDir "../libs-xw4dNoSMSRelease"
jniLibs.srcDir "../libs-xw4dupNoSMSRelease"
}
debug {
jniLibs.srcDir "../libs-xw4dNoSMSDebug"
jniLibs.srcDir "../libs-xw4dupNoSMSDebug"
}
}
xw4SMS {
@ -284,10 +302,10 @@ dependencies {
// 2.6.8 is probably as far forward as I can go without upping my
// min-supported SDK version
xw4dImplementation('com.crashlytics.sdk.android:crashlytics:2.6.3@aar') { // rm-for-fdroid
xw4dupImplementation('com.crashlytics.sdk.android:crashlytics:2.6.3@aar') { // rm-for-fdroid
transitive = true // rm-for-fdroid
} // rm-for-fdroid
xw4dNoSMSImplementation('com.crashlytics.sdk.android:crashlytics:2.6.3@aar') { // rm-for-fdroid
xw4dupNoSMSImplementation('com.crashlytics.sdk.android:crashlytics:2.6.3@aar') { // rm-for-fdroid
transitive = true // rm-for-fdroid
} // rm-for-fdroid
@ -357,12 +375,13 @@ afterEvaluate {
String nameLC = variant.getBuildType().getName().toLowerCase()
String lib = "libs-${variant.name}"
String ndkBuildTask = "ndkBuild${variantCaps}"
String btUUID = BT_UUIDS[variant.name]
task "$ndkBuildTask"(type: Exec) {
workingDir '../'
commandLine './scripts/ndkbuild.sh', '-j3',
"BUILD_TARGET=${nameLC}", "INITIAL_CLIENT_VERS=$INITIAL_CLIENT_VERS",
"VARIANT=${FLAVOR}", "NDK_LIBS_OUT=${lib}",
"NDK_OUT=./obj-${variant.name}"
workingDir '../'
commandLine './scripts/ndkbuild.sh', '-j3',
"BUILD_TARGET=${nameLC}", "INITIAL_CLIENT_VERS=$INITIAL_CLIENT_VERS",
"VARIANT=${FLAVOR}", "NDK_LIBS_OUT=${lib}",
"NDK_OUT=./obj-${variant.name}", "XW_BT_UUID=\"${btUUID}\""
}
String compileTask = "compile${variantCaps}Ndk"
@ -383,18 +402,18 @@ afterEvaluate {
String copyStringsTask = "copyStringsXw4D"
task "$copyStringsTask"(type: Exec) {
workingDir './'
environment.put('APPNAME', 'CrossDbg')
environment.put('APPNAME', 'CrossDup')
commandLine 'make', '-f', '../scripts/Variant.mk',
"src/xw4d/res/values/strings.xml"
"src/xw4dup/res/values/strings.xml"
}
preBuild.dependsOn copyStringsTask
String copyStringsTaskNoSMS = "copyStringsXw4DNoSMS"
task "$copyStringsTaskNoSMS"(type: Exec) {
workingDir './'
environment.put('APPNAME', 'CrossDbg')
environment.put('APPNAME', 'CrossDup')
commandLine 'make', '-f', '../scripts/Variant.mk',
"src/xw4dNoSMS/res/values/strings.xml"
"src/xw4dupNoSMS/res/values/strings.xml"
}
preBuild.dependsOn copyStringsTaskNoSMS
}

View file

@ -138,6 +138,7 @@
<receiver android:name="RelayReceiver"/>
<receiver android:name="NagTurnReceiver"/>
<receiver android:name="SMSResendReceiver"/>
<receiver android:name="DupeModeTimer"/>
<receiver android:name="UpdateCheckReceiver">
<intent-filter>

View file

@ -80,6 +80,7 @@ public class BoardCanvas extends Canvas implements DrawCtx {
private CommonPrefs m_prefs;
private int m_lastSecsLeft;
private int m_lastTimerPlayer;
private boolean m_lastTimerTurnDone;
private boolean m_inTrade;
private boolean m_darkOnLight;
private Drawable m_origin;
@ -334,18 +335,48 @@ public class BoardCanvas extends Canvas implements DrawCtx {
}
@Override
// public void drawTimer( Rect rect, int player, int secondsLeft,
// boolean turnDone )
// {
// if ( m_lastSecsLeft != secondsLeft
// || m_lastTimerPlayer != player
// || m_lastTimerTurnDone != turnDone ) {
// if ( null != m_activity && null != m_jniThread ) {
// Rect rectCopy = new Rect(rect);
// m_lastSecsLeft = secondsLeft;
// m_lastTimerPlayer = player;
// m_lastTimerTurnDone = turnDone;
// String negSign = secondsLeft < 0? "-" : "";
// secondsLeft = Math.abs( secondsLeft );
// String time = String.format( "%s%d:%02d", negSign,
// secondsLeft/60, secondsLeft%60 );
// fillRectOther( rectCopy, CommonPrefs.COLOR_BACKGRND );
// int color = m_playerColors[player];
// if ( turnDone ) {
// color &= NOT_TURN_ALPHA;
// }
// m_fillPaint.setColor( color );
public void drawTimer( Rect rect, final int player,
int secondsLeft )
int secondsLeft, final boolean turnDone )
{
if ( m_lastSecsLeft != secondsLeft || m_lastTimerPlayer != player ) {
Activity activity = m_activity;
if ( null == activity ) {
// Do nothing
} else if ( m_lastSecsLeft != secondsLeft
|| m_lastTimerPlayer != player
|| m_lastTimerTurnDone != turnDone ) {
final Rect rectCopy = new Rect(rect);
final int secondsLeftCopy = secondsLeft;
m_activity.runOnUiThread( new Runnable() {
activity.runOnUiThread( new Runnable() {
@Override
public void run() {
if ( null != m_jniThread ) {
m_lastSecsLeft = secondsLeftCopy;
m_lastTimerPlayer = player;
m_lastTimerTurnDone = turnDone;
String negSign = secondsLeftCopy < 0? "-":"";
int secondsLeft = Math.abs( secondsLeftCopy );
@ -519,9 +550,9 @@ public class BoardCanvas extends Canvas implements DrawCtx {
@Override
public void score_pendingScore( Rect rect, int score, int playerNum,
int curTurn, int flags )
boolean curTurn, int flags )
{
Log.d( TAG, "pendingScore(playerNum=%d, curTurn=%d)",
Log.d( TAG, "pendingScore(playerNum=%d, curTurn=%b)",
playerNum, curTurn );
int otherIndx = (0 == (flags & CELL_ISCURSOR))
@ -530,7 +561,7 @@ public class BoardCanvas extends Canvas implements DrawCtx {
fillRectOther( rect, otherIndx );
int playerColor = m_playerColors[playerNum];
if ( playerNum != curTurn ) {
if ( !curTurn ) {
playerColor &= NOT_TURN_ALPHA;
}
m_fillPaint.setColor( playerColor );

View file

@ -1,6 +1,6 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
* Copyright 2009 - 2017 by Eric House (xwords@eehouse.org). All rights
* Copyright 2009 - 2019 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
@ -33,6 +33,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
@ -59,6 +60,7 @@ import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;
import org.eehouse.android.xw4.jni.CommsAddrRec;
import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
import org.eehouse.android.xw4.jni.CurGameInfo;
import org.eehouse.android.xw4.jni.DUtilCtxt;
import org.eehouse.android.xw4.jni.GameSummary;
import org.eehouse.android.xw4.jni.JNIThread.JNICmd;
import org.eehouse.android.xw4.jni.JNIThread;
@ -85,6 +87,8 @@ public class BoardDelegate extends DelegateBase
private static final String SAVE_MYSIS = TAG + "/MYSIS";
static final String PAUSER_KEY = TAG + "/pauser";
private Activity m_activity;
private BoardView m_view;
private GamePtr m_jniGamePtr;
@ -366,6 +370,31 @@ public class BoardDelegate extends DelegateBase
}
break;
case ASK_DUP_PAUSE: {
final boolean isPause = (Boolean)params[0];
final ConfirmPauseView pauseView =
((ConfirmPauseView)inflate( R.layout.pause_view ))
.setIsPause( isPause );
int buttonId = isPause ? R.string.board_menu_game_pause
: R.string.board_menu_game_unpause;
dialog = ab
.setTitle(isPause ? R.string.pause_title : R.string.unpause_title)
.setView( pauseView )
.setPositiveButton( buttonId, new OnClickListener() {
@Override
public void
onClick( DialogInterface dlg,
int whichButton ) {
String msg = pauseView.getMsg();
handleViaThread( isPause ? JNICmd.CMD_PAUSE
: JNICmd.CMD_UNPAUSE, msg );
}
})
.setNegativeButton( android.R.string.cancel, null )
.create();
}
break;
case QUERY_ENDGAME:
dialog = ab.setTitle( R.string.query_title )
.setMessage( R.string.ids_endnow )
@ -565,8 +594,8 @@ public class BoardDelegate extends DelegateBase
mNFCWrapper = Wrapper.init( m_activity, this, devID );
m_utils = new BoardUtilCtxt();
m_timers = new TimerRunnable[4]; // needs to be in sync with
// XWTimerReason
// needs to be in sync with XWTimerReason
m_timers = new TimerRunnable[UtilCtxt.NUM_TIMERS_PLUS_ONE];
m_view = (BoardView)findViewById( R.id.board_view );
if ( ! ABUtils.haveActionBar() ) {
m_tradeButtons = findViewById( R.id.exchange_buttons );
@ -644,6 +673,7 @@ public class BoardDelegate extends DelegateBase
}
}
@Override
protected void onPause()
{
Wrapper.setResumed( mNFCWrapper, false );
@ -672,7 +702,6 @@ public class BoardDelegate extends DelegateBase
if ( null != m_jniThreadRef ) {
m_jniThreadRef.release();
m_jniThreadRef = null;
// Assert.assertNull( m_jniThreadRef ); // firing
}
GamesListDelegate.boardDestroyed( m_rowid );
super.onDestroy();
@ -770,7 +799,11 @@ public class BoardDelegate extends DelegateBase
@Override
protected void setTitle()
{
setTitle( GameUtils.getName( m_activity, m_rowid ) );
String title = GameUtils.getName( m_activity, m_rowid );
if ( null != m_gi && m_gi.inDuplicateMode ) {
title = LocUtils.getString( m_activity, R.string.dupe_title_fmt, title );
}
setTitle( title );
}
private void initToolbar()
@ -849,6 +882,11 @@ public class BoardDelegate extends DelegateBase
m_gsi.canTrade );
Utils.setItemVisible( menu, R.id.board_menu_undo_last,
m_gsi.canUndo );
Utils.setItemVisible( menu, R.id.board_menu_game_pause,
m_gsi.canPause );
Utils.setItemVisible( menu, R.id.board_menu_game_unpause,
m_gsi.canUnpause );
}
Utils.setItemVisible( menu, R.id.board_menu_trade_cancel, inTrade );
@ -905,7 +943,7 @@ public class BoardDelegate extends DelegateBase
JNICmd cmd = JNICmd.CMD_NONE;
Runnable proc = null;
int id = item.getItemId();
final int id = item.getItemId();
switch ( id ) {
case R.id.board_menu_done:
int nTiles = XwJNI.model_getNumTilesInTray( m_jniGamePtr,
@ -985,6 +1023,11 @@ public class BoardDelegate extends DelegateBase
.show();
break;
case R.id.board_menu_game_pause:
case R.id.board_menu_game_unpause:
getConfirmPause( R.id.board_menu_game_pause == id );
break;
// small devices only
case R.id.board_menu_dict:
String dictName = m_gi.dictName( m_view.getCurPlayer() );
@ -1600,13 +1643,13 @@ public class BoardDelegate extends DelegateBase
private void deleteAndClose( int gameID )
{
if ( gameID == m_gi.gameID ) {
GameUtils.deleteGame( m_activity, m_jniThread.getLock(), false );
waitCloseGame( false );
finish();
if ( null != m_gi && gameID == m_gi.gameID ) {
GameUtils.deleteGame( m_activity, m_jniThread.getLock(), false, false );
} else {
Log.e( TAG, "deleteAndClose() called with wrong gameID %d", gameID );
}
waitCloseGame( false );
finish();
}
private void askDropRelay()
@ -1757,6 +1800,19 @@ public class BoardDelegate extends DelegateBase
handleViaThread( JNICmd.CMD_REMAINING, R.string.tiles_left_title );
}
@Override
public void timerSelected( boolean inDuplicateMode, final boolean canPause )
{
if ( inDuplicateMode ) {
runOnUiThread( new Runnable() {
@Override
public void run() {
getConfirmPause( canPause );
}
} );
}
}
@Override
public void setIsServer( boolean isServer )
{
@ -1843,6 +1899,7 @@ public class BoardDelegate extends DelegateBase
int inHowLong;
switch ( why ) {
case UtilCtxt.TIMER_COMMS:
case UtilCtxt.TIMER_DUP_TIMERCHECK:
inHowLong = when * 1000;
break;
case UtilCtxt.TIMER_TIMERTICK:
@ -1942,6 +1999,20 @@ public class BoardDelegate extends DelegateBase
showDialogFragment( DlgID.QUERY_TRADE, dlgBytes );
}
@Override
public void notifyDupStatus( boolean amHost, final String msg )
{
final int key = amHost ? R.string.key_na_dupstatus_host
: R.string.key_na_dupstatus_guest;
runOnUiThread( new Runnable() {
@Override
public void run() {
makeNotAgainBuilder( msg, key )
.show();
}
} );
}
@Override
public void userError( int code )
{
@ -2166,6 +2237,38 @@ public class BoardDelegate extends DelegateBase
}
} );
}
@Override
public String formatPauseHistory( int pauseTyp, int player,
int whenPrev, int whenCur, String msg )
{
Log.d( TAG, "formatPauseHistory(prev: %d, cur: %d)", whenPrev, whenCur );
String result = null;
String name = 0 > player ? null : m_gi.players[player].name;
switch ( pauseTyp ) {
case DUtilCtxt.UNPAUSED:
String interval = DateUtils
.formatElapsedTime( whenCur - whenPrev )
.toString();
result = LocUtils.getString( m_activity, R.string.history_unpause_fmt,
name, interval );
break;
case DUtilCtxt.PAUSED:
result = LocUtils.getString( m_activity, R.string.history_pause_fmt,
name );
break;
case DUtilCtxt.AUTOPAUSED:
result = LocUtils.getString( m_activity, R.string.history_autopause );
break;
}
if ( null != msg ) {
result += " " + LocUtils
.getString( m_activity, R.string.history_msg_fmt, msg );
}
return result;
}
} // class BoardUtilCtxt
private void doResume( boolean isStart )
@ -2225,11 +2328,6 @@ public class BoardDelegate extends DelegateBase
invalidateOptionsMenuIf();
}
break;
case JNIThread.GOT_WORDS:
CurGameInfo gi = m_jniThreadRef.getGI();
launchLookup( wordsToArray((String)msg.obj),
gi.dictLang );
break;
case JNIThread.GAME_OVER:
if ( m_isFirstLaunch ) {
runOnUiThread( new Runnable() {
@ -2249,6 +2347,16 @@ public class BoardDelegate extends DelegateBase
showToast( getQuantityString( R.plurals.resent_msgs_fmt,
nSent, nSent ) );
break;
case JNIThread.GOT_PAUSE:
runOnUiThread( new Runnable() {
@Override
public void run() {
makeOkOnlyBuilder( (String)msg.obj )
.show();
}
} );
break;
}
}
};
@ -2303,10 +2411,11 @@ public class BoardDelegate extends DelegateBase
}
}
if ( 0 != flags ) {
DBUtils.setMsgFlags( m_rowid, GameSummary.MSG_FLAGS_NONE );
DBUtils.setMsgFlags( m_activity, m_rowid,
GameSummary.MSG_FLAGS_NONE );
}
Utils.cancelNotification( m_activity, (int)m_rowid );
Utils.cancelNotification( m_activity, m_rowid );
askNBSPermissions();
@ -2315,6 +2424,11 @@ public class BoardDelegate extends DelegateBase
tickle( isStart );
tryInvites();
}
Bundle args = getArguments();
if ( args.getBoolean( PAUSER_KEY, false ) ) {
getConfirmPause( true );
}
}
} // resumeGame
@ -2547,6 +2661,11 @@ public class BoardDelegate extends DelegateBase
names, locs );
}
private void getConfirmPause( boolean isPause )
{
showDialogFragment( DlgID.ASK_DUP_PAUSE, isPause );
}
private void closeIfFinishing( boolean force )
{
if ( null == m_handler ) {

View file

@ -209,7 +209,7 @@ public class BoardView extends View implements BoardHandler, SyncedDraw {
} else {
Bitmap bitmap = s_bitmap;
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
bitmap = Bitmap.createBitmap(bitmap);
bitmap = Bitmap.createBitmap( bitmap );
}
canvas.drawBitmap( bitmap, 0, 0, new Paint() );

View file

@ -20,22 +20,27 @@
package org.eehouse.android.xw4;
import android.os.Build;
import android.content.Context;
import java.util.HashSet;
import java.util.Set;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class Channels {
private static final String TAG = Channels.class.getSimpleName();
enum ID {
NBSPROXY(R.string.nbsproxy_channel_expl,
NotificationManager.IMPORTANCE_LOW),
GAME_EVENT(R.string.gameevent_channel_expl,
NotificationManager.IMPORTANCE_LOW),
SERVICE_STALL(R.string.servicestall_channel_expl,
NotificationManager.IMPORTANCE_LOW);
public enum ID {
NBSPROXY( R.string.nbsproxy_channel_expl )
,GAME_EVENT( R.string.gameevent_channel_expl )
,SERVICE_STALL( R.string.servicestall_channel_expl )
,DUP_TIMER_RUNNING( R.string.dup_timer_expl )
,DUP_PAUSED( R.string.dup_paused_expl )
;
private int mExpl;
private int mImportance;
@ -45,7 +50,15 @@ public class Channels {
mImportance = imp;
}
private ID( int expl )
{
this( expl, NotificationManager.IMPORTANCE_LOW );
}
public int getDesc() { return mExpl; }
public int idFor( long rowid ) {
return notificationId( rowid, this );
}
private int getImportance() { return mImportance; }
}
@ -71,4 +84,63 @@ public class Channels {
}
return name;
}
private static final String IDS_KEY = TAG + "/ids_key";
private static class IdsData implements Serializable {
HashMap<ID, HashMap<Long, Integer>> mMap = new HashMap<>();
HashSet<Integer> mInts = new HashSet<>();
int newID()
{
int result;
for ( ; ; ) {
int one = Utils.nextRandomInt();
if ( !mInts.contains( one ) ) {
mInts.add( one );
result = one;
break;
}
}
return result;
}
}
private static IdsData sData;
// I want each rowid to be able to have a notification active for it for
// each channel. So let's try generating and storing random ints.
private static int notificationId( long rowid, ID channel )
{
Context context = XWApp.getContext();
int result;
synchronized ( Channels.class ) {
if ( null == sData ) {
sData = (IdsData)DBUtils.getSerializableFor( context, IDS_KEY );
if ( null == sData ) {
sData = new IdsData();
}
}
}
synchronized ( sData ) {
boolean dirty = false;
if ( ! sData.mMap.containsKey( channel ) ) {
sData.mMap.put( channel, new HashMap<Long, Integer>() );
dirty = true;
}
Map<Long, Integer> map = sData.mMap.get( channel );
if ( ! map.containsKey( rowid ) ) {
map.put( rowid, sData.newID() );
dirty = true;
}
if ( dirty ) {
DBUtils.setSerializableFor( context, IDS_KEY, sData );
}
result = map.get( rowid );
}
Log.d( TAG, "notificationId(%s, %d) => %d", channel, rowid, result );
return result;
}
}

View file

@ -0,0 +1,165 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
* Copyright 2019 by Eric House (xwords@eehouse.org). All
* rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.eehouse.android.xw4;
import android.text.Editable;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import java.util.HashSet;
// Edit text should start out empty
public class ConfirmPauseView extends LinearLayout
implements View.OnClickListener, OnItemSelectedListener, EditWClear.TextWatcher {
private static final String TAG = ConfirmPauseView.class.getSimpleName();
private static final String PAUSE_MSGS_KEY = TAG + "/pause_msgs";
private static final String UNPAUSE_MSGS_KEY = TAG + "/unpause_msgs";
private Boolean mIsPause;
private boolean mInflateFinished;
private boolean mInited;
private HashSet<String> mSavedMsgs;
private Button mForgetButton;
private Button mRememberButton;
private Spinner mSpinner;
private EditWClear mMsgEdit;
public ConfirmPauseView( Context context, AttributeSet as ) {
super( context, as );
}
@Override
protected void onFinishInflate()
{
mInflateFinished = true;
initIfReady();
}
private void initIfReady()
{
if ( !mInited && mInflateFinished && null != mIsPause ) {
mInited = true;
Context context = getContext();
int id = mIsPause ? R.string.pause_expl : R.string.unpause_expl;
((TextView)findViewById(R.id.confirm_pause_expl))
.setText( id );
mForgetButton = (Button)findViewById( R.id.pause_forget_msg );
mForgetButton.setOnClickListener( this );
mRememberButton = (Button)findViewById( R.id.pause_save_msg );
mRememberButton.setOnClickListener( this );
mSpinner = (Spinner)findViewById( R.id.saved_msgs );
mMsgEdit = (EditWClear)findViewById( R.id.msg_edit );
mMsgEdit.addTextChangedListener( this );
String key = mIsPause ? PAUSE_MSGS_KEY : UNPAUSE_MSGS_KEY;
mSavedMsgs = (HashSet<String>)DBUtils
.getSerializableFor( context, key );
if ( null == mSavedMsgs ) {
mSavedMsgs = new HashSet<>();
}
populateSpinner();
mSpinner.setOnItemSelectedListener( this );
setMsg( "" );
// onTextChanged( "" );
}
}
private void populateSpinner()
{
final ArrayAdapter<String> adapter =
new ArrayAdapter<>( getContext(), android.R.layout.simple_spinner_item );
for ( String msg : mSavedMsgs ) {
adapter.add( msg );
}
mSpinner.setAdapter( adapter );
}
@Override
public void onItemSelected( AdapterView<?> parent, View spinner,
int position, long id )
{
String msg = (String)parent.getAdapter().getItem( position );
setMsg( msg );
onTextChanged( msg );
}
@Override
public void onNothingSelected( AdapterView<?> p ) {}
@Override
public void onClick( View view )
{
Log.d( TAG, "onClick() called" );
String msg = getMsg();
if ( view == mRememberButton && 0 < msg.length() ) {
mSavedMsgs.add( msg );
} else if ( view == mForgetButton ) {
mSavedMsgs.remove( msg );
setMsg( "" );
} else {
Assert.assertFalse( BuildConfig.DEBUG );
}
String key = mIsPause ? PAUSE_MSGS_KEY : UNPAUSE_MSGS_KEY;
DBUtils.setSerializableFor( getContext(), key, mSavedMsgs );
populateSpinner();
}
// from EditWClear.TextWatcher
@Override
public void onTextChanged( String msg )
{
Log.d( TAG, "onTextChanged(%s)", msg );
boolean hasText = 0 < msg.length();
boolean matches = mSavedMsgs.contains( msg );
mForgetButton.setEnabled( hasText && matches );
mRememberButton.setEnabled( hasText && !matches );
}
ConfirmPauseView setIsPause( boolean isPause )
{
mIsPause = isPause;
initIfReady();
return this;
}
String getMsg()
{
return mMsgEdit.getText().toString();
}
private void setMsg( String msg )
{
mMsgEdit.setText( msg );
}
}

View file

@ -1,6 +1,6 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
* Copyright 2009-2016 by Eric House (xwords@eehouse.org). All
* Copyright 2009 - 2019 by Eric House (xwords@eehouse.org). All
* rights reserved.
*
* This program is free software; you can redistribute it and/or
@ -56,7 +56,7 @@ public class DBHelper extends SQLiteOpenHelper {
private int addedVersion() { return mAddedVersion; }
}
private static final String DB_NAME = BuildConfig.DB_NAME;
private static final int DB_VERSION = 29;
private static final int DB_VERSION = 30;
public static final String GAME_NAME = "GAME_NAME";
public static final String VISID = "VISID";
@ -90,6 +90,7 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String SEED = "SEED";
public static final String SMSPHONE = "SMSPHONE"; // unused -- so far
public static final String LASTMOVE = "LASTMOVE";
public static final String NEXTDUPTIMER = "NEXTDUPTIMER";
public static final String NEXTNAG = "NEXTNAG";
public static final String GROUPID = "GROUPID";
public static final String NPACKETSPENDING = "NPACKETSPENDING";
@ -159,6 +160,7 @@ public class DBHelper extends SQLiteOpenHelper {
,{ REMOTEDEVS, "TEXT" }
,{ EXTRAS, "TEXT" } // json data, most likely
,{ LASTMOVE, "INTEGER DEFAULT 0" }
,{ NEXTDUPTIMER, "INTEGER DEFAULT 0" }
,{ NEXTNAG, "INTEGER DEFAULT 0" }
,{ GROUPID, "INTEGER" }
// HASMSGS: sqlite doesn't have bool; use 0 and 1
@ -347,6 +349,10 @@ public class DBHelper extends SQLiteOpenHelper {
if ( !madeChatTable ) {
addColumn( db, TABLE_NAMES.CHAT, s_chatsSchema, CHATTIME );
}
case 29:
if ( !madeSumTable ) {
addSumColumn( db, NEXTDUPTIMER );
}
break;
default:

View file

@ -85,7 +85,8 @@ public class DBUtils {
};
public static interface DBChangeListener {
public void gameSaved( long rowid, GameChangeType change );
public void gameSaved( Context context, long rowid,
GameChangeType change );
}
private static HashSet<DBChangeListener> s_listeners =
new HashSet<DBChangeListener>();
@ -146,7 +147,7 @@ public class DBUtils {
DBHelper.SCORES,
DBHelper.LASTPLAY_TIME, DBHelper.REMOTEDEVS,
DBHelper.LASTMOVE, DBHelper.NPACKETSPENDING,
DBHelper.EXTRAS,
DBHelper.EXTRAS, DBHelper.NEXTDUPTIMER,
};
String selection = String.format( ROW_ID_FMT, lock.getRowid() );
@ -194,6 +195,8 @@ public class DBUtils {
summary.gameOver = tmp != 0;
summary.lastMoveTime =
cursor.getInt(cursor.getColumnIndex(DBHelper.LASTMOVE));
summary.dupTimerExpires =
cursor.getInt(cursor.getColumnIndex(DBHelper.NEXTDUPTIMER));
String str = cursor
.getString(cursor.getColumnIndex(DBHelper.EXTRAS));
summary.setExtras( str );
@ -298,6 +301,7 @@ public class DBUtils {
values.put( DBHelper.GAMEID, summary.gameID );
values.put( DBHelper.GAME_OVER, summary.gameOver? 1 : 0 );
values.put( DBHelper.LASTMOVE, summary.lastMoveTime );
values.put( DBHelper.NEXTDUPTIMER, summary.dupTimerExpires );
// Don't overwrite extras! Sometimes this method is called from
// JNIThread which has created the summary from common code that
@ -357,7 +361,7 @@ public class DBUtils {
long result = update( TABLE_NAMES.SUM, values, selection );
Assert.assertTrue( result >= 0 );
}
notifyListeners( rowid, GameChangeType.GAME_CHANGED );
notifyListeners( context, rowid, GameChangeType.GAME_CHANGED );
invalGroupsCache();
}
@ -663,10 +667,10 @@ public class DBUtils {
updateRow( null, TABLE_NAMES.SUM, rowid, values );
}
public static void setMsgFlags( long rowid, int flags )
public static void setMsgFlags( Context context, long rowid, int flags )
{
setSummaryInt( rowid, DBHelper.HASMSGS, flags );
notifyListeners( rowid, GameChangeType.GAME_CHANGED );
notifyListeners( context, rowid, GameChangeType.GAME_CHANGED );
}
public static void setExpanded( long rowid, boolean expanded )
@ -730,7 +734,7 @@ public class DBUtils {
Assert.assertTrue( result >= 0 );
notifyListeners( rowid, GameChangeType.GAME_CHANGED );
notifyListeners( context, rowid, GameChangeType.GAME_CHANGED );
}
}
@ -742,7 +746,7 @@ public class DBUtils {
synchronized( s_dbHelper ) {
long result = update( TABLE_NAMES.SUM, values, null );
notifyListeners( ROWIDS_ALL, GameChangeType.GAME_CHANGED );
notifyListeners( context, ROWIDS_ALL, GameChangeType.GAME_CHANGED );
}
}
@ -1066,7 +1070,7 @@ public class DBUtils {
lock = GameLock.tryLock( rowid );
Assert.assertNotNull( lock );
notifyListeners( rowid, GameChangeType.GAME_CREATED );
notifyListeners( context, rowid, GameChangeType.GAME_CREATED );
}
invalGroupsCache(); // then again after
@ -1092,7 +1096,7 @@ public class DBUtils {
setCached( rowid, null ); // force reread
if ( ROWID_NOTFOUND != rowid ) { // Means new game?
notifyListeners( rowid, GameChangeType.GAME_CHANGED );
notifyListeners( context, rowid, GameChangeType.GAME_CHANGED );
}
invalGroupsCache();
return rowid;
@ -1154,7 +1158,7 @@ public class DBUtils {
deleteCurChatsSync( s_db, rowid );
}
notifyListeners( lock.getRowid(), GameChangeType.GAME_DELETED );
notifyListeners( context, lock.getRowid(), GameChangeType.GAME_DELETED );
invalGroupsCache();
}
@ -1775,7 +1779,47 @@ public class DBUtils {
values.put( DBHelper.GROUPID, groupID );
updateRow( context, TABLE_NAMES.SUM, rowid, values );
invalGroupsCache();
notifyListeners( rowid, GameChangeType.GAME_MOVED );
notifyListeners( context, rowid, GameChangeType.GAME_MOVED );
}
public static Map<Long, Integer> getDupModeGames( Context context )
{
return getDupModeGames( context, ROWID_NOTFOUND );
}
// Return all games whose DUP_MODE_MASK bit is set. Return also (as map
// value) the nextTimer value, which will be negative if the game's
// paused. As a bit of a hack, set it to 0 if the local player has already
// committed his turn so caller (DupeModeTimer) will know not to show a
// notification.
public static Map<Long, Integer> getDupModeGames( Context context, long rowid )
{
// select giflags from summaries where 0x100 & giflags != 0;
Map<Long, Integer> result = new HashMap<>();
String[] columns = { ROW_ID, DBHelper.NEXTDUPTIMER, DBHelper.TURN_LOCAL };
String selection = String.format( "%d & %s != 0",
GameSummary.DUP_MODE_MASK,
DBHelper.GIFLAGS );
if ( ROWID_NOTFOUND != rowid ) {
selection += String.format( " AND %s = %d", ROW_ID, rowid );
}
initDB( context );
synchronized( s_dbHelper ) {
Cursor cursor = query( TABLE_NAMES.SUM, columns, selection );
int count = cursor.getCount();
int indxRowid = cursor.getColumnIndex( ROW_ID );
int indxTimer = cursor.getColumnIndex( DBHelper.NEXTDUPTIMER );
int indxIsLocal = cursor.getColumnIndex( DBHelper.TURN_LOCAL );
while ( cursor.moveToNext() ) {
boolean isLocal = 0 != cursor.getInt( indxIsLocal );
int timer = isLocal ? cursor.getInt( indxTimer ) : 0;
result.put( cursor.getLong( indxRowid ), timer );
}
cursor.close();
}
Log.d( TAG, "getDupModeGames(%d) => %s", rowid, result );
return result;
}
private static String getChatHistoryStr( Context context, long rowid )
@ -2592,12 +2636,13 @@ public class DBUtils {
}
}
private static void notifyListeners( long rowid, GameChangeType change )
private static void notifyListeners( Context context, long rowid,
GameChangeType change )
{
synchronized( s_listeners ) {
Iterator<DBChangeListener> iter = s_listeners.iterator();
while ( iter.hasNext() ) {
iter.next().gameSaved( rowid, change );
iter.next().gameSaved( context, rowid, change );
}
}
}

View file

@ -99,18 +99,23 @@ public class DbgUtils {
Log.d( tag, stackTrace );
}
static String extrasToString( Intent intent )
static String extrasToString( Bundle extras )
{
Bundle bundle = intent.getExtras();
ArrayList<String> al = new ArrayList<String>();
if ( null != bundle ) {
for ( String key : bundle.keySet() ) {
al.add( key + ":" + bundle.get(key) );
if ( null != extras ) {
for ( String key : extras.keySet() ) {
al.add( key + ":" + extras.get(key) );
}
}
return TextUtils.join( ", ", al );
}
static String extrasToString( Intent intent )
{
Bundle bundle = intent.getExtras();
return extrasToString( bundle );
}
public static void dumpCursor( Cursor cursor )
{
String dump = DatabaseUtils.dumpCursorToString( cursor );

View file

@ -45,8 +45,8 @@ import java.util.Set;
public class DictLangCache {
private static final String TAG = DictLangCache.class.getSimpleName();
private static String[] s_langNames;
private static HashMap<String, Integer> s_langCodes;
private static Map<Integer, String> s_langNames;
private static Map<String, Integer> s_langCodes;
private static int s_adaptedLang = -1;
private static LangsArrayAdapter s_langsAdapter;
@ -151,11 +151,12 @@ public class DictLangCache {
public static String getLangName( Context context, int code )
{
String[] namesArray = getLangNames( context );
if ( code < 0 || code >= namesArray.length ) {
code = 0;
Map<Integer, String> namesArray = getLangNames( context );
String name = namesArray.get( code );
if ( null == name ) {
name = namesArray.get( 0 );
}
return namesArray[code];
return name;
}
// This populates the cache and will take significant time if it's
@ -248,7 +249,7 @@ public class DictLangCache {
for ( DictAndLoc dal : dals ) {
DictInfo info = getInfo( context, dal );
int langCode = info.langCode;
if ( langCode >= s_langNames.length ) {
if ( !s_langNames.containsKey( langCode ) ) {
langCode = 0;
}
if ( null != info && code == langCode ) {
@ -305,13 +306,11 @@ public class DictLangCache {
public static int getLangLangCode( Context context, String lang )
{
int code = 0;
String[] namesArray = getLangNames( context );
for ( int ii = 0; ii < namesArray.length; ++ii ) {
if ( namesArray[ii].equals( lang ) ) {
code = ii;
break;
}
getLangNames( context ); /* inits s_langCodes */
Integer code = s_langCodes.get( lang );
if ( null == code ) {
code = 0;
}
return code;
}
@ -431,16 +430,24 @@ public class DictLangCache {
return s_dictsAdapter;
}
public static String[] getLangNames( Context context )
private static Map<Integer, String> getLangNames( Context context )
{
if ( null == s_langNames ) {
Resources res = context.getResources();
s_langNames = res.getStringArray( R.array.language_names );
String[] names = res.getStringArray( R.array.language_names );
s_langCodes = new HashMap<String, Integer>();
for ( int ii = 0; ii < s_langNames.length; ++ii ) {
s_langCodes.put( s_langNames[ii], ii );
s_langCodes = new HashMap<>();
s_langNames = new HashMap<>();
for ( int ii = 0; ii < names.length; ++ii ) {
String name = names[ii];
s_langCodes.put( name, ii );
s_langNames.put( ii, name );
}
// Hex is out-of-order, so can't be in the res-based array. Hard
// code it: it's a hack anyway.
s_langCodes.put( "Hex", 127 );
s_langNames.put( 127, "Hex" );
}
return s_langNames;
}

View file

@ -503,7 +503,7 @@ public class DictsDelegate extends ListDelegateBase
int lang = args.getInt( DICT_LANG_EXTRA, 0 );
if ( 0 < lang ) {
m_filterLang = DictLangCache.getLangNames( m_activity )[lang];
m_filterLang = DictLangCache.getLangName( m_activity, lang );
m_closedLangs.remove( m_filterLang );
}
String name = args.getString( DICT_NAME_EXTRA );

View file

@ -67,6 +67,7 @@ public enum DlgID {
, GAMES_LIST_NEWGAME
, CHANGE_CONN
, GAMES_LIST_NAME_REMATCH
, ASK_DUP_PAUSE
;
private boolean m_addToStack;

View file

@ -0,0 +1,294 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
* Copyright 2019 by Eric House (xwords@eehouse.org). All rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.eehouse.android.xw4;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.SystemClock;
import java.text.DateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.eehouse.android.xw4.DBUtils.GameChangeType;
import org.eehouse.android.xw4.jni.CurGameInfo;
import org.eehouse.android.xw4.jni.JNIThread;
import org.eehouse.android.xw4.jni.XwJNI.GamePtr;
import org.eehouse.android.xw4.jni.XwJNI;
import org.eehouse.android.xw4.loc.LocUtils;
/**
* This class owns the problem of timers in duplicate-mode games. Unlike the
* existing timers that run only when a game is open and visible, they run
* with the clock for any game where it's a local player's turn. So this
* module needs to be aware of all games in that state and to be counting
* their timers down at all times. For each game for which a timer's running
* it's either 1) sending updates to the game (if it's open) OR 2) keeping an
* unhideable notification open with the relevant time counting down.
*/
public class DupeModeTimer extends BroadcastReceiver {
private static final String TAG = DupeModeTimer.class.getSimpleName();
private static final Channels.ID sMyChannel = Channels.ID.DUP_TIMER_RUNNING;
private static RowidQueue sQueue;
private static Map<Long, Integer> sDirtyVals = new HashMap<>();
private static DateFormat s_df =
DateFormat.getTimeInstance( /*DateFormat.SHORT*/ );
private static long sCurTimer = Long.MAX_VALUE;
static {
sQueue = new RowidQueue();
sQueue.start();
DBUtils.setDBChangeListener( new DBUtils.DBChangeListener() {
@Override
public void gameSaved( Context context, long rowid,
GameChangeType change )
{
Log.d( TAG, "gameSaved(rowid=%d,change=%s) called", rowid, change );
switch( change ) {
case GAME_CHANGED:
case GAME_CREATED:
synchronized ( sDirtyVals ) {
if ( sDirtyVals.containsKey( rowid ) ) {
sQueue.addOne( context, rowid );
} else {
Log.d( TAG, "skipping; not dirty" );
}
}
break;
case GAME_DELETED:
cancelNotification( context, rowid );
break;
}
}
} );
}
@Override
public void onReceive( Context context, Intent intent )
{
Log.d( TAG, "onReceive()" );
sCurTimer = Long.MAX_VALUE; // clear so we'll set again
sQueue.addAll( context );
}
/**
* Called when
*/
static void init( Context context )
{
Log.d( TAG, "init()" );
sQueue.addAll( context );
}
public static void gameOpened( Context context, long rowid )
{
Log.d( TAG, "gameOpened(%s, %d)", context, rowid );
sQueue.addOne( context, rowid );
}
public static void gameClosed( Context context, long rowid )
{
Log.d( TAG, "gameClosed(%s, %d)", context, rowid );
sQueue.addOne( context, rowid );
}
// public static void timerPauseChanged( Context context, long rowid )
// {
// sQueue.addOne( context, rowid );
// }
public static void timerChanged( Context context, int gameID, int newVal )
{
long[] rowids = DBUtils.getRowIDsFor( context, gameID );
for ( long rowid : rowids ) {
Log.d( TAG, "timerChanged(rowid=%d, newVal=%d)", rowid, newVal );
synchronized ( sDirtyVals ) {
sDirtyVals.put( rowid, newVal );
}
}
}
private static void postNotification( Context context, long rowid, long when )
{
Log.d( TAG, "postNotification(rowid=%d)", rowid );
if ( !JNIThread.gameIsOpen( rowid ) ) {
String title = LocUtils.getString( context, R.string.dup_notif_title );
if ( BuildConfig.DEBUG ) {
title += " (" + rowid + ")";
}
String body = context.getString( R.string.dup_notif_title_fmt,
s_df.format( new Date( 1000 * when ) ) );
Intent intent = GamesListDelegate.makeRowidIntent( context, rowid );
Intent pauseIntent = GamesListDelegate.makeRowidIntent( context, rowid );
pauseIntent.putExtra( BoardDelegate.PAUSER_KEY, true );
Utils.postOngoingNotification( context, intent, title, body,
rowid, sMyChannel,
pauseIntent, R.string.board_menu_game_pause );
} else {
Log.d( TAG, "postOngoingNotification(%d): open, so skipping", rowid );
}
}
private static void cancelNotification( Context context, long rowid )
{
Log.d( TAG, "cancelNotification(rowid=%d)", rowid );
Utils.cancelNotification( context, sMyChannel, rowid );
}
private static void setTimer( Context context, long whenSeconds )
{
if ( whenSeconds < sCurTimer ) {
sCurTimer = whenSeconds;
Intent intent = new Intent( context, DupeModeTimer.class );
PendingIntent pi = PendingIntent.getBroadcast( context, 0, intent, 0 );
long now = Utils.getCurSeconds();
long fire_millis = SystemClock.elapsedRealtime()
+ (1000 * (whenSeconds - now));
((AlarmManager)context.getSystemService( Context.ALARM_SERVICE ))
.set( AlarmManager.ELAPSED_REALTIME, fire_millis, pi );
}
}
private static class RowidQueue extends Thread {
private Set<Long> mSet = new HashSet<>();
private Context mContext;
void addAll( Context context )
{
addOne( context, 0 );
}
synchronized void addOne( Context context, long rowid )
{
mContext = context;
synchronized ( mSet ) {
mSet.add( rowid );
mSet.notify();
}
}
@Override
public void run()
{
long rowid = DBUtils.ROWID_NOTFOUND;
for ( ; ; ) {
synchronized( mSet ) {
mSet.remove( rowid );
if ( 0 == mSet.size() ) {
try {
mSet.wait();
Assert.assertTrue( 0 < mSet.size() );
} catch ( InterruptedException ie ) {
break;
}
}
rowid = mSet.iterator().next();
}
inventoryGames( rowid );
}
}
private void inventoryGames( long onerow )
{
Log.d( TAG, "inventoryGames(%d)", onerow );
Map<Long, Integer> dupeGames = onerow == 0
? DBUtils.getDupModeGames( mContext )
: DBUtils.getDupModeGames( mContext, onerow );
Log.d( TAG, "inventoryGames(%s)", dupeGames );
long now = Utils.getCurSeconds();
long minTimer = sCurTimer;
for ( long rowid : dupeGames.keySet() ) {
int timerFires = dupeGames.get( rowid );
synchronized ( sDirtyVals ) {
if ( sDirtyVals.containsKey(rowid) && timerFires == sDirtyVals.get(rowid) ) {
sDirtyVals.remove(rowid);
}
}
if ( timerFires > now ) {
Log.d( TAG, "found dupe game with %d seconds left",
timerFires - now );
postNotification( mContext, rowid, timerFires );
if ( timerFires < minTimer ) {
minTimer = timerFires;
}
} else {
cancelNotification( mContext, rowid );
Log.d( TAG, "found dupe game with expired or inactive timer" );
if ( timerFires > 0 ) {
giveGameTime( rowid );
}
}
}
setTimer( mContext, minTimer );
}
private void giveGameTime( long rowid )
{
Log.d( TAG, "giveGameTime(%d)() starting", rowid );
try ( GameLock lock = GameLock.tryLock( rowid ) ) {
if ( null != lock ) {
CurGameInfo gi = new CurGameInfo( mContext );
MultiMsgSink sink = new MultiMsgSink( mContext, rowid );
try ( final XwJNI.GamePtr gamePtr = GameUtils
.loadMakeGame( mContext, gi, sink, lock ) ) { // calls getJNI()
Log.d( TAG, "got gamePtr: %H", gamePtr );
if ( null != gamePtr ) {
boolean draw = false;
for ( int ii = 0; ii < 3; ++ii ) {
draw = XwJNI.server_do( gamePtr ) || draw;
}
GameUtils.saveGame( mContext, gamePtr, gi, lock, false );
if ( draw && XWPrefs.getThumbEnabled( mContext ) ) {
Bitmap bitmap = GameUtils
.takeSnapshot( mContext, gamePtr, gi );
DBUtils.saveThumbnail( mContext, lock, bitmap );
}
}
}
}
}
Log.d( TAG, "giveGameTime(%d)() DONE", rowid );
}
}
}

View file

@ -23,14 +23,33 @@ import android.widget.SearchView;
import android.content.Context;
import android.util.AttributeSet;
public class EditWClear extends SearchView {
import java.util.HashSet;
import java.util.Set;
public class EditWClear extends SearchView
implements SearchView.OnQueryTextListener {
private static final String TAG = EditWClear.class.getSimpleName();
private Set<TextWatcher> mWatchers;
public interface TextWatcher {
void onTextChanged( String newText );
}
public EditWClear( Context context, AttributeSet as )
{
super( context, as );
}
synchronized void addTextChangedListener( TextWatcher proc )
{
if ( null == mWatchers ) {
mWatchers = new HashSet<>();
setOnQueryTextListener( this );
}
mWatchers.add( proc );
}
void setText( String txt )
{
super.setQuery( txt, false );
@ -40,4 +59,22 @@ public class EditWClear extends SearchView {
{
return super.getQuery();
}
// from SearchView.OnQueryTextListener
@Override
public synchronized boolean onQueryTextChange( String newText )
{
for ( TextWatcher proc : mWatchers ) {
proc.onTextChanged( newText );
}
return true;
}
// from SearchView.OnQueryTextListener
@Override
public boolean onQueryTextSubmit( String query )
{
Assert.assertFalse( BuildConfig.DEBUG );
return true;
}
}

View file

@ -123,6 +123,7 @@ public class GameConfigDelegate extends DelegateBase
R.id.room_spinner,
R.id.refresh_button,
R.id.hints_allowed,
R.id.duplicate_check,
R.id.pick_faceup,
R.id.boardsize_spinner,
R.id.use_timer,
@ -620,26 +621,66 @@ public class GameConfigDelegate extends DelegateBase
setSmartnessSpinner();
tweakTimerStuff();
setChecked( R.id.hints_allowed, !m_gi.hintsNotAllowed );
setChecked( R.id.pick_faceup, m_gi.allowPickTiles );
setInt( R.id.timer_minutes_edit,
m_gi.gameSeconds/60/m_gi.nPlayers );
setBoardsizeSpinner();
}
} // loadGame
private boolean mTimerStuffInited = false;
private void tweakTimerStuff()
{
// one-time only stuff
if ( ! mTimerStuffInited ) {
mTimerStuffInited = true;
// dupe-mode check is GONE by default (in the .xml)
if ( CommonPrefs.getDupModeHidden( m_activity ) ) {
setChecked( R.id.duplicate_check, false );
} else {
CheckBox check = (CheckBox)findViewById( R.id.duplicate_check );
check.setVisibility( View.VISIBLE );
check.setChecked( m_gi.inDuplicateMode );
check.setOnCheckedChangeListener( new OnCheckedChangeListener() {
@Override
public void onCheckedChanged( CompoundButton buttonView,
boolean checked ) {
tweakTimerStuff();
}
} );
}
CheckBox check = (CheckBox)findViewById( R.id.use_timer );
OnCheckedChangeListener lstnr =
new OnCheckedChangeListener() {
public void onCheckedChanged( CompoundButton buttonView,
boolean checked ) {
showTimerSet( checked );
tweakTimerStuff();
}
};
check.setOnCheckedChangeListener( lstnr );
setChecked( R.id.use_timer, m_gi.timerEnabled );
showTimerSet( m_gi.timerEnabled );
setBoardsizeSpinner();
check.setChecked( m_gi.timerEnabled );
}
} // loadGame
boolean dupModeChecked = getChecked( R.id.duplicate_check );
CheckBox check = (CheckBox)findViewById( R.id.use_timer );
check.setText( dupModeChecked ? R.string.use_duptimer : R.string.use_timer );
boolean timerSet = getChecked( R.id.use_timer );
showTimerSet( timerSet );
int id = dupModeChecked ? R.string.dup_minutes_label : R.string.minutes_label;
TextView label = (TextView)findViewById(R.id.timer_label );
label.setText( id );
// setInt( R.id.timer_minutes_edit,
// m_gi.gameSeconds/60/m_gi.nPlayers );
// setChecked( R.id.use_timer, m_gi.timerEnabled );
// showTimerSet( m_gi.timerEnabled );
}
private void showTimerSet( boolean show )
{
@ -852,7 +893,7 @@ public class GameConfigDelegate extends DelegateBase
private void deleteGame()
{
GameUtils.deleteGame( m_activity, m_rowid, false );
GameUtils.deleteGame( m_activity, m_rowid, false, false );
}
private void loadPlayersList()
@ -1199,11 +1240,19 @@ public class GameConfigDelegate extends DelegateBase
}
}
m_gi.inDuplicateMode = getChecked( R.id.duplicate_check );
m_gi.hintsNotAllowed = !getChecked( R.id.hints_allowed );
m_gi.allowPickTiles = getChecked( R.id.pick_faceup );
m_gi.timerEnabled = getChecked( R.id.use_timer );
m_gi.gameSeconds =
60 * m_gi.nPlayers * getInt( R.id.timer_minutes_edit );
// Get timer value. It's per-move minutes in duplicate mode, otherwise
// it's for the whole game.
int seconds = 60 * getInt( R.id.timer_minutes_edit );
if ( m_gi.inDuplicateMode ) {
m_gi.gameSeconds = seconds;
} else {
m_gi.gameSeconds = seconds * m_gi.nPlayers;
}
int position = m_phoniesSpinner.getSelectedItemPosition();
m_gi.phoniesAction = CurGameInfo.XWPhoniesChoice.values()[position];

View file

@ -348,6 +348,9 @@ public class GameListItem extends LinearLayout
m_role.setText( roleSummary );
}
findViewById( R.id.dup_tag )
.setVisibility( summary.inDuplicateMode() ? View.VISIBLE : View.GONE );
update( expanded, summary.lastMoveTime, haveATurn,
haveALocalTurn );
}

View file

@ -179,7 +179,7 @@ public class GameUtils {
tellDied( context, lock, true );
resetGame( context, lock, lock, DBUtils.GROUPID_UNSPEC, false );
Utils.cancelNotification( context, (int)rowidIn );
Utils.cancelNotification( context, rowidIn );
success = true;
} else {
DbgUtils.toastNoLock( TAG, context, rowidIn,
@ -299,11 +299,13 @@ public class GameUtils {
}
public static void deleteGame( Context context, GameLock lock,
boolean informNow )
boolean informNow, boolean skipTell )
{
if ( null != lock ) {
tellDied( context, lock, informNow );
Utils.cancelNotification( context, (int)lock.getRowid() );
if ( !skipTell ) {
tellDied( context, lock, informNow );
}
Utils.cancelNotification( context, lock.getRowid() );
DBUtils.deleteGame( context, lock );
} else {
Log.e( TAG, "deleteGame(): null lock; doing nothing" );
@ -311,13 +313,13 @@ public class GameUtils {
}
public static boolean deleteGame( Context context, long rowid,
boolean informNow )
boolean informNow, boolean skipTell )
{
boolean success;
// does this need to be synchronized?
try ( GameLock lock = GameLock.tryLock( rowid ) ) {
if ( null != lock ) {
deleteGame( context, lock, informNow );
deleteGame( context, lock, informNow, skipTell );
success = true;
} else {
DbgUtils.toastNoLock( TAG, context, rowid,
@ -334,7 +336,7 @@ public class GameUtils {
int nSuccesses = 0;
long[] rowids = DBUtils.getGroupGames( context, groupid );
for ( int ii = rowids.length - 1; ii >= 0; --ii ) {
if ( deleteGame( context, rowids[ii], ii == 0 ) ) {
if ( deleteGame( context, rowids[ii], ii == 0, false ) ) {
++nSuccesses;
}
}
@ -938,8 +940,18 @@ public class GameUtils {
public static void launchGame( Delegator delegator, long rowid,
boolean invited )
{
launchGame( delegator, rowid, invited, null );
}
public static void launchGame( Delegator delegator, long rowid,
boolean invited, Bundle moreExtras )
{
Bundle extras = makeLaunchExtras( rowid, invited );
if ( null != moreExtras ) {
extras.putAll( moreExtras );
}
if ( delegator.inDPMode() ) {
delegator.addFragment( BoardFrag.newInstance( delegator ), extras );
} else {
@ -1055,7 +1067,7 @@ public class GameUtils {
if ( GameSummary.MSG_FLAGS_NONE != flags ) {
draw = true;
int curFlags = DBUtils.getMsgFlags( context, rowid );
DBUtils.setMsgFlags( rowid, flags | curFlags );
DBUtils.setMsgFlags( context, rowid, flags | curFlags );
}
}
}
@ -1245,7 +1257,7 @@ public class GameUtils {
if ( 0 != titleID ) {
String title = LocUtils.getString( context, titleID,
getName( context, rowid ) );
Utils.postNotification( context, intent, title, msg, (int)rowid );
Utils.postNotification( context, intent, title, msg, rowid );
}
} else {
Log.d( TAG, "postMoveNotification(): posting nothing for lack"
@ -1258,7 +1270,7 @@ public class GameUtils {
{
Intent intent = GamesListDelegate.makeGameIDIntent( context, gameID );
Utils.postNotification( context, intent, R.string.invite_notice_title,
body, (int)rowid );
body, rowid );
}
private static void tellDied( Context context, GameLock lock,

View file

@ -1,7 +1,7 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
* Copyright 2009 - 2016 by Eric House (xwords@eehouse.org). All
* rights reserved.
* Copyright 2009 - 2019 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@ -1012,6 +1012,17 @@ public class GamesListDelegate extends ListDelegateBase
} else if ( isFirstLaunch ) {
warnSMSBannedIf();
}
Set<Long> dupModeGames = DBUtils.getDupModeGames( m_activity ).keySet();
long[] asArray = new long[dupModeGames.size()];
int ii = 0;
for ( long rowid : dupModeGames ) {
Log.d( TAG, "row %d is dup-mode", rowid );
asArray[ii++] = rowid;
}
if ( false ) {
deleteGames( asArray, true );
}
} // init
@Override
@ -1172,7 +1183,9 @@ public class GamesListDelegate extends ListDelegateBase
//////////////////////////////////////////////////////////////////////
// DBUtils.DBChangeListener interface
//////////////////////////////////////////////////////////////////////
public void gameSaved( final long rowid, final GameChangeType change )
@Override
public void gameSaved( Context context, final long rowid,
final GameChangeType change )
{
post( new Runnable() {
public void run() {
@ -1324,7 +1337,7 @@ public class GamesListDelegate extends ListDelegateBase
mkListAdapter();
break;
case DELETE_GAMES:
deleteGames( (long[])params[0] );
deleteGames( (long[])params[0], false );
break;
case OPEN_GAME:
doOpenGame( params );
@ -2140,7 +2153,7 @@ public class GamesListDelegate extends ListDelegateBase
return launched;
}
private boolean startFirstHasDict( final long rowid )
private boolean startFirstHasDict( final long rowid, final Bundle extras )
{
boolean handled = -1 != rowid && DBUtils.haveGame( m_activity, rowid );
if ( handled ) {
@ -2154,7 +2167,7 @@ public class GamesListDelegate extends ListDelegateBase
.gameDictsHere( m_activity, lock );
lock.release();
if ( haveDict ) {
launchGame( rowid );
launchGame( rowid, extras );
}
}
}
@ -2171,7 +2184,7 @@ public class GamesListDelegate extends ListDelegateBase
String[] relayIDs = intent.getStringArrayExtra( RELAYIDS_EXTRA );
if ( !startFirstHasDict( relayIDs ) ) {
long rowid = intent.getLongExtra( ROWID_EXTRA, -1 );
result = startFirstHasDict( rowid );
result = startFirstHasDict( rowid, intent.getExtras() );
}
}
return result;
@ -2455,10 +2468,10 @@ public class GamesListDelegate extends ListDelegateBase
}
}
private void deleteGames( long[] rowids )
private void deleteGames( long[] rowids, boolean skipTell )
{
for ( long rowid : rowids ) {
GameUtils.deleteGame( m_activity, rowid, false );
GameUtils.deleteGame( m_activity, rowid, false, skipTell );
m_mySIS.selGames.remove( rowid );
}
invalidateOptionsMenuIf();
@ -2547,7 +2560,7 @@ public class GamesListDelegate extends ListDelegateBase
return madeGame;
}
private void launchGame( long rowid, boolean invited )
private void launchGame( long rowid, boolean invited, Bundle extras )
{
if ( DBUtils.ROWID_NOTFOUND == rowid ) {
Log.d( TAG, "launchGame(): dropping bad rowid" );
@ -2556,20 +2569,25 @@ public class GamesListDelegate extends ListDelegateBase
if ( m_adapter.inExpandedGroup( rowid ) ) {
setSelGame( rowid );
}
GameUtils.launchGame( getDelegator(), rowid, invited );
GameUtils.launchGame( getDelegator(), rowid, invited, extras );
}
}
private void launchGame( long rowid )
{
launchGame( rowid, false );
launchGame( rowid, false, null );
}
private void launchGame( long rowid, Bundle extras )
{
launchGame( rowid, false, extras );
}
private void makeNewNetGame( NetLaunchInfo nli )
{
long rowid = DBUtils.ROWID_NOTFOUND;
rowid = GameUtils.makeNewMultiGame( m_activity, nli );
launchGame( rowid, true );
launchGame( rowid, true, null );
}
private void tryStartsFromIntent( Intent intent )

View file

@ -51,6 +51,7 @@ public class MultiService {
public static final String BT_ADDRESS = "BT_ADDRESS";
public static final String P2P_MAC_ADDRESS = "P2P_MAC_ADDRESS";
private static final String NLI_DATA = "nli";
public static final String DUPEMODE = "du";
public enum DictFetchOwner { _NONE,
OWNER_SMS,

View file

@ -101,7 +101,7 @@ public class NagTurnReceiver extends BroadcastReceiver {
}
Utils.postNotification( context, msgIntent,
R.string.nag_title, body,
(int)rowid );
rowid );
}
DBUtils.updateNeedNagging( context, needNagging );

View file

@ -65,6 +65,7 @@ public class NetLaunchInfo implements Serializable {
private static final String FORCECHANNEL_KEY = "fc";
private static final String NAME_KEY = "nm";
private static final String P2P_MAC_KEY = "p2";
private static final String DUPMODE_KEY = "du";
protected String gameName;
protected String dict;
@ -87,6 +88,7 @@ public class NetLaunchInfo implements Serializable {
private CommsConnTypeSet m_addrs;
private boolean m_valid;
private String inviteID;
private boolean dupeMode;
public NetLaunchInfo()
{
@ -244,6 +246,8 @@ public class NetLaunchInfo implements Serializable {
val = data.getQueryParameter( FORCECHANNEL_KEY );
forceChannel = null == val ? 0 : Integer.decode( val );
gameName = data.getQueryParameter( NAME_KEY );
val = data.getQueryParameter( DUPMODE_KEY );
dupeMode = null != val && Integer.decode(val) != 0;
}
calcValid();
} catch ( Exception e ) {
@ -254,7 +258,7 @@ public class NetLaunchInfo implements Serializable {
}
private NetLaunchInfo( int gamID, String gamNam, int dictLang,
String dictName, int nPlayers )
String dictName, int nPlayers, boolean dupMode )
{
this();
gameName = gamNam;
@ -263,6 +267,7 @@ public class NetLaunchInfo implements Serializable {
nPlayersT = nPlayers;
nPlayersH = 1;
gameID = gamID;
dupeMode = dupMode;
}
public NetLaunchInfo( Context context, GameSummary summary, CurGameInfo gi,
@ -275,7 +280,8 @@ public class NetLaunchInfo implements Serializable {
public NetLaunchInfo( CurGameInfo gi )
{
this( gi.gameID, gi.getName(), gi.dictLang, gi.dictName, gi.nPlayers );
this( gi.gameID, gi.getName(), gi.dictLang, gi.dictName, gi.nPlayers,
gi.inDuplicateMode );
}
public NetLaunchInfo( Context context, GameSummary summary, CurGameInfo gi )
@ -350,12 +356,17 @@ public class NetLaunchInfo implements Serializable {
bundle.putString( MultiService.GAMENAME, gameName );
bundle.putInt( MultiService.NPLAYERST, nPlayersT );
bundle.putInt( MultiService.NPLAYERSH, nPlayersH );
bundle.putBoolean( MultiService.REMOTES_ROBOTS, remotesAreRobots );
if ( remotesAreRobots ) {
bundle.putBoolean( MultiService.REMOTES_ROBOTS, true );
}
bundle.putInt( MultiService.GAMEID, gameID() );
bundle.putString( MultiService.BT_NAME, btName );
bundle.putString( MultiService.BT_ADDRESS, btAddress );
bundle.putString( MultiService.P2P_MAC_ADDRESS, p2pMacAddress );
bundle.putInt( MultiService.FORCECHANNEL, forceChannel );
if ( dupeMode ) {
bundle.putBoolean( MultiService.DUPEMODE, true );
}
int flags = m_addrs.toInt();
bundle.putInt( ADDRS_KEY, flags );
@ -374,6 +385,7 @@ public class NetLaunchInfo implements Serializable {
&& forceChannel == other.forceChannel
&& nPlayersT == other.nPlayersT
&& nPlayersH == other.nPlayersH
&& dupeMode == other.dupeMode
&& remotesAreRobots == other.remotesAreRobots
&& TextUtils.equals( room, other.room )
&& TextUtils.equals( btName, other.btName )
@ -406,7 +418,12 @@ public class NetLaunchInfo implements Serializable {
.put( MultiService.NPLAYERSH, nPlayersH )
.put( MultiService.REMOTES_ROBOTS, remotesAreRobots )
.put( MultiService.GAMEID, gameID() )
.put( MultiService.FORCECHANNEL, forceChannel );
.put( MultiService.FORCECHANNEL, forceChannel )
;
if ( dupeMode ) {
obj.put( MultiService.DUPEMODE, dupeMode );
}
if ( m_addrs.contains( CommsConnType.COMMS_CONN_RELAY ) ) {
obj.put( MultiService.ROOM, room )
@ -477,6 +494,7 @@ public class NetLaunchInfo implements Serializable {
lang = json.optInt( MultiService.LANG, -1 );
forceChannel = json.optInt( MultiService.FORCECHANNEL, 0 );
dupeMode = json.optBoolean( MultiService.DUPEMODE, false );
dict = json.optString( MultiService.DICT );
gameName = json.optString( MultiService.GAMENAME );
nPlayersT = json.optInt( MultiService.NPLAYERST, -1 );
@ -549,6 +567,9 @@ public class NetLaunchInfo implements Serializable {
appendInt( ub, FORCECHANNEL_KEY, forceChannel );
appendInt( ub, ADDRS_KEY, addrs );
ub.appendQueryParameter( NAME_KEY, gameName );
if ( dupeMode ) {
appendInt( ub, DUPMODE_KEY, 1 );
}
if ( null != dict ) {
ub.appendQueryParameter( WORDLIST_KEY, dict );

View file

@ -36,8 +36,8 @@ import android.preference.PreferenceScreen;
import android.view.View;
import android.widget.Button;
import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.jni.CommonPrefs;
import org.eehouse.android.xw4.loc.LocUtils;
import java.io.File;
@ -387,6 +387,10 @@ public class PrefsDelegate extends DelegateBase
if ( null == FBMService.getFCMDevID( m_activity ) ) {
hideOne( R.string.key_show_fcm, R.string.pref_group_relay_title );
}
if ( CommonPrefs.getDupModeHidden( m_activity ) ) {
hideOne( R.string.key_init_dupmodeon, R.string.key_prefs_defaults );
}
}
public static void launch( Context context )

View file

@ -1330,6 +1330,10 @@ public class RelayService extends XWJIService
if ( null == udpSocket ) {
// will be null if e.g. device or emulator doesn't have network
udpSocket = getService().connectSocketOnce(); // block until this is done
// Assert.assertTrue( null != udpSocket || !BuildConfig.DEBUG ); // firing
if ( null == udpSocket ) {
Log.e( TAG, "connectSocketOnce() failed; no socket" );
}
}
byte[] buf = new byte[1024];

View file

@ -326,11 +326,11 @@ public class StudyListDelegate extends ListDelegateBase
startLang = startIntent.getIntExtra( START_LANG, NO_LANG );
}
String[] names = DictLangCache.getLangNames( m_activity );
String[] myNames = new String[m_langCodes.length];
for ( int ii = 0; ii < m_langCodes.length; ++ii ) {
int lang = m_langCodes[ii];
myNames[ii] = xlateLang( names[lang], true );
String name = DictLangCache.getLangName( m_activity, lang );
myNames[ii] = xlateLang( name, true );
if ( lang == startLang ) {
startIndex = ii;
}

View file

@ -88,6 +88,8 @@ public class Utils {
private static final String FIRST_VERSION_KEY = "FIRST_VERSION_KEY";
private static final String SHOWN_VERSION_KEY = "SHOWN_VERSION_KEY";
private static final Channels.ID sDefaultChannel = Channels.ID.GAME_EVENT;
private static Boolean s_isFirstBootThisVersion = null;
private static Boolean s_firstVersion = null;
private static Boolean s_isFirstBootEver = null;
@ -244,24 +246,68 @@ public class Utils {
LocUtils.getString( context, bodyID ), id );
}
public static void postNotification( Context context, Intent intent,
String title, String body, long rowid )
{
int id = sDefaultChannel.idFor( rowid );
postNotification( context, intent, title, body, id );
}
public static void postNotification( Context context, Intent intent,
int titleId, String body, long rowid )
{
postNotification( context, intent, titleId, body, rowid,
sDefaultChannel );
}
public static void postNotification( Context context, Intent intent,
int titleID, String body, int id )
{
postNotification( context, intent, titleID, body, id,
sDefaultChannel );
}
public static void postNotification( Context context, Intent intent,
int titleID, String body, long rowid,
Channels.ID channel )
{
int id = channel.idFor( rowid );
postNotification( context, intent, titleID, body, id, channel );
}
private static void postNotification( Context context, Intent intent,
int titleID, String body, int id,
Channels.ID channel )
{
String title = LocUtils.getString( context, titleID );
postNotification( context, intent, title, body, id );
// Log.d( TAG, "posting with title %s", title );
postNotification( context, intent, title, body, id, channel, false,
null, 0 );
}
public static void postNotification( Context context, Intent intent,
String title, String body,
int id )
{
String channelID = Channels.getChannelID( context, Channels.ID.GAME_EVENT );
postNotification( context, intent, title, body, id, channelID );
postNotification( context, intent, title, body, id,
sDefaultChannel, false, null, 0 );
}
static void postOngoingNotification( Context context, Intent intent,
String title, String body,
long rowid, Channels.ID channel,
Intent actionIntent,
int actionString )
{
int id = channel.idFor( rowid );
postNotification( context, intent, title, body, id, channel, true,
actionIntent, actionString );
}
private static void postNotification( Context context, Intent intent,
String title, String body,
int id, String channelID )
int id, Channels.ID channel, boolean ongoing,
Intent actionIntent, int actionString )
{
/* nextRandomInt: per this link
http://stackoverflow.com/questions/10561419/scheduling-more-than-one-pendingintent-to-same-activity-using-alarmmanager
@ -269,9 +315,8 @@ public class Utils {
Intents is to send a different second param each time,
though the docs say that param's ignored.
*/
PendingIntent pi = null == intent ? null
: PendingIntent.getActivity( context, nextRandomInt(), intent,
PendingIntent.FLAG_ONE_SHOT );
PendingIntent pi = null == intent
? null : getPendingIntent( context, intent );
int defaults = Notification.FLAG_AUTO_CANCEL;
if ( CommonPrefs.getSoundNotify( context ) ) {
@ -281,42 +326,63 @@ public class Utils {
defaults |= Notification.DEFAULT_VIBRATE;
}
Notification notification =
String channelID = Channels.getChannelID( context, channel );
NotificationCompat.Builder builder =
new NotificationCompat.Builder( context, channelID )
.setContentIntent( pi )
.setSmallIcon( R.drawable.notify )
//.setTicker(body)
//.setWhen(time)
.setOngoing( ongoing )
.setAutoCancel( true )
.setDefaults( defaults )
.setContentTitle( title )
.setContentText( body )
.build();
;
if ( null != actionIntent ) {
PendingIntent actionPI = getPendingIntent( context, actionIntent );
builder.addAction( 0, LocUtils.getString(context, actionString),
actionPI );
}
Notification notification = builder.build();
NotificationManager nm = (NotificationManager)
context.getSystemService( Context.NOTIFICATION_SERVICE );
nm.notify( id, notification );
}
private static PendingIntent getPendingIntent( Context context, Intent intent )
{
PendingIntent pi = PendingIntent
.getActivity( context, Utils.nextRandomInt(), intent,
PendingIntent.FLAG_ONE_SHOT );
return pi;
}
private static final String KEY_LAST_STALL_NOT = TAG + ".last_stall_note";
private static final long MIN_STALL_NOTE_INTERVAL_MS = 1000 * 60 * 30;
public static void showStallNotification( Context context, long ageMS )
public static void showStallNotification( Context context, String typ,
long ageMS )
{
String body = LocUtils.getString( context, R.string.notify_stall_body_fmt,
typ, (ageMS + 500) / 1000,
MIN_STALL_NOTE_INTERVAL_MS / (1000 * 60));
long now = System.currentTimeMillis();
long lastStallNotify = DBUtils.getLongFor( context, KEY_LAST_STALL_NOT, 0 );
if ( now - lastStallNotify > MIN_STALL_NOTE_INTERVAL_MS ) {
String title = LocUtils.getString( context, R.string.notify_stall_title );
String body = LocUtils.getString( context, R.string.notify_stall_body_fmt,
(ageMS + 500) / 1000,
MIN_STALL_NOTE_INTERVAL_MS / (1000 * 60));
String channelID = Channels.getChannelID( context,
Channels.ID.SERVICE_STALL );
Intent intent = GamesListDelegate
.makeAlertWithEmailIntent( context, body );
postNotification( context, intent, title, body,
R.string.notify_stall_title, channelID );
R.string.notify_stall_title,
Channels.ID.SERVICE_STALL, false, null, 0 );
DBUtils.setLongFor( context, KEY_LAST_STALL_NOT, now );
} else {
Log.e( TAG, "stalled, but too recent for notification: %s",
body );
}
}
@ -328,6 +394,18 @@ public class Utils {
cancelNotification( context, R.string.notify_stall_title );
}
public static void cancelNotification( Context context, Channels.ID channel,
long rowid )
{
int id = channel.idFor( rowid );
cancelNotification( context, id );
}
public static void cancelNotification( Context context, long rowid )
{
cancelNotification( context, sDefaultChannel, rowid );
}
public static void cancelNotification( Context context, int id )
{
NotificationManager nm = (NotificationManager)
@ -522,6 +600,7 @@ public class Utils {
return result;
}
// Called from andutils.c in the jni world
public static long getCurSeconds()
{
// Note: an int is big enough for *seconds* (not milliseconds) since 1970

View file

@ -97,6 +97,8 @@ public class XWApp extends Application
mPort = Short.valueOf( getString( R.string.nbs_port ) );
NBSProxy.register( this, mPort, BuildConfig.APPLICATION_ID, this );
DupeModeTimer.init( this );
}
@OnLifecycleEvent(ON_ANY)

View file

@ -132,6 +132,7 @@ abstract class XWJIService extends JobIntentService {
if ( stallCheckEnabled( context ) ) {
long now = System.currentTimeMillis();
long maxAge = 0;
String maxName = null;
synchronized ( sPendingIntents ) {
for ( String simpleName : sPendingIntents.keySet() ) {
List<Intent> intents = sPendingIntents.get( simpleName );
@ -141,6 +142,7 @@ abstract class XWJIService extends JobIntentService {
long age = now - timestamp;
if ( age > maxAge ) {
maxAge = age;
maxName = simpleName;
}
}
}
@ -148,7 +150,7 @@ abstract class XWJIService extends JobIntentService {
if ( maxAge > AGE_THRESHOLD_MS ) {
// ConnStatusHandler.noteStall( sTypes.get( clazz ), maxAge );
Utils.showStallNotification( context, maxAge );
Utils.showStallNotification( context, maxName, maxAge );
}
}
}

View file

@ -237,6 +237,16 @@ public class CommonPrefs extends XWPrefs {
return getPrefsBoolean( context, key, true );
}
public static boolean getDefaultDupMode( Context context )
{
return getPrefsBoolean( context, R.string.key_init_dupmodeon, false );
}
public static boolean getDupModeHidden( Context context )
{
return !getPrefsBoolean( context, R.string.key_unhide_dupmode, false );
}
public static boolean getAutoJuggle( Context context )
{
return getPrefsBoolean( context, R.string.key_init_autojuggle, false );

View file

@ -49,6 +49,7 @@ public class CurGameInfo implements Serializable {
private static final String TIMER = "TIMER";
private static final String ALLOW_PICK = "ALLOW_PICK";
private static final String PHONIES = "PHONIES";
private static final String DUP = "DUP";
public enum XWPhoniesChoice { PHONIES_IGNORE, PHONIES_WARN, PHONIES_DISALLOW };
public enum DeviceRole { SERVER_STANDALONE, SERVER_ISSERVER, SERVER_ISCLIENT };
@ -63,6 +64,7 @@ public class CurGameInfo implements Serializable {
public int forceChannel;
public DeviceRole serverRole;
public boolean inDuplicateMode;
public boolean hintsNotAllowed;
public boolean timerEnabled;
public boolean allowPickTiles;
@ -83,8 +85,9 @@ public class CurGameInfo implements Serializable {
{
boolean isNetworked = null != inviteID;
nPlayers = 2;
gameSeconds = 60 * nPlayers *
CommonPrefs.getDefaultPlayerMinutes( context );
inDuplicateMode = CommonPrefs.getDefaultDupMode( context );
gameSeconds = inDuplicateMode ? (5 * 60)
: 60 * nPlayers * CommonPrefs.getDefaultPlayerMinutes( context );
boardSize = CommonPrefs.getDefaultBoardSize( context );
players = new LocalPlayer[MAX_NUM_PLAYERS];
serverRole = isNetworked ? DeviceRole.SERVER_ISCLIENT
@ -142,6 +145,7 @@ public class CurGameInfo implements Serializable {
dictName = src.dictName;
dictLang = src.dictLang;
hintsNotAllowed = src.hintsNotAllowed;
inDuplicateMode = src.inDuplicateMode;
phoniesAction = src.phoniesAction;
timerEnabled = src.timerEnabled;
allowPickTiles = src.allowPickTiles;
@ -169,6 +173,8 @@ public class CurGameInfo implements Serializable {
}
sb.append( "], gameID: ").append( gameID )
.append( ", hashCode: ").append( hashCode() )
.append( ", timerEnabled: ").append( timerEnabled )
.append( ", gameSeconds: ").append( gameSeconds )
.append('}');
result = sb.toString();
@ -185,6 +191,7 @@ public class CurGameInfo implements Serializable {
JSONObject obj = new JSONObject()
.put( BOARD_SIZE, boardSize )
.put( NO_HINTS, hintsNotAllowed )
.put( DUP, inDuplicateMode )
.put( TIMER, timerEnabled )
.put( ALLOW_PICK, allowPickTiles )
.put( PHONIES, phoniesAction.ordinal() )
@ -204,6 +211,7 @@ public class CurGameInfo implements Serializable {
JSONObject obj = new JSONObject( jsonData );
boardSize = obj.optInt( BOARD_SIZE, boardSize );
hintsNotAllowed = obj.optBoolean( NO_HINTS, hintsNotAllowed );
inDuplicateMode = obj.optBoolean( DUP, inDuplicateMode );
timerEnabled = obj.optBoolean( TIMER, timerEnabled );
allowPickTiles = obj.optBoolean( ALLOW_PICK, allowPickTiles );
int tmp = obj.optInt( PHONIES, phoniesAction.ordinal() );
@ -280,6 +288,7 @@ public class CurGameInfo implements Serializable {
|| dictLang != other.dictLang
|| boardSize != other.boardSize
|| hintsNotAllowed != other.hintsNotAllowed
|| inDuplicateMode != other.inDuplicateMode
|| allowPickTiles != other.allowPickTiles
|| phoniesAction != other.phoniesAction;
@ -313,6 +322,7 @@ public class CurGameInfo implements Serializable {
&& boardSize == other.boardSize
&& forceChannel == other.forceChannel
&& hintsNotAllowed == other.hintsNotAllowed
&& inDuplicateMode == other.inDuplicateMode
&& timerEnabled == other.timerEnabled
&& allowPickTiles == other.allowPickTiles
&& allowHintRect == other.allowHintRect

View file

@ -21,14 +21,20 @@
package org.eehouse.android.xw4.jni;
import android.content.Context;
import android.content.Intent;
import android.telephony.PhoneNumberUtils;
import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.Channels;
import org.eehouse.android.xw4.DBUtils;
import org.eehouse.android.xw4.DevID;
import org.eehouse.android.xw4.DupeModeTimer;
import org.eehouse.android.xw4.FBMService;
import org.eehouse.android.xw4.GameUtils;
import org.eehouse.android.xw4.GamesListDelegate;
import org.eehouse.android.xw4.Log;
import org.eehouse.android.xw4.R;
import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.XWApp;
import org.eehouse.android.xw4.loc.LocUtils;
@ -102,15 +108,17 @@ public class DUtilCtxt {
static final int STRD_CUMULATIVE_SCORE = 14;
static final int STRS_NEW_TILES = 15;
static final int STR_COMMIT_CONFIRM = 16;
static final int STR_BONUS_ALL = 17;
static final int STRD_TURN_SCORE = 18;
static final int STRD_REMAINS_HEADER = 19;
static final int STRD_REMAINS_EXPL = 20;
static final int STRSD_RESIGNED = 21;
static final int STRSD_WINNER = 22;
static final int STRDSD_PLACER = 23;
static final int STR_SUBMIT_CONFIRM = 17;
static final int STR_BONUS_ALL = 18;
static final int STRD_TURN_SCORE = 19;
static final int STRD_REMAINS_HEADER = 20;
static final int STRD_REMAINS_EXPL = 21;
static final int STRSD_RESIGNED = 22;
static final int STRSD_WINNER = 23;
static final int STRDSD_PLACER = 24;
static final int STR_DUP_CLIENT_SENT = 25;
static final int STRDD_DUP_HOST_RECEIVED = 26;
public String getUserString( int stringCode )
{
Log.d( TAG, "getUserString(%d)", stringCode );
@ -161,6 +169,9 @@ public class DUtilCtxt {
case STR_COMMIT_CONFIRM:
id = R.string.str_commit_confirm;
break;
case STR_SUBMIT_CONFIRM:
id = R.string.str_submit_confirm;
break;
case STR_BONUS_ALL:
id = R.string.str_bonus_all;
break;
@ -177,6 +188,14 @@ public class DUtilCtxt {
id = R.string.str_placer_fmt;
break;
case STR_DUP_CLIENT_SENT:
id = R.string.dup_client_sent;
break;
case STRDD_DUP_HOST_RECEIVED:
id = R.string.dup_host_received_fmt;
break;
default:
Log.w( TAG, "no such stringCode: %d", stringCode );
}
@ -236,4 +255,71 @@ public class DUtilCtxt {
Log.d( TAG, "load(%s) returning %d bytes", key, resultLen );
return result;
}
// Must match enum DupPauseType
public static final int UNPAUSED = 0;
public static final int PAUSED = 1;
public static final int AUTOPAUSED = 2;
// A pause can come in when a game's open or when it's not. If it's open,
// we want to post an alert. If it's not, we want to post a notification,
// or at least kick off DupeModeTimer to cancel or start the timer-running
// notification.
public void notifyPause( int gameID, int pauseType, int pauser,
String pauserName, String expl )
{
long[] rowids = DBUtils.getRowIDsFor( m_context, gameID );
Log.d( TAG, "got %d games with gameid", null == rowids ? 0 : rowids.length );
final boolean isPause = UNPAUSED != pauseType;
for ( long rowid : rowids ) {
String msg = msgForPause( rowid, pauseType, pauserName, expl );
try ( JNIThread thread = JNIThread.getRetained( rowid ) ) {
if ( null != thread ) {
thread.notifyPause( pauser, isPause, msg );
} else {
Intent intent = GamesListDelegate
.makeRowidIntent( m_context, rowid );
int titleID = isPause ? R.string.game_paused_title
: R.string.game_unpaused_title;
Channels.ID channelID = Channels.ID.DUP_PAUSED;
Utils.postNotification( m_context, intent, titleID, msg,
rowid, channelID );
// DupeModeTimer.timerPauseChanged( m_context, rowid );
}
}
}
}
private String msgForPause( long rowid, int pauseType, String pauserName, String expl )
{
String msg;
final String gameName = GameUtils.getName( m_context, rowid );
if ( AUTOPAUSED == pauseType ) {
msg = LocUtils.getString( m_context, R.string.autopause_expl_fmt,
gameName );
} else {
boolean isPause = PAUSED == pauseType;
if ( null != expl && 0 < expl.length() ) {
msg = LocUtils.getString( m_context,
isPause ? R.string.pause_notify_expl_fmt
: R.string.unpause_notify_expl_fmt,
pauserName, expl );
} else {
msg = LocUtils.getString( m_context,
isPause ? R.string.pause_notify_fmt
: R.string.unpause_notify_fmt,
pauserName );
}
}
return msg;
}
public void onDupTimerChanged( int gameID, int oldVal, int newVal )
{
DupeModeTimer.timerChanged( m_context, gameID, newVal );
}
}

View file

@ -57,7 +57,7 @@ public interface DrawCtx {
// void score_drawPlayers( Rect scoreRect, DrawScoreInfo[] playerData,
// Rect[] playerRects );
void drawTimer( Rect rect, int player, int secondsLeft );
void drawTimer( Rect rect, int player, int secondsLeft, boolean inDuplicateMode );
boolean drawCell( Rect rect, String text, int tile, int value,
int owner, int bonus, int hintAtts, int flags );
@ -69,8 +69,8 @@ public interface DrawCtx {
int flags );
boolean drawTileBack( Rect rect, int flags );
void drawTrayDivider( Rect rect, int flags );
void score_pendingScore( Rect rect, int score, int playerNum, int curTurn,
int flags );
void score_pendingScore( Rect rect, int score, int playerNum,
boolean curTurn, int flags );
public static final int BONUS_NONE = 0;
public static final int BONUS_DOUBLE_LETTER = 1;

View file

@ -37,6 +37,7 @@ import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.XWApp;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet;
import org.eehouse.android.xw4.jni.CurGameInfo;
import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
import org.eehouse.android.xw4.loc.LocUtils;
@ -55,8 +56,10 @@ public class GameSummary implements Serializable {
public static final int MSG_FLAGS_CHAT = 2;
public static final int MSG_FLAGS_GAMEOVER = 4;
public static final int MSG_FLAGS_ALL = 7;
public static final int DUP_MODE_MASK = 1 << (CurGameInfo.MAX_NUM_PLAYERS * 2);
public int lastMoveTime; // set by jni's server.c on move receipt
public int dupTimerExpires;
public int nMoves;
public int turn;
public boolean turnIsLocal;
@ -110,6 +113,7 @@ public class GameSummary implements Serializable {
GameSummary other = (GameSummary)obj;
result = lastMoveTime == other.lastMoveTime
&& nMoves == other.nMoves
&& dupTimerExpires == other.dupTimerExpires
&& turn == other.turn
&& turnIsLocal == other.turnIsLocal
&& nPlayers == other.nPlayers
@ -361,10 +365,21 @@ public class GameSummary implements Serializable {
result |= 1 << (ii * 2);
}
}
Assert.assertTrue( (result & DUP_MODE_MASK) == 0 );
if ( m_gi.inDuplicateMode ) {
result |= DUP_MODE_MASK;
}
}
return result;
}
public boolean inDuplicateMode()
{
int flags = giflags();
return (flags & DUP_MODE_MASK) != 0;
}
public void setGiFlags( int flags )
{
m_giFlags = new Integer( flags );

View file

@ -1,6 +1,6 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
* Copyright 2009 - 2017 by Eric House (xwords@eehouse.org). All rights
* Copyright 2009 - 2019 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
@ -33,11 +33,12 @@ import org.eehouse.android.xw4.ConnStatusHandler;
import org.eehouse.android.xw4.DBUtils;
import org.eehouse.android.xw4.DbgUtils;
import org.eehouse.android.xw4.DictUtils;
import org.eehouse.android.xw4.DupeModeTimer;
import org.eehouse.android.xw4.GameLock;
import org.eehouse.android.xw4.GameUtils;
import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.Log;
import org.eehouse.android.xw4.R;
import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.XWPrefs;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
@ -97,6 +98,8 @@ public class JNIThread extends Thread implements AutoCloseable {
CMD_NETSTATS,
CMD_PASS_PASSWD,
CMD_SET_BLANK,
CMD_PAUSE,
CMD_UNPAUSE,
// CMD_DRAW_CONNS_STATUS,
// CMD_DRAW_BT_STATUS,
// CMD_DRAW_SMS_STATUS,
@ -106,7 +109,7 @@ public class JNIThread extends Thread implements AutoCloseable {
public static final int DIALOG = 2;
public static final int QUERY_ENDGAME = 3;
public static final int TOOLBAR_STATES = 4;
public static final int GOT_WORDS = 5;
public static final int GOT_PAUSE = 5;
public static final int GAME_OVER = 6;
public static final int MSGS_SENT = 7;
@ -124,6 +127,8 @@ public class JNIThread extends Thread implements AutoCloseable {
public boolean curTurnSelected;
public boolean canHideRack;
public boolean canTrade;
public boolean canPause;
public boolean canUnpause;
public GameStateInfo clone() {
GameStateInfo obj = null;
try {
@ -167,7 +172,7 @@ public class JNIThread extends Thread implements AutoCloseable {
{
m_lock = lock.retain();
m_rowid = lock.getRowid();
m_queue = new LinkedBlockingQueue<QueueElem>();
m_queue = new LinkedBlockingQueue<>();
}
public boolean configure( Context context, SyncedDraw drawer,
@ -239,6 +244,8 @@ public class JNIThread extends Thread implements AutoCloseable {
}
m_lastSavedState = Arrays.hashCode( stream );
DupeModeTimer.gameOpened( m_context, m_rowid );
}
Log.d( TAG, "configure() => %b", success );
return success;
@ -729,6 +736,12 @@ public class JNIThread extends Thread implements AutoCloseable {
((Integer)args[2]).intValue() );
break;
case CMD_PAUSE:
XwJNI.board_pause( m_jniGamePtr, ((String)args[0]) );
break;
case CMD_UNPAUSE:
XwJNI.board_unpause( m_jniGamePtr, ((String)args[0]) );
break;
case CMD_NONE: // ignored
break;
default:
@ -785,6 +798,12 @@ public class JNIThread extends Thread implements AutoCloseable {
handle( JNICmd.CMD_SENDCHAT, chat );
}
public void notifyPause( int pauser, boolean isPause, String msg )
{
Message.obtain( m_handler, GOT_PAUSE, msg )
.sendToTarget();
}
public void handle( JNICmd cmd, Object... args )
{
if ( m_stopped && ! JNICmd.CMD_NONE.equals(cmd) ) {
@ -837,6 +856,7 @@ public class JNIThread extends Thread implements AutoCloseable {
if ( stop ) {
waitToStop( true );
DupeModeTimer.gameClosed( m_context, m_rowid );
} else if ( save && 0 != m_lastSavedState ) { // has configure() run?
handle( JNICmd.CMD_SAVE ); // in case releaser has made changes
}
@ -873,4 +893,14 @@ public class JNIThread extends Thread implements AutoCloseable {
}
return result;
}
public static boolean gameIsOpen( long rowid )
{
boolean result = false;
try ( JNIThread thread = JNIThread.getRetained( rowid ) ) {
result = null != thread;
}
Log.d( TAG, "gameIsOpen(%d) => %b", rowid, result );
return result;
}
}

View file

@ -1,6 +1,7 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
* Copyright 2014 by Eric House (xwords@eehouse.org). All rights reserved.
* Copyright 2014 - 2019 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@ -20,6 +21,7 @@
package org.eehouse.android.xw4.jni;
import android.content.Context;
import android.text.TextUtils;
import org.eehouse.android.xw4.R;
import org.eehouse.android.xw4.loc.LocUtils;
@ -33,7 +35,8 @@ public class LastMoveInfo {
private static final int PHONY_TYPE = 3;
public boolean isValid = false; // modified in jni world
public String name;
public boolean inDuplicateMode;
public String[] names;
public int moveType;
public int score;
public int nTiles;
@ -45,25 +48,42 @@ public class LastMoveInfo {
if ( isValid ) {
switch( moveType ) {
case ASSIGN_TYPE:
result = LocUtils.getString( context, R.string.lmi_tiles_fmt, name );
result = inDuplicateMode
? LocUtils.getString( context, R.string.lmi_tiles_dup )
: LocUtils.getString( context, R.string.lmi_tiles_fmt, names[0] );
break;
case MOVE_TYPE:
if ( 0 == nTiles ) {
result = LocUtils.getString( context, R.string.lmi_pass_fmt,
name );
result = inDuplicateMode
// Nobody scoring in dup mode is usually followed
// automatically by a trade. So this first will be
// rare.
? LocUtils.getString( context, R.string.lmi_pass_dup )
: LocUtils.getString( context, R.string.lmi_pass_fmt, names[0] );
} else if ( inDuplicateMode ) {
if ( names.length == 1 ) {
result = LocUtils.getString( context, R.string.lmi_move_one_dup_fmt,
names[0], word, score );
} else {
String joiner = LocUtils.getString( context, R.string.name_concat_dup );
String players = TextUtils.join( joiner, names);
result = LocUtils.getString( context, R.string.lmi_move_tie_dup_fmt,
players, score, word );
}
} else {
result = LocUtils.getQuantityString( context, R.plurals.lmi_move_fmt,
score, name, word, score );
score, names[0], word, score );
}
break;
case TRADE_TYPE:
result = LocUtils
.getQuantityString( context, R.plurals.lmi_trade_fmt,
nTiles, name, nTiles );
result = inDuplicateMode
? LocUtils.getString( context, R.string.lmi_trade_dup_fmt, nTiles )
: LocUtils.getQuantityString( context, R.plurals.lmi_trade_fmt,
nTiles, names[0], nTiles );
break;
case PHONY_TYPE:
result = LocUtils.getString( context, R.string.lmi_phony_fmt,
name );
names[0] );
break;
}
}

View file

@ -53,11 +53,14 @@ public interface UtilCtxt {
public static final int TIMER_TIMERTICK = 2;
public static final int TIMER_COMMS = 3;
public static final int TIMER_SLOWROBOT = 4;
public static final int TIMER_DUP_TIMERCHECK = 5;
public static final int NUM_TIMERS_PLUS_ONE = 6;
void setTimer( int why, int when, int handle );
void clearTimer( int why );
void requestTime();
void remSelected();
void timerSelected( boolean inDuplicateMode, boolean canPause );
void setIsServer( boolean isServer );
void bonusSquareHeld( int bonus );
@ -66,6 +69,7 @@ public interface UtilCtxt {
void notifyMove( String query );
void notifyTrade( String[] tiles );
void notifyDupStatus( boolean amHost, String msg );
// These can't be an ENUM! The set is open-ended, with arbitrary values
// added to ERR_RELAY_BASE, so no way to create an enum from an int in the
@ -110,4 +114,7 @@ public interface UtilCtxt {
boolean turnLost );
void showChat( String msg, int fromIndx, String fromName, int tsSeconds );
String formatPauseHistory( int pauseTyp, int player, int whenPrev,
int whenCur, String msg );
}

View file

@ -94,6 +94,12 @@ public class UtilCtxtImpl implements UtilCtxt {
subclassOverride( "remSelected" );
}
@Override
public void timerSelected( boolean inDuplicateMode, boolean canPause )
{
subclassOverride( "timerSelected" );
}
@Override
public void setIsServer( boolean isServer )
{
@ -127,6 +133,12 @@ public class UtilCtxtImpl implements UtilCtxt {
subclassOverride( "notifyTrade" );
}
@Override
public void notifyDupStatus( boolean amHost, String msg )
{
subclassOverride( "notifyDupStatus" );
}
@Override
public void userError( int id )
{
@ -183,6 +195,14 @@ public class UtilCtxtImpl implements UtilCtxt {
subclassOverride( "showChat" );
}
@Override
public String formatPauseHistory( int pauseTyp, int player, int whenPrev,
int whenCur, String msg )
{
subclassOverride( "formatPauseHistory" );
return null;
}
private void subclassOverride( String name ) {
// DbgUtils.logf( "%s::%s() called", getClass().getName(), name );
}

View file

@ -350,6 +350,9 @@ public class XwJNI {
public static native String board_formatRemainingTiles( GamePtr gamePtr );
public static native void board_sendChat( GamePtr gamePtr, String msg );
// Duplicate mode to start and stop timer
public static native void board_pause( GamePtr gamePtr, String msg );
public static native void board_unpause( GamePtr gamePtr, String msg );
public enum XP_Key {
XP_KEY_NONE,

View file

@ -189,63 +189,75 @@
android:layout_marginTop="15dp"
/>
<CheckBox android:id="@+id/hints_allowed"
<CheckBox android:id="@+id/duplicate_check"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hints_allowed"
android:text="@string/duplicate_check"
android:visibility="gone"
/>
<CheckBox android:id="@+id/use_timer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/use_timer"
android:layout_marginLeft="20dip"
/>
<LinearLayout android:id="@+id/timer_set"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:orientation="vertical">
android:orientation="horizontal"
>
<TextView android:layout_height="wrap_content"
<TextView android:id="@+id/timer_label"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginLeft="20dip"
android:layout_marginRight="20dip"
android:text="@string/minutes_label"
android:gravity="left"
android:textAppearance="?android:attr/textAppearanceMedium"
/>
<EditText android:id="@+id/timer_minutes_edit"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_marginLeft="20dip"
android:layout_marginRight="20dip"
android:layout_width="wrap_content"
android:scrollHorizontally="true"
android:autoText="false"
android:capitalize="none"
android:numeric="decimal"
android:hint="25"
android:gravity="fill_horizontal"
android:textAppearance="?android:attr/textAppearanceMedium"
/>
</LinearLayout>
<Spinner android:id="@+id/smart_robot"
android:prompt="@string/robot_spinner_prompt"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:drawSelectorOnTop="true"
android:entries="@array/robot_levels"
/>
<CheckBox android:id="@+id/hints_allowed"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hints_allowed"
/>
<Spinner android:id="@+id/phonies_spinner"
android:prompt="@string/phonies_spinner_prompt"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:drawSelectorOnTop="true"
android:entries="@array/phony_names"
/>
<LinearLayout style="@style/config_spinner_container"
>
<TextView style="@style/config_spinner_label"
android:text="@string/robot_iq_label"
/>
<Spinner style="@style/config_spinner_spinner"
android:id="@+id/smart_robot"
android:prompt="@string/robot_spinner_prompt"
android:entries="@array/robot_levels"
/>
</LinearLayout>
<LinearLayout style="@style/config_spinner_container"
>
<TextView style="@style/config_spinner_label"
android:text="@string/phonies_label"
/>
<Spinner style="@style/config_spinner_spinner"
android:id="@+id/phonies_spinner"
android:prompt="@string/phonies_spinner_prompt"
android:entries="@array/phony_names"
/>
</LinearLayout>
<CheckBox android:id="@+id/pick_faceup"
android:layout_width="fill_parent"
@ -253,28 +265,18 @@
android:text="@string/pick_faceup"
/>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginLeft="6dp"
<LinearLayout style="@style/config_spinner_container"
>
<TextView android:layout_height="wrap_content"
android:layout_width="wrap_content"
<TextView style="@style/config_spinner_label"
android:text="@string/board_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_weight="1"
/>
<Spinner android:id="@+id/boardsize_spinner"
<Spinner style="@style/config_spinner_spinner"
android:id="@+id/boardsize_spinner"
android:prompt="@string/board_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawSelectorOnTop="true"
android:entries="@array/board_sizes"
/>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"

View file

@ -38,11 +38,6 @@
android:paddingLeft="8dip"
android:paddingRight="8dip"
>
<!-- Shown only when BuildConfig.DEBUG == true -->
<TextView android:id="@+id/n_pending"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<ImageView android:id="@+id/game_type_marker"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
@ -54,6 +49,19 @@
android:src="@drawable/green_chat__gen"
android:layout_alignParentBottom="true"
/>
<TextView android:id="@+id/dup_tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dup_tag"
android:layout_alignParentTop="true"
android:textSize="14dp"
android:visibility="gone"
/>
<TextView android:id="@+id/n_pending"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/dup_tag"
/>
</RelativeLayout>
<ImageView android:id="@+id/thumbnail"

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<org.eehouse.android.xw4.ConfirmPauseView
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/confirm_pause_expl"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
/>
<org.eehouse.android.xw4.EditWClear
android:id="@+id/msg_edit"
style="@style/edit_w_clear"
android:hint="@string/pause_expl_hint"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textCapSentences|textMultiLine"
android:layout_weight="1"
android:scrollHorizontally="false"
android:layout_marginTop="10sp"
/>
<Spinner android:id="@+id/saved_msgs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawSelectorOnTop="true"
android:layout_marginTop="10sp"
/>
<!-- android:hint="@string/pause_msg_hint" -->
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button android:id="@+id/pause_save_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pause_save_msg"
/>
<Button android:id="@+id/pause_forget_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pause_forget_msg"
/>
</LinearLayout>
</org.eehouse.android.xw4.ConfirmPauseView>

View file

@ -82,4 +82,9 @@
<item android:id="@+id/board_menu_game_invites"
android:title="@string/board_menu_game_showInvites" />
<item android:id="@+id/board_menu_game_pause"
android:title="@string/board_menu_game_pause" />
<item android:id="@+id/board_menu_game_unpause"
android:title="@string/board_menu_game_unpause" />
</menu>

View file

@ -46,6 +46,8 @@
<string name="key_logging_on">key_logging_on</string>
<string name="key_show_sms">key_show_sms</string>
<string name="key_init_hintsallowed">key_init_hintsallowed</string>
<string name="key_init_dupmodeon">key_init_dupmodeon</string>
<string name="key_unhide_dupmode">key_unhide_dupmode</string> <!--temporary-->
<string name="key_init_nethintsallowed">key_init_nethintsallowed</string>
<string name="key_init_autojuggle">key_init_autojuggle</string>
<string name="key_board_size">key_board_size</string>
@ -65,6 +67,7 @@
<string name="key_enable_nbs">key_enable_nbs</string>
<string name="key_enable_p2p">key_enable_p2p</string>
<string name="key_enable_stallnotify">key_enable_stallnotify</string>
<string name="key_prefs_defaults">key_prefs_defaults</string>
<string name="key_network_behavior">key_network_behavior</string>
<string name="key_keep_screenon">key_keep_screenon</string>
<string name="key_thumbsize">key_thumbsize3</string>
@ -147,6 +150,9 @@
<string name="key_na_longtap_lookup">key_na_longtap_lookup</string>
<string name="key_na_perms_phonestate">key_na_perms_phonestate</string>
<string name="key_na_dupstatus_host">key_na_dupstatus_host</string>
<string name="key_na_dupstatus_guest">key_na_dupstatus_guest</string>
<!-- Nor is my email address -->
<string name="email_author_email">xwords@eehouse.org</string>

View file

@ -314,10 +314,24 @@
for network play will by default have the hint feature
enabled. -->
<string name="nethints_allowed">Allow hints (networked)</string>
<!-- text of checkbox controlling whether there's a game timer -->
<!-- text of checkbox controlling whether there's a game timer, when duplicate is no checked -->
<string name="use_timer">Enable game timer</string>
<!-- label for the field used to set the timer's inital value -->
<!-- text of checkbox controlling whether there's a game timer, when duplicate is checked -->
<string name="use_duptimer">Enable per-move timer</string>
<!-- label for the field used to set the timer's inital value, when duplicate is not checked -->
<string name="minutes_label">Minutes per player</string>
<!-- label for the field used to set the timer's inital value, when duplicate IS checked -->
<string name="dup_minutes_label">Minutes per turn</string>
<!-- Label to left of dropdown allowing to choose how smart robot is -->
<string name="robot_iq_label">Robot IQ</string>
<!-- Label to left of dropdown allowing to choose how to deal with words not in wordlist -->
<string name="phonies_label">Phonies</string>
<!-- title of popup used to select how "smart" (how capable) the
robot player will be. Note that on newer version of Android
spinners no longer have titles: this won't show up. -->
@ -2113,7 +2127,8 @@
<string name="new_game_message_nodflt">This game must be
configured before it can be opened.</string>
<string name="new_game_message_net">(You will have a chance to
invite other players when it is open.)</string>
invite other players after it is created.)</string>
<string name="use_defaults">Use defaults</string>
<string name="nplayers_prompt">Number on this device</string>
<plurals name="nplayers_fmt">
@ -2386,6 +2401,7 @@
\nIf it happens again, e-mail the developer logs and info about your device.
\n
\nThis message will be seen at most once every %2$d minutes.</string>
<string name="sms_banned_ok_only">The Google Play version of CrossWords no longer supports invitations or play via data SMS.</string>
<string name="button_more_info">Read more</string>
<string name="banned_nbs_perms">This game is set up to communicate via data SMS, but apps from the Google Play Store are no longer allowed to do so (with rare exceptions). You can still open the game, but it may not be able to send or receive moves.
@ -2399,7 +2415,104 @@
they\'re committed as moves -- by long-tapping, same as committed
words.\n\nUse this feature to check the validity of words you\'re
thinking of playing, or to look up an unfamiliar word provided as a
hint.</string>
hint.</string>
<string name="servicedesc">For transmitting CrossWords moves</string>
<string name="servicedesc">For transmitting CrossWords moves</string>
<!-- New strings for duplicate mode play -->
<!-- Checkbox in game config dialog -->
<string name="duplicate_check">Duplicate mode</string>
<!-- New strings for "Duplicate" mode -->
<!-- Tags duplicate-mode games in Games List, above thumbnail of board -->
<string name="dup_tag">Dup</string>
<!-- Mark game title; will produce something like "Kati vs. Eric (dup.)" -->
<string name="dupe_title_fmt">%1$s (dup.)</string>
<!-- Used instead of str_commit_confirm when in duplicate mode -->
<string name="str_submit_confirm">Submit the current move?\n</string>
<!-- In new-game preferences, do we default to dup-mode being on or off? -->
<string name="offerdupmode_title">Play in \"duplicate-mode\"</string>
<string name="offerdupmode_sum">(Experimental!!!) Style of play
where all players have the same tiles</string>
<!-- These are temporary: debug-only preferences to unhide duplicate
mode. Will go away once it's no longer experimental. -->
<string name="unhide_dupmode_title">Unhide duplicate-mode options</string>
<string name="unhide_dupmode_summary">(It\'s too experimental right now)</string>
<!-- dup mode: tile assignment -->
<string name="lmi_tiles_dup">Same tiles assigned to all players</string>
<!-- dup mode: A "pass" is when nobody can score -->
<string name="lmi_pass_dup">No players found a move (all passed)</string>
<!-- dupe mode: app trades for everybody when nobody scored last time -->
<string name="lmi_trade_dup_fmt">%1$d tiles exchanged for all players</string>
<!-- dup mode: when one player wins the round (no tie) -->
<string name="lmi_move_one_dup_fmt">%1$s won this round playing %2$s for %3$d points</string>
<!-- dup mode: when two or more get the same score -->
<string name="lmi_move_tie_dup_fmt">Players %1$s tied with %2$d
points each. %3$s was played.</string>
<!-- Used to concatenate names, stuck between all players' names when they tie -->
<string name="name_concat_dup">\u0020and\u0020</string>
<!-- Shown when in duplicate mode a guest commits a move. The
point is to educate user why he won't see a change immediately. -->
<string name="dup_client_sent">This device has sent its moves to
the host. When all devices have sent their moves it will be
your turn again.</string>
<!-- Shown in duplicate mode on host when player commits a turn
and not all guests have been heard from yet. The point is to
educate user why he won't see a change immediately. -->
<string name="dup_host_received_fmt">%1$d of %2$d players have
reported their moves. When all moves have been received it will be
your turn again.</string>
<!-- Explanation, shown in Android settings app, for the class of
notifications letting users know a duplicate-mode timer is
running while they're doing something else. -->
<string name="dup_timer_expl">Duplicate-mode timers running</string>
<!-- Explanation, shown in Android settings app, for the class of
notifications letting users know a game with a timer has been
paused or un-paused. -->
<string name="dup_paused_expl">Notice of games being paused and un-paused</string>
<string name="dup_notif_title">Timer running</string>
<string name="dup_notif_title_fmt">You have until %1$s to move.</string>
<string name="board_menu_game_pause">Pause</string>
<string name="board_menu_game_unpause">Un-pause</string>
<string name="pause_expl">Pausing this game will stop the timers, and hide the game, on all devices.</string>
<string name="unpause_expl">Un-Pausing this game will restart the timers on all devices.</string>
<!-- Title for notification that another player paused a duplicate-mode game -->
<string name="game_paused_title">Game paused</string>
<!-- Title for notification that another player un-paused a duplicate-mode game -->
<string name="game_unpaused_title">Game un-paused</string>
<!-- Title of pause confirm/message edit dialog -->
<string name="pause_title">Game pause</string>
<!-- Title of unpause confirm/message edit dialog -->
<string name="unpause_title">Game Unpause</string>
<!-- Button in pause confirm/message edit dialog -->
<string name="pause_save_msg">Remember message</string>
<!-- Button in pause confirm/message edit dialog -->
<string name="pause_forget_msg">Forget message</string>
<!-- Hint in message edit when pausing -->
<string name="pause_expl_hint">Why I\'m doing this</string>
<string name="pause_notify_fmt">Player %1$s has paused this game.</string>
<string name="unpause_notify_fmt">Player %1$s has un-paused this game.</string>
<string name="pause_notify_expl_fmt">Player %1$s has paused this game, with this explanation: %2$s</string>
<string name="unpause_notify_expl_fmt">Player %1$s has un-paused
this game, with this explanation: %2$s</string>
<string name="autopause_expl_fmt">The timer for game \"%1$s\" expired without
any moves being made, so it has been paused for you.</string>
<string name="history_unpause_fmt">Unpaused after %2$s by: %1$s.</string>
<string name="history_pause_fmt">Paused by: %1$s.</string>
<string name="history_msg_fmt"> Message: %1$s.</string>
<string name="history_autopause">Auto-paused.</string>
</resources>

View file

@ -111,4 +111,25 @@
<item name="android:searchIcon">@null</item>
<item name="android:searchHintIcon">@null</item>
</style>
<style name="config_spinner_container">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:orientation">horizontal</item>
<item name="android:layout_marginLeft">6dp</item>
</style>
<style name="config_spinner_label">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
<item name="android:layout_weight">1</item>
</style>
<style name="config_spinner_spinner">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item>
<item name="android:drawSelectorOnTop">true</item>
</style>
</resources>

View file

@ -5,6 +5,7 @@
<PreferenceScreen android:title="@string/prefs_defaults"
android:summary="@string/prefs_defaults_summary"
android:key="@string/key_prefs_defaults"
>
<PreferenceScreen android:title="@string/prefs_names"
@ -61,6 +62,12 @@
android:defaultValue="true"
/>
<CheckBoxPreference android:key="@string/key_init_dupmodeon"
android:title="@string/offerdupmode_title"
android:summary="@string/offerdupmode_sum"
android:defaultValue="false"
/>
<CheckBoxPreference android:key="@string/key_init_nethintsallowed"
android:title="@string/nethints_allowed"
android:summary="@string/nethints_allowed_sum"
@ -379,6 +386,13 @@
android:defaultValue="false"
/>
<!-- Keep all dup-mode related stuff hidden -->
<CheckBoxPreference android:key="@string/key_unhide_dupmode"
android:title="@string/unhide_dupmode_title"
android:summary="@string/unhide_dupmode_summary"
android:defaultValue="false"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_nag_intervals"
android:title="@string/nag_intervals"

View file

@ -35,7 +35,6 @@ import java.util.Map;
public class FBMService extends FirebaseMessagingService {
private static final String TAG = FBMService.class.getSimpleName();
private static final String KEY_FCMID = TAG + "_fcmid";
public static void init( Context context )
{
@ -121,7 +120,7 @@ public class FBMService extends FirebaseMessagingService {
public static String getFCMDevID( Context context )
{
String result = DBUtils.getStringFor( context, KEY_FCMID, null );
String result = DBUtils.getStringFor( context, BuildConfig.KEY_FCMID, null );
if ( null == result ) {
getTokenAsync( context );
@ -158,7 +157,7 @@ public class FBMService extends FirebaseMessagingService {
// Don't call this with empty tokens!!!
Assert.assertTrue( token.length() > 0 || !BuildConfig.DEBUG );
DBUtils.setStringFor( context, KEY_FCMID, token );
DBUtils.setStringFor( context, BuildConfig.KEY_FCMID, token );
DevID.setFCMDevID( context, token );
RelayService.fcmConfirmed( context, true );

View file

@ -0,0 +1 @@
../xw4d/AndroidManifest.xml

View file

@ -0,0 +1,127 @@
{
"project_info": {
"project_number": "801272813571",
"firebase_url": "https://fcmtest-9fe99.firebaseio.com",
"project_id": "fcmtest-9fe99",
"storage_bucket": "fcmtest-9fe99.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:801272813571:android:15f4eb80a9b07720",
"android_client_info": {
"package_name": "com.google.firebase.fiamquickstart"
}
},
"oauth_client": [
{
"client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCl3lfUITEX0EscF2aeDZY4G-DNL2xeEZ8"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:801272813571:android:2d4684b9d573e182",
"android_client_info": {
"package_name": "org.eehouse.android.xw4"
}
},
"oauth_client": [
{
"client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCl3lfUITEX0EscF2aeDZY4G-DNL2xeEZ8"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:801272813571:android:8c4ed916336414b2",
"android_client_info": {
"package_name": "org.eehouse.android.xw4dbg"
}
},
"oauth_client": [
{
"client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCl3lfUITEX0EscF2aeDZY4G-DNL2xeEZ8"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:801272813571:android:ded92ad1a9f5b318d41636",
"android_client_info": {
"package_name": "org.eehouse.android.xw4dup"
}
},
"oauth_client": [
{
"client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCl3lfUITEX0EscF2aeDZY4G-DNL2xeEZ8"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "801272813571-g3lfciu89q8ffb7ahasrce5nj3vsghot.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View file

@ -0,0 +1 @@
../../../../../../xw4NoSMS/java/org/eehouse/android/xw4/CrashTrack.java

View file

@ -0,0 +1 @@
../../../../../../xw4NoSMS/java/org/eehouse/android/xw4/FBMService.java

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1 @@
../../../../../../../src/xw4d/java/org/eehouse/android/xw4/CrashTrack.java

View file

@ -0,0 +1 @@
../../../../../../../src/xw4NoSMS/java/org/eehouse/android/xw4/FBMService.java

View file

@ -24,7 +24,6 @@ import android.content.Intent;
public class FBMService {
private static final String TAG = FBMService.class.getSimpleName();
private static final String KEY_FCMID = TAG + "_fcmid";
public static void init( Context context )
{

View file

@ -44,6 +44,7 @@ LOCAL_DEFINES += \
-DNATIVE_NLI \
-DCOMMS_VERSION=1 \
-DINITIAL_CLIENT_VERS=${INITIAL_CLIENT_VERS} \
-DXW_BT_UUID=\"${XW_BT_UUID}\" \
-DVARIANT_${VARIANT} \
-DRELAY_ROOM_DEFAULT=\"\" \
-D__LITTLE_ENDIAN \

View file

@ -20,13 +20,16 @@
# define STRD_CUMULATIVE_SCORE 14
# define STRS_NEW_TILES 15
# define STR_COMMIT_CONFIRM 16
# define STR_BONUS_ALL 17
# define STRD_TURN_SCORE 18
# define STRD_REMAINS_HEADER 19
# define STRD_REMAINS_EXPL 20
# define STRSD_RESIGNED 21
# define STRSD_WINNER 22
# define STRDSD_PLACER 23
# define STR_SUBMIT_CONFIRM 17
# define STR_BONUS_ALL 18
# define STRD_TURN_SCORE 19
# define STRD_REMAINS_HEADER 20
# define STRD_REMAINS_EXPL 21
# define STRSD_RESIGNED 22
# define STRSD_WINNER 23
# define STRDSD_PLACER 24
# define STR_DUP_CLIENT_SENT 25
# define STRDD_DUP_HOST_RECEIVED 26
# define N_AND_USER_STRINGS 23
# define N_AND_USER_STRINGS 26
#endif

View file

@ -31,9 +31,9 @@ void
and_assert( const char* test, int line, const char* file, const char* func )
{
XP_LOGF( "assertion \"%s\" failed: line %d in %s() in %s",
test, line, file, func );
test, line, func, file );
__android_log_assert( test, "ASSERT", "line %d in %s() in %s",
line, file, func );
line, func, file );
}
#ifdef __LITTLE_ENDIAN
@ -115,7 +115,7 @@ getInts( JNIEnv* env, void* cobj, jobject jobj, const SetInfo* sis, XP_U16 nSis
void
setInt( JNIEnv* env, jobject obj, const char* name, int value )
{
// XP_LOGF( "%s(name=%s)", __func__, name );
// XP_LOGF( "%s(name=%s, val=%d)", __func__, name, value );
jclass cls = (*env)->GetObjectClass( env, obj );
XP_ASSERT( !!cls );
jfieldID fid = (*env)->GetFieldID( env, cls, name, "I");
@ -341,6 +341,15 @@ makeIntArray( JNIEnv *env, int count, const void* vals, size_t elemSize )
return array;
}
void
setIntArray( JNIEnv *env, jobject jowner, const char* fieldName,
int count, const void* vals, size_t elemSize )
{
jintArray jarr = makeIntArray( env, count, vals, elemSize );
setObject( env, jowner, fieldName, "[I", jarr );
deleteLocalRef( env, jarr );
}
jbyteArray
makeByteArray( JNIEnv *env, int siz, const jbyte* vals )
{
@ -413,14 +422,14 @@ setIntInArray( JNIEnv* env, jintArray arr, int index, int val )
}
jobjectArray
makeStringArray( JNIEnv *env, int siz, const XP_UCHAR** vals )
makeStringArray( JNIEnv *env, const int count, const XP_UCHAR** vals )
{
jclass clas = (*env)->FindClass(env, "java/lang/String");
jstring empty = (*env)->NewStringUTF( env, "" );
jobjectArray jarray = (*env)->NewObjectArray( env, siz, clas, empty );
jobjectArray jarray = (*env)->NewObjectArray( env, count, clas, empty );
deleteLocalRefs( env, clas, empty, DELETE_NO_REF );
for ( int ii = 0; !!vals && ii < siz; ++ii ) {
for ( int ii = 0; !!vals && ii < count; ++ii ) {
jstring jstr = (*env)->NewStringUTF( env, vals[ii] );
(*env)->SetObjectArrayElement( env, jarray, ii, jstr );
deleteLocalRef( env, jstr );
@ -429,6 +438,15 @@ makeStringArray( JNIEnv *env, int siz, const XP_UCHAR** vals )
return jarray;
}
void
setStringArray( JNIEnv *env, jobject jowner, const char* ownerField,
int count, const XP_UCHAR** vals )
{
jobjectArray jaddrs = makeStringArray( env, count, vals );
setObject( env, jowner, ownerField, "[Ljava/lang/String;", jaddrs );
deleteLocalRef( env, jaddrs );
}
jobjectArray
makeByteArrayArray( JNIEnv *env, int siz )
{
@ -771,6 +789,8 @@ android_debugf( const char* format, ... )
"xw4"
# elif defined VARIANT_xw4d || defined VARIANT_xw4dNoSMS
"x4bg"
# elif defined VARIANT_xw4dup || defined VARIANT_xw4dupNoSMS
"x4du"
# endif
, buf );
}

View file

@ -69,6 +69,8 @@ bool getObject( JNIEnv* env, jobject obj, const char* name, const char* sig,
jobject* ret );
jintArray makeIntArray( JNIEnv *env, int size, const void* vals, size_t elemSize );
void setIntArray( JNIEnv *env, jobject jowner, const char* ownerField,
int count, const void* vals, size_t elemSize );
void getIntsFromArray( JNIEnv* env, int dest[], jintArray arr, int count, bool del );
void setIntInArray( JNIEnv* env, jintArray arr, int index, int val );
@ -80,6 +82,9 @@ void setBoolArray( JNIEnv* env, jbooleanArray jarr, int count,
const jboolean* vals );
jobjectArray makeStringArray( JNIEnv *env, int size, const XP_UCHAR** vals );
void setStringArray( JNIEnv *env, jobject jowner, const char* ownerField,
int count, const XP_UCHAR** vals );
jstring streamToJString( JNIEnv* env, XWStreamCtxt* stream );
jbyteArray streamToBArray( JNIEnv *env, XWStreamCtxt* stream );

View file

@ -369,16 +369,16 @@ and_draw_score_drawPlayer( DrawCtx* dctx, const XP_Rect* rInner,
static void
and_draw_drawTimer( DrawCtx* dctx, const XP_Rect* rect, XP_U16 player,
XP_S16 secondsLeft )
XP_S16 secondsLeft, XP_Bool inDuplicateMode )
{
if ( rect->width == 0 ) {
XP_LOGF( "%s: exiting b/c rect empty", __func__ );
} else {
DRAW_CBK_HEADER("drawTimer", "(Landroid/graphics/Rect;II)V" );
DRAW_CBK_HEADER("drawTimer", "(Landroid/graphics/Rect;IIZ)V" );
jobject jrect = makeJRect( draw, JCACHE_RECT0, rect );
(*env)->CallVoidMethod( env, draw->jdraw, mid,
jrect, player, secondsLeft );
jrect, player, secondsLeft, inDuplicateMode );
returnJRect( draw, JCACHE_RECT0, jrect );
}
}
@ -526,9 +526,9 @@ and_draw_drawTrayDivider( DrawCtx* dctx, const XP_Rect* rect, CellFlags flags )
static void
and_draw_score_pendingScore( DrawCtx* dctx, const XP_Rect* rect,
XP_S16 score, XP_U16 playerNum,
XP_S16 curTurn, CellFlags flags )
XP_Bool curTurn, CellFlags flags )
{
DRAW_CBK_HEADER( "score_pendingScore", "(Landroid/graphics/Rect;IIII)V" );
DRAW_CBK_HEADER( "score_pendingScore", "(Landroid/graphics/Rect;IIZI)V" );
jobject jrect = makeJRect( draw, JCACHE_RECT0, rect );

View file

@ -242,6 +242,16 @@ and_util_informMove( XW_UtilCtxt* uc, XP_S16 turn, XWStreamCtxt* expl,
UTIL_CBK_TAIL();
}
static void
and_util_notifyDupStatus( XW_UtilCtxt* uc, XP_Bool amHost, const XP_UCHAR* msg )
{
UTIL_CBK_HEADER( "notifyDupStatus", "(ZLjava/lang/String;)V" );
jstring jmsg = (*env)->NewStringUTF( env, msg );
(*env)->CallVoidMethod( env, util->jutil, mid, amHost, jmsg );
deleteLocalRefs( env, jmsg, DELETE_NO_REF );
UTIL_CBK_TAIL();
}
static void
and_util_informUndo( XW_UtilCtxt* uc )
{
@ -581,6 +591,34 @@ and_util_remSelected(XW_UtilCtxt* uc)
UTIL_CBK_TAIL();
}
static void
and_util_timerSelected( XW_UtilCtxt* uc, XP_Bool inDuplicateMode, XP_Bool canPause )
{
UTIL_CBK_HEADER("timerSelected", "(ZZ)V" );
(*env)->CallVoidMethod( env, util->jutil, mid, inDuplicateMode, canPause );
UTIL_CBK_TAIL();
}
static void
and_util_formatPauseHistory( XW_UtilCtxt* uc, XWStreamCtxt* stream,
DupPauseType typ, XP_S16 turn,
XP_U32 secsPrev, XP_U32 secsCur,
const XP_UCHAR* msg )
{
UTIL_CBK_HEADER( "formatPauseHistory",
"(IIIILjava/lang/String;)Ljava/lang/String;" );
jstring jmsg = !! msg ? (*env)->NewStringUTF( env, msg ) : NULL;
jstring jresult = (*env)->CallObjectMethod( env, util->jutil, mid, typ,
turn, secsPrev, secsCur, jmsg );
const char* jchars = (*env)->GetStringUTFChars( env, jresult, NULL );
stream_catString( stream, jchars );
(*env)->ReleaseStringUTFChars( env, jresult, jchars );
deleteLocalRefs( env, jresult, jmsg, DELETE_NO_REF );
UTIL_CBK_TAIL();
}
#ifndef XWFEATURE_MINIWIN
static void
and_util_bonusSquareHeld( XW_UtilCtxt* uc, XWBonusType bonus )
@ -768,6 +806,28 @@ and_dutil_md5sum( XW_DUtilCtxt* duc, const XP_U8* ptr, XP_U16 len )
}
#endif
static void
and_dutil_notifyPause( XW_DUtilCtxt* duc, XP_U32 gameID, DupPauseType pauseTyp,
XP_U16 pauser, const XP_UCHAR* name,
const XP_UCHAR* msg )
{
DUTIL_CBK_HEADER( "notifyPause", "(IIILjava/lang/String;Ljava/lang/String;)V" );
jstring jname = (*env)->NewStringUTF( env, name );
jstring jmsg = (*env)->NewStringUTF( env, msg );
(*env)->CallVoidMethod( env, dutil->jdutil, mid, gameID, pauseTyp, pauser,
jname, jmsg );
deleteLocalRefs( env, jname, jmsg, DELETE_NO_REF );
DUTIL_CBK_TAIL();
}
static void
and_dutil_onDupTimerChanged( XW_DUtilCtxt* duc, XP_U32 gameID,
XP_U32 oldVal, XP_U32 newVal )
{
DUTIL_CBK_HEADER( "onDupTimerChanged", "(III)V" );
(*env)->CallVoidMethod( env, dutil->jdutil, mid, gameID, oldVal, newVal );
DUTIL_CBK_TAIL();
}
XW_UtilCtxt*
makeUtil( MPFORMAL EnvThreadInfo* ti, jobject jutil, CurGameInfo* gi,
@ -803,6 +863,7 @@ makeUtil( MPFORMAL EnvThreadInfo* ti, jobject jutil, CurGameInfo* gi,
SET_PROC(turnChanged);
#endif
SET_PROC(informMove);
SET_PROC(notifyDupStatus);
SET_PROC(informUndo);
SET_PROC(informNetDict);
SET_PROC(notifyGameOver);
@ -820,6 +881,8 @@ makeUtil( MPFORMAL EnvThreadInfo* ti, jobject jutil, CurGameInfo* gi,
SET_PROC(showChat);
#endif
SET_PROC(remSelected);
SET_PROC(timerSelected);
SET_PROC(formatPauseHistory);
#ifndef XWFEATURE_MINIWIN
SET_PROC(bonusSquareHeld);
@ -899,6 +962,8 @@ makeDUtil( MPFORMAL EnvThreadInfo* ti, jobject jdutil, VTableMgr* vtMgr,
#ifdef COMMS_CHECKSUM
SET_DPROC(md5sum);
#endif
SET_DPROC(notifyPause);
SET_DPROC(onDupTimerChanged);
return &dutil->dutil;
}

View file

@ -40,6 +40,4 @@ void destroyUtil( XW_UtilCtxt** util );
bool utilTimerFired( XW_UtilCtxt* util, XWTimerReason why, int handle );
XP_U32 and_util_getCurSeconds( XW_UtilCtxt* uc ); /* uc can be NULL */
#endif

View file

@ -52,6 +52,7 @@
typedef struct _EnvThreadEntry {
JNIEnv* env;
pthread_t owner;
XP_U16 refcount;
#ifdef LOG_MAPPING
const char* ownerFunc;
#endif
@ -186,6 +187,8 @@ map_thread_prv( EnvThreadInfo* ti, JNIEnv* env, const char* caller )
XP_ASSERT( !!firstEmpty );
firstEmpty->owner = self;
firstEmpty->env = env;
XP_ASSERT( 0 == firstEmpty->refcount );
++firstEmpty->refcount;
#ifdef LOG_MAPPING
firstEmpty->ownerFunc = caller;
XP_LOGF( "%s: entry %zu: mapped env %p to thread %x", __func__,
@ -216,12 +219,15 @@ map_remove_prv( EnvThreadInfo* ti, JNIEnv* env, const char* func )
EnvThreadEntry* entry = &ti->entries[ii];
found = env == entry->env;
if ( found ) {
XP_ASSERT( pthread_self() == entry->owner );
#ifdef LOG_MAPPING
XP_LOGF( "%s: UNMAPPED env %p to thread %x (from %s; mapped by %s)", __func__,
entry->env, (int)entry->owner, func, entry->ownerFunc );
XP_LOGF( "%s: %d entries left", __func__, countUsed( ti ) );
entry->ownerFunc = NULL;
#endif
XP_ASSERT( 1 == entry->refcount );
--entry->refcount;
entry->env = NULL;
entry->owner = 0;
}
@ -359,6 +365,7 @@ static const SetInfo gi_bools[] = {
,ARR_MEMBER( CurGameInfo, timerEnabled )
,ARR_MEMBER( CurGameInfo, allowPickTiles )
,ARR_MEMBER( CurGameInfo, allowHintRect )
,ARR_MEMBER( CurGameInfo, inDuplicateMode )
};
static const SetInfo pl_ints[] = {
@ -701,13 +708,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_comms_1getUUID
{
jstring jstr =
#ifdef XWFEATURE_BLUETOOTH
(*env)->NewStringUTF( env,
# if defined VARIANT_xw4NoSMS || defined VARIANT_xw4fdroid || defined VARIANT_xw4SMS
XW_BT_UUID
# elif defined VARIANT_xw4d || defined VARIANT_xw4dNoSMS
XW_BT_UUID_DBG
# endif
)
(*env)->NewStringUTF( env, XW_BT_UUID )
#else
NULL
#endif
@ -1227,19 +1228,6 @@ Java_org_eehouse_android_xw4_jni_XwJNI_board_1setScoreboardLoc
XWJNI_END();
}
JNIEXPORT void JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_board_1setTimerLoc
( JNIEnv* env, jclass C, GamePtrType gamePtr, jint timerLeft, jint timerTop,
jint timerWidth, jint timerHeight )
{
XWJNI_START();
XP_LOGF( "%s(%d,%d,%d,%d)", __func__, timerLeft, timerTop,
timerWidth, timerHeight );
board_setTimerLoc( state->game.board, timerLeft, timerTop,
timerWidth, timerHeight );
XWJNI_END();
}
JNIEXPORT void JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_board_1setTrayLoc
( JNIEnv *env, jclass C, GamePtrType gamePtr, jint left, jint top,
@ -1692,10 +1680,11 @@ Java_org_eehouse_android_xw4_jni_XwJNI_model_1getPlayersLastScore
player, &lmi );
setBool( env, jlmi, "isValid", valid );
if ( valid ) {
setBool( env, jlmi, "inDuplicateMode", lmi.inDuplicateMode );
setInt( env, jlmi, "score", lmi.score );
setInt( env, jlmi, "nTiles", lmi.nTiles );
setInt( env, jlmi, "moveType", lmi.moveType );
setString( env, jlmi, "name", lmi.name );
setStringArray( env, jlmi, "names", lmi.nWinners, lmi.names );
setString( env, jlmi, "word", lmi.word );
}
XWJNI_END();
@ -1861,16 +1850,19 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1summarize
{
XWJNI_START();
ModelCtxt* model = state->game.model;
ServerCtxt* server = state->game.server;
XP_S16 nMoves = model_getNMoves( model );
setInt( env, jsummary, "nMoves", nMoves );
XP_Bool gameOver = server_getGameIsOver( state->game.server );
XP_Bool gameOver = server_getGameIsOver( server );
setBool( env, jsummary, "gameOver", gameOver );
XP_Bool isLocal = XP_FALSE;
setInt( env, jsummary, "turn",
server_getCurrentTurn( state->game.server, &isLocal ) );
server_getCurrentTurn( server, &isLocal ) );
setBool( env, jsummary, "turnIsLocal", isLocal );
setInt( env, jsummary, "lastMoveTime",
server_getLastMoveTime(state->game.server) );
server_getLastMoveTime(server) );
setInt( env, jsummary, "dupTimerExpires",
server_getDupTimerExpires(server) );
if ( !!state->game.comms ) {
CommsAddrRec addr;
@ -1878,7 +1870,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1summarize
comms_getAddr( comms, &addr );
setInt( env, jsummary, "seed", comms_getChannelSeed( comms ) );
setInt( env, jsummary, "missingPlayers",
server_getMissingPlayers( state->game.server ) );
server_getMissingPlayers( server ) );
setInt( env, jsummary, "nPacketsPending",
comms_countPendingPackets( state->game.comms ) );
@ -1917,10 +1909,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1summarize
}
XP_LOGF( "%s: adding btaddr/phone/mac %s", __func__, addrps[ii] );
}
jobjectArray jaddrs = makeStringArray( env, count, addrps );
setObject( env, jsummary, "remoteDevs", "[Ljava/lang/String;",
jaddrs );
deleteLocalRef( env, jaddrs );
setStringArray( env, jsummary, "remoteDevs", count, addrps );
}
break;
#endif
@ -1943,9 +1932,8 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1summarize
jvals[ii] = model_getPlayerScore( model, ii );
}
}
jintArray jarr = makeIntArray( env, nPlayers, jvals, sizeof(jvals[0]) );
setObject( env, jsummary, "scores", "[I", jarr );
deleteLocalRef( env, jarr );
setIntArray( env, jsummary, "scores", nPlayers, jvals, sizeof(jvals[0]) );
XWJNI_END();
}
@ -2039,6 +2027,8 @@ static const SetInfo gsi_bools[] = {
ARR_MEMBER( GameStateInfo, curTurnSelected ),
ARR_MEMBER( GameStateInfo, canHideRack ),
ARR_MEMBER( GameStateInfo, canTrade ),
ARR_MEMBER( GameStateInfo, canPause ),
ARR_MEMBER( GameStateInfo, canUnpause ),
};
JNIEXPORT void JNICALL
@ -2268,6 +2258,30 @@ Java_org_eehouse_android_xw4_jni_XwJNI_server_1endGame
XWJNI_END();
}
JNIEXPORT void JNICALL Java_org_eehouse_android_xw4_jni_XwJNI_board_1pause
( JNIEnv* env, jclass C, GamePtrType gamePtr, jstring jmsg )
{
XWJNI_START();
XP_ASSERT( !!state->game.board );
const char* msg = (*env)->GetStringUTFChars( env, jmsg, NULL );
board_pause( state->game.board, msg );
(*env)->ReleaseStringUTFChars( env, jmsg, msg );
XWJNI_END();
}
JNIEXPORT void JNICALL Java_org_eehouse_android_xw4_jni_XwJNI_board_1unpause
( JNIEnv* env, jclass C, GamePtrType gamePtr, jstring jmsg )
{
XWJNI_START();
XP_ASSERT( !!state->game.board );
const char* msg = (*env)->GetStringUTFChars( env, jmsg, NULL );
board_unpause( state->game.board, msg );
(*env)->ReleaseStringUTFChars( env, jmsg, msg );
XWJNI_END();
}
#ifdef XWFEATURE_CHAT
JNIEXPORT void JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_board_1sendChat

View file

@ -96,7 +96,9 @@ static void boardTurnChanged( void* board );
static void boardGameOver( void* board, XP_S16 quitter );
static void setArrow( BoardCtxt* board, XP_U16 row, XP_U16 col, XP_Bool* vp );
static XP_Bool setArrowVisible( BoardCtxt* board, XP_Bool visible );
static void board_setTimerLoc( BoardCtxt* board,
XP_U16 timerLeft, XP_U16 timerTop,
XP_U16 timerWidth, XP_U16 timerHeight );
#ifdef XWFEATURE_MINIWIN
static void invalTradeWindow( BoardCtxt* board, XP_S16 turn, XP_Bool redraw );
#else
@ -657,7 +659,7 @@ board_setPos( BoardCtxt* board, XP_U16 left, XP_U16 top,
figureBoardRect( board );
} /* board_setPos */
void
static void
board_setTimerLoc( BoardCtxt* board,
XP_U16 timerLeft, XP_U16 timerTop,
XP_U16 timerWidth, XP_U16 timerHeight )
@ -812,7 +814,7 @@ board_getYOffset( const BoardCtxt* board )
XP_Bool
board_curTurnSelected( const BoardCtxt* board )
{
return MY_TURN( board );
return server_isPlayersTurn( board->server, board->selPlayer );
}
XP_U16
@ -822,6 +824,21 @@ board_visTileCount( const BoardCtxt* board )
TRAY_REVEALED == board->trayVisState );
}
void
board_pause( BoardCtxt* board, const XP_UCHAR* msg )
{
server_pause( board->server, board->selPlayer, msg );
board_invalAll( board );
}
void
board_unpause( BoardCtxt* board, const XP_UCHAR* msg )
{
server_unpause( board->server, board->selPlayer, msg );
setTimerIf( board );
board_invalAll( board );
}
XP_Bool
board_canShuffle( const BoardCtxt* board )
{
@ -841,6 +858,7 @@ XP_Bool
board_canTrade( BoardCtxt* board )
{
XP_Bool result = preflight( board, XP_FALSE )
&& !board->gi->inDuplicateMode
&& MIN_TRADE_TILES(board) <= server_countTilesInPool( board->server );
return result;
}
@ -1060,17 +1078,18 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
{
XP_Bool result = XP_FALSE;
const XP_S16 turn = server_getCurrentTurn( board->server, NULL );
PerTurnInfo* pti = board->pti + turn;
const XP_U16 selPlayer = board->selPlayer;
ModelCtxt* model = board->model;
if ( board->gameOver || turn < 0 ) {
/* do nothing */
} else if ( turn != board->selPlayer ) {
} else if ( !server_isPlayersTurn( board->server, selPlayer ) ) {
util_userError( board->util, ERR_NOT_YOUR_TURN );
} else if ( 0 == model_getNumTilesTotal( model, turn ) ) {
} else if ( 0 == model_getNumTilesTotal( model, selPlayer ) ) {
/* game's over but still undoable so turn hasn't changed; do
nothing */
} else if ( phoniesConfirmed || turnConfirmed || checkRevealTray( board ) ) {
PerTurnInfo* pti = board->pti + selPlayer;
if ( pti->tradeInProgress ) {
TileBit traySelBits = pti->traySelBits;
int count = 0;
@ -1088,7 +1107,7 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
TrayTileSet selTiles;
getSelTiles( board, traySelBits, &selTiles );
if ( turnConfirmed ) {
if ( !server_askPickTiles( board->server, turn, newTiles,
if ( !server_askPickTiles( board->server, selPlayer, newTiles,
selTiles.nTiles ) ) {
/* server_commitTrade() changes selPlayer, so board_endTrade
must be called first() */
@ -1111,8 +1130,9 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
stream = mem_stream_make_raw( MPPARM(board->mpool)
dutil_getVTManager(board->dutil) );
const XP_UCHAR* str = dutil_getUserString( board->dutil,
STR_COMMIT_CONFIRM );
XP_U16 stringCode = board->gi->inDuplicateMode
? STR_SUBMIT_CONFIRM : STR_COMMIT_CONFIRM;
const XP_UCHAR* str = dutil_getUserString( board->dutil, stringCode );
stream_catString( stream, str );
XP_Bool warn = board->util->gameInfo->phoniesAction == PHONIES_WARN;
@ -1121,14 +1141,14 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
info.proc = saveBadWords;
info.closure = &bwl;
}
legal = model_checkMoveLegal( model, turn, stream,
legal = model_checkMoveLegal( model, selPlayer, stream,
warn? &info:(WordNotifierInfo*)NULL);
}
if ( 0 < bwl.bwi.nWords && !phoniesConfirmed ) {
bwl.bwi.dictName =
dict_getShortName( model_getPlayerDict( model, turn ) );
util_notifyIllegalWords( board->util, &bwl.bwi, turn, XP_FALSE );
dict_getShortName( model_getPlayerDict( model, selPlayer ) );
util_notifyIllegalWords( board->util, &bwl.bwi, selPlayer, XP_FALSE );
} else {
/* Hide the tray so no peeking. Leave it hidden even if user
cancels as otherwise another player could get around
@ -1140,10 +1160,11 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
if ( board->skipCommitConfirm || turnConfirmed ) {
XP_U16 nToPick = MAX_TRAY_TILES -
model_getNumTilesInTray( model, turn );
if ( !server_askPickTiles( board->server, turn, newTiles,
model_getNumTilesInTray( model, selPlayer );
if ( !server_askPickTiles( board->server, selPlayer, newTiles,
nToPick ) ) {
result = server_commitMove( board->server, newTiles )
result = server_commitMove( board->server, selPlayer,
newTiles )
|| result;
/* invalidate all tiles in case we'll be drawing this tray
again rather than some other -- as is the case in a
@ -1163,7 +1184,7 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed,
}
if ( result ) {
setArrowVisibleFor( board, turn, XP_FALSE );
setArrowVisibleFor( board, selPlayer, XP_FALSE );
}
}
}
@ -1386,7 +1407,9 @@ timerFiredForPen( BoardCtxt* board )
static void
setTimerIf( BoardCtxt* board )
{
XP_Bool timerWanted = board->gi->timerEnabled && !board->gameOver;
XP_Bool timerWanted = board->gi->timerEnabled
&& !board->gameOver
&& !server_canUnpause( board->server );
if ( timerWanted && !board->timerPending ) {
util_setTimer( board->util, TIMER_TIMERTICK, 0,
@ -1399,16 +1422,20 @@ static void
timerFiredForTimer( BoardCtxt* board )
{
board->timerPending = XP_FALSE;
if ( !board->gameOver ) {
XP_S16 turn = server_getCurrentTurn( board->server, NULL );
if ( !board->gameOver || !server_canUnpause( board->server ) ) {
XP_Bool doDraw = board->gi->inDuplicateMode;
if ( !doDraw ) {
XP_S16 turn = server_getCurrentTurn( board->server, NULL );
if ( turn >= 0 ) {
++board->gi->players[turn].secondsUsed;
if ( turn >= 0 ) {
++board->gi->players[turn].secondsUsed;
if ( turn == board->selPlayer ) {
drawTimer( board );
doDraw = turn == board->selPlayer;
}
}
if ( doDraw ) {
drawTimer( board );
}
}
setTimerIf( board );
} /* timerFiredForTimer */
@ -2084,7 +2111,7 @@ static XP_Bool
preflight( BoardCtxt* board, XP_Bool reveal )
{
return !board->gameOver
&& server_getCurrentTurn(board->server, NULL) >= 0
&& server_getCurrentTurn( board->server, NULL) >= 0
&& ( !reveal || checkRevealTray( board ) )
&& !TRADE_IN_PROGRESS(board);
} /* preflight */
@ -2098,6 +2125,24 @@ MIN_TRADE_TILES( const BoardCtxt* board )
return 6 == langCode ? 1 : MAX_TRAY_TILES;
}
#ifdef DEBUG
/* static void */
/* assertTilesInTiles( const BoardCtxt* board, const MoveInfo* mi, */
/* const Tile* tiles, XP_U16 nTiles ) */
/* { */
/* Tile blank = dict_getBlankTile( model_getDictionary( board->model ) ); */
/* for ( XP_U16 ii = 0; ii < mi->nTiles; ++ii ) { */
/* Tile tile = mi->tiles[ii].tile; */
/* XP_Bool found = XP_FALSE; */
/* for ( XP_U16 jj = 0; !found && jj < nTiles; ++jj ) { */
/* found = tiles[jj] == tile */
/* || (tiles[jj] == blank && IS_BLANK(tile)); */
/* } */
/* XP_ASSERT( found ); */
/* } */
/* } */
#endif
/* Refuse with error message if any tiles are currently on board in this turn.
* Then call the engine, and display the first move. Return true if there's
* any redrawing to be done.
@ -2190,7 +2235,7 @@ board_requestHint( BoardCtxt* board,
# endif
#endif
searchComplete =
engine_findMove( engine, model, selPlayer, XP_FALSE,
engine_findMove( engine, model, selPlayer, XP_FALSE, XP_FALSE,
tiles, nTiles, usePrev,
#ifdef XWFEATURE_BONUSALL
allTilesBonus,
@ -2199,12 +2244,13 @@ board_requestHint( BoardCtxt* board,
lp, useTileLimits,
#endif
0, /* 0: not a robot */
&canMove, &newMove );
&canMove, &newMove, NULL );
board_popTimerSave( board );
if ( searchComplete && canMove ) {
// assertTilesInTiles( board, &newMove, tiles, nTiles );
juggleMoveIfDebug( &newMove );
model_makeTurnFromMoveInfo( model, selPlayer, &newMove);
model_makeTurnFromMoveInfo( model, selPlayer, &newMove );
} else {
result = XP_FALSE;
XP_STATUSF( "unable to complete hint request\n" );
@ -2492,6 +2538,9 @@ pointOnSomething( const BoardCtxt* board, XP_U16 xx, XP_U16 yy,
*wp = OBJ_TRAY;
} else if ( rectContainsPt( &board->scoreBdBounds, xx, yy ) ) {
*wp = OBJ_SCORE;
} else if ( board->gi->timerEnabled
&& rectContainsPt( &board->timerBounds, xx, yy ) ) {
*wp = OBJ_TIMER;
} else {
*wp = OBJ_NONE;
result = XP_FALSE;
@ -3055,6 +3104,10 @@ handlePenUpInternal( BoardCtxt* board, XP_U16 xx, XP_U16 yy, XP_Bool isPen,
draw = handlePenUpTray( board, xx, yy ) || draw;
}
break;
case OBJ_TIMER:
util_timerSelected( board->util, board->gi->inDuplicateMode,
server_canPause( board->server ) );
break;
default:
XP_ASSERT( XP_FALSE );
}
@ -3117,6 +3170,7 @@ focusToCoords( BoardCtxt* board, XP_U16* xp, XP_U16* yp )
if ( result ) {
switch( board->focussed ) {
case OBJ_NONE:
case OBJ_TIMER:
result = XP_FALSE;
break;
case OBJ_BOARD:
@ -3380,6 +3434,7 @@ invalFocusOwner( BoardCtxt* board )
}
break;
case OBJ_NONE:
case OBJ_TIMER:
draw = XP_FALSE;
break;
}
@ -3606,17 +3661,18 @@ board_moveCursor( BoardCtxt* board, XP_Key cursorKey, XP_Bool preflightOnly,
#endif
XP_Bool
rectContainsPt( const XP_Rect* rect, XP_S16 x, XP_S16 y )
rectContainsPt( const XP_Rect* rect, XP_S16 xx, XP_S16 yy )
{
/* 7/4 Made <= into <, etc., because a tap on the right boundary of the
board was still mapped onto the board but dividing by scale put it in
the 15th column. If this causes other problems and the '=' chars have
to be put back then deal with that, probably by forcing an
out-of-bounds col/row to the nearest possible. */
return ( rect->top <= y
&& rect->left <= x
&& (rect->top + rect->height) >= y
&& (rect->left + rect->width) >= x );
XP_Bool result = ( rect->top <= yy
&& rect->left <= xx
&& (rect->top + rect->height) >= yy
&& (rect->left + rect->width) >= xx );
return result;
} /* rectContainsPt */
XP_Bool

View file

@ -117,9 +117,6 @@ void board_setScoreboardLoc( BoardCtxt* board,
XP_U16 scoreLeft, XP_U16 scoreTop,
XP_U16 scoreWidth, XP_U16 scoreHeight,
XP_Bool divideHorizontally );
void board_setTimerLoc( BoardCtxt* board,
XP_U16 timerLeft, XP_U16 timerTop,
XP_U16 timerWidth, XP_U16 timerHeight );
void board_setTrayLoc( BoardCtxt* board, XP_U16 trayLeft, XP_U16 trayTop,
XP_U16 trayWidth, XP_U16 trayHeight );
@ -129,6 +126,8 @@ XP_U16 board_getYOffset( const BoardCtxt* board );
XP_Bool board_curTurnSelected( const BoardCtxt* board );
XP_U16 board_visTileCount( const BoardCtxt* board );
void board_pause( BoardCtxt* board, const XP_UCHAR* msg );
void board_unpause( BoardCtxt* board, const XP_UCHAR* msg );
XP_Bool board_canShuffle( const BoardCtxt* board );
XP_Bool board_canHideRack( const BoardCtxt* board );
XP_Bool board_canTrade( BoardCtxt* board );

View file

@ -373,7 +373,7 @@ drawCell( BoardCtxt* board, const XP_U16 col, const XP_U16 row, XP_Bool skipBlan
XP_Bool success = XP_TRUE;
XP_Rect cellRect = {0};
Tile tile;
XP_Bool isBlank, isEmpty, recent = XP_FALSE, pending = XP_FALSE;
XP_Bool isBlank, isEmpty, pending = XP_FALSE;
XWBonusType bonus;
ModelCtxt* model = board->model;
DictionaryCtxt* dict = model_getDictionary( model );
@ -395,6 +395,7 @@ drawCell( BoardCtxt* board, const XP_U16 col, const XP_U16 row, XP_Bool skipBlan
/* This 'while' is only here so I can 'break' below */
while ( board->trayVisState == TRAY_HIDDEN ||
!rectContainsRect( &board->trayBounds, &cellRect ) ) {
XP_Bool recent = XP_FALSE;
XP_UCHAR ch[4] = {'\0'};
XP_S16 owner = -1;
XP_Bitmaps bitmaps;

View file

@ -240,7 +240,6 @@ struct BoardCtxt {
# define valHintMiniWindowActive( board ) \
((XP_Bool)((board)->miniWindowStuff[MINIWINDOW_VALHINT].text != NULL))
#endif
#define MY_TURN(b) ((b)->selPlayer == server_getCurrentTurn( (b)->server, NULL ))
#define TRADE_IN_PROGRESS(b) ((b)->selInfo->tradeInProgress==XP_TRUE)
/* tray-related functions */

View file

@ -63,8 +63,9 @@ typedef enum {
} CommsRelayState;
#ifdef XWFEATURE_BLUETOOTH
# define XW_BT_UUID "7be0d084-ff89-4d6d-9c78-594773a6f963"
# define XW_BT_UUID_DBG "b079b640-35fe-11e5-a432-0002a5d5c51b"
# ifndef XW_BT_UUID
# define XW_BT_UUID "7be0d084-ff89-4d6d-9c78-594773a6f963"
# endif
# define XW_BT_NAME "CrossWords"
#endif

View file

@ -127,7 +127,8 @@ typedef enum {
OBJ_NONE,
OBJ_BOARD,
OBJ_SCORE,
OBJ_TRAY
OBJ_TRAY,
OBJ_TIMER,
} BoardObjectType;
enum {
@ -177,6 +178,7 @@ typedef enum {
#ifdef XWFEATURE_SLOW_ROBOT
TIMER_SLOWROBOT,
#endif
TIMER_DUP_TIMERCHECK,
NUM_TIMERS_PLUS_ONE /* must be last */
} XWTimerReason;

View file

@ -79,7 +79,10 @@ StackMoveType_2str( StackMoveType typ )
CASESTR(MOVE_TYPE);
CASESTR(TRADE_TYPE);
CASESTR(PHONY_TYPE);
default: return FUNC(__func__) " unknown";
CASESTR(PAUSE_TYPE);
default:
XP_ASSERT(0);
return "<unknown>";
}
}

View file

@ -169,11 +169,12 @@ typedef struct DrawCtxVTable {
const XP_Rect* rect,
XP_S16 score,
XP_U16 playerNum,
XP_S16 curTurn,
XP_Bool curTurn,
CellFlags flags );
void DRAW_VTABLE_NAME(drawTimer) ( DrawCtx* dctx, const XP_Rect* rect,
XP_U16 player, XP_S16 secondsLeft );
XP_U16 player, XP_S16 secondsLeft,
XP_Bool turnDone );
XP_Bool DRAW_VTABLE_NAME(drawCell) ( DrawCtx* dctx, const XP_Rect* rect,
/* at least one of these two will be
@ -307,8 +308,8 @@ struct DrawCtx {
#endif
#define draw_score_pendingScore(dc, r, s, p, t, f ) \
CALL_DRAW_NAME5(score_pendingScore,(dc), (r), (s), (p), (t), (f))
#define draw_drawTimer( dc, r, plyr, sec ) \
CALL_DRAW_NAME3(drawTimer,(dc),(r),(plyr),(sec))
#define draw_drawTimer( dc, r, plyr, sec, dm ) \
CALL_DRAW_NAME4(drawTimer,(dc),(r),(plyr),(sec), (dm))
#define draw_drawCell( dc, rect, txt, bmap, t, v,o, bon, hi, f ) \
CALL_DRAW_NAME9(drawCell,(dc),(rect),(txt),(bmap),(t),(v),(o),(bon),(hi), \
(f))

View file

@ -26,6 +26,11 @@
#include "xwrelay.h"
#include "vtabmgr.h"
typedef enum { UNPAUSED,
PAUSED,
AUTOPAUSED,
} DupPauseType;
typedef struct _DUtilVtable {
XP_U32 (*m_dutil_getCurSeconds)( XW_DUtilCtxt* duc );
const XP_UCHAR* (*m_dutil_getUserString)( XW_DUtilCtxt* duc,
@ -34,7 +39,7 @@ typedef struct _DUtilVtable {
XP_U16 stringCode,
XP_U16 quantity );
void (*m_dutil_storeStream)( XW_DUtilCtxt* duc, const XP_UCHAR* key,
XWStreamCtxt* data );
XWStreamCtxt* data );
/* Pass in an empty stream, and it'll be returned full */
void (*m_dutil_loadStream)( XW_DUtilCtxt* duc, const XP_UCHAR* key,
XWStreamCtxt* inOut );
@ -56,6 +61,12 @@ typedef struct _DUtilVtable {
#ifdef COMMS_CHECKSUM
XP_UCHAR* (*m_dutil_md5sum)( XW_DUtilCtxt* duc, const XP_U8* ptr, XP_U16 len );
#endif
void (*m_dutil_notifyPause)( XW_DUtilCtxt* duc, XP_U32 gameID,
DupPauseType pauseTyp, XP_U16 pauser,
const XP_UCHAR* name, const XP_UCHAR* msg );
void (*m_dutil_onDupTimerChanged)( XW_DUtilCtxt* duc, XP_U32 gameID,
XP_U32 oldVal, XP_U32 newVal );
} DUtilVtable;
struct XW_DUtilCtxt {
@ -101,4 +112,9 @@ struct XW_DUtilCtxt {
# define dutil_md5sum( duc, p, l ) (duc)->vtable.m_dutil_md5sum((duc), (p), (l))
#endif
#define dutil_notifyPause( duc, id, ip, p, n, m ) \
(duc)->vtable.m_dutil_notifyPause( (duc), (id), (ip), (p), (n), (m) )
#define dutil_onDupTimerChanged(duc, id, ov, nv) \
(duc)->vtable.m_dutil_onDupTimerChanged( (duc), (id), (ov), (nv))
#endif

View file

@ -95,6 +95,7 @@ struct EngineCtxt {
XP_U16 nMovesToSave;
XP_U16 star_row;
XP_Bool returnNOW;
XP_Bool skipProgressCallback;
XP_Bool isRobot;
XP_Bool includePending;
MoveIterationData miData;
@ -376,7 +377,7 @@ normalizeIQ( EngineCtxt* engine, XP_U16 iq )
*/
XP_Bool
engine_findMove( EngineCtxt* engine, const ModelCtxt* model,
XP_S16 turn, XP_Bool includePending,
XP_S16 turn, XP_Bool includePending, XP_Bool skipCallback,
const Tile* tiles, const XP_U16 nTiles, XP_Bool usePrev,
#ifdef XWFEATURE_BONUSALL
XP_U16 allTilesBonus,
@ -385,7 +386,8 @@ engine_findMove( EngineCtxt* engine, const ModelCtxt* model,
const BdHintLimits* searchLimits,
XP_Bool useTileLimits,
#endif
XP_U16 robotIQ, XP_Bool* canMoveP, MoveInfo* newMove )
XP_U16 robotIQ, XP_Bool* canMoveP, MoveInfo* newMove,
XP_U16* score )
{
XP_Bool result = XP_TRUE;
XP_U16 star_row;
@ -432,6 +434,7 @@ engine_findMove( EngineCtxt* engine, const ModelCtxt* model,
engine->usePrev = usePrev;
engine->blankTile = dict_getBlankTile( engine->dict );
engine->returnNOW = XP_FALSE;
engine->skipProgressCallback = skipCallback;
#ifdef XWFEATURE_SEARCHLIMIT
engine->searchLimits = searchLimits;
#endif
@ -525,6 +528,9 @@ engine_findMove( EngineCtxt* engine, const ModelCtxt* model,
if ( chooseMove( engine, &move ) ) {
XP_ASSERT( !!newMove );
XP_MEMCPY( newMove, &move->moveInfo, sizeof(*newMove) );
if ( !!score ) {
*score = move->score;
}
} else {
newMove->nTiles = 0;
canMove = XP_FALSE;
@ -1060,7 +1066,7 @@ considerMove( EngineCtxt* engine, Tile* tiles, XP_S16 tileLength,
short col;
BlankTuple blankTuples[MAX_NUM_BLANKS];
if ( !util_engineProgressCallback( engine->util ) ) {
if ( !engine->skipProgressCallback && !util_engineProgressCallback( engine->util ) ) {
engine->returnNOW = XP_TRUE;
} else {

View file

@ -48,8 +48,11 @@ void engine_init( EngineCtxt* ctxt );
void engine_reset( EngineCtxt* ctxt );
void engine_destroy( EngineCtxt* ctxt );
XP_Bool engine_findMove( EngineCtxt* ctxt, const ModelCtxt* model,
XP_S16 turn, XP_Bool includePending,
XP_Bool engine_findMove( EngineCtxt* ctxt, const ModelCtxt* model, XP_S16 turn,
/* includePending: include pending tiles as part of words */
XP_Bool includePending,
/* skipCallback: skip the callback that lets client cancel */
XP_Bool skipCallback,
const Tile* tiles, XP_U16 nTiles, XP_Bool usePrev,
#ifdef XWFEATURE_BONUSALL
XP_U16 allTilesBonus,
@ -58,7 +61,8 @@ XP_Bool engine_findMove( EngineCtxt* ctxt, const ModelCtxt* model,
const BdHintLimits* boardLimits,
XP_Bool useTileLimits,
#endif
XP_U16 robotIQ, XP_Bool* canMove, MoveInfo* result );
XP_U16 robotIQ, XP_Bool* canMove,
MoveInfo* result, XP_U16* score );
XP_Bool engine_check( DictionaryCtxt* dict, Tile* buf, XP_U16 buflen );
#ifdef CPLUS

View file

@ -85,6 +85,26 @@ makeGameID( XW_UtilCtxt* util )
return gameID;
}
static void
timerChangeListener( void* data, const XP_U32 gameID,
XP_S32 oldVal, XP_S32 newVal )
{
XWGame* game = (XWGame*)data;
const CurGameInfo* gi = game->util->gameInfo;
XP_ASSERT( gi->gameID == gameID );
XP_LOGF( "%s(oldVal=%d, newVal=%d, id=%d)", __func__, oldVal, newVal, gameID );
dutil_onDupTimerChanged( util_getDevUtilCtxt( game->util ),
gameID, oldVal, newVal );
}
static void
setListeners( XWGame* game, const CommonPrefs* cp )
{
server_prefsChanged( game->server, cp );
board_prefsChanged( game->board, cp );
server_setTimerChangeListener( game->server, timerChangeListener, game );
}
void
game_makeNewGame( MPFORMAL XWGame* game, CurGameInfo* gi,
XW_UtilCtxt* util, DrawCtx* draw,
@ -105,6 +125,8 @@ game_makeNewGame( MPFORMAL XWGame* game, CurGameInfo* gi,
gi->gameID = makeGameID( util );
}
game->util = util;
game->model = model_make( MPPARM(mpool) (DictionaryCtxt*)NULL, NULL, util,
gi->boardSize );
@ -133,16 +155,15 @@ game_makeNewGame( MPFORMAL XWGame* game, CurGameInfo* gi,
NULL, util );
board_setCallbacks( game->board );
server_prefsChanged( game->server, cp );
board_prefsChanged( game->board, cp );
board_setDraw( game->board, draw );
setListeners( game, cp );
} /* game_makeNewGame */
XP_Bool
game_reset( MPFORMAL XWGame* game, CurGameInfo* gi,
XW_UtilCtxt* XP_UNUSED_STANDALONE(util),
game_reset( MPFORMAL XWGame* game, CurGameInfo* gi, XW_UtilCtxt* util,
CommonPrefs* cp, const TransportProcs* procs )
{
XP_ASSERT( util == game->util );
XP_Bool result = XP_FALSE;
XP_U16 ii;
@ -195,8 +216,7 @@ game_reset( MPFORMAL XWGame* game, CurGameInfo* gi,
gi->players[ii].secondsUsed = 0;
}
server_prefsChanged( game->server, cp );
board_prefsChanged( game->board, cp );
setListeners( game, cp );
result = XP_TRUE;
}
return result;
@ -242,6 +262,7 @@ game_makeFromStream( MPFORMAL XWStreamCtxt* stream, XWGame* game,
XP_LOGF( "%s: gi was all we got; failing.", __func__ );
break;
}
game->util = util;
/* Previous stream versions didn't save anything if built
* standalone. Now we always save something. But we need to know
@ -274,8 +295,7 @@ game_makeFromStream( MPFORMAL XWStreamCtxt* stream, XWGame* game,
game->board = board_makeFromStream( MPPARM(mpool) stream,
game->model, game->server,
NULL, util, gi->nPlayers );
server_prefsChanged( game->server, cp );
board_prefsChanged( game->board, cp );
setListeners( game, cp );
board_setDraw( game->board, draw );
success = XP_TRUE;
} while( XP_FALSE );
@ -370,9 +390,10 @@ game_receiveMessage( XWGame* game, XWStreamCtxt* stream,
void
game_getState( const XWGame* game, GameStateInfo* gsi )
{
XP_Bool gameOver = server_getGameIsOver( game->server );
const ServerCtxt* server = game->server;
BoardCtxt* board = game->board;
XP_Bool gameOver = server_getGameIsOver( server );
gsi->curTurnSelected = board_curTurnSelected( board );
gsi->trayVisState = board_getTrayVisState( board );
gsi->visTileCount = board_visTileCount( board );
@ -386,6 +407,9 @@ game_getState( const XWGame* game, GameStateInfo* gsi )
gsi->canTrade = board_canTrade( board );
gsi->nPendingMessages = !!game->comms ?
comms_countPendingPackets(game->comms) : 0;
gsi->canPause = server_canPause( server );
gsi->canUnpause = server_canUnpause( server );
}
XP_Bool

View file

@ -47,9 +47,12 @@ typedef struct _GameStateInfo {
XP_Bool curTurnSelected;
XP_Bool canHideRack;
XP_Bool canTrade;
XP_Bool canPause; /* duplicate-mode only */
XP_Bool canUnpause; /* duplicate-mode only */
} GameStateInfo;
typedef struct XWGame {
XW_UtilCtxt* util;
BoardCtxt* board;
ModelCtxt* model;
ServerCtxt* server;

View file

@ -404,6 +404,8 @@ mem_stream_getHash( const XWStreamCtxt* p_sctx, XWStreamPos pos,
--len;
}
log_hex( ptr, len, __func__ );
hash = augmentHash( 0, ptr, len );
if ( 0 != bits ) {
XP_U8 byt = ptr[len];
@ -413,10 +415,11 @@ mem_stream_getHash( const XWStreamCtxt* p_sctx, XWStreamPos pos,
byt &= 1 << bits;
}
hash = augmentHash( hash, &byt, 1 );
log_hex( &byt, 1, __func__ );
}
hash = finishHash( hash );
LOG_RETURNF( "%X (nBytes=%d;nBits=%d)", hash, len, bits );
XP_LOGF( "%s(nBytes=%d, nBits=%d) => %X", __func__, len, bits, hash );
return hash;
} /* mem_stream_getHash */

View file

@ -59,8 +59,6 @@ static CellTile getModelTileRaw( const ModelCtxt* model, XP_U16 col,
XP_U16 row );
static void setModelTileRaw( ModelCtxt* model, XP_U16 col, XP_U16 row,
CellTile tile );
static void assignPlayerTiles( ModelCtxt* model, XP_S16 turn,
const TrayTileSet* tiles );
static void makeTileTrade( ModelCtxt* model, XP_S16 player,
const TrayTileSet* oldTiles,
const TrayTileSet* newTiles );
@ -112,11 +110,11 @@ model_make( MPFORMAL DictionaryCtxt* dict, const PlayerDicts* dicts,
result->vol.wni.proc = recordWord;
result->vol.wni.closure = &result->vol.rwi;
model_setSize( result, nCols );
XP_ASSERT( !!util->gameInfo );
result->vol.gi = util->gameInfo;
model_setSize( result, nCols );
model_setDictionary( result, dict );
model_setPlayerDicts( result, dicts );
}
@ -177,7 +175,6 @@ model_makeFromStream( MPFORMAL XWStreamCtxt* stream, DictionaryCtxt* dict,
stack_loadFromStream( model->vol.stack, stream );
MovePrintFuncPre pre = NULL;
MovePrintFuncPost post = NULL;
void* closure = NULL;
@ -293,10 +290,11 @@ model_setSize( ModelCtxt* model, XP_U16 nCols )
XP_MEMSET( model->vol.tiles, TILE_EMPTY_BIT, TILES_SIZE(model, nCols) );
if ( !!model->vol.stack ) {
stack_init( model->vol.stack );
stack_init( model->vol.stack, model->vol.gi->inDuplicateMode );
} else {
model->vol.stack = stack_make( MPPARM(model->vol.mpool)
dutil_getVTManager(model->vol.dutil));
dutil_getVTManager(model->vol.dutil),
model->vol.gi->inDuplicateMode );
}
} /* model_setSize */
@ -348,6 +346,7 @@ model_popToHash( ModelCtxt* model, const XP_U32 hash, PoolContext* pool )
foundAt = ii;
break;
}
if ( ! stack_popEntry( stack, &entries[ii] ) ) {
break;
}
@ -356,6 +355,7 @@ model_popToHash( ModelCtxt* model, const XP_U32 hash, PoolContext* pool )
for ( XP_S16 ii = nPopped - 1; ii >= 0; --ii ) {
stack_redo( stack, &entries[ii] );
stack_freeEntry( stack, &entries[ii] );
}
XP_Bool found = -1 != foundAt;
@ -378,8 +378,9 @@ model_popToHash( ModelCtxt* model, const XP_U32 hash, PoolContext* pool )
XP_LOGF( "%s(%X) => %s (nEntries=%d)", __func__, hash, boolToStr(found),
nEntries );
#endif
return found;
}
} /* model_popToHash */
#ifdef STREAM_VERS_BIGBOARD
void
@ -445,6 +446,40 @@ model_getSquareBonus( const ModelCtxt* model, XP_U16 col, XP_U16 row )
return result;
}
static XP_U16
makeAndCommit( ModelCtxt* model, XP_U16 turn, const MoveInfo* mi,
const TrayTileSet* tiles, XWStreamCtxt* stream,
XP_Bool useStack, WordNotifierInfo* wni )
{
model_makeTurnFromMoveInfo( model, turn, mi );
XP_U16 moveScore = commitTurn( model, turn, tiles,
stream, wni, useStack );
return moveScore;
}
static void
dupe_adjustScores( ModelCtxt* model, XP_Bool add, XP_U16 nScores, const XP_U16* scores )
{
XP_S16 mult = add ? 1 : -1;
for ( XP_U16 ii = 0; ii < nScores; ++ii ) {
model->players[ii].score += mult * scores[ii];
}
}
void
model_cloneDupeTrays( ModelCtxt* model )
{
XP_ASSERT( model->vol.gi->inDuplicateMode );
XP_U16 nTiles = model->players[DUP_PLAYER].trayTiles.nTiles;
for ( XP_U16 ii = 0; ii < model->nPlayers; ++ii ) {
if ( ii != DUP_PLAYER ) {
model_resetCurrentTurn( model, ii );
model->players[ii].trayTiles = model->players[DUP_PLAYER].trayTiles;
notifyTrayListeners( model, ii, 0, nTiles );
}
}
}
static void
modelAddEntry( ModelCtxt* model, XP_U16 indx, const StackEntry* entry,
XP_Bool useStack, XWStreamCtxt* stream,
@ -458,20 +493,29 @@ modelAddEntry( ModelCtxt* model, XP_U16 indx, const StackEntry* entry,
switch ( entry->moveType ) {
case MOVE_TYPE:
model_makeTurnFromMoveInfo( model, entry->playerNum,
&entry->u.move.moveInfo);
moveScore = commitTurn( model, entry->playerNum,
&entry->u.move.newTiles,
stream, wni, useStack );
moveScore = makeAndCommit( model, entry->playerNum, &entry->u.move.moveInfo,
&entry->u.move.newTiles, stream, useStack, wni );
if ( model->vol.gi->inDuplicateMode ) {
XP_ASSERT( DUP_PLAYER == entry->playerNum );
dupe_adjustScores( model, XP_TRUE, entry->u.move.dup.nScores,
entry->u.move.dup.scores );
model_cloneDupeTrays( model );
}
break;
case TRADE_TYPE:
makeTileTrade( model, entry->playerNum, &entry->u.trade.oldTiles,
&entry->u.trade.newTiles );
if ( model->vol.gi->inDuplicateMode ) {
XP_ASSERT( DUP_PLAYER == entry->playerNum );
model_cloneDupeTrays( model );
}
break;
case ASSIGN_TYPE:
assignPlayerTiles( model, entry->playerNum,
&entry->u.assign.tiles );
model_addNewTiles( model, entry->playerNum, &entry->u.assign.tiles );
if ( model->vol.gi->inDuplicateMode ) {
XP_ASSERT( DUP_PLAYER == entry->playerNum );
model_cloneDupeTrays( model );
}
break;
case PHONY_TYPE: /* nothing to add */
model_makeTurnFromMoveInfo( model, entry->playerNum,
@ -482,6 +526,9 @@ modelAddEntry( ModelCtxt* model, XP_U16 indx, const StackEntry* entry,
moveScore = 0;
model_resetCurrentTurn( model, entry->playerNum );
break;
case PAUSE_TYPE:
// XP_LOGF( "%s(): nothing to do with PAUSE_TYPE", __func__ );
break;
default:
XP_ASSERT(0);
@ -499,11 +546,10 @@ buildModelFromStack( ModelCtxt* model, StackCtxt* stack, XP_Bool useStack,
MovePrintFuncPost mpf_post, void* closure )
{
StackEntry entry;
XP_U16 ii;
for ( ii = initial; stack_getNthEntry( stack, ii, &entry ); ++ii ) {
for ( XP_U16 ii = initial; stack_getNthEntry( stack, ii, &entry ); ++ii ) {
modelAddEntry( model, ii, &entry, useStack, stream, wni,
mpf_pre, mpf_post, closure );
stack_freeEntry( stack, &entry );
}
} /* buildModelFromStack */
@ -794,20 +840,19 @@ getModelTileRaw( const ModelCtxt* model, XP_U16 col, XP_U16 row )
} /* getModelTileRaw */
static void
undoFromMoveInfo( ModelCtxt* model, XP_U16 turn, Tile blankTile, MoveInfo* mi )
undoFromMove( ModelCtxt* model, XP_U16 turn, Tile blankTile, MoveRec* move )
{
XP_U16 col, row, ii;
XP_U16* other;
MoveInfoTile* tinfo;
const MoveInfo* mi = &move->moveInfo;
XP_U16 col, row;
col = row = mi->commonCoord;
other = mi->isHorizontal? &col: &row;
XP_U16* other = mi->isHorizontal? &col: &row;
const MoveInfoTile* tinfo;
XP_U16 ii;
for ( tinfo = mi->tiles, ii = 0; ii < mi->nTiles; ++tinfo, ++ii ) {
Tile tile;
Tile tile = tinfo->tile;
*other = tinfo->varCoord;
tile = tinfo->tile;
setModelTileRaw( model, col, row, EMPTY_TILE );
notifyBoardListeners( model, turn, col, row, XP_FALSE );
@ -818,8 +863,13 @@ undoFromMoveInfo( ModelCtxt* model, XP_U16 turn, Tile blankTile, MoveInfo* mi )
}
model_addPlayerTile( model, turn, -1, tile );
}
adjustScoreForUndone( model, mi, turn );
} /* undoFromMoveInfo */
if ( model->vol.gi->inDuplicateMode ) {
dupe_adjustScores( model, XP_FALSE, move->dup.nScores, move->dup.scores );
} else {
adjustScoreForUndone( model, mi, turn );
}
} /* undoFromMove */
/* Remove tiles in a set from tray and put them back in the pool.
*/
@ -857,12 +907,13 @@ model_rejectPreviousMove( ModelCtxt* model, PoolContext* pool, XP_U16* turn )
XP_ASSERT( entry.moveType == MOVE_TYPE );
replaceNewTiles( model, pool, entry.playerNum, &entry.u.move.newTiles );
undoFromMoveInfo( model, entry.playerNum, blankTile,
&entry.u.move.moveInfo );
XP_ASSERT( !model->vol.gi->inDuplicateMode );
undoFromMove( model, entry.playerNum, blankTile, &entry.u.move );
stack_addPhony( stack, entry.playerNum, &entry.u.phony.moveInfo );
*turn = entry.playerNum;
stack_freeEntry( stack, &entry );
} /* model_rejectPreviousMove */
XP_Bool
@ -872,7 +923,8 @@ model_canUndo( const ModelCtxt* model )
XP_U16 nStackEntries = stack_getNEntries( stack );
/* More than just tile assignment? */
XP_Bool result = nStackEntries > model->nPlayers;
XP_U16 assignCount = model->vol.gi->inDuplicateMode ? 1 : model->nPlayers;
XP_Bool result = nStackEntries > assignCount;
return result;
}
@ -887,10 +939,12 @@ model_undoLatestMoves( ModelCtxt* model, PoolContext* pool,
XP_Bool success;
XP_S16 moveSought = !!moveNumP ? *moveNumP : -1;
XP_U16 nStackEntries = stack_getNEntries( stack );
const XP_U16 assignCount = model->vol.gi->inDuplicateMode
? 1 : model->nPlayers;
if ( (0 <= moveSought && moveSought >= nStackEntries)
|| ( nStackEntries < nMovesSought )
|| ( nStackEntries <= model->nPlayers ) ) {
|| ( nStackEntries <= assignCount ) ) {
success = XP_FALSE;
} else {
XP_U16 nMovesUndone = 0;
@ -911,17 +965,22 @@ model_undoLatestMoves( ModelCtxt* model, PoolContext* pool,
if ( entry.moveType == MOVE_TYPE ) {
/* get the tiles out of player's tray and back into the
pool */
replaceNewTiles( model, pool, turn, &entry.u.move.newTiles);
replaceNewTiles( model, pool, turn, &entry.u.move.newTiles );
undoFromMoveInfo( model, turn, blankTile,
&entry.u.move.moveInfo );
undoFromMove( model, turn, blankTile, &entry.u.move );
model_sortTiles( model, turn );
if ( model->vol.gi->inDuplicateMode ) {
XP_ASSERT( DUP_PLAYER == turn );
model_cloneDupeTrays( model );
}
} else if ( entry.moveType == TRADE_TYPE ) {
replaceNewTiles( model, pool, turn,
&entry.u.trade.newTiles );
if ( pool != NULL ) {
pool_removeTiles( pool, &entry.u.trade.oldTiles );
}
assignPlayerTiles( model, turn, &entry.u.trade.oldTiles );
model_addNewTiles( model, turn, &entry.u.trade.oldTiles );
} else if ( entry.moveType == PHONY_TYPE ) {
/* nothing to do, since nothing happened */
} else {
@ -940,6 +999,7 @@ model_undoLatestMoves( ModelCtxt* model, PoolContext* pool,
} else if ( nMovesSought == nMovesUndone ) {
break;
}
stack_freeEntry( stack, &entry );
}
/* Find the first MOVE still on the stack and highlight its tiles since
@ -977,6 +1037,7 @@ model_undoLatestMoves( ModelCtxt* model, PoolContext* pool,
}
break;
}
stack_freeEntry( stack, &entry );
}
/* We fail if we didn't undo as many as requested UNLESS the lower
@ -1020,23 +1081,63 @@ model_trayToStream( ModelCtxt* model, XP_S16 turn, XWStreamCtxt* stream )
traySetToStream( stream, &player->trayTiles );
} /* model_trayToStream */
void
model_currentMoveToMoveInfo( ModelCtxt* model, XP_S16 turn,
MoveInfo* moveInfo )
{
XP_ASSERT( turn >= 0 );
const XP_S16 numTiles = model->players[turn].nPending;
moveInfo->nTiles = numTiles;
XP_U16 cols[MAX_TRAY_TILES];
XP_U16 rows[MAX_TRAY_TILES];
for ( XP_S16 ii = 0; ii < numTiles; ++ii ) {
XP_Bool isBlank;
Tile tile;
model_getCurrentMoveTile( model, turn, &ii, &tile,
&cols[ii], &rows[ii], &isBlank );
if ( isBlank ) {
tile |= TILE_BLANK_BIT;
}
moveInfo->tiles[ii].tile = tile;
}
XP_Bool isHorizontal = XP_TRUE;
if ( 1 == numTiles ) { /* horizonal/vertical makes no sense */
moveInfo->tiles[0].varCoord = cols[0];
moveInfo->commonCoord = rows[0];
} else if ( 1 < numTiles ) {
isHorizontal = rows[0] == rows[1];
moveInfo->commonCoord = isHorizontal ? rows[0] : cols[0];
for ( XP_U16 ii = 0; ii < numTiles; ++ii ) {
moveInfo->tiles[ii].varCoord =
isHorizontal ? cols[ii] : rows[ii];
/* MoveInfo assumes legal moves! Check here */
if ( isHorizontal ) {
XP_ASSERT( rows[ii] == rows[0] );
} else {
XP_ASSERT( cols[ii] == cols[0] );
}
}
}
moveInfo->isHorizontal = isHorizontal;
normalizeMI( moveInfo, moveInfo );
}
void
model_currentMoveToStream( ModelCtxt* model, XP_S16 turn,
XWStreamCtxt* stream )
{
PlayerCtxt* player;
XP_S16 numTiles;
XP_U16 nColsNBits;
#ifdef STREAM_VERS_BIGBOARD
nColsNBits = 16 <= model_numCols( model ) ? NUMCOLS_NBITS_5
XP_U16 nColsNBits = 16 <= model_numCols( model ) ? NUMCOLS_NBITS_5
: NUMCOLS_NBITS_4;
#else
nColsNBits = NUMCOLS_NBITS_4;
XP_U16 nColsNBits = NUMCOLS_NBITS_4;
#endif
XP_ASSERT( turn >= 0 );
player = &model->players[turn];
numTiles = player->nPending;
XP_S16 numTiles = model->players[turn].nPending;
stream_putBits( stream, NTILES_NBITS, numTiles );
@ -1065,66 +1166,63 @@ XP_Bool
model_makeTurnFromStream( ModelCtxt* model, XP_U16 playerNum,
XWStreamCtxt* stream )
{
XP_Bool success = XP_TRUE;
XP_U16 numTiles, ii;
Tile blank = dict_getBlankTile( model_getDictionary(model) );
XP_U16 nColsNBits;
XP_U16 nColsNBits =
#ifdef STREAM_VERS_BIGBOARD
nColsNBits = 16 <= model_numCols( model ) ? NUMCOLS_NBITS_5
: NUMCOLS_NBITS_4;
16 <= model_numCols( model ) ? NUMCOLS_NBITS_5 : NUMCOLS_NBITS_4
#else
nColsNBits = NUMCOLS_NBITS_4;
NUMCOLS_NBITS_4
#endif
;
model_resetCurrentTurn( model, playerNum );
if ( success ) {
numTiles = (XP_U16)stream_getBits( stream, NTILES_NBITS );
XP_LOGF( "%s: numTiles=%d", __func__, numTiles );
XP_U16 numTiles = (XP_U16)stream_getBits( stream, NTILES_NBITS );
XP_LOGF( "%s: numTiles=%d", __func__, numTiles );
Tile tileFaces[numTiles];
XP_U16 cols[numTiles];
XP_U16 rows[numTiles];
XP_Bool isBlanks[numTiles];
Tile moveTiles[numTiles];
TrayTileSet curTiles = *model_getPlayerTiles( model, playerNum );
Tile tileFaces[numTiles];
XP_U16 cols[numTiles];
XP_U16 rows[numTiles];
XP_Bool isBlanks[numTiles];
Tile moveTiles[numTiles];
TrayTileSet curTiles = *model_getPlayerTiles( model, playerNum );
for ( ii = 0; success && ii < numTiles; ++ii ) {
tileFaces[ii] = (Tile)stream_getBits( stream, TILE_NBITS );
cols[ii] = (XP_U16)stream_getBits( stream, nColsNBits );
rows[ii] = (XP_U16)stream_getBits( stream, nColsNBits );
isBlanks[ii] = stream_getBits( stream, 1 );
XP_Bool success = XP_TRUE;
for ( XP_U16 ii = 0; success && ii < numTiles; ++ii ) {
tileFaces[ii] = (Tile)stream_getBits( stream, TILE_NBITS );
cols[ii] = (XP_U16)stream_getBits( stream, nColsNBits );
rows[ii] = (XP_U16)stream_getBits( stream, nColsNBits );
isBlanks[ii] = stream_getBits( stream, 1 );
if ( isBlanks[ii] ) {
moveTiles[ii] = blank;
} else {
moveTiles[ii] = tileFaces[ii];
}
XP_S16 index = setContains( &curTiles, moveTiles[ii] );
if ( 0 <= index ) {
removeTile( &curTiles, index );
} else {
success = XP_FALSE;
}
if ( isBlanks[ii] ) {
moveTiles[ii] = blank;
} else {
moveTiles[ii] = tileFaces[ii];
}
if ( success ) {
for ( ii = 0; ii < numTiles; ++ii ) {
XP_S16 foundAt = model_trayContains( model, playerNum, moveTiles[ii] );
if ( foundAt == -1 ) {
XP_ASSERT( EMPTY_TILE == model_getPlayerTile(model, playerNum,
0));
/* Does this ever happen? */
XP_LOGF( "%s: found empty tile and it's ok", __func__ );
XP_S16 index = setContains( &curTiles, moveTiles[ii] );
if ( 0 <= index ) {
removeTile( &curTiles, index );
} else {
success = XP_FALSE;
}
}
(void)model_removePlayerTile( model, playerNum, -1 );
model_addPlayerTile( model, playerNum, -1, moveTiles[ii] );
}
if ( success ) {
for ( XP_U16 ii = 0; ii < numTiles; ++ii ) {
XP_S16 foundAt = model_trayContains( model, playerNum, moveTiles[ii] );
if ( foundAt == -1 ) {
XP_ASSERT( EMPTY_TILE == model_getPlayerTile(model, playerNum,
0));
/* Does this ever happen? */
XP_LOGF( "%s: found empty tile and it's ok", __func__ );
model_moveTrayToBoard( model, playerNum, cols[ii], rows[ii], foundAt,
tileFaces[ii] );
(void)model_removePlayerTile( model, playerNum, -1 );
model_addPlayerTile( model, playerNum, -1, moveTiles[ii] );
}
model_moveTrayToBoard( model, playerNum, cols[ii], rows[ii], foundAt,
tileFaces[ii] );
}
}
return success;
@ -1326,6 +1424,45 @@ model_removePlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index )
return tile;
} /* model_removePlayerTile */
void
model_removePlayerTiles( ModelCtxt* model, XP_S16 turn, const MoveInfo* mi )
{
XP_ASSERT( turn >= 0 );
PlayerCtxt* player = &model->players[turn];
for ( XP_U16 ii = 0; ii < mi->nTiles; ++ii ) {
Tile tile = mi->tiles[ii].tile;
if ( IS_BLANK( tile ) ) {
tile = dict_getBlankTile( model_getDictionary(model) );
}
XP_S16 index = -1;
for ( XP_U16 jj = 0; index < 0 && jj < player->trayTiles.nTiles; ++jj ) {
if ( tile == player->trayTiles.tiles[jj] ) {
index = jj;
}
}
XP_ASSERT( index >= 0 );
model_removePlayerTile( model, turn, index );
}
}
void
model_removePlayerTiles2( ModelCtxt* model, XP_S16 turn, const TrayTileSet* tiles )
{
XP_ASSERT( turn >= 0 );
PlayerCtxt* player = &model->players[turn];
for ( XP_U16 ii = 0; ii < tiles->nTiles; ++ii ) {
Tile tile = tiles->tiles[ii];
XP_S16 index = -1;
for ( XP_U16 jj = 0; index < 0 && jj < player->trayTiles.nTiles; ++jj ) {
if ( tile == player->trayTiles.tiles[jj] ) {
index = jj;
}
}
XP_ASSERT( index >= 0 );
model_removePlayerTile( model, turn, index );
}
}
void
model_packTilesUtil( ModelCtxt* model, PoolContext* pool,
XP_Bool includeBlank,
@ -1593,8 +1730,8 @@ model_resetCurrentTurn( ModelCtxt* model, XP_S16 whose )
XP_S16
model_getNMoves( const ModelCtxt* model )
{
XP_U16 result = stack_getNEntries( model->vol.stack );
result -= model->nPlayers; /* tile assignment doesn't count */
XP_U16 nAssigns = model->vol.gi->inDuplicateMode ? 1 : model->nPlayers;
XP_U16 result = stack_getNEntries( model->vol.stack ) - nAssigns;
return result;
}
@ -1689,6 +1826,14 @@ putBackOtherPlayersTiles( ModelCtxt* model, XP_U16 notMyTurn,
}
} /* putBackOtherPlayersTiles */
static void
invalidateScores( ModelCtxt* model )
{
for ( int ii = 0; ii < model->nPlayers; ++ii ) {
invalidateScore( model, ii );
}
}
/* Make those tiles placed by 'turn' a permanent part of the board. If any
* other players have placed pending tiles on those same squares, replace them
* in their trays.
@ -1720,7 +1865,7 @@ commitTurn( ModelCtxt* model, XP_S16 turn, const TrayTileSet* newTiles,
XP_ASSERT( inLine );
MoveInfo moveInfo = {0};
normalizeMoves( model, turn, isHorizontal, &moveInfo );
stack_addMove( model->vol.stack, turn, &moveInfo, newTiles );
}
@ -1751,13 +1896,12 @@ commitTurn( ModelCtxt* model, XP_S16 turn, const TrayTileSet* newTiles,
(void)getCurrentMoveScoreIfLegal( model, turn, stream, wni, &score );
XP_ASSERT( score >= 0 );
player->score += score;
/* Why is this next loop necessary? */
for ( int ii = 0; ii < model->nPlayers; ++ii ) {
invalidateScore( model, ii );
if ( ! model->vol.gi->inDuplicateMode ) {
player->score += score;
}
invalidateScores( model );
player->nPending = 0;
player->nUndone = 0;
@ -1776,6 +1920,34 @@ model_commitTurn( ModelCtxt* model, XP_S16 turn, TrayTileSet* newTiles )
return 0 <= score;
} /* model_commitTurn */
void
model_commitDupeTurn( ModelCtxt* model, const MoveInfo* moveInfo,
XP_U16 nScores, XP_U16* scores, TrayTileSet* newTiles )
{
model_resetCurrentTurn( model, DUP_PLAYER );
model_makeTurnFromMoveInfo( model, DUP_PLAYER, moveInfo );
(void)commitTurn( model, DUP_PLAYER, newTiles, NULL, NULL, XP_FALSE );
dupe_adjustScores( model, XP_TRUE, nScores, scores );
invalidateScores( model );
stack_addDupMove( model->vol.stack, moveInfo, nScores, scores, newTiles );
}
void
model_commitDupeTrade( ModelCtxt* model, const TrayTileSet* oldTiles,
const TrayTileSet* newTiles )
{
stack_addDupTrade( model->vol.stack, oldTiles, newTiles );
}
void
model_noteDupePause( ModelCtxt* model, DupPauseType typ, XP_S16 turn,
const XP_UCHAR* msg )
{
XP_U32 when = dutil_getCurSeconds( model->vol.dutil );
stack_addPause( model->vol.stack, typ, turn, when, msg );
}
/* Given a rack of new tiles and of old, remove all the old from the tray and
* replace them with new. Replace in the same place so that user sees an
* in-place change.
@ -1784,13 +1956,11 @@ static void
makeTileTrade( ModelCtxt* model, XP_S16 player, const TrayTileSet* oldTiles,
const TrayTileSet* newTiles )
{
XP_U16 ii;
XP_U16 nTiles;
XP_ASSERT( newTiles->nTiles == oldTiles->nTiles );
XP_ASSERT( oldTiles != &model->players[player].trayTiles );
for ( nTiles = newTiles->nTiles, ii = 0; ii < nTiles; ++ii ) {
const XP_U16 nTiles = newTiles->nTiles;
for ( XP_U16 ii = 0; ii < nTiles; ++ii ) {
Tile oldTile = oldTiles->tiles[ii];
XP_S16 tileIndex = model_trayContains( model, player, oldTile );
@ -1911,8 +2081,8 @@ model_setDividerLoc( ModelCtxt* model, XP_S16 turn, XP_U16 loc )
player->dividerLoc = (XP_U8)loc;
}
static void
assignPlayerTiles( ModelCtxt* model, XP_S16 turn, const TrayTileSet* tiles )
void
model_addNewTiles( ModelCtxt* model, XP_S16 turn, const TrayTileSet* tiles )
{
const Tile* tilep = tiles->tiles;
XP_U16 nTiles = tiles->nTiles;
@ -1926,13 +2096,21 @@ model_assignPlayerTiles( ModelCtxt* model, XP_S16 turn,
const TrayTileSet* tiles )
{
XP_ASSERT( turn >= 0 );
XP_ASSERT( turn == DUP_PLAYER || !model->vol.gi->inDuplicateMode );
TrayTileSet sorted;
sortTiles( &sorted, tiles, 0 );
stack_addAssign( model->vol.stack, turn, &sorted );
assignPlayerTiles( model, turn, tiles );
model_addNewTiles( model, turn, &sorted );
} /* model_assignPlayerTiles */
void
model_assignDupeTiles( ModelCtxt* model, const TrayTileSet* tiles )
{
model_assignPlayerTiles( model, DUP_PLAYER, tiles );
model_cloneDupeTrays( model );
}
void
model_sortTiles( ModelCtxt* model, XP_S16 turn )
{
@ -1946,7 +2124,7 @@ model_sortTiles( ModelCtxt* model, XP_S16 turn )
removePlayerTile( model, turn, --nTiles );
}
assignPlayerTiles( model, turn, &sorted );
model_addNewTiles( model, turn, &sorted );
}
} /* model_sortTiles */
@ -1956,7 +2134,9 @@ model_getNumTilesInTray( ModelCtxt* model, XP_S16 turn )
PlayerCtxt* player;
XP_ASSERT( turn >= 0 );
player = &model->players[turn];
return player->trayTiles.nTiles;
XP_U16 result = player->trayTiles.nTiles;
// XP_LOGF( "%s(turn=%d) => %d", __func__, turn, result );
return result;
} /* model_getNumTilesInTray */
XP_U16
@ -2060,6 +2240,7 @@ typedef struct MovePrintClosure {
DictionaryCtxt* dict;
XP_U16 nPrinted;
XP_Bool keepHidden;
XP_U32 lastPauseWhen;
} MovePrintClosure;
static void
@ -2077,8 +2258,11 @@ printMovePre( ModelCtxt* model, XP_U16 XP_UNUSED(moveN), const StackEntry* entry
entry->playerNum+1 );
printString( stream, (XP_UCHAR*)buf );
if ( entry->moveType == TRADE_TYPE ) {
} else {
switch ( entry->moveType ) {
case TRADE_TYPE:
case PAUSE_TYPE:
break;
default: {
XP_UCHAR letter[2] = {'\0','\0'};
XP_Bool isHorizontal = entry->u.move.moveInfo.isHorizontal;
XP_U16 col, row;
@ -2116,15 +2300,17 @@ printMovePre( ModelCtxt* model, XP_U16 XP_UNUSED(moveN), const StackEntry* entry
XP_SNPRINTF( buf, sizeof(buf), format, traybuf );
}
printString( stream, (XP_UCHAR*)buf );
}
if ( !closure->keepHidden ) {
format = dutil_getUserString( model->vol.dutil, STRS_TRAY_AT_START );
formatTray( model_getPlayerTiles( model, entry->playerNum ),
closure->dict, (XP_UCHAR*)traybuf, sizeof(traybuf),
XP_FALSE );
XP_SNPRINTF( buf, sizeof(buf), format, traybuf );
printString( stream, buf );
if ( !closure->keepHidden ) {
format = dutil_getUserString( model->vol.dutil, STRS_TRAY_AT_START );
formatTray( model_getPlayerTiles( model, entry->playerNum ),
closure->dict, (XP_UCHAR*)traybuf, sizeof(traybuf),
XP_FALSE );
XP_SNPRINTF( buf, sizeof(buf), format, traybuf );
printString( stream, buf );
}
}
break;
}
}
} /* printMovePre */
@ -2146,6 +2332,7 @@ printMovePost( ModelCtxt* model, XP_U16 XP_UNUSED(moveN),
XP_UCHAR traybuf2[MAX_TRAY_TILES+1];
const MoveInfo* mi;
XP_S16 totalScore = model_getPlayerScore( model, entry->playerNum );
XP_Bool addCR = XP_FALSE;
switch( entry->moveType ) {
case TRADE_TYPE:
@ -2157,13 +2344,25 @@ printMovePost( ModelCtxt* model, XP_U16 XP_UNUSED(moveN),
format = dutil_getUserString( model->vol.dutil, STRSS_TRADED_FOR );
XP_SNPRINTF( buf, sizeof(buf), format, traybuf1, traybuf2 );
printString( stream, buf );
printString( stream, (XP_UCHAR*)XP_CR );
addCR = XP_TRUE;
break;
case PHONY_TYPE:
format = dutil_getUserString( model->vol.dutil, STR_PHONY_REJECTED );
printString( stream, format );
/* FALLTHRU */
case MOVE_TYPE:
/* Duplicate case */
if ( model->vol.gi->inDuplicateMode ) {
XP_U16 offset = XP_SNPRINTF( buf, VSIZE(buf), "%s", "All scores: " );
for ( XP_U16 ii = 0; ii < entry->u.move.dup.nScores; ++ii ) {
offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset, "%d,",
entry->u.move.dup.scores[ii] );
}
buf[offset-1] = '\n'; /* replace last ',' */
printString( stream, buf );
}
format = dutil_getUserString( model->vol.dutil, STRD_CUMULATIVE_SCORE );
XP_SNPRINTF( buf, sizeof(buf), format, totalScore );
printString( stream, buf );
@ -2185,11 +2384,25 @@ printMovePost( ModelCtxt* model, XP_U16 XP_UNUSED(moveN),
traybuf1, sizeof(traybuf1),
XP_FALSE ) );
printString( stream, buf );
stream_catString( stream, (XP_UCHAR*)XP_CR );
addCR = XP_TRUE;
}
}
break;
case PAUSE_TYPE:
util_formatPauseHistory( model->vol.util, stream, entry->u.pause.pauseType,
entry->playerNum, closure->lastPauseWhen,
entry->u.pause.when, entry->u.pause.msg );
closure->lastPauseWhen = entry->u.pause.when;
addCR = XP_TRUE;
break;
default:
XP_ASSERT( 0 );
}
if ( addCR ) {
printString( stream, (XP_UCHAR*)XP_CR );
}
printString( stream, (XP_UCHAR*)XP_CR );
@ -2231,16 +2444,15 @@ void
model_writeGameHistory( ModelCtxt* model, XWStreamCtxt* stream,
ServerCtxt* server, XP_Bool gameOver )
{
ModelCtxt* tmpModel;
MovePrintClosure closure;
MovePrintClosure closure = {
.stream = stream,
.dict = model_getDictionary( model ),
.keepHidden = !gameOver && !model->vol.gi->inDuplicateMode,
.nPrinted = 0
};
closure.stream = stream;
closure.dict = model_getDictionary( model );
closure.keepHidden = !gameOver;
closure.nPrinted = 0;
tmpModel = makeTmpModel( model, stream, printMovePre, printMovePost,
&closure );
ModelCtxt* tmpModel = makeTmpModel( model, stream, printMovePre,
printMovePost, &closure );
model_destroy( tmpModel );
if ( gameOver ) {
@ -2305,23 +2517,25 @@ model_getRecentPassCount( ModelCtxt* model )
{
StackCtxt* stack = model->vol.stack;
XP_U16 nPasses = 0;
XP_S16 nEntries, which;
StackEntry entry;
XP_ASSERT( !!stack );
nEntries = stack_getNEntries( stack );
for ( which = nEntries - 1; which >= 0; --which ) {
if ( stack_getNthEntry( stack, which, &entry ) ) {
if ( entry.moveType == MOVE_TYPE
&& entry.u.move.moveInfo.nTiles == 0 ) {
++nPasses;
} else {
break;
}
} else {
XP_S16 nEntries = stack_getNEntries( stack );
for ( XP_S16 which = nEntries - 1; which >= 0; --which ) {
StackEntry entry;
if ( !stack_getNthEntry( stack, which, &entry ) ) {
break;
}
switch ( entry.moveType ) {
case MOVE_TYPE:
if ( entry.u.move.moveInfo.nTiles == 0 ) {
++nPasses;
}
break;
default:
break;
}
stack_freeEntry( stack, &entry );
}
return nPasses;
} /* model_getRecentPassCount */
@ -2330,7 +2544,10 @@ XP_Bool
model_recentPassCountOk( ModelCtxt* model )
{
XP_U16 count = model_getRecentPassCount( model );
XP_U16 okCount = model->nPlayers * MAX_PASSES;
XP_U16 okCount = MAX_PASSES;
if ( !model->vol.gi->inDuplicateMode ) {
okCount *= model->nPlayers;
}
XP_ASSERT( count <= okCount ); /* should never be more than 1 over */
return count < okCount;
}
@ -2410,8 +2627,15 @@ model_listWordsThrough( ModelCtxt* model, XP_U16 col, XP_U16 row,
MoveInfo moveInfo = {0};
normalizeMoves( model, turn, isHorizontal, &moveInfo );
model_makeTurnFromMoveInfo( tmpModel, turn, &moveInfo );
TrayTileSet newTiles = {0};
commitTurn( tmpModel, turn, &newTiles, NULL, NULL, XP_TRUE );
/* Might not be a legal move. If isn't, don't add it! */
if ( getCurrentMoveScoreIfLegal( tmpModel, turn, (XWStreamCtxt*)NULL,
(WordNotifierInfo*)NULL, NULL ) ) {
TrayTileSet newTiles = {.nTiles = 0};
commitTurn( tmpModel, turn, &newTiles, NULL, NULL, XP_TRUE );
} else {
model_resetCurrentTurn( tmpModel, turn );
}
}
XP_ASSERT( !!stream );
@ -2451,6 +2675,26 @@ model_listWordsThrough( ModelCtxt* model, XP_U16 col, XP_U16 row,
} /* model_listWordsThrough */
#endif
/* Set array of 1-4 (>1 in case of tie) with highest scores' owners */
static void
listHighestScores( const ModelCtxt* model, LastMoveInfo* lmi, MoveRec* move )
{
/* find highest */
XP_U16 max = 0;
lmi->nWinners = 0;
for ( XP_U16 ii = 0; ii < move->dup.nScores; ++ii ) {
XP_U16 score = move->dup.scores[ii];
if ( 0 == score || score < max ) {
continue;
} else if ( score > max ) {
max = score;
lmi->nWinners = 0;
lmi->score = score;
}
lmi->names[lmi->nWinners++] = model->vol.gi->players[ii].name;
}
}
XP_Bool
model_getPlayersLastScore( ModelCtxt* model, XP_S16 player, LastMoveInfo* lmi )
{
@ -2458,6 +2702,7 @@ model_getPlayersLastScore( ModelCtxt* model, XP_S16 player, LastMoveInfo* lmi )
XP_S16 nEntries, which;
StackEntry entry;
XP_Bool found = XP_FALSE;
XP_Bool inDuplicateMode = model->vol.gi->inDuplicateMode;
XP_MEMSET( lmi, 0, sizeof(*lmi) );
XP_ASSERT( !!stack );
@ -2466,37 +2711,46 @@ model_getPlayersLastScore( ModelCtxt* model, XP_S16 player, LastMoveInfo* lmi )
for ( which = nEntries; which >= 0; ) {
if ( stack_getNthEntry( stack, --which, &entry ) ) {
if ( -1 == player || entry.playerNum == player ) {
if ( -1 == player || inDuplicateMode || entry.playerNum == player ) {
found = XP_TRUE;
break;
}
}
stack_freeEntry( stack, &entry );
}
if ( found ) { /* success? */
XP_ASSERT( -1 == player || player == entry.playerNum );
XP_ASSERT( -1 == player || inDuplicateMode || player == entry.playerNum );
XP_LOGF( "%s: found move %d", __func__, which );
lmi->name = model->vol.gi->players[entry.playerNum].name;
lmi->names[0] = model->vol.gi->players[entry.playerNum].name;
lmi->nWinners = 1;
lmi->moveType = entry.moveType;
lmi->inDuplicateMode = inDuplicateMode;
switch ( entry.moveType ) {
case MOVE_TYPE:
XP_ASSERT( !inDuplicateMode || entry.playerNum == DUP_PLAYER );
lmi->nTiles = entry.u.move.moveInfo.nTiles;
if ( 0 < entry.u.move.moveInfo.nTiles ) {
scoreLastMove( model, &entry.u.move.moveInfo, nEntries - which,
lmi );
scoreLastMove( model, &entry.u.move.moveInfo,
nEntries - which, lmi );
if ( inDuplicateMode ) {
listHighestScores( model, lmi, &entry.u.move );
}
}
break;
case TRADE_TYPE:
XP_ASSERT( !inDuplicateMode || entry.playerNum == DUP_PLAYER );
lmi->nTiles = entry.u.trade.oldTiles.nTiles;
break;
case PHONY_TYPE:
break;
case ASSIGN_TYPE:
// found = XP_FALSE;
case PAUSE_TYPE:
break;
default:
XP_ASSERT( 0 );
}
}
@ -2597,7 +2851,7 @@ static void
assertDiffTurn( ModelCtxt* model, XP_U16 XP_UNUSED(turn),
const StackEntry* entry, void* closure )
{
if ( 1 < model->nPlayers ) {
if ( 1 < model->nPlayers && ! model->vol.gi->inDuplicateMode ) {
DiffTurnState* state = (DiffTurnState*)closure;
if ( -1 != state->lastPlayerNum ) {
XP_ASSERT( state->lastPlayerNum != entry->playerNum );
@ -2607,6 +2861,44 @@ assertDiffTurn( ModelCtxt* model, XP_U16 XP_UNUSED(turn),
state->lastMoveNum = entry->moveNum;
}
}
void
model_printTrays( const ModelCtxt* model )
{
for ( XP_U16 ii = 0; ii < model->nPlayers; ++ii ) {
const PlayerCtxt* player = &model->players[ii];
XP_UCHAR buf[128];
XP_LOGF( "%s(): player %d: %s", __func__, ii,
formatTileSet( &player->trayTiles, buf, VSIZE(buf) ) );
}
}
void
model_dumpSelf( const ModelCtxt* model, const XP_UCHAR* msg )
{
XP_LOGF( "%s(msg=%s)", __func__, msg );
XP_UCHAR buf[256];
XP_U16 offset = 0;
for ( XP_U16 col = 0; col < model_numCols( model ); ++col ) {
offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset,
"%.2d ", col );
}
XP_LOGF( " %s", buf );
for ( XP_U16 row = 0; row < model_numRows( model ); ++row ) {
XP_UCHAR buf[256];
XP_U16 offset = 0;
for ( XP_U16 col = 0; col < model_numCols( model ); ++col ) {
Tile tile = getModelTileRaw( model, col, row );
offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset,
"%.2x ", tile );
}
XP_LOGF( "%.2d: %s", row, buf );
}
}
#endif
#ifdef CPLUS

View file

@ -24,6 +24,7 @@
#include "comtypes.h"
#include "dictnry.h"
#include "mempool.h"
#include "dutil.h"
#ifdef CPLUS
extern "C" {
@ -48,8 +49,8 @@ extern "C" {
#define TILE_PENDING_BIT 0x0100
#define PREV_MOVE_BIT 0x200
#define CELL_OWNER_MASK 0x0C00
#define CELL_OWNER_OFFSET 10
#define CELL_OWNER_MASK (0x0003 << CELL_OWNER_OFFSET)
#define CELL_OWNER(t) (((t)&CELL_OWNER_MASK) >> CELL_OWNER_OFFSET)
#define MAX_UNIQUE_TILES 64 /* max tile non-blank faces */
@ -72,15 +73,17 @@ typedef struct MoveInfo {
} MoveInfo;
typedef struct _LastMoveInfo {
const XP_UCHAR* name;
XP_U8 moveType;
const XP_UCHAR* names[MAX_NUM_PLAYERS];
XP_U16 nWinners; /* >1 possible in duplicate case only */
XP_U16 score;
XP_U16 nTiles;
XP_UCHAR word[MAX_COLS+1];
XP_UCHAR word[MAX_COLS * 2]; /* be safe */
XP_U8 moveType;
XP_Bool inDuplicateMode;
} LastMoveInfo;
typedef XP_U8 TrayTile;
typedef struct TrayTileSet {
typedef struct _TrayTileSet {
XP_U8 nTiles;
TrayTile tiles[MAX_TRAY_TILES];
} TrayTileSet;
@ -140,12 +143,17 @@ void model_listPlacedBlanks( ModelCtxt* model, XP_U16 turn,
XP_Bool includePending, BlankQueue* bcp );
XP_U16 model_getCellOwner( ModelCtxt* model, XP_U16 col, XP_U16 row );
void model_addNewTiles( ModelCtxt* model, XP_S16 turn,
const TrayTileSet* tiles );
void model_assignPlayerTiles( ModelCtxt* model, XP_S16 turn,
const TrayTileSet* tiles );
void model_assignDupeTiles( ModelCtxt* model, const TrayTileSet* tiles );
Tile model_getPlayerTile( const ModelCtxt* model, XP_S16 turn, XP_S16 index );
Tile model_removePlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index );
void model_removePlayerTiles( ModelCtxt* model, XP_S16 turn, const MoveInfo* mi );
void model_removePlayerTiles2( ModelCtxt* model, XP_S16 turn, const TrayTileSet* tiles );
void model_addPlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index,
Tile tile );
void model_moveTileOnTray( ModelCtxt* model, XP_S16 turn, XP_S16 indexCur,
@ -159,6 +167,7 @@ const TrayTileSet* model_getPlayerTiles( const ModelCtxt* model, XP_S16 turn );
#ifdef DEBUG
XP_UCHAR* formatTileSet( const TrayTileSet* tiles, XP_UCHAR* buf, XP_U16 len );
void model_printTrays( const ModelCtxt* model );
#endif
void model_sortTiles( ModelCtxt* model, XP_S16 turn );
@ -196,6 +205,15 @@ void model_getCurrentMoveTile( ModelCtxt* model, XP_S16 turn, XP_S16* index,
XP_Bool model_commitTurn( ModelCtxt* model, XP_S16 player,
TrayTileSet* newTiles );
void model_commitDupeTurn( ModelCtxt* model, const MoveInfo* moveInfo,
XP_U16 nScores, XP_U16* scores,
TrayTileSet* newTiles );
void model_commitDupeTrade( ModelCtxt* model, const TrayTileSet* oldTiles,
const TrayTileSet* newTiles );
void model_noteDupePause( ModelCtxt* model, DupPauseType typ, XP_S16 turn,
const XP_UCHAR* msg );
void model_cloneDupeTrays( ModelCtxt* model );
void model_commitRejectedPhony( ModelCtxt* model, XP_S16 player );
void model_makeTileTrade( ModelCtxt* model, XP_S16 player,
const TrayTileSet* oldTiles,
@ -210,7 +228,9 @@ void model_rejectPreviousMove( ModelCtxt* model, PoolContext* pool,
void model_trayToStream( ModelCtxt* model, XP_S16 turn,
XWStreamCtxt* stream );
void model_currentMoveToStream( ModelCtxt* model, XP_S16 turn,
XWStreamCtxt* stream);
XWStreamCtxt* stream );
void model_currentMoveToMoveInfo( ModelCtxt* model, XP_S16 turn,
MoveInfo* moveInfo );
XP_Bool model_makeTurnFromStream( ModelCtxt* model, XP_U16 playerNum,
XWStreamCtxt* stream );
void model_makeTurnFromMoveInfo( ModelCtxt* model, XP_U16 playerNum,
@ -218,8 +238,10 @@ void model_makeTurnFromMoveInfo( ModelCtxt* model, XP_U16 playerNum,
#ifdef DEBUG
void juggleMoveIfDebug( MoveInfo* move );
void model_dumpSelf( const ModelCtxt* model, const XP_UCHAR* msg );
#else
# define juggleMoveIfDebug(newMove)
# define model_dumpSelf( model, msg )
#endif
void model_resetCurrentTurn( ModelCtxt* model, XP_S16 turn );
@ -304,9 +326,9 @@ void model_figureFinalScores( ModelCtxt* model, ScoresArray* scores,
ScoresArray* tilePenalties );
/* figureMoveScore is meant only for the engine's use */
XP_U16 figureMoveScore( const ModelCtxt* model, XP_U16 turn, MoveInfo* mvInfo,
EngineCtxt* engine, XWStreamCtxt* stream,
WordNotifierInfo* notifyInfo );
XP_U16 figureMoveScore( const ModelCtxt* model, XP_U16 turn,
const MoveInfo* mvInfo, EngineCtxt* engine,
XWStreamCtxt* stream, WordNotifierInfo* notifyInfo );
/* tap into internal WordNotifierInfo */
WordNotifierInfo* model_initWordCounter( ModelCtxt* model, XWStreamCtxt* stream );

View file

@ -92,7 +92,8 @@ void invalidateScore( ModelCtxt* model, XP_S16 player );
XP_Bool tilesInLine( ModelCtxt* model, XP_S16 turn, XP_Bool* isHorizontal );
void normalizeMoves( const ModelCtxt* model, XP_S16 turn,
XP_Bool isHorizontal, MoveInfo* moveInfo );
void adjustScoreForUndone( ModelCtxt* model, MoveInfo* mi, XP_U16 turn );
void normalizeMI( MoveInfo* moveInfoOut, const MoveInfo* moveInfoIn );
void adjustScoreForUndone( ModelCtxt* model, const MoveInfo* mi, XP_U16 turn );
#ifdef CPLUS
}
#endif

View file

@ -1,6 +1,6 @@
/* -*- compile-command: "cd ../linux && make -j5 MEMDEBUG=TRUE"; -*- */
/*
* Copyright 2001-2015 by Eric House (xwords@eehouse.org). All rights
* Copyright 2001 - 2019 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
@ -41,27 +41,30 @@ extern "C" {
struct StackCtxt {
VTableMgr* vtmgr;
XWStreamCtxt* data;
XWStreamPos top;
XWStreamPos cachedPos;
XP_U16 cacheNext;
XP_U16 nEntries;
XP_U16 bitsPerTile;
XP_U16 highWaterMark;
XP_U16 typeBits;
XP_U8 flags;
XP_Bool inDuplicateMode;
DIRTY_SLOT
MPSLOT
};
#define HAVE_FLAGS_MASK ((XP_U16)0x8000)
void
stack_init( StackCtxt* stack )
stack_init( StackCtxt* stack, XP_Bool inDuplicateMode )
{
stack->nEntries = stack->highWaterMark = 0;
stack->top = START_OF_STREAM;
stack->inDuplicateMode = inDuplicateMode;
/* I see little point in freeing or shrinking stack->data. It'll get
shrunk to fit as soon as we serialize/deserialize anyway. */
@ -88,13 +91,14 @@ stack_setBitsPerTile( StackCtxt* stack, XP_U16 bitsPerTile )
}
StackCtxt*
stack_make( MPFORMAL VTableMgr* vtmgr )
stack_make( MPFORMAL VTableMgr* vtmgr, XP_Bool inDuplicateMode )
{
StackCtxt* result = (StackCtxt*)XP_MALLOC( mpool, sizeof( *result ) );
if ( !!result ) {
XP_MEMSET( result, 0, sizeof(*result) );
MPASSIGN(result->mpool, mpool);
result->vtmgr = vtmgr;
result->inDuplicateMode = inDuplicateMode;
}
return result;
@ -114,7 +118,24 @@ stack_destroy( StackCtxt* stack )
void
stack_loadFromStream( StackCtxt* stack, XWStreamCtxt* stream )
{
/* Problem: the moveType field is getting bigger to support
* DUP_MOVE_TYPE. So 3 bits are needed rather than 2. I can't use the
* parent stream's version since the parent stream is re-written each time
* the game's saved (with the new version) but the stack is not rewritten,
* only appended to (normally). The solution is to take advantage of the
* extra bits at the top of the stack's data size (nBytes below). If the
* first bit's set, the stream was created by code that assumes 3 bits for
* the moveType field.
*/
XP_U16 nBytes = stream_getU16( stream );
if ( (HAVE_FLAGS_MASK & nBytes) != 0 ) {
stack->flags = stream_getU8( stream );
stack->typeBits = 3;
} else {
XP_ASSERT( 0 == stack->flags );
stack->typeBits = 2;
}
nBytes &= ~HAVE_FLAGS_MASK;
if ( nBytes > 0 ) {
stack->highWaterMark = stream_getU16( stream );
@ -133,18 +154,23 @@ stack_loadFromStream( StackCtxt* stack, XWStreamCtxt* stream )
void
stack_writeToStream( const StackCtxt* stack, XWStreamCtxt* stream )
{
XP_U16 nBytes;
XP_U16 nBytes = 0;
XWStreamCtxt* data = stack->data;
XWStreamPos oldPos = START_OF_STREAM;
/* XP_LOGF( "%s(): writing stream; hash: %X", __func__, hash ); */
/* XP_U32 hash = stream_getHash( data, START_OF_STREAM, XP_TRUE ); */
if ( !!data ) {
oldPos = stream_setPos( data, POS_READ, START_OF_STREAM );
nBytes = stream_getSize( data );
} else {
nBytes = 0;
}
stream_putU16( stream, nBytes );
XP_ASSERT( 0 == (HAVE_FLAGS_MASK & nBytes) ); /* under 32K? I hope so */
stream_putU16( stream, nBytes | (stack->typeBits == 3 ? HAVE_FLAGS_MASK : 0) );
if ( stack->typeBits == 3 ) {
stream_putU8( stream, stack->flags );
}
if ( nBytes > 0 ) {
stream_putU16( stream, stack->highWaterMark );
@ -166,7 +192,8 @@ stack_copy( const StackCtxt* stack )
stack->vtmgr );
stack_writeToStream( stack, stream );
newStack = stack_make( MPPARM(stack->mpool) stack->vtmgr );
newStack = stack_make( MPPARM(stack->mpool) stack->vtmgr,
stack->inDuplicateMode );
stack_loadFromStream( newStack, stream );
stack_setBitsPerTile( newStack, stack->bitsPerTile );
stream_destroy( stream );
@ -176,46 +203,39 @@ stack_copy( const StackCtxt* stack )
static void
pushEntryImpl( StackCtxt* stack, const StackEntry* entry )
{
XP_U16 ii, bitsPerTile;
XWStreamPos oldLoc;
XP_U16 nTiles = entry->u.move.moveInfo.nTiles;
XWStreamCtxt* stream = stack->data;
XP_LOGF( "%s(typ=%s, player=%d)", __func__,
StackMoveType_2str(entry->moveType), entry->playerNum );
if ( !stream ) {
stream = mem_stream_make_raw( MPPARM(stack->mpool) stack->vtmgr );
stack->data = stream;
stack->typeBits = stack->inDuplicateMode ? 3 : 2; /* the new size */
XP_ASSERT( 0 == stack->flags );
}
oldLoc = stream_setPos( stream, POS_WRITE, stack->top );
XWStreamPos oldLoc = stream_setPos( stream, POS_WRITE, stack->top );
stream_putBits( stream, 2, entry->moveType );
stream_putBits( stream, stack->typeBits, entry->moveType );
stream_putBits( stream, 2, entry->playerNum );
switch( entry->moveType ) {
case MOVE_TYPE:
moveInfoToStream( stream, &entry->u.move.moveInfo, stack->bitsPerTile );
traySetToStream( stream, &entry->u.move.newTiles );
if ( stack->inDuplicateMode ) {
stream_putBits( stream, NPLAYERS_NBITS, entry->u.move.dup.nScores );
scoresToStream( stream, entry->u.move.dup.nScores, entry->u.move.dup.scores );
}
break;
case PHONY_TYPE:
stream_putBits( stream, NTILES_NBITS, nTiles );
stream_putBits( stream, 5, entry->u.move.moveInfo.commonCoord );
stream_putBits( stream, 1, entry->u.move.moveInfo.isHorizontal );
bitsPerTile = stack->bitsPerTile;
XP_ASSERT( bitsPerTile == 5 || bitsPerTile == 6 );
for ( ii = 0; ii < nTiles; ++ii ) {
Tile tile;
stream_putBits( stream, 5,
entry->u.move.moveInfo.tiles[ii].varCoord );
tile = entry->u.move.moveInfo.tiles[ii].tile;
stream_putBits( stream, bitsPerTile, tile & TILE_VALUE_MASK );
stream_putBits( stream, 1, (tile & TILE_BLANK_BIT) != 0 );
}
if ( entry->moveType == MOVE_TYPE ) {
traySetToStream( stream, &entry->u.move.newTiles );
}
moveInfoToStream( stream, &entry->u.phony.moveInfo, stack->bitsPerTile );
break;
case ASSIGN_TYPE:
traySetToStream( stream, &entry->u.assign.tiles );
XP_ASSERT( entry->playerNum == DUP_PLAYER || !stack->inDuplicateMode );
break;
case TRADE_TYPE:
@ -226,6 +246,13 @@ pushEntryImpl( StackCtxt* stack, const StackEntry* entry )
second guy */
traySetToStream( stream, &entry->u.trade.newTiles );
break;
case PAUSE_TYPE:
stream_putBits( stream, 2, entry->u.pause.pauseType );
stream_putU32( stream, entry->u.pause.when );
stringToStream( stream, entry->u.pause.msg );
break;
default:
XP_ASSERT(0);
}
++stack->nEntries;
@ -262,37 +289,22 @@ pushEntry( StackCtxt* stack, const StackEntry* entry )
static void
readEntry( const StackCtxt* stack, StackEntry* entry )
{
XP_U16 nTiles, ii, bitsPerTile;
XWStreamCtxt* stream = stack->data;
entry->moveType = (StackMoveType)stream_getBits( stream, 2 );
entry->moveType = (StackMoveType)stream_getBits( stream, stack->typeBits );
entry->playerNum = (XP_U8)stream_getBits( stream, 2 );
switch( entry->moveType ) {
case MOVE_TYPE:
moveInfoFromStream( stream, &entry->u.move.moveInfo, stack->bitsPerTile );
traySetFromStream( stream, &entry->u.move.newTiles );
if ( stack->inDuplicateMode ) {
entry->u.move.dup.nScores = stream_getBits( stream, NPLAYERS_NBITS );
scoresFromStream( stream, entry->u.move.dup.nScores, entry->u.move.dup.scores );
}
break;
case PHONY_TYPE:
nTiles = entry->u.move.moveInfo.nTiles =
(XP_U8)stream_getBits( stream, NTILES_NBITS );
XP_ASSERT( nTiles <= MAX_TRAY_TILES );
entry->u.move.moveInfo.commonCoord = (XP_U8)stream_getBits(stream, 5);
entry->u.move.moveInfo.isHorizontal = (XP_U8)stream_getBits(stream, 1);
bitsPerTile = stack->bitsPerTile;
XP_ASSERT( bitsPerTile == 5 || bitsPerTile == 6 );
for ( ii = 0; ii < nTiles; ++ii ) {
Tile tile;
entry->u.move.moveInfo.tiles[ii].varCoord =
(XP_U8)stream_getBits(stream, 5);
tile = (Tile)stream_getBits( stream, bitsPerTile );
if ( 0 != stream_getBits( stream, 1 ) ) {
tile |= TILE_BLANK_BIT;
}
entry->u.move.moveInfo.tiles[ii].tile = tile;
}
if ( entry->moveType == MOVE_TYPE ) {
traySetFromStream( stream, &entry->u.move.newTiles );
}
moveInfoFromStream( stream, &entry->u.phony.moveInfo, stack->bitsPerTile );
break;
case ASSIGN_TYPE:
@ -305,13 +317,23 @@ readEntry( const StackCtxt* stack, StackEntry* entry )
XP_ASSERT( entry->u.trade.newTiles.nTiles
== entry->u.trade.oldTiles.nTiles );
break;
case PAUSE_TYPE:
entry->u.pause.pauseType = (DupPauseType)stream_getBits( stream, 2 );
entry->u.pause.when = stream_getU32( stream );
entry->u.pause.msg = stringFromStream( stack->mpool, stream );
break;
default:
XP_ASSERT(0);
}
} /* readEntry */
void
stack_addMove( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo,
const TrayTileSet* newTiles )
static void
addMove( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo,
XP_U16 nScores, XP_U16* scores, const TrayTileSet* newTiles )
{
StackEntry move = {.playerNum = (XP_U8)turn,
.moveType = MOVE_TYPE,
@ -320,9 +342,31 @@ stack_addMove( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo,
XP_MEMCPY( &move.u.move.moveInfo, moveInfo, sizeof(move.u.move.moveInfo));
move.u.move.newTiles = *newTiles;
XP_ASSERT( 0 == nScores || stack->inDuplicateMode );
if ( stack->inDuplicateMode ) {
move.u.move.dup.nScores = nScores;
XP_MEMCPY( &move.u.move.dup.scores[0], scores,
nScores * sizeof(move.u.move.dup.scores[0]) );
}
pushEntry( stack, &move );
}
void
stack_addMove( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo,
const TrayTileSet* newTiles )
{
addMove( stack, turn, moveInfo, 0, NULL, newTiles );
} /* stack_addMove */
void
stack_addDupMove( StackCtxt* stack, const MoveInfo* moveInfo,
XP_U16 nScores, XP_U16* scores, const TrayTileSet* newTiles )
{
XP_ASSERT( stack->inDuplicateMode );
addMove( stack, DUP_PLAYER, moveInfo, nScores, scores, newTiles );
}
void
stack_addPhony( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo )
{
@ -336,10 +380,21 @@ stack_addPhony( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo )
pushEntry( stack, &move );
} /* stack_addPhony */
void
stack_addDupTrade( StackCtxt* stack, const TrayTileSet* oldTiles,
const TrayTileSet* newTiles )
{
XP_ASSERT( stack->inDuplicateMode );
XP_ASSERT( oldTiles->nTiles == newTiles->nTiles );
stack_addTrade( stack, DUP_PLAYER, oldTiles, newTiles );
}
void
stack_addTrade( StackCtxt* stack, XP_U16 turn,
const TrayTileSet* oldTiles, const TrayTileSet* newTiles )
{
XP_ASSERT( oldTiles->nTiles == newTiles->nTiles );
StackEntry move = { .playerNum = (XP_U8)turn,
.moveType = TRADE_TYPE,
};
@ -362,6 +417,26 @@ stack_addAssign( StackCtxt* stack, XP_U16 turn, const TrayTileSet* tiles )
pushEntry( stack, &move );
} /* stack_addAssign */
void
stack_addPause( StackCtxt* stack, DupPauseType pauseType, XP_S16 turn,
XP_U32 when, const XP_UCHAR* msg )
{
StackEntry move = { .moveType = PAUSE_TYPE,
.u.pause.pauseType = pauseType,
.u.pause.when = when,
.u.pause.msg = copyString( stack->mpool, msg ),
};
if ( 0 <= turn ) {
move.playerNum = turn; /* don't store the -1 case (pauseType==AUTOPAUSED) */
} else {
XP_ASSERT( AUTOPAUSED == pauseType );
}
pushEntry( stack, &move );
stack_freeEntry( stack, &move );
}
static XP_Bool
setCacheReadyFor( StackCtxt* stack, XP_U16 nn )
{
@ -371,6 +446,7 @@ setCacheReadyFor( StackCtxt* stack, XP_U16 nn )
for ( ii = 0; ii < nn; ++ii ) {
StackEntry dummy;
readEntry( stack, &dummy );
stack_freeEntry( stack, &dummy );
}
stack->cacheNext = nn;
@ -386,7 +462,7 @@ stack_getNEntries( const StackCtxt* stack )
} /* stack_getNEntries */
XP_Bool
stack_getNthEntry( StackCtxt* stack, XP_U16 nn, StackEntry* entry )
stack_getNthEntry( StackCtxt* stack, const XP_U16 nn, StackEntry* entry )
{
XP_Bool found;
@ -409,8 +485,10 @@ stack_getNthEntry( StackCtxt* stack, XP_U16 nn, StackEntry* entry )
stack->cachedPos = stream_setPos( stack->data, POS_READ, oldPos );
++stack->cacheNext;
}
/* XP_LOGF( "%s(%d) (typ=%s, player=%d, num=%d)", __func__, nn, */
/* StackMoveType_2str(entry->moveType), entry->playerNum, entry->moveNum ); */
}
return found;
} /* stack_getNthEntry */
@ -446,6 +524,18 @@ stack_redo( StackCtxt* stack, StackEntry* entry )
return canRedo;
} /* stack_redo */
void
stack_freeEntry( StackCtxt* stack, StackEntry* entry )
{
XP_ASSERT( entry->moveType != __BOGUS );
switch( entry->moveType ) {
case PAUSE_TYPE:
XP_FREEP( stack->mpool, &entry->u.pause.msg );
break;
}
entry->moveType = __BOGUS;
}
#ifdef CPLUS
}
#endif

View file

@ -22,15 +22,21 @@
#include "comtypes.h"
#include "model.h"
#include "dutil.h"
#include "vtabmgr.h"
#ifdef CPLUS
extern "C" {
#endif
enum { ASSIGN_TYPE, MOVE_TYPE, TRADE_TYPE, PHONY_TYPE };
enum { ASSIGN_TYPE, MOVE_TYPE, TRADE_TYPE, PHONY_TYPE, PAUSE_TYPE,
/* used for debugging, and can be changed because never stored: */
__BOGUS,
};
typedef XP_U8 StackMoveType;
#define DUP_PLAYER 0
typedef struct AssignRec {
TrayTileSet tiles;
} AssignRec;
@ -43,17 +49,28 @@ typedef struct TradeRec {
typedef struct MoveRec {
MoveInfo moveInfo;
TrayTileSet newTiles;
struct {
XP_U16 nScores;
XP_U16 scores[MAX_NUM_PLAYERS];
} dup;
} MoveRec;
typedef struct PhonyRec {
MoveInfo moveInfo;
} PhonyRec;
typedef struct _PauseRec {
DupPauseType pauseType;
XP_U32 when;
const XP_UCHAR* msg; /* requires stack_freeEntry() */
} PauseRec;
typedef union EntryData {
AssignRec assign;
TradeRec trade;
MoveRec move;
PhonyRec phony;
PauseRec pause;
} EntryData;
typedef struct StackEntry {
@ -65,10 +82,10 @@ typedef struct StackEntry {
typedef struct StackCtxt StackCtxt;
StackCtxt* stack_make( MPFORMAL VTableMgr* vtmgr );
StackCtxt* stack_make( MPFORMAL VTableMgr* vtmgr, XP_Bool inDuplicateMode );
void stack_destroy( StackCtxt* stack );
void stack_init( StackCtxt* stack );
void stack_init( StackCtxt* stack, XP_Bool inDuplicateMode );
XP_U32 stack_getHash( const StackCtxt* stack, XP_Bool correct );
void stack_setBitsPerTile( StackCtxt* stack, XP_U16 bitsPerTile );
@ -78,19 +95,30 @@ StackCtxt* stack_copy( const StackCtxt* stack );
void stack_addMove( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo,
const TrayTileSet* newTiles );
void stack_addDupMove( StackCtxt* stack, const MoveInfo* moveInfo,
XP_U16 nScores, XP_U16* scores,
const TrayTileSet* tiles );
void stack_addPhony( StackCtxt* stack, XP_U16 turn, const MoveInfo* moveInfo );
void stack_addTrade( StackCtxt* stack, XP_U16 turn,
const TrayTileSet* oldTiles,
const TrayTileSet* newTiles );
void stack_addDupTrade( StackCtxt* stack, const TrayTileSet* oldTiles,
const TrayTileSet* newTiles );
void stack_addAssign( StackCtxt* stack, XP_U16 turn,
const TrayTileSet* tiles );
void stack_addPause( StackCtxt* stack, DupPauseType pauseTYpe, XP_S16 turn,
XP_U32 when, const XP_UCHAR* msg );
XP_U16 stack_getNEntries( const StackCtxt* stack );
XP_Bool stack_getNthEntry( StackCtxt* stack, XP_U16 n, StackEntry* entry );
XP_Bool stack_popEntry( StackCtxt* stack, StackEntry* entry );
XP_Bool stack_redo( StackCtxt* stack, StackEntry* entry );
void stack_freeEntry( StackCtxt* stack, StackEntry* entry );
#ifdef CPLUS
}

View file

@ -22,6 +22,7 @@
#include "util.h"
#include "engine.h"
#include "game.h"
#include "strutils.h"
#include "LocalizedStrIncludes.h"
#ifdef CPLUS
@ -94,7 +95,7 @@ scoreCurrentMove( ModelCtxt* model, XP_S16 turn, XWStreamCtxt* stream,
} /* scoreCurrentMove */
void
adjustScoreForUndone( ModelCtxt* model, MoveInfo* mi, XP_U16 turn )
adjustScoreForUndone( ModelCtxt* model, const MoveInfo* mi, XP_U16 turn )
{
XP_U16 moveScore;
PlayerCtxt* player = &model->players[turn];
@ -102,8 +103,8 @@ adjustScoreForUndone( ModelCtxt* model, MoveInfo* mi, XP_U16 turn )
if ( mi->nTiles == 0 ) {
moveScore = 0;
} else {
moveScore = figureMoveScore( model, turn, mi, (EngineCtxt*)NULL,
(XWStreamCtxt*)NULL,
moveScore = figureMoveScore( model, turn, mi, (EngineCtxt*)NULL,
(XWStreamCtxt*)NULL,
(WordNotifierInfo*)NULL );
}
player->score -= moveScore;
@ -130,14 +131,16 @@ invalidateScore( ModelCtxt* model, XP_S16 turn )
XP_Bool
getCurrentMoveScoreIfLegal( ModelCtxt* model, XP_S16 turn,
XWStreamCtxt* stream,
WordNotifierInfo* wni, XP_S16* score )
WordNotifierInfo* wni, XP_S16* scoreP )
{
PlayerCtxt* player = &model->players[turn];
if ( !player->curMoveValid ) {
scoreCurrentMove( model, turn, stream, wni );
}
*score = player->curMoveScore;
if ( !!scoreP ) {
*scoreP = player->curMoveScore;
}
return player->curMoveScore != ILLEGAL_MOVE_SCORE;
} /* getCurrentMoveScoreIfLegal */
@ -277,42 +280,56 @@ tilesInLine( ModelCtxt* model, XP_S16 turn, XP_Bool* isHorizontal )
} /* tilesInLine */
void
normalizeMoves( const ModelCtxt* model, XP_S16 turn, XP_Bool isHorizontal,
MoveInfo* moveInfo )
normalizeMI( MoveInfo* moveInfoOut, const MoveInfo* moveInfoIn )
{
XP_S16 lowCol, ii, jj, thisCol; /* unsigned is a problem on palm */
const PlayerCtxt* player = &model->players[turn];
XP_U16 nTiles = player->nPending;
XP_S16 lastTaken;
short lowIndex = 0;
const PendingTile* pt;
/* use scratch in case in and out are same */
MoveInfo tmp = *moveInfoIn;
// const XP_Bool isHorizontal = tmp.isHorizontal;
moveInfo->isHorizontal = isHorizontal;
moveInfo->nTiles = (XP_U8)nTiles;
lastTaken = -1;
for ( ii = 0; ii < nTiles; ++ii ) {
lowCol = 100; /* high enough to always be changed */
for ( jj = 0; jj < nTiles; ++jj ) {
pt = &player->pendingTiles[jj];
thisCol = isHorizontal? pt->col:pt->row;
if (thisCol < lowCol && thisCol > lastTaken ) {
lowCol = thisCol;
XP_S16 lastTaken = -1;
XP_U16 next = 0;
for ( XP_U16 ii = 0; ii < tmp.nTiles; ++ii ) {
XP_U16 lowest = 100; /* high enough to always be changed */
XP_U16 lowIndex = 100;
for ( XP_U16 jj = 0; jj < tmp.nTiles; ++jj ) {
XP_U16 cur = moveInfoIn->tiles[jj].varCoord;
if ( cur < lowest && cur > lastTaken ) {
lowest = cur;
lowIndex = jj;
}
}
/* we've found the next to transfer (4 bytes smaller without a temp
local ptr. */
pt = &player->pendingTiles[lowIndex];
lastTaken = lowCol;
moveInfo->tiles[ii].varCoord = (XP_U8)lastTaken;
moveInfo->tiles[ii].tile = pt->tile;
XP_ASSERT( lowIndex < MAX_ROWS );
tmp.tiles[next++] = moveInfoIn->tiles[lowIndex];
lastTaken = lowest;
}
XP_ASSERT( next == tmp.nTiles );
*moveInfoOut = tmp;
}
void
normalizeMoves( const ModelCtxt* model, XP_S16 turn, XP_Bool isHorizontal,
MoveInfo* moveInfo )
{
const PlayerCtxt* player = &model->players[turn];
const XP_U16 nTiles = player->nPending;
moveInfo->isHorizontal = isHorizontal;
moveInfo->nTiles = nTiles;
if ( 0 < nTiles ) {
pt = &player->pendingTiles[0];
const PendingTile* pt = &player->pendingTiles[0];
moveInfo->commonCoord = isHorizontal? pt->row:pt->col;
for ( XP_U16 ii = 0; ii < nTiles; ++ii ) {
const PendingTile* pt = &player->pendingTiles[ii];
moveInfo->tiles[ii].tile = pt->tile;
moveInfo->tiles[ii].varCoord = isHorizontal? pt->col:pt->row;
}
normalizeMI( moveInfo, moveInfo );
}
} /* normalizeMoves */
@ -452,7 +469,7 @@ isLegalMove( ModelCtxt* model, MoveInfo* mInfo, XP_Bool silent )
} /* isLegalMove */
XP_U16
figureMoveScore( const ModelCtxt* model, XP_U16 turn, MoveInfo* moveInfo,
figureMoveScore( const ModelCtxt* model, XP_U16 turn, const MoveInfo* moveInfo,
EngineCtxt* engine, XWStreamCtxt* stream,
WordNotifierInfo* notifyInfo )
{
@ -464,7 +481,7 @@ figureMoveScore( const ModelCtxt* model, XP_U16 turn, MoveInfo* moveInfo,
short moveMultiplier = 1;
short multipliers[MAX_TRAY_TILES];
MoveInfo tmpMI;
MoveInfoTile* tiles;
const MoveInfoTile* tiles;
XP_U16 nTiles = moveInfo->nTiles;
XP_ASSERT( nTiles > 0 );
@ -573,6 +590,8 @@ scoreWord( const ModelCtxt* model, XP_U16 turn,
XP_U16 firstCoord = tiles->varCoord;
DictionaryCtxt* dict = model_getPlayerDict( model, turn );
assertSorted( movei );
if ( movei->isHorizontal ) {
row = movei->commonCoord;
incr = &col;

View file

@ -27,7 +27,8 @@
/* Don't check in other than 0 for a few releases!!! */
#ifndef NLI_VERSION
# define NLI_VERSION 0
// # define NLI_VERSION 0
# define NLI_VERSION 1 /* adds inDuplicateMode */
#endif
void
@ -41,6 +42,7 @@ nli_init( NetLaunchInfo* nli, const CurGameInfo* gi, const CommsAddrRec* addr,
nli->nPlayersT = gi->nPlayers;
nli->nPlayersH = nPlayers;
nli->forceChannel = forceChannel;
nli->inDuplicateMode = gi->inDuplicateMode;
CommsConnType typ;
for ( XP_U32 st = 0; addr_iter( addr, &typ, &st ); ) {
@ -118,6 +120,7 @@ nli_saveToStream( const NetLaunchInfo* nli, XWStreamCtxt* stream )
if ( NLI_VERSION > 0 ) {
stream_putBits( stream, 1, nli->remotesAreRobots ? 1 : 0 );
stream_putBits( stream, 1, nli->inDuplicateMode ? 1 : 0 );
}
}
@ -157,9 +160,13 @@ nli_makeFromStream( NetLaunchInfo* nli, XWStreamCtxt* stream )
nli->osVers = stream_getU32( stream );
}
if ( version > 0 ) {
if ( version > 0 && 0 < stream_getSize( stream ) ) {
nli->remotesAreRobots = 0 != stream_getBits( stream, 1 );
XP_LOGF( "%s(): remotesAreRobots: %d", __func__, nli->remotesAreRobots );
nli->inDuplicateMode = stream_getBits( stream, 1 );
XP_LOGF( "%s(): remotesAreRobots: %d; inDuplicateMode: %d", __func__,
nli->remotesAreRobots, nli->inDuplicateMode );
} else {
nli->inDuplicateMode = XP_FALSE;
}
XP_ASSERT( 0 == stream_getSize( stream ) );

View file

@ -46,6 +46,7 @@ typedef struct _InviteInfo {
XP_U8 nPlayersT;
XP_U8 nPlayersH;
XP_Bool remotesAreRobots;
XP_Bool inDuplicateMode;
/* Relay */
XP_UCHAR room[MAX_INVITE_LEN + 1];

View file

@ -53,6 +53,7 @@ struct NewGameCtx {
#ifndef XWFEATURE_STANDALONE_ONLY
XP_TriEnable settingsEnabled;
#endif
XP_Bool duplicateEnabled;
MPSLOT
};
@ -150,6 +151,15 @@ newg_load( NewGameCtx* ngc, const CurGameInfo* gi )
(*ngc->enableAttrProc)( closure, NG_ATTR_NPLAYERS, ngc->isNewGame?
TRI_ENAB_ENABLED : TRI_ENAB_DISABLED );
ngc->timerSeconds = gi->gameSeconds;
value.ng_u16 = ngc->timerSeconds;
(*ngc->setAttrProc)( closure, NG_ATTR_TIMER, value );
ngc->duplicateEnabled = gi->inDuplicateMode;
value.ng_bool = ngc->duplicateEnabled;
(*ngc->setAttrProc)( closure, NG_ATTR_DUPLICATE, value );
ngc->timerSeconds = gi->gameSeconds;
value.ng_u16 = ngc->timerSeconds;
(*ngc->setAttrProc)( closure, NG_ATTR_TIMER, value );
@ -220,7 +230,6 @@ XP_Bool
newg_store( NewGameCtx* ngc, CurGameInfo* gi,
XP_Bool XP_UNUSED_STANDALONE(warn) )
{
XP_U16 player;
XP_Bool consistent = checkConsistent( ngc, warn );
if ( consistent ) {
@ -230,10 +239,15 @@ newg_store( NewGameCtx* ngc, CurGameInfo* gi,
gi->serverRole = ngc->role;
makeLocal = ngc->role != SERVER_ISSERVER;
#endif
gi->gameSeconds = ngc->timerSeconds;
gi->timerEnabled = gi->gameSeconds > 0;
for ( player = 0; player < MAX_NUM_PLAYERS; ++player ) {
gi->inDuplicateMode = ngc->duplicateEnabled;
gi->gameSeconds = ngc->timerSeconds;
gi->timerEnabled = gi->gameSeconds > 0;
for ( XP_U16 player = 0; player < MAX_NUM_PLAYERS; ++player ) {
storePlayer( ngc, player, &gi->players[player] );
if ( makeLocal ) {
gi->players[player].isLocal = XP_TRUE;
@ -273,6 +287,9 @@ newg_attrChanged( NewGameCtx* ngc, NewGameAttr attr, NGValue value )
case NG_ATTR_TIMER:
ngc->timerSeconds = value.ng_u16;
break;
case NG_ATTR_DUPLICATE:
ngc->duplicateEnabled = value.ng_bool;
break;
default:
XP_ASSERT( 0 );
}

View file

@ -1,6 +1,7 @@
/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */
/*
* Copyright 1997 - 2006 by Eric House (xwords@eehouse.org). All rights reserved.
* Copyright 1997 - 2019 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -60,6 +61,7 @@ typedef enum {
,NG_ATTR_NPLAYHEADER
,NG_ATTR_CANJUGGLE
,NG_ATTR_TIMER
,NG_ATTR_DUPLICATE
} NewGameAttr;
typedef union NGValue {

View file

@ -144,7 +144,7 @@ getRandomTile( PoolContext* pool )
} /* getRandomTile */
void
pool_requestTiles( PoolContext* pool, Tile* tiles, XP_U8* maxNum )
pool_requestTiles( PoolContext* pool, Tile* tiles, XP_U16* maxNum )
{
XP_S16 numWanted = *maxNum;
XP_U16 numWritten = 0;
@ -163,19 +163,24 @@ pool_requestTiles( PoolContext* pool, Tile* tiles, XP_U8* maxNum )
*tiles++ = t;
++numWritten;
}
*maxNum = (XP_U8)numWritten;
*maxNum = numWritten;
#ifdef BLANKS_FIRST
pool->lettersLeft[pool->blankIndex] = oldCount - 1;
#endif
XP_LOGF( "%s: %d tiles left in pool", __func__, pool->numTilesLeft );
} /* pool_requestTiles */
void
pool_replaceTiles( PoolContext* pool, const TrayTileSet* tiles )
{
XP_U16 nTiles = tiles->nTiles;
const Tile* tilesP = tiles->tiles;
pool_replaceTiles2( pool, tiles->nTiles, tiles->tiles );
}
void
pool_replaceTiles2( PoolContext* pool, XP_U16 nTiles, const Tile* tilesP )
{
while ( nTiles-- ) {
Tile tile = *tilesP++; /* do I need to filter off high bits? */
@ -238,8 +243,7 @@ pool_getNTilesLeftFor( const PoolContext* pool, Tile tile )
void
pool_initFromDict( PoolContext* pool, DictionaryCtxt* dict )
{
XP_U16 numFaces = dict_numTileFaces( dict );
Tile ii;
const XP_U16 numFaces = dict_numTileFaces( dict );
XP_FREEP( pool->mpool, &pool->lettersLeft );
@ -248,7 +252,7 @@ pool_initFromDict( PoolContext* pool, DictionaryCtxt* dict )
numFaces * sizeof(pool->lettersLeft[0]) );
pool->numTilesLeft = 0;
for ( ii = 0; ii < numFaces; ++ii ) {
for ( Tile ii = 0; ii < numFaces; ++ii ) {
XP_U16 numTiles = dict_numTiles( dict, ii );
pool->lettersLeft[ii] = (XP_U8)numTiles;
pool->numTilesLeft += numTiles;
@ -276,5 +280,20 @@ checkTilesLeft( const PoolContext* pool )
}
XP_ASSERT( count == pool->numTilesLeft );
}
void
pool_dumpSelf( const PoolContext* pool )
{
XP_UCHAR buf[256] = {0};
XP_U16 offset = 0;
for ( Tile tile = 0; tile < pool->numFaces; ++tile ) {
XP_U16 count = pool->lettersLeft[tile];
if ( count > 0 ) {
offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset, "%x/%d,", tile, count );
}
}
XP_LOGF( "%s(): {numTiles: %d, pool: %s}", __func__,
pool->numTilesLeft, buf );
}
#endif

View file

@ -25,8 +25,10 @@
#include "model.h"
void pool_requestTiles( PoolContext* pool, Tile* tiles,
/*in out*/ XP_U8* maxNum );
/*in out*/ XP_U16* maxNum );
void pool_replaceTiles( PoolContext* pool, const TrayTileSet* tiles );
void pool_replaceTiles2( PoolContext* pool, XP_U16 nTiles, const Tile* tilesP );
void pool_removeTiles( PoolContext* pool, const TrayTileSet* tiles );
XP_Bool pool_containsTiles( const PoolContext* pool,
const TrayTileSet* tiles );
@ -42,4 +44,8 @@ void pool_initFromDict( PoolContext* pool, DictionaryCtxt* dict );
void pool_writeToStream( PoolContext* pool, XWStreamCtxt* stream );
PoolContext* pool_makeFromStream( MPFORMAL XWStreamCtxt* stream );
#ifdef DEBUG
void pool_dumpSelf( const PoolContext* pool );
#endif
#endif

View file

@ -57,7 +57,6 @@ drawScoreBoard( BoardCtxt* board )
if ( draw_scoreBegin( board->draw, &board->scoreBdBounds, nPlayers,
scores.arr, nTilesInPool, dfs ) ) {
XP_S16 curTurn = server_getCurrentTurn( board->server );
XP_U16 selPlayer = board->selPlayer;
XP_Rect playerRects[nPlayers];
XP_U16 remDim;
@ -117,7 +116,7 @@ drawScoreBoard( BoardCtxt* board )
#endif
dsi->playerNum = ii;
dsi->totalScore = scores.arr[ii];
dsi->isTurn = (ii == curTurn);
dsi->isTurn = server_isPlayersTurn( board->server, ii );
dsi->name = emptyStringIfNull(lp->name);
dsi->selected = board->trayVisState != TRAY_HIDDEN
&& ii==selPlayer;
@ -175,7 +174,6 @@ drawScoreBoard( BoardCtxt* board )
XP_ASSERT( nPlayers <= MAX_NUM_PLAYERS );
if ( nPlayers > 0 ) {
ModelCtxt* model = board->model;
XP_S16 curTurn = server_getCurrentTurn( board->server, NULL );
XP_U16 selPlayer = board->selPlayer;
XP_S16 nTilesInPool = server_countTilesInPool( board->server );
XP_Rect scoreRect = board->scoreBdBounds;
@ -259,7 +257,7 @@ drawScoreBoard( BoardCtxt* board )
#endif
dp->dsi.playerNum = ii;
dp->dsi.totalScore = scores.arr[ii];
dp->dsi.isTurn = (ii == curTurn);
dp->dsi.isTurn = server_isPlayersTurn( board->server, ii );
dp->dsi.name = emptyStringIfNull(lp->name);
dp->dsi.selected = board->trayVisState != TRAY_HIDDEN
&& ii==selPlayer;
@ -341,25 +339,17 @@ drawScoreBoard( BoardCtxt* board )
} /* drawScoreBoard */
#endif
static XP_S16
figureSecondsLeft( BoardCtxt* board )
{
CurGameInfo* gi = board->gi;
XP_U16 secondsUsed = gi->players[board->selPlayer].secondsUsed;
XP_U16 secondsAvailable = gi->gameSeconds / gi->nPlayers;
XP_ASSERT( gi->timerEnabled );
return secondsAvailable - secondsUsed;
} /* figureSecondsLeft */
void
drawTimer( BoardCtxt* board )
drawTimer( const BoardCtxt* board )
{
if ( board->gi->timerEnabled && 0 < board->timerBounds.width
&& 0 < board->timerBounds.height ) {
XP_S16 secondsLeft = figureSecondsLeft( board );
if ( !!board->draw && board->gi->timerEnabled ) {
XP_S16 secondsLeft = server_getTimerSeconds( board->server,
board->selPlayer );
XP_Bool turnDone = board->gi->inDuplicateMode
? server_dupTurnDone( board->server, board->selPlayer )
: XP_FALSE;
draw_drawTimer( board->draw, &board->timerBounds,
board->selPlayer, secondsLeft );
board->selPlayer, secondsLeft, turnDone );
}
} /* drawTimer */

View file

@ -24,7 +24,7 @@
void drawScoreBoard( BoardCtxt* board );
XP_S16 figureScoreRectTapped( const BoardCtxt* board, XP_U16 x, XP_U16 y );
void drawTimer( BoardCtxt* board );
void drawTimer( const BoardCtxt* board );
void penTimerFiredScore( const BoardCtxt* board );
#if defined POINTER_SUPPORT || defined KEYBOARD_NAV

File diff suppressed because it is too large Load diff

View file

@ -30,17 +30,6 @@
extern "C" {
#endif
/* typedef struct ServerCtxt ServerCtxt; */
/* typedef struct ServerVtable { */
/* void (*m_registerPlayer)( ServerCtxt* server, XP_U16 playerNum, */
/* XP_PlayerSocket socket ); */
/* void (*m_getTileValueInfo)( ServerCtxt* server, void* valueBuf ); */
/* } ServerVtable; */
ServerCtxt* server_make( MPFORMAL ModelCtxt* model, CommsCtxt* comms,
XW_UtilCtxt* util );
@ -59,6 +48,11 @@ typedef void (*TurnChangeListener)( void* data );
void server_setTurnChangeListener( ServerCtxt* server, TurnChangeListener tl,
void* data );
typedef void (*TimerChangeListener)( void* data, XP_U32 gameID,
XP_S32 oldVal, XP_S32 newVal );
void server_setTimerChangeListener( ServerCtxt* server, TimerChangeListener tl,
void* data );
typedef void (*GameOverListener)( void* data, XP_S16 quitter );
void server_setGameOverListener( ServerCtxt* server, GameOverListener gol,
void* data );
@ -80,8 +74,17 @@ XP_U16 server_secondsUsedBy( ServerCtxt* server, XP_U16 playerNum );
XP_Bool server_handleUndo( ServerCtxt* server, XP_U16 limit );
/* signed because negative number means nobody's turn yet */
XP_S16 server_getCurrentTurn( ServerCtxt* server, XP_Bool* isLocal );
XP_Bool server_getGameIsOver( ServerCtxt* server );
XP_S16 server_getCurrentTurn( const ServerCtxt* server, XP_Bool* isLocal );
XP_Bool server_isPlayersTurn( const ServerCtxt* server, XP_U16 turn );
XP_Bool server_getGameIsOver( const ServerCtxt* server );
XP_S32 server_getDupTimerExpires( const ServerCtxt* server );
XP_S16 server_getTimerSeconds( const ServerCtxt* server, XP_U16 turn );
XP_Bool server_dupTurnDone( const ServerCtxt* server, XP_U16 turn );
XP_Bool server_canPause( const ServerCtxt* server );
XP_Bool server_canUnpause( const ServerCtxt* server );
void server_pause( ServerCtxt* server, XP_S16 turn, const XP_UCHAR* msg );
void server_unpause( ServerCtxt* server, XP_S16 turn, const XP_UCHAR* msg );
/* return bitvector marking players still not arrived in networked game */
XP_U16 server_getMissingPlayers( const ServerCtxt* server );
XP_U32 server_getLastMoveTime( const ServerCtxt* server );
@ -97,7 +100,8 @@ XP_U16 server_getPendingRegs( const ServerCtxt* server );
XP_Bool server_do( ServerCtxt* server );
XP_Bool server_commitMove( ServerCtxt* server, TrayTileSet* newTiles );
XP_Bool server_commitMove( ServerCtxt* server, XP_U16 player,
TrayTileSet* newTiles );
XP_Bool server_commitTrade( ServerCtxt* server, const TrayTileSet* oldTiles,
TrayTileSet* newTiles );

View file

@ -672,12 +672,12 @@ restorePartials( SMSProto* state )
XP_UCHAR phone[32];
(void)stringFromStreamHere( stream, phone, VSIZE(phone) );
int nMsgIDs = stream_getU8( stream );
XP_LOGF( "%s(): got %d message records for phone %s", __func__,
nMsgIDs, phone );
/* XP_LOGF( "%s(): got %d message records for phone %s", __func__, */
/* nMsgIDs, phone ); */
for ( int jj = 0; jj < nMsgIDs; ++jj ) {
XP_U16 msgID = stream_getU16( stream );
int count = stream_getU8( stream );
XP_LOGF( "%s(): got %d records for msgID %d", __func__, count, msgID );
/* XP_LOGF( "%s(): got %d records for msgID %d", __func__, count, msgID ); */
for ( int kk = 0; kk < count; ++kk ) {
int len = stream_getU8( stream );
if ( 0 < len ) {

View file

@ -21,18 +21,18 @@
#define _STATES_H_
enum {
XWSTATE_NONE,
XWSTATE_BEGIN,
XWSTATE_NONE, /* 0 */
XWSTATE_BEGIN, /* 1 */
__UNUSED1, /* was XWSTATE_POOL_INITED */
XWSTATE_NEED_SHOWSCORE, /* client-only */
__XWSTATE_WAITING_ALL_REG, /* unused */
__XWSTATE_WAITING_ALL_REG, /* 4 (unused) */
XWSTATE_RECEIVED_ALL_REG, /* includes waiting for dict from server */
XWSTATE_NEEDSEND_BADWORD_INFO,
XWSTATE_NEEDSEND_BADWORD_INFO, /* 6 */
XWSTATE_MOVE_CONFIRM_WAIT, /* client's waiting to hear back */
XWSTATE_MOVE_CONFIRM_MUSTSEND,/* server should tell client asap */
XWSTATE_NEEDSEND_ENDGAME,
XWSTATE_INTURN,
XWSTATE_GAMEOVER,
XWSTATE_NEEDSEND_ENDGAME, /* 9 */
XWSTATE_INTURN, /* 10 */
XWSTATE_GAMEOVER, /* 11 */
XWSTATE_LAST /* for asserts only :-) */
};

View file

@ -28,13 +28,13 @@ extern "C" {
#endif
XP_U16
bitsForMax( XP_U32 n )
bitsForMax( XP_U32 nn )
{
XP_U16 result = 0;
XP_ASSERT( n > 0 );
XP_ASSERT( nn > 0 );
while ( n != 0 ) {
n >>= 1;
while ( nn != 0 ) {
nn >>= 1;
++result;
}
@ -65,6 +65,37 @@ tilesFromStream( XWStreamCtxt* stream, Tile* tiles, XP_U16 nTiles )
}
} /* tilesFromStream */
void
scoresToStream( XWStreamCtxt* stream, XP_U16 nScores, const XP_U16* scores )
{
if ( 0 < nScores ) {
XP_U16 maxScore = 1; /* 0 will confuse bitsForMax */
for ( XP_U16 ii = 0; ii < nScores; ++ii ) {
XP_U16 score = scores[ii];
if ( score > maxScore ) {
maxScore = score;
}
}
XP_U16 bits = bitsForMax( maxScore );
stream_putBits( stream, 4, bits );
for ( XP_U16 ii = 0; ii < nScores; ++ii ) {
stream_putBits( stream, bits, scores[ii] );
}
}
}
void
scoresFromStream( XWStreamCtxt* stream, XP_U16 nScores, XP_U16* scores )
{
if ( 0 < nScores ) {
XP_U16 bits = (XP_U16)stream_getBits( stream, 4 );
for ( XP_U16 ii = 0; ii < nScores; ++ii ) {
scores[ii] = stream_getBits( stream, bits );
}
}
}
void
traySetFromStream( XWStreamCtxt* stream, TrayTileSet* ts )
{
@ -73,6 +104,69 @@ traySetFromStream( XWStreamCtxt* stream, TrayTileSet* ts )
ts->nTiles = (XP_U8)nTiles;
} /* traySetFromStream */
#ifdef DEBUG
void
assertSorted( const MoveInfo* mi )
{
for ( XP_U16 ii = 1; ii < mi->nTiles; ++ii ) {
XP_ASSERT( mi->tiles[ii-1].varCoord < mi->tiles[ii].varCoord );
}
}
#endif
void
moveInfoToStream( XWStreamCtxt* stream, const MoveInfo* mi, XP_U16 bitsPerTile )
{
#ifdef DEBUG
/* XP_UCHAR buf[64] = {0}; */
/* XP_U16 offset = 0; */
#endif
assertSorted( mi );
stream_putBits( stream, NTILES_NBITS, mi->nTiles );
stream_putBits( stream, NUMCOLS_NBITS_5, mi->commonCoord );
stream_putBits( stream, 1, mi->isHorizontal );
XP_ASSERT( bitsPerTile == 5 || bitsPerTile == 6 );
for ( XP_U16 ii = 0; ii < mi->nTiles; ++ii ) {
stream_putBits( stream, NUMCOLS_NBITS_5, mi->tiles[ii].varCoord );
Tile tile = mi->tiles[ii].tile;
#ifdef DEBUG
/* offset += XP_SNPRINTF( &buf[offset], VSIZE(buf)-offset, "%x,", tile ); */
#endif
stream_putBits( stream, bitsPerTile, tile & TILE_VALUE_MASK );
stream_putBits( stream, 1, (tile & TILE_BLANK_BIT) != 0 );
}
// XP_LOGF( "%s(): tiles: %s", __func__, buf );
}
void
moveInfoFromStream( XWStreamCtxt* stream, MoveInfo* mi, XP_U16 bitsPerTile )
{
#ifdef DEBUG
/* XP_UCHAR buf[64] = {0}; */
/* XP_U16 offset = 0; */
#endif
mi->nTiles = stream_getBits( stream, NTILES_NBITS );
XP_ASSERT( mi->nTiles <= MAX_TRAY_TILES );
mi->commonCoord = stream_getBits( stream, NUMCOLS_NBITS_5 );
mi->isHorizontal = stream_getBits( stream, 1 );
for ( XP_U16 ii = 0; ii < mi->nTiles; ++ii ) {
mi->tiles[ii].varCoord = stream_getBits( stream, NUMCOLS_NBITS_5 );
Tile tile = stream_getBits( stream, bitsPerTile );
if ( 0 != stream_getBits( stream, 1 ) ) {
tile |= TILE_BLANK_BIT;
}
mi->tiles[ii].tile = tile;
#ifdef DEBUG
/* offset += XP_SNPRINTF( &buf[offset], VSIZE(buf)-offset, "%x,", tile ); */
#endif
}
assertSorted( mi );
// XP_LOGF( "%s(): tiles: %s", __func__, buf );
}
void
removeTile( TrayTileSet* tiles, XP_U16 index )
{
@ -465,6 +559,7 @@ smsToBin( XP_U8* out, XP_U16* outlenp, const XP_UCHAR* sms, XP_U16 smslen )
void
log_hex( const XP_U8* memp, XP_U16 len, const char* tag )
{
XP_LOGF( "%s(len=%d[0x%x])", __func__, len, len );
const char* hex = "0123456789ABCDEF";
XP_U16 ii, jj;
XP_U16 offset = 0;

View file

@ -37,6 +37,14 @@ void traySetFromStream( XWStreamCtxt* stream, TrayTileSet* ts );
void sortTiles( TrayTileSet* dest, const TrayTileSet* src, XP_U16 skip );
void removeTile( TrayTileSet* tiles, XP_U16 index );
void scoresToStream( XWStreamCtxt* stream, XP_U16 nScores, const XP_U16* scores );
void scoresFromStream( XWStreamCtxt* stream, XP_U16 nScores, XP_U16* scores );
void moveInfoToStream( XWStreamCtxt* stream, const MoveInfo* mi,
XP_U16 bitsPerTile );
void moveInfoFromStream( XWStreamCtxt* stream, MoveInfo* mi,
XP_U16 bitsPerTile );
XP_S32 signedFromStream( XWStreamCtxt* stream, XP_U16 nBits );
void signedToStream( XWStreamCtxt* stream, XP_U16 nBits, XP_S32 num );
@ -101,9 +109,11 @@ XP_Bool smsToBin( XP_U8* out, XP_U16* outlen, const XP_UCHAR* in, XP_U16 inlen )
#endif
#ifdef DEBUG
void assertSorted( const MoveInfo* mi );
void log_hex( const XP_U8* memp, XP_U16 len, const char* tag );
# define LOG_HEX(m,l,t) log_hex((const XP_U8*)(m),(l),(t))
#else
# define assertSorted(mi)
# define LOG_HEX(m,l,t)
#endif

View file

@ -271,7 +271,6 @@ drawTray( BoardCtxt* board )
board->trayInvalBits = trayInvalBits;
}
}
} /* drawTray */
const XP_UCHAR*
@ -339,7 +338,7 @@ drawPendingScore( BoardCtxt* board, XP_S16 turnScore, XP_Bool hasCursor )
/* Draw the pending score down in the last tray's rect */
if ( countTilesToShow( board ) < MAX_TRAY_TILES ) {
XP_U16 selPlayer = board->selPlayer;
XP_S16 curTurn = server_getCurrentTurn( board->server, NULL );
XP_Bool curTurn = server_isPlayersTurn( board->server, selPlayer );
XP_Rect lastTileR;
figureTrayTileRect( board, MAX_TRAY_TILES-1, &lastTileR );

View file

@ -116,6 +116,8 @@ typedef struct UtilVtable {
#ifdef XWFEATURE_TURNCHANGENOTIFY
void (*m_util_turnChanged)(XW_UtilCtxt* uc, XP_S16 newTurn);
#endif
void (*m_util_notifyDupStatus)( XW_UtilCtxt* uc, XP_Bool amHost,
const XP_UCHAR* msg );
void (*m_util_informMove)( XW_UtilCtxt* uc, XP_S16 turn,
XWStreamCtxt* expl, XWStreamCtxt* words );
void (*m_util_informUndo)( XW_UtilCtxt* uc );
@ -146,6 +148,14 @@ typedef struct UtilVtable {
void (*m_util_remSelected)(XW_UtilCtxt* uc);
void (*m_util_timerSelected)(XW_UtilCtxt* uc, XP_Bool inDuplicateMode,
XP_Bool canPause);
void (*m_util_formatPauseHistory)( XW_UtilCtxt* uc, XWStreamCtxt* stream,
DupPauseType typ, XP_S16 turn,
XP_U32 secsPrev, XP_U32 secsCur,
const XP_UCHAR* msg );
#ifndef XWFEATURE_MINIWIN
void (*m_util_bonusSquareHeld)( XW_UtilCtxt* uc, XWBonusType bonus );
void (*m_util_playerScoreHeld)( XW_UtilCtxt* uc, XP_U16 player );
@ -229,6 +239,8 @@ struct XW_UtilCtxt {
# define util_turnChanged( uc, t )
#endif
#define util_notifyDupStatus(uc, h, m) \
(uc)->vtable->m_util_notifyDupStatus( (uc), (h), (m) )
#define util_informMove(uc,t,e,w) \
(uc)->vtable->m_util_informMove( (uc), (t), (e), (w))
#define util_informUndo(uc) \
@ -267,6 +279,13 @@ struct XW_UtilCtxt {
#define util_remSelected( uc ) \
(uc)->vtable->m_util_remSelected((uc))
#define util_timerSelected( uc, dm, cp ) \
(uc)->vtable->m_util_timerSelected((uc), (dm), (cp))
#define util_formatPauseHistory( uc, s, typ, turn, secsPrev, secsCur, msg ) \
(uc)->vtable->m_util_formatPauseHistory( (uc), (s), (typ), (turn), \
(secsPrev), (secsCur), (msg) )
#ifndef XWFEATURE_MINIWIN
# define util_bonusSquareHeld( uc, b ) \
(uc)->vtable->m_util_bonusSquareHeld( (uc), (b) )

Some files were not shown because too many files have changed in this diff Show more