Skip to content

fix(interact): widen click dedup window to stop cross-tick double-firing#118

Merged
Arbousier1 merged 1 commit into
devfrom
codex/fix-click-dedup-cross-tick
Jul 21, 2026
Merged

fix(interact): widen click dedup window to stop cross-tick double-firing#118
Arbousier1 merged 1 commit into
devfrom
codex/fix-click-dedup-cross-tick

Conversation

@Arbousier1

Copy link
Copy Markdown
Collaborator

Problem

Players observed that a single physical click on any ray-based table control (join seat, hand tile, ready toggle, viewer commands) was being processed twice. For hand tiles this was especially destructive because the click toggle is select → discard, so one press discarded immediately.

Root cause

PlayerUseUnknownEntityEvent (fired by the Sparrow Heart client-only click proxies) does not implement Cancellable, so onClientInteractionProxy cannot suppress the event. Paper then fires a follow-up PlayerInteractEvent for the same physical click, which onFlatDisplayInteract processes again. The previous dedup window was 40ms, which is shorter than one server tick (50ms); when the two events landed on different ticks the second one slipped through and the action ran twice. The right-click + ARM_SWING pairing has the same cross-tick failure mode.

Fix

Raise the dedup windows in TableEventCoordinator and MahjongTableManager from 40ms to 150ms (3 ticks) so the follow-up event is always caught:

  • \TableEventCoordinator.DUPLICATE_DISPLAY_ACTION_WINDOW_NANOS\ — first dedup layer, covers all action types
  • \MahjongTableManager.DUPLICATE_DISPLAY_ACTION_WINDOW_NANOS\ — second dedup layer
  • \MahjongTableManager.DUPLICATE_HAND_TILE_CLICK_WINDOW_NANOS\ — third dedup layer, hand-tile specific

The fix is uniform across all \DisplayClickAction\ types because \sameDisplayAction\ compares \�ctionType + tableId + ownerId + tileIndex + seatWind + command, so JOIN_SEAT / TOGGLE_READY / PLAYER_COMMAND benefit from the same window.

Safety

  • Sichuan exchange phase already bypasses hand-tile dedup (!session.isSichuanExchangePhase(ownerId)), so rapid exchange clicks are unaffected.
  • 150ms is well below human double-click perception for distinct actions; legitimate consecutive clicks on different tiles / seats remain distinguishable because \sameDisplayAction\ requires field-level equality.
  • No behavior change to game rules — only the dedup timing window moves.

Verification

  • \compileJava\ passes.
  • \TableEventCoordinatorFlatInteractionTest\ passes, including:

    • ightClickAndArmSwingBothCancelButExecuteTheSameActionOnlyOnce\ (existing dedup guarantee)
    • \ownedClientProxyUsesTheSameExactRayActionAndSwingsForRightClick\ (proxy path)
  • \SessionHandSelectionCoordinatorTest\ passes (select → discard toggle semantics preserved).
  • \MahjongTableManagerTest, \TableEventCoordinatorDisconnectTest, \TableEventCoordinatorSeatAttendanceTest\ pass.

This is a behavior-preserving bug fix; no AB gate needed.

PlayerUseUnknownEntityEvent is not Cancellable, so Paper keeps firing a follow-up PlayerInteractEvent for the same physical click. The previous 40ms dedup window is shorter than one server tick (50ms), so when the two events land on different ticks the duplicate slips through and the hand-tile toggle (select then discard) triggers on a single click.

Raise the dedup windows in TableEventCoordinator and MahjongTableManager to 150ms (3 ticks) so the follow-up event is always caught. Sichuan exchange phase already bypasses hand-tile dedup, so rapid exchange clicks are unaffected.
@Arbousier1
Arbousier1 merged commit 02eb8bf into dev Jul 21, 2026
9 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 082f1adcb6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 46 to 47

public final class MahjongTableManager implements Listener {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve fast same-tile discard clicks

With these windows at 150ms, a legitimate quick second click on the same hand tile is treated as a duplicate before it reaches the normal select→discard path in SessionHandSelectionCoordinator.clickHandTile: both clicks have the same table/owner/tileIndex, so clickTile returns as if handled and never calls session.clickHandTile to discard. This only affects players who intentionally double-click the selected tile within 150ms, but that is a plausible way to discard; the proxy follow-up needs to be filtered without debouncing deliberate second physical clicks for this long (and the matching display-action window in TableEventCoordinator has the same effect).

Useful? React with 👍 / 👎.

Comment on lines +49 to +50
private static final long DUPLICATE_DISPLAY_ACTION_WINDOW_NANOS = 150_000_000L;
private static final long DUPLICATE_HAND_TILE_CLICK_WINDOW_NANOS = 150_000_000L;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve fast same-tile discard clicks

With these windows at 150ms, a legitimate quick second click on the same hand tile is treated as a duplicate before it reaches the normal select→discard path in SessionHandSelectionCoordinator.clickHandTile: both clicks have the same table/owner/tileIndex, so clickTile returns as if handled and never calls session.clickHandTile to discard. This only affects players who intentionally double-click the selected tile within 150ms, but that is a plausible way to discard; the proxy follow-up needs to be filtered without debouncing deliberate second physical clicks for this long (and the matching display-action window in TableEventCoordinator has the same effect).

Useful? React with 👍 / 👎.


final class TableEventCoordinator implements Listener {
private static final long DUPLICATE_DISPLAY_ACTION_WINDOW_NANOS = 40_000_000L;
private static final long DUPLICATE_DISPLAY_ACTION_WINDOW_NANOS = 150_000_000L;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate by tick/source instead of wall-clock time

If a server tick stalls for more than 150ms, the proxy event and its follow-up PlayerInteractEvent/PlayerAnimationEvent can still land on consecutive ticks but exceed this wall-clock window, so isDuplicateDisplayAction returns false and the same physical click is processed twice. Since the duplicate being fixed is explicitly a cross-tick event pair, this should key off the event source/tick (or another deterministic click marker) rather than assuming three normal 50ms ticks is an upper bound.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant