Skip to content

fix: harden client cleanup, audio, and Pikafish lifecycle - #8

Merged
Barbatosishere merged 2 commits into
mainfrom
fix/client-lifecycle-audio-pikafish
Oct 13, 2026
Merged

Barbatosishere merged 2 commits into
mainfrom
fix/client-lifecycle-audio-pikafish

Conversation

@Barbatosishere

Copy link
Copy Markdown
Owner

Summary

This PR hardens client lifecycle and external-engine handling in the upstream game_console project.

Changes

  • Clear pending and accepted multiplayer invitations when the inviter disconnects or the local client logs out.
  • Keep the invitation cleanup subscriber client-only so dedicated servers do not load client classes.
  • Prevent Memory, Mouse Tunnel, and Piano Tiles from enqueueing duplicate UI sounds within one GUI tick.
  • Preserve the existing sound pitch and volume while using non-positional UI playback for Piano Tiles.
  • Terminate Pikafish and its descendant processes before closing streams, including forced shutdown and interrupted waits.
  • Give each Pikafish UCI handshake phase one monotonic deadline so continuous informational output cannot extend the timeout indefinitely.
  • Add regression coverage for process shutdown ordering, stream-close failures, interruption, EOF, informational output, and handshake responses.

Verification

  • ./gradlew test build --offline --no-daemon --no-configuration-cache --console=plain
  • 265 tests across 39 suites, 0 failures, 0 errors, 0 skipped.
  • Full accelerated client pressure: 66 checks, 0 failures.
  • Client invitation/lifecycle regression: 18 checks, 0 failures.
  • Real-tick audio pressure: 120 ticks and 120,000 requests, 0 failures, no pool exhaustion.
  • Real-time client cadence: 9 games, 1,800 ticks, 90.961 seconds, 0 failures.
  • Dedicated server startup and shutdown: mod/item registration verified, 40 ticks, normal save and exit.
  • Actual chatty-engine probe: missing uciok and readyok both terminate in about 15 seconds; normal handshake succeeds.

The accelerated pressure test still reports 1,203 vanilla button-click sound-pool warnings because it injects thousands of button key events into one real client tick. A separate real-tick audio run has no pool warnings; the captured stack identifies the remaining accelerated warnings as vanilla AbstractWidget.playDownSound calls.


static void playUiSound(Minecraft minecraft, SoundEvent sound, float pitch, float volume) {
if (minecraft == null || sound == null) return;
int tick = minecraft.gui.getGuiTicks();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Potential NPE if minecraft.gui is null

minecraft.gui.getGuiTicks() is called without checking if gui is non-null. In early startup or edge cases, minecraft.gui could be null. Add a null check: if (minecraft.gui == null) return; before accessing getGuiTicks().


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if (this.minecraft != null && this.minecraft.getSoundManager() != null) {
this.minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.NOTE_BLOCK_BASS, 0.5f));
}
GameSoundHelper.playUiSound(this.minecraft, SoundEvents.NOTE_BLOCK_BASS.value(), 0.5f, 0.25f);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Inconsistent .value() usage on SoundEvents

SoundEvents.NOTE_BLOCK_BASS.value() is used here, but SoundEvents.EXPERIENCE_ORB_PICKUP (line 435) and SoundEvents.VILLAGER_NO (line 439) are used without .value(). In PianoTilesGameScreen, all SoundEvents use .value() (e.g., SoundEvents.NOTE_BLOCK_HARP.value()). This inconsistency suggests a potential compile error or runtime issue if SoundEvents fields are Holder<SoundEvent>.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if (this.minecraft != null && this.minecraft.getSoundManager() != null) {
this.minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.EXPERIENCE_ORB_PICKUP, 1.0f + currentSequenceIndex * 0.1f));
}
GameSoundHelper.playUiSound(this.minecraft, SoundEvents.EXPERIENCE_ORB_PICKUP,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Missing .value() on SoundEvents

SoundEvents.EXPERIENCE_ORB_PICKUP and SoundEvents.VILLAGER_NO (lines 270, 274) are used without .value(), while PianoTilesGameScreen consistently uses .value() on all SoundEvents (e.g., SoundEvents.NOTE_BLOCK_HARP.value()). If SoundEvents fields are Holder<SoundEvent>, this will cause a type mismatch since GameSoundHelper.playUiSound expects SoundEvent.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

private void playSound(SoundEvent sound) {
if (minecraft != null && minecraft.level != null) {
minecraft.level.playLocalSound(0, 0, 0, sound, SoundSource.MASTER, 0.8f, 1.0f, false);
GameSoundHelper.playUiSound(minecraft, sound, 1.0f, 0.8f);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: GameSoundHelper called without gui null check

playSound checks minecraft != null && minecraft.level != null but GameSoundHelper.playUiSound accesses minecraft.gui.getGuiTicks() without verifying gui is non-null. If gui is null (early startup, headless), this will NPE. Consider adding && minecraft.gui != null to the condition.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

static void closeProcess(Process process, Closeable writer, Closeable reader) {
// 先终止进程解除读线程的阻塞,再关闭管道;反过来会在 BufferedReader.close() 卡住。
if (process != null && process.isAlive()) {
java.util.List<ProcessHandle> descendants = process.descendants().toList();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Redundant descendant destruction in closeProcess

Descendants are destroyed twice: first with destroy() (lines 259-261), then again with destroyForcibly() (lines 269-271). The second loop is redundant since destroyForcibly() on the parent process (line 267) should also terminate children. Consider removing the second descendant loop.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

*/
/** 客户端网络处理与断线清理,专服不会注册此订阅者。 */
@OnlyIn(Dist.CLIENT)
@EventBusSubscriber(modid = ModMain.MODID, value = Dist.CLIENT)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Changed from manual reflection to @EventBusSubscriber

Previously this class was explicitly NOT registered as an @EventBusSubscriber to avoid server-side class loading crashes, relying on ModNetworks reflection. Now it uses @EventBusSubscriber(modid = ModMain.MODID, value = Dist.CLIENT) with @OnlyIn(Dist.CLIENT). While this is the modern NeoForge pattern, verify that @OnlyIn stripping works correctly in your build pipeline to prevent the class from appearing in server JARs.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


final class GameSoundHelper {
private static final Set<ResourceLocation> PLAYED_THIS_TICK = new HashSet<>();
private static int lastTick = Integer.MIN_VALUE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: lastTick initialization could be clearer

lastTick = Integer.MIN_VALUE works but is a magic value. Consider using a boolean firstTick = true flag or lastTick = -1 with a comment explaining it's a sentinel for "not yet initialized."


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
src/main/java/com/wzz/game_console/client/screens/games/GameSoundHelper.java 14 Fields lastTick and hasTick should be explicitly private
Files Reviewed (3 files)
  • src/main/java/com/wzz/game_console/client/screens/games/GameSoundHelper.java - 1 issue
  • src/main/java/com/wzz/game_console/client/screens/games/MouseTunnelGameScreen.java - 0 issues
  • src/main/java/com/wzz/game_console/client/screens/games/chess/PikafishChessAI.java - 0 issues

Previous Issues Resolution

All 7 issues from the previous review have been addressed:

  • Fixed in this PR: NPE guards added in GameSoundHelper, lastTick initialization clarified, PikafishChessAI shutdown hook uses snapshot iteration, descendant cleanup checks isAlive()
  • Resolved by GameSoundHelper fix: PianoTilesGameScreen no longer needs explicit gui null check (callee handles it)
  • Obsolete due to API change: MemoryGameScreen and MouseTunnelGameScreen now correctly pass Holder<SoundEvent> to new overload

Fix these issues in Kilo Cloud

Previous Review Summary (commit 50d9d99)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 50d9d99)

Status: 8 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 5
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
src/main/java/com/wzz/game_console/client/screens/games/GameSoundHelper.java 19 Potential NPE if minecraft.gui is null
src/main/java/com/wzz/game_console/client/screens/games/MouseTunnelGameScreen.java 431 Inconsistent .value() usage on SoundEvents
src/main/java/com/wzz/game_console/client/screens/games/MemoryGameScreen.java 265 Missing .value() on SoundEvents
src/main/java/com/wzz/game_console/client/screens/games/PianoTilesGameScreen.java 1666 GameSoundHelper called without gui null check
src/main/java/com/wzz/game_console/network/ClientPayloadHandler.java 16 Changed from manual reflection to @EventBusSubscriber

SUGGESTION

File Line Issue
src/main/java/com/wzz/game_console/client/screens/games/chess/PikafishChessAI.java 258 Redundant descendant destruction in closeProcess
src/main/java/com/wzz/game_console/client/screens/games/GameSoundHelper.java 13 lastTick initialization could be clearer
src/main/java/com/wzz/game_console/client/screens/games/chess/PikafishChessAI.java ~101 Shutdown hook iterates LIVE_INSTANCES while shutdown() removes from it
Files Reviewed (8 files)
  • src/main/java/com/wzz/game_console/client/screens/MultiplayerLobbyScreen.java - 0 issues
  • src/main/java/com/wzz/game_console/client/screens/games/GameSoundHelper.java - 2 issues
  • src/main/java/com/wzz/game_console/client/screens/games/MemoryGameScreen.java - 1 issue
  • src/main/java/com/wzz/game_console/client/screens/games/MouseTunnelGameScreen.java - 1 issue
  • src/main/java/com/wzz/game_console/client/screens/games/PianoTilesGameScreen.java - 1 issue
  • src/main/java/com/wzz/game_console/client/screens/games/chess/PikafishChessAI.java - 2 issues
  • src/main/java/com/wzz/game_console/network/ClientPayloadHandler.java - 1 issue
  • src/test/java/com/wzz/game_console/client/screens/games/chess/PikafishChessAITest.java - 0 issues

Fix these issues in Kilo Cloud


Reviewed by free · Input: 212.7K · Output: 11.9K · Cached: 185.8K

@Barbatosishere

Copy link
Copy Markdown
Owner Author

Review follow-up for commit 3b500af:

  • Added null guards for minecraft.gui and SoundManager before reading GUI ticks or playing a sound.
  • Added a Holder<SoundEvent> overload and changed the note-block call to use it. EXPERIENCE_ORB_PICKUP and VILLAGER_NO are direct SoundEvent constants in Minecraft 1.21.1, so those calls intentionally do not use .value(); this is verified by the successful build.
  • Replaced the magic initial tick sentinel with an explicit hasTick state.
  • The shutdown hook now snapshots the concurrent live-instance set before calling shutdown().
  • Descendants are force-killed only when still alive. The second pass is intentional: the actual Windows .cmd wrapper probe showed that destroying the parent does not reliably terminate its child JVM.
  • The @EventBusSubscriber(value = Dist.CLIENT) registration remains intentional. Dedicated-server startup and save/shutdown regression passed, and the production build check passed after this commit.

Verification after the follow-up: compileJava and test passed using the existing NeoForge artifacts; 265 tests across 39 suites passed with 0 failures, errors, or skips. The GitHub Build & Verify check for this PR is also successful.


final class GameSoundHelper {
private static final Set<ResourceLocation> PLAYED_THIS_TICK = new HashSet<>();
private static int lastTick;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Fields lastTick and hasTick should be explicitly private

These static fields in a final class have package-private (default) access. Since they are only used internally within the class, they should be explicitly marked private for encapsulation.

Suggested change
private static int lastTick;
private static int lastTick;
private static boolean hasTick;

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@Barbatosishere
Barbatosishere merged commit 4de66cf into main Oct 13, 2026
2 checks passed
@Barbatosishere
Barbatosishere deleted the fix/client-lifecycle-audio-pikafish branch October 16, 2026 04:25
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