Repository navigation
feat(csa): CSA migration — USI/WS → CSA v1.2 TCP server - #1
Merged
Merged
Conversation
…ld (#1) Phase A + B + C of the migration plan in docs/plans/kiou_engine_bridge_binpatch.md. Behaviour-preserving on the jailbroken (MobileSubstrate) build; the binpatch build is wired up but not yet exercised — the dispatcher is a stub and Phase D (gating sweep, dispatcher wiring, README) follows in a separate PR. Phase A — defer post-orig reads onto the main queue - Hook_MatchModeObserve.m's DEFINE_START_HOOK now latches `self`, returns, and reads `_localPlayer` plus calls usi_engine_on_match_start / meta_emit_match_start inside dispatch_async(main, ...). - Hook_LowLevelObserve.m's hook_AdapterTryMakeMoveOut latches the adapter and game-controller caches, then defers sfenFromGameController / moveToUsi / usi_engine_on_move_observed to the main queue. - Neither hook calls `orig` directly anymore — MSHookFunction's trampoline still runs orig under the JB build, and the future cave will do it under the binpatch build. The same hook body now works under both. Phase B — binpatch dispatcher scaffolding - Internal.h gains enum kiou_bridge_hook_id (25 ids, 1:1 with cave sites), a dispatcher typedef, an extern slot, and a publish() prototype. No #if guard — the symbols are cheap and harmless in the JB build. - Sources/KiouEngineBridge/BinpatchDispatcher.m is a new file gated on #if KIOU_BINPATCH. Constructor publishes a stub dispatcher into g_kiou_bridge_hook_slot; the stub file_log's the hook_id so we can verify SLOT plumbing before wiring real hook bodies in Phase D. - Makefile loses the `jailed::` target and the JAILED/dobby branch, gains a `BINPATCH=1` branch (-DKIOU_BINPATCH=1 -Wl,-undefined,error, no -lsubstrate) and a `binpatch::` target that copies to packages/binpatch/ and runs otool -L for libsubstrate/libdobby leak detection. - vendor/dobby/ removed. Phase C — patch_macho recipe - recipes/kiouenginebridge.py: 25 cave entries (OnMatchEndAsync × 5, InitializeAsync × 5, OnPlayerMoveAsync × 5, OnMatchStart × 5, Adapter.TryMakeMove(Move,out), Online.UpdateAuthoritativeSnapshot, Online.HandleMoveResult, CPUStream.UpdateAuthoritativeSnapshot, GameOrchestrator.ActivateAsync) + 1 inline IsAfkEnabled patch (8-byte MOVZ W0,#0 + RET). - HOOK_SLOT_RVA = 0x8F90CC0 (reserve_hook_slot tail minus 16, validated in __DATA,__bss; placed 16 bytes ahead of KifExporter's 0x8F90CD0 so the two recipes can coexist on the same Mach-O). - CAVE_REGION = (0x826A000, 0x826C000) — the back half of __TEXT,__oslogstring's zero-fill, partitioned away from KifExporter's (0x8268024, 0x826C000) front-half allocation. 2100 B used of 8192 B. - recipes/kioukifexporter.py copied verbatim from KifExporter so this repo can drive both recipes through `shared/tools/patch_macho.py`. - All 26 entries (1 inline + 25 cave) verified with apply_patches(verify_only=True) against the clean Kiou-1.0.1 build 11 UnityFramework extracted from tmp/Kiou-1.0.1.ipa. Infra - pyproject.toml + uv.lock for the Python recipes package (lief, pytest, ruff). PYTHONPATH layered as `shared/` (the IPA-Patch/Shared submodule) + `.` so `from tools.encode import ...` resolves alongside `from recipes.kioukifexporter import ...`. - shared/ submodule added (IPA-Patch/Shared) — hosts tools/encode.py, tools/machoops.py, tools/caves.py, tools/patch_macho.py. - Sources/Common submodule added (IPA-Patch/Common) — the eventual successor to _shared/ for the Obj-C runtime headers. Not yet consumed by sources; migration happens in a later PR. - .gitattributes pins binary extensions for the IPA-Patch repo set. - .devcontainer/ provides Theos + Ghidra + ipsw + radare2 + jadx for the reverse-engineering loop. - docs/plans/kiou_engine_bridge_binpatch.md and docs/plans/kiou_kif_exporter_binpatch.md document Phase A..D and the patched-hook design that both projects share. Phase D will gate Meta_Emitter.m / Hook_GameStateStoreObserve.m / hook_GameCtrlTryMakeMove behind #if !KIOU_BINPATCH, wire the dispatcher stub to real hook bodies, and rewrite README for iOS 15.0 – 18.x coverage on the binpatch flavour. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rig calls (#2) Phase D of the migration plan in docs/plans/kiou_engine_bridge_binpatch.md. Same source tree, two flavours: default (JB / MobileSubstrate / iOS 15.0–16.5): full feature set including meta sidecar. BINPATCH=1 (Sideloadly / TrollStore / AltStore / ADP / iOS 18.x): meta sidecar dropped, hook bodies driven by the static cave + __DATA,__bss SLOT dispatcher (stub for now; Phase E wires it up). ### Hot fix from PR #1 The Phase A refactor in PR #1 removed the `orig()` calls from `DEFINE_START_HOOK × 5` and `hook_AdapterTryMakeMoveOut` on the assumption that MSHookFunction's trampoline runs the original synchronously around our hook. That was wrong: MSHookFunction replaces the call site, so the original body only runs when the hook explicitly calls `orig`. PR #1 therefore broke the JB build — `Adapter.TryMakeMove` never appended the post-move position to `_positionHistory`, and KIOU's board stopped advancing. This PR adds `KIOU_CALL_ORIG_VOID(orig, …)` and `KIOU_CALL_ORIG_RET(RET_T, orig, …)` macros to `Internal.h`. On the JB build they expand to `if (orig) orig(…)`; on the binpatch build they expand to a no-op (the cave already runs orig via the displaced prologue + `B orig+4`, calling orig from the hook would double-execute it). The two affected hooks now use these macros so a single hook body works under both build flavours. ### Gating sweep - `install_*_hook` bodies in Hook_AfkSuppress.m, Hook_GameOrchestratorObserve.m, Hook_LowLevelObserve.m, Hook_MatchModeObserve.m, Hook_OnlineObserve.m are wrapped in `#if !KIOU_BINPATCH`. The `LowLevelObserve` installer keeps the NativeFunction-style symbol-pointer resolves (`g_Position_ToSFEN` etc.) outside the gate — Inject_Move and the observation hooks both need them on both builds. - `Hook_GameStateStoreObserve.m` and `Meta_Emitter.m` are wrapped in `#if KIOU_BINPATCH { /* no-op stubs */ } #else { /* impl */ } #endif`. Stubs cover `install_GameStateStoreObserve_hook`, `meta_set_match_config`, `meta_emit_match_start`, `meta_emit_move`, `meta_emit_match_end`, `meta_on_player_info_set`, so every call site links cleanly under both flavours without per-call `#if` guards. - `Tweak.m`'s `installUnityHooks()` branches between the JB cascade and the binpatch path (`kiou_bridge_binpatch_publish()` + `install_LowLevelObserve_hook` for symbol resolves + `install_Inject_hook` + `usi_engine_install()`). - `_shared/kiou_hookengine.h` no longer pulls in `<substrate.h>` on binpatch (the legacy `KIOU_JAILED` Dobby shim is gone — removed in PR #1). It's now a thin `#if !KIOU_BINPATCH` gate around the Substrate import. ### docs/plans § 4 ("Why post-orig reads can move to the main queue") rewritten to correct the MSHookFunction misreading from PR #1's plan and to document the new `KIOU_CALL_ORIG_*` macros. § 6 Phase D step list updated. A new § 6 Phase E section spells out the remaining work for end-to-end binpatch coverage: 1. Move the cave-payload hook_id register from W2 to W6 because W2 collides with the third argument register on several Bridge sites (`OnPlayerMoveAsync`'s `ct`, `Adapter.TryMakeMove`'s `outMove`, `UpdateAuthoritativeSnapshot`'s `turn`). 2. Replace `BinpatchDispatcher.m`'s file_log stub with a real switch on `hook_id` dispatching to each `hook_<foo>` body. 3. Un-`static` the dispatched hook function bodies and add forward declarations to `Internal.h`. 4. Sideload + verify end-to-end against iOS 18. After Phase E lands, the README's `iOS 15.0–18.x` promise is real; today it's "structural only — cave + SLOT publish work, but the dispatcher is a stub." ### README (produced by the parallel Phase D agent earlier) - Platform badge bumped to `iOS 15.0–18.x`. - Compatibility table iOS row split into the JB rootless (15.0–16.5) and non-JB binpatch (15.0–18.x) flavours. - Install § "Sideloadly / AltStore / Apple Developer Program" renamed to "Non-JB (Sideloadly / TrollStore / AltStore / Apple Developer Program)" and rewritten around `make binpatch` + `shared/tools/build_patched_ipa.sh`. Notes that the binpatch build drops the meta sidecar. - Layout: the `vendor/dobby` line is gone (PR #1), `BinpatchDispatcher.m` is mentioned. - "Sibling tweaks" gains a line explaining KifExporter and Bridge share the static-binpatch toolchain and can layer the same IPA. - New "Plan documents" subsection linking `docs/plans/kiou_engine_bridge_binpatch.md` and `docs/plans/kiou_kif_exporter_binpatch.md`. ### tests `tests/test_recipes_kiouenginebridge.py` adds 8 structural smoke tests against `recipes/kiouenginebridge.py` — TARGET_BASENAME, DYLIB_PATH, HOOK_SLOT_RVA alignment + value, CAVE_REGION partition, inline patch count, cave patch count, cave budget, PLIST_KEYS empty. All 8 pass via `uv run pytest tests/`. ### .gitignore Added `assets/` alongside `tmp/` — the IPA + il2cpp dump.cs landed under `assets/` instead of `tmp/` for one run, so both root names should be ignored. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wraps `make binpatch` + `shared/tools/build_patched_ipa.sh` in a single
target so the non-jailbroken distribution flow becomes:
make ipa
The default input is `assets/Kiou-1.0.1.ipa` (under the .gitignored
`assets/` root so a clean KIOU IPA can sit alongside the source tree
without being checked in). Override with
`make ipa KIOU_CLEAN_IPA=/path/to/clean.ipa`.
The pipeline:
1. Builds packages/binpatch/KiouEngineBridge.dylib (BINPATCH=1).
2. Runs shared/tools/build_patched_ipa.sh which:
- extracts the clean IPA
- patches Payload/<app>.app/Frameworks/UnityFramework.framework/
UnityFramework using recipes.kiouenginebridge
- applies recipe PLIST_KEYS to Info.plist (none for Bridge today)
- drops KiouEngineBridge.dylib alongside the framework
- re-zips into packages/ipa/KiouEngineBridge-binpatch.ipa
Existing `make` (JB / libsubstrate) and `make binpatch` (non-JB dylib
only) targets unchanged — three flavours coexist:
- `make package install THEOS_DEVICE_IP=<ip>` — JB rootless deb deploy
- `make binpatch` — non-JB dylib alone
- `make ipa` — non-JB end-to-end IPA
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update Common and Shared to their latest upstream revisions and include the current local bridge/devcontainer adjustments. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Squashes five small follow-ups into a single refactor: - Replace the legacy _shared directory with the Sources/Common submodule for logging and il2cpp/hookengine headers. - Enable binpatch IPA plist updates: define the Bridge recipe plist keys consumed by the patched IPA pipeline and route binpatch logs to Documents through Common logging. - Pin the Bridge hook slot probe: track the probed __bss tail separately from Bridge's sibling hook slot so the IPA patcher can keep its layout drift check without warning on the intentional offset. - Add assets/.gitkeep so the empty assets/ directory is tracked. - Track .claude and logs directories via .gitkeep placeholders so a fresh clone has them ready; logs/ contents stay ignored, only the directory itself is tracked. Tests: make clean all; make BINPATCH=1 clean all; make ipa Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Squashed: see PR #6 for the full message and rationale.
Squashed: see PR #8 for the full message.
Squashed: see PR #7 for the full message and Copilot review reply.
Squashed: see PR #10.
- sync deployment.yaml with integration.yaml (submodules, ldid, toolchain, cache key) - add control-vs-tag version verification step - bump GitHub Actions to latest majors (checkout v6 / cache v5 / upload-artifact v7 / action-gh-release v3) - add scripts/pre-commit + make hooks (KiouEditor parity) - pre-commit verifies every staged recipe instead of a hard-coded one - add _SITES alias in recipes/kiouenginebridge.py to satisfy verify_sites contract - add Versioning section and developer-hooks docs to README
Binpatch injection on iOS 18 was failing in two ways: 1. orig_*OnPlayerMoveAsync was left NULL by the binpatch installer, so inject_pickRoute() rejected every mode as 'no_session'. 2. After resolving orig_* from the RVA table, those pointers landed on the patched 'B <cave>' instruction and the inject path re-entered the dispatcher (infinite recursion / crash, then a silent revert). This PR restores the F-trampoline (cherry-picked from ba1465f + 0259cf8): per-site cave-bypass entries published into g_inject_entry[] at constructor time, exposed via KIOU_BR_BINPATCH_ORIG_OR_BYPASS macros. - install_MatchModeObserve_hook is a no-op on binpatch so orig_* stays NULL and the bypass-entry path is taken. - Local Adapter.TryMakeMove in CALL_OPM now routes through KIOU_BR_BINPATCH_ADAPTER_CALLABLE() so the headless engine actually advances on binpatch (CPUStream's OPM body only forwards to the server and waits for HandleMoveResult; without the local apply the next snapshot reverts the board). - Drop an accidental double fn(self, move, NULL) call that fired the dispatcher twice per injection. - Startup log now includes build flavor and __DATE__ __TIME__ so a stray log can be matched to the exact dylib that wrote it. Verified end-to-end on KIOU 1.0.1 CPUStreamMode: injection lands, board holds across the next snapshot, opponent replies arrive normally.
… docs - Rename all public/exported C functions from snake_case to PascalCase (ObjC convention) across all KiouEngineBridge source files: hook_* -> Hook*, install_*_hook -> Install*Hook, kiou_ws_* -> KiouWs*, kiou_inject_* -> KiouInject*, usi_engine_* -> UsiEngine*, meta_* -> Meta*, sfenFromGameController -> SfenFromGameController, usiTextFromGameController -> UsiTextFromGameController - Update macro definitions in Hook_MatchModeObserve.m to generate PascalCase function names via MODE_PASCAL token concatenation - Static (file-internal) functions retain snake_case per C convention Protocol documentation overhaul: - Reframe KEB as a USI match server (analogous to Floodgate/shogi-server) rather than a USI GUI; KIOU owns board state and clocks - Replace JSON meta sidecar with extended USI line protocol: meta BEGIN_GAME/END_GAME block (match info, time control, player names) meta move +7g7f T10 (per-move, CSA-style: side + usi + time spent) BEGIN_RESULT/END_RESULT removed (gameover on USI channel is sufficient) - Add CSA protocol correspondence table to docs/usi-compatibility.md - Document go command detail: clock-aware (btime/wtime/byoyomi) vs movetime fallback; usinewgame policy (per match, not per WS session) - Add stop/quit inbound handling semantics - Add wrapper-mediated deployment section Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KiouWsServer* -> KebWsServer* KiouInjectDumpRecent -> KebInjectDumpRecent KiouBridgeBinpatchPublish -> KebBridgeBinpatchPublish Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KebWsServer* -> KEBWsServer* KebInjectDumpRecent -> KEBInjectDumpRecent KebBridgeBinpatchPublish -> KEBBridgeBinpatchPublish Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove BEGIN_GAME/END_GAME block delimiters; usinewgame now signals end of meta block naturally - Rename 'meta move' to standalone 'move' command (not meta, not USI) - meta lines are now flat key-value pairs before usinewgame - Update CSA correspondence table: protocol_version, game_id, name+/-, your_turn now mapped to meta lines; move notification mapped to 'move' - Update all example sessions and sequence diagrams accordingly Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow the same naming style as YaneuraOu extension options (USI_Hash, BookFile, etc.) for all meta subcommands: protocol_version -> ProtocolVersion game_id -> GameId mode -> Mode started_at -> StartedAt start_position -> StartPosition your_turn -> YourTurn name+/- -> Name+/- rank+/- -> Rank+/- rate+/- -> Rate+/- user_id+/- -> UserId+/- time_unit -> TimeUnit total_time -> TotalTime byoyomi -> Byoyomi increment -> Increment Update all example sessions and CSA correspondence tables. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 1: Csa_Convert (pure)
- Sources/KiouEngineBridge/Csa_Convert.{h,m}
- Square <-> CSA coordinate (square 60 <-> '77')
- PSC PieceType <-> CSA piece mnemonic (14 types: FU..RY)
- Move bits <-> CSA move text (+7776FU, +0077FU drop, +8822UM promote)
- SFEN -> CSA position block (P1..P9 + P+ / P- hand + side)
- il2cpp-free, Foundation-only — linkable into host-side tests
Task 2: USI deprecation
- Wrap Server_WebSocket.m and Usi_Engine.m in '#if 0' with DEPRECATED
headers and a pointer to the CSA migration plan
- Exclude both from the Theos build via Makefile find filter
- Add Csa_Stubs.m: no-op shims for KEBWsServer* / UsiEngine* so the
half-migrated hook call sites keep linking until Task 4-5 rewrites
them. Stubs will be deleted in Task 5.
Task 3: CSA TCP server
- Sources/KiouEngineBridge/Server_CSA.m
- LF-terminated UTF-8 lines, single-client TCP server on port 4081
- New-client-wins preempt, SO_KEEPALIVE for fast dead-peer detection
- KEBCsaServerStart / Push / Close / SetLineHandler public API in
Internal.h
- Mirrors Server_WebSocket.m's accept-queue + recv-queue split with
WS framing / handshake stripped out
- Tweak.m constructor now binds 4081 instead of 9527
All three build flavors (JB / JAILED=1 / BINPATCH=1) link clean. The
hooks still drive the no-op USI stubs; CSA wire-up against KIOU events
follows in Task 4-5.
Plan: docs/plans/kiou_engine_bridge_csa_migration.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 4: Csa_Engine.h/.m
- BOOT -> LOGIN -> AGREE_WAIT -> PLAYING -> GAME_OVER state machine
- Inbound: LOGIN (any creds OK), LOGOUT, AGREE, REJECT, %TORYO,
%KACHI, %CHUDAN, ±<from><to><piece>[,T<n>] move submissions
- Outbound: LOGIN reply, Game_Summary, START, #RESIGN/#WIN/#LOSE/#DRAW
- Engine-side moves: CSA -> USI translation -> inject_apply()
- KIOU-side moves: CsaEngineOnMoveObserved with snapshot-delta T value
Task 5: Csa_GameInfo.m
- CsaBuildGameSummary reads MatchConfig / PlayerInfo / TimeControlConfig
using the same offsets Meta_Emitter walked. KIOU_* extension lines
(Mode, StartPosition, Rank+/-, Rate+/-, UserId+/-, StartedAt) live
between Position and END Game_Summary so strict CSA parsers ignore
them.
- CsaBuildMatchResult emits #REASON + #OUTCOME pairs.
- Csa_Convert.{h,m} grows PscPieceTypeAtSquare so the engine driver can
recover the piece type sitting on a SFEN square (needed because the
Move bits' upper-16 piece-type encoding is still under RE).
- Hook_MatchModeObserve.m's Init/Start/End macros now feed Csa_GameInfo
alongside the legacy meta path (both fire each event; Meta_Emitter is
no-op'd via Csa_Stubs.m until full retirement in a follow-up commit).
- Hook_GameStateStoreObserve.m's NotifyPieceMoved forwards
(move, playerSide, sfen_after) into CsaEngineOnMoveObserved.
Task 6: Inject_Resign.m
- %TORYO -> GameOrchestrator.RequestSurrender (RVA 0x594A91C). Uses the
already-cached g_gameOrchestratorCache pointer; resolves the void
function pointer at first call and dispatches it onto the main queue.
- %KACHI logs but does not drive KIOU — no first-class declaration API
surfaced in dump.cs yet; the CSA session still emits #JISHOGI/#WIN
so the engine learns the outcome.
All three build flavors (JB / JAILED=1 / BINPATCH=1) link clean.
Plan: docs/plans/kiou_engine_bridge_csa_migration.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 8: tests/test_csa_convert_expectations.py
- 85 pinned test cases for the CSA conversion library
- Python port of Csa_Convert.{h,m} runs alongside the ObjC
implementation; any divergence is a regression
- Covers square/piece/move/SFEN-position conversion in both
directions, including malformed-input rejection paths
- Caught a real format bug in csa_lineFromSfenRank: empty cells
must render as ' * ' (three columns wide) so the row aligns,
with the trailing column-padding trimmed off the rightmost cell.
Fix shipped in Csa_Convert.m's csa_lineFromSfenRank.
Task 9: documentation pass
- git mv docs/bridge_protocol.md docs/archive/usi_bridge_protocol.md
- git mv docs/usi-compatibility.md docs/archive/usi_compatibility.md
- Archive headers point at the CSA replacement docs
- New docs/csa_protocol.md — wire contract, state machine,
Game_Summary schema, KIOU_* extension keys, example session
- New docs/csa_compatibility.md — table mapping every CSA v1.2
command onto KEB's behaviour (supported / partial / omitted)
- README rewritten: badges, intro paragraph, sequence diagram,
flow diagram, command table, install steps, Layout block all
point at the CSA TCP server on :4081 and document the historical
USI WebSocket sink as deprecated.
162 tests pass; all three build flavours (JB / JAILED=1 / BINPATCH=1)
link clean.
Plan: docs/plans/kiou_engine_bridge_csa_migration.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pped KIOU's match-start event fires its in-game CPU immediately; KEB has no way to pause KIOU until the connected engine has replied with AGREE. Sitting in AGREE_WAIT meant the CPU's first moves landed in NotifyPieceMoved → CsaEngineOnMoveObserved while the state was not yet PLAYING, so the per-move dispatch dropped them. Send START:<Game_ID> together with Game_Summary and roll straight into PLAYING. A later inbound AGREE from the engine in PLAYING is now logged as a no-op (csa_handle_agree's new PLAYING short-circuit) instead of treated as a state error. The AGREE_WAIT enum value is kept reserved for a future build flavour that pauses KIOU during the handshake. Caught on a live VsAI session: Game_Summary arrived but no moves until the user typed AGREE. With this fix the test client sees the CSA spec's START line right after Game_Summary and immediately gets +7776FU,T0 once the KIOU CPU thinks. docs/csa_protocol.md updated to call out the deviation explicitly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The snapshot path (Hook_OnlineObserve::UpdateAuthoritativeSnapshot) only
fires reliably on OnlinePvP — VsAI / Local sessions either leave the
g_latestBlackTimeSec floats at zero or update them on an unrelated cadence,
which produced T<n> values that were dramatically off (showing T7 for a
move that took ~40s).
GameStateStore (dump.cs:1422268) keeps the live clocks for every match
mode at:
+0x80 _blackTimeRemaining : ReactiveProperty<float>
+0x90 _whiteTimeRemaining : ReactiveProperty<float>
Live probing confirmed the boxed float lives at +0x20 inside the
ReactiveProperty<T> object. VsAI uses 86400.0f (24 h) as a 'no time
limit' sentinel on the CPU side; we filter that to -1 so it doesn't ship
as a wild T value.
HookNotifyPieceMoved now reads both clocks at observation time and
hands them to CsaEngineOnMoveObserved alongside the move bits and SFEN.
Csa_Engine caches the post-move remaining seconds per side and computes
the next move's T<n> as the integer delta — independent of the
snapshot-driven Hook_OnlineObserve path.
Also fixes the engine-seat calculation: the connected CSA engine stands
in for KIOU's local player (Your_Turn maps directly to lp), not the
opposite side. The 'move side mismatch' warning was firing on every
legitimate move because of the inversion; now the warning only fires on
genuine seat confusion and the log notes that the move is applied
anyway.
Verified on-device: 30-min VsAI session shows correct T values per move
(e.g. +2625FU,T37 for a 37s think) and the 'side mismatch' warnings are
gone for normal play.
All three build flavors link clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous structure picked path (a) (live KIOU clock) OR path (b)
(wall-clock fallback) but never both, so a side's very first move ate
the live-clock branch — last cache is NaN at that point so the delta
can't be computed — and the wall-clock branch was skipped because
'remain >= 0' had already taken path (a).
Real-world symptom: in VsAI, the human side's first move (e.g.
'-3334FU') landed with no ,T<n> suffix even though wall time was
~10 seconds.
Restructure: try path (a) first and update the live-clock cache as
before, then fall through to path (b) if T<n> is still -1. The two
paths now compose:
- subsequent moves with live clocks: path (a) wins (more accurate,
accounts for animation latency)
- first move on any side: path (a) updates cache but emits no T<n>,
path (b) supplies a wall-clock-based one
- VsAI CPU sentinel (86400s -> filtered to -1): path (a) skipped,
path (b) supplies T<n>
OnMatchStart seeds both sides' mach_absolute_time baselines so the
opening move on either colour can compute against 'match start ->
first move'.
Also adds <mach/mach_time.h> to Csa_Engine.m and a small
csa_machTicksToSec helper that converts mach ticks to integer seconds
via mach_timebase_info.
Builds clean on all three flavors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a CSA engine reconnected to KEB while a KIOU match was already in
progress, the engine landed in LOGIN state and had no way to discover
the current board / clock / Game_ID short of receiving Game_Summary —
which only fires on KIOU's OnMatchStart, not on TCP accept. The
result: every subsequent move from KIOU got dropped by
csa_handle_move_from_engine's PLAYING-state guard and the user's
inputs were rejected as 'move in state LOGIN — ignoring'.
CsaEngineOnTcpClientConnected now checks g_csaLocalPlayer at accept
time; if a match is mid-flight (lp 0 or 1), it auto-ships
Game_Summary and rolls straight into PLAYING via the existing auto-
START path. The engine doesn't need to send LOGIN to discover the
state.
csa_handle_login still accepts an explicit LOGIN from standard CSA
clients (Apery, technique, etc.) but skips re-sending Game_Summary
when we're already in PLAYING — no duplicate handshake noise.
Net behaviour:
- TCP-only client (csa_test_client, nc): auto Game_Summary + START
- Standard CSA engine sending LOGIN: same auto Game_Summary + START,
LOGIN reply still goes out for protocol compliance
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two related cases the static Move bits / CSA piece mnemonic couldn't
disambiguate were both reading the wrong thing:
(A) Drop emission. KIOU's Move bits don't carry a reliably-decoded
upper-16 piece encoding for drops yet — the bit-15 drop flag also
isn't always set on KIOU-side drops we observe. CsaTextFromMoveBits
therefore returned nil whenever the player dropped a piece, and the
CSA engine got no notification of the move.
(B) Promote bit on engine-submitted moves. CSA encodes both 'promote
while moving' and 'move the already-promoted piece' with the same
promoted mnemonic (TO/NY/NK/NG/UM/RY). MoveBitsFromCsaText flagged
promote=true unconditionally, which then made inject_buildMove read
USI '+' as a promoting move, look for an unpromoted piece on the
from-square, and fail with empty_from when the piece on the board
was already the promoted form. Symptom: the engine sent '-8786RY'
to slide a dragon back, KEB rejected it as 'empty_from'.
Both fixes lean on a small SFEN-snapshot cache (g_csaPrevSfen) the
engine maintains across observed moves:
- DropPieceTypeFromHandDelta walks the SFEN hand string and finds
the piece type whose count decreased by one. Lives in Csa_Convert
so the conversion side stays pure / testable.
- Csa_Engine cross-checks the bit-15 drop flag with the hand delta
every move; if either says 'drop,' we treat it as a drop and patch
the Move bits to the canonical drop form (drop=1, from=0).
- csa_handle_move_from_engine reads PscPieceTypeAtSquare(prev, from)
when MoveBitsFromCsaText says promote=true; if the from-square
already holds the promoted form, we clear the promote bit before
building the USI string for inject_apply. Csa_Convert is untouched
so the existing 85 pinned tests keep passing.
A nil-return failure path now logs prev/post SFEN, drop/promote flags,
and piece type so the next mismatch is debuggable from a single log
line. Match-end clears g_csaPrevSfen so the next match's first move
doesn't inherit stale hand counts.
All three build flavors link clean; 85 Csa_Convert tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KIOU's GameController silently bounces illegal moves back without
clearing inject_apply's intermediate state, leaving the on-engine view
desynced (the 'piece bounces back' symptom reported on-device while
testing pawn drops on occupied squares and dropping pawns on the
back rank).
Add lightweight legality checks that run against the cached pre-move
SFEN before the move reaches inject_apply, so the worst categories of
input are filtered out and the engine learns its move was rejected:
- ValidateCsaDrop catches:
* dropping onto an occupied square
* dropping a promoted piece type (caller sanity)
* pawn / lance lands on the deepest rank (dead end)
* knight lands on the two deepest ranks (dead end)
* nifu (two unpromoted pawns of the moving side on the same file)
- ValidateCsaMove catches:
* from-square empty or out of range
* the piece on from doesn't match the named CSA piece type
(allowing the named-promoted-form + promote-bit case for the
ordinary 'promoting move' line)
* capturing one's own piece
When a check fires we log the short reason, send '#ILLEGAL_MOVE' so
the connected CSA engine knows its submission was discarded, and skip
inject_apply entirely. The session stays in PLAYING so the engine can
submit a different move; no KIOU state change happens.
These checks deliberately stop short of replicating the full shogi
rule engine — KIOU is the authority on position-specific rules
(uchifuzume, double check, pinning, etc). They just catch the cheap
input categories KEB has observed leaving KIOU's state inconsistent.
All three build flavors link clean; 85 Csa_Convert tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two related issues that surfaced once the move-legality validators
were live:
(1) Promote-outside-enemy-camp acceptance.
An engine sending a move with the promote bit set but neither
from-rank nor to-rank in the enemy camp (e.g. mid-board +5958KI+
or a silver moving entirely within own territory) was passing
ValidateCsaMove, hitting inject_apply, and getting bounced by
KIOU's GameController. Adds three new checks:
* promote_already_promoted (from-square already holds the
promoted form — covers the case where the engine clones a
promoted-mnemonic line but sets the bit redundantly)
* promote_unpromotable (King and Gold can never promote)
* promote_outside_enemy_camp (Black needs from or to rank ≤ 3;
White needs from or to rank ≥ 7)
On rejection KEB emits #ILLEGAL_MOVE and keeps PLAYING so the
engine can retry.
(2) Stale g_csaPrevSfen after a CsaTextFromMoveBits failure.
CsaEngineOnMoveObserved bailed early when PscPieceTypeAtSquare
couldn't resolve a piece type at the destination square (drop
bit lies, hand-delta ambiguous, etc.) without advancing the
prev-SFEN cache. The next inject_apply then validated against
the board from BEFORE the unobservable move, letting otherwise
illegal follow-ups (notably a from-square the cache thought was
empty) sneak past from_piece_mismatch. Always copy sfenAfter
into g_csaPrevSfen before returning, even on the early-exit
path.
All three build flavors link clean; 85 Csa_Convert tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KEB's own ValidateCsaDrop / ValidateCsaMove catches the cheap categories
of illegal input before the move reaches inject_apply, but they
deliberately don't replicate KIOU's full rule engine — position-specific
rules (piece movement geometry, pinned pieces, occupied path on sliding
moves, uchifuzume, leaving own king in check, etc.) are still owned by
KIOU's GameController.
inject_apply already surfaces 'parse_fail' / 'empty_from' / 'dropfmt'
type rejections via ok=false, but Csa_Engine.m was silently dropping
them on the floor: the move never reached the engine's view of the
board, but neither did any signal that the move was rejected. The
engine kept submitting the same illegal move (or worse, assumed it
stuck and built a follow-up).
Send #ILLEGAL_MOVE whenever inject_apply returns ok=false, with the
inject-side err string preserved in the file log for postmortem. The
CSA spec lets the server emit #ILLEGAL_MOVE on the engine's submission
and keep the session alive, which is exactly what we do — the state
machine stays in PLAYING so the engine can try another move.
Together with the pre-inject validators this gives two layers of
defense:
layer 1 — KEB's cheap rules (drop on occupied, nifu, dead-end drop,
from/to piece mismatch, promote outside enemy camp, ...)
reject before inject_apply ever runs.
layer 2 — KIOU's GameController rejects anything else and KEB now
forwards that rejection as #ILLEGAL_MOVE.
A future revision should also propagate Adapter.TryMakeMove's tryOk
into inject_apply's ok (today the OPM path forces ok=true for
benign-failure reasons documented at Inject_Move.m:790), but that
involves disentangling the 'move already applied via HandleMoveResult'
case and is held back from this commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CsaEngineOnTcpClientConnected runs on the CSA accept queue (a serial GCD queue inside Server_CSA.m). The auto-renegotiate path it added called csa_send_game_summary, which eventually walks SfenFromGameController — and that helper dereferences il2cpp objects that are only valid on Unity's main thread. Touching them off-main crashes the il2cpp runtime instantly. The crash reproduces with: connect a CSA client mid-match, kill it, kill KIOU; bring KIOU back, resume the saved match; reconnect the client. The reconnect lands CsaEngineOnTcpClientConnected on the accept queue with g_csaLocalPlayer still set from the resumed match, which trips the auto-renegotiate branch and dies inside the il2cpp SFEN read. Move both auto-renegotiate sites (the TCP-accept path and the explicit-LOGIN path in csa_handle_login, which runs on the recv queue) onto dispatch_async(main_queue). Re-check g_csaState inside the block so a LOGOUT or a new match landing between dispatch and execution doesn't ship a stale Game_Summary. All three build flavors link clean; 85 Csa_Convert tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-safe) The previous fix used SfenFromGameController(g_gameCtrlCache) directly to give the validators an up-to-date board view that accounts for KIOU rolling back illegal moves. That helper dereferences il2cpp objects inline, but csa_handle_move_from_engine runs on the CSA recv queue, so the read happens off the Unity main thread and crashes the il2cpp runtime — exactly the same SfenFromGameController-off-main pathology the auto-renegotiate fix just resolved on the accept queue. Switch to inject_currentSfen(), which is the canonical safe path: Inject_Move.m wraps the GameController.GetUSIText call in a dispatch_sync onto the main queue, so callers on any GCD queue get a valid SFEN string back without violating il2cpp threading. Reproducer was: connect, send any move (e.g. +1837HI), KEB dies during the validator's pre-inject SFEN read. With this commit the validator still gets the post-rollback live board the previous fix intended, just through the queue-safe entry point. All three build flavors link clean; 85 Csa_Convert tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
csa_handle_move_from_engine runs on the CSA recv queue (Server_CSA.m's GCD serial recv queue). The body called inject_currentSfen (which delegates to inject_sfenFromCachedGameCtrl → Position.ToSFEN, a raw il2cpp method invocation) and then inject_apply, both of which dereference il2cpp objects that are only valid on Unity's main thread. inject_apply itself wraps every il2cpp call in dispatch_sync, so the later half of the path was safe; but the validator's live-SFEN probe (inject_currentSfen → Position.ToSFEN) was added without the same guard, so any move submitted while the validator was active crashed the runtime — same off-main pathology as the auto-renegotiate fix landed minutes ago. Restructure: parse the CSA line on the recv queue (just byte shuffling), then dispatch_async the entire validator + USI build + inject pipeline onto the main queue. A new csa_apply_engine_move holds the body, csa_handle_move_from_engine is now just a thin parser + queue hop. We re-check g_csaState inside the main-queue block so a LOGOUT / match_end that lands between the two queues doesn't ship a move into a dead session. All three build flavors link clean; 85 Csa_Convert tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A move (e.g. +1828HI) sent on a live session still crashes the runtime even after both the auto-renegotiate fix and the inject_currentSfen → main-queue refactor. The log line right before the crash is just '[CSA<] +1828HI'; no follow-up #ILLEGAL_MOVE, no inject_apply trace, no engine echo. The handler is dying somewhere between MoveBitsFromCsaText and the next file_log, but we can't see where. Sprinkle [CSA-ENG-DBG] markers through the whole path so the next device log will pin down which line dies: - just before / inside / after the dispatch_async hop - on entry to csa_apply_engine_move - after state recheck - after move-bits decode - after the promote-bit-clear branch - after building toUsi - around the validator call - just before inject_apply Also drop the inject_currentSfen / SfenFromGameController live read in the validator. That path was the earlier suspect, but moving the whole handler onto the main queue should have made it safe, and KIOU performs its own validation downstream — using the cached g_csaPrevSfen is enough to catch the cheap rejection categories KEB needs to flag. All three build flavors link clean; 85 Csa_Convert tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When csa_apply_engine_move dispatches to the main queue and then calls inject_apply, inject_apply's internal dispatch_sync(dispatch_get_main_ queue(), ...) calls block forever because you can't dispatch_sync onto your own queue. Add KIOU_RUN_ON_MAIN_SYNC(block) helper macro that detects [NSThread isMainThread] and inlines the block directly instead of wrapping it in dispatch_sync. All 4 dispatch_sync sites in inject_apply's hot path (lines 1044, 1062, 1132, 1167) now use this macro. Verified on device: +1828HI (飛車 1八→2八) previously crashed; now logs 'inject_apply ok=1' and 'csa_apply_engine_move returned cleanly'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add moveDirsForPiece(), squareOccupied(), and pieceCanReach() helpers to Csa_Convert.m. These implement a lightweight movement table for all 14 PSC piece types, matching YaneuraOu's per-piece direction logic: - Non-sliding pieces (FU, KE, GI, KI/TO/NY/NK/NG, OU): fixed (dFile, dRank) offsets relative to playerSide (Black fwd=rank-1, White fwd=+1) - Sliding pieces (KY, KA, HI): raycast along direction until board edge or blocking piece - Promoted sliders (UM=bishop+adjacent ortho, RY=rook+adjacent diag) ValidateCsaMove now calls pieceCanReach() before the to_own_piece check, returning "unreachable" when the piece cannot legally travel from fromSquare to toSquare given board occupancy. This fixes the +2716HI (飛車 2七→1六 斜め移動) false-OK observed on device. 85 unit tests pass. All 3 build flavors clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FU and KY cannot be left unpromoted on the back rank (rank 1 for Black, rank 9 for White). KE cannot be left unpromoted on ranks 1-2 (Black) or ranks 8-9 (White). ValidateCsaMove now returns "must_promote" for these cases, matching the bitboard engine's approach of excluding no-legal- follow-up moves at generation time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First-move validator was always skipping (sfen len=0) because g_csaPrevSfen starts nil and NotifyPieceMoved only fires after a move lands — leaving the board snapshot blind for the engine's very first move. CsaBuildGameSummary now accepts outStartSfen and writes the SFEN obtained from SfenFromGameController into it. csa_send_game_summary caches it as g_csaPrevSfen immediately after building the Game_Summary block, before sending START. The validator now has a real board snapshot for move 1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause: OnMatchStart's else branch called csa_send_game_summary in PLAYING state, racing with csa_handle_login's dispatch_async(main) and causing repeated PLAYING→LOGIN→PLAYING state flips. Fix: - OnMatchStart: only send Game_Summary in LOGIN or GAME_OVER state. In PLAYING the engine is already running; the LOGIN handler owns renegotiation when the engine reconnects. - csa_handle_login: send Game_Summary in both LOGIN and PLAYING states (the connect-time race may have already advanced state to PLAYING before this dispatch lands). - Removed connect-time auto-renegotiate dispatch from CsaEngineOnTcpClientConnected — LOGIN is the sole trigger, which eliminates the double-dispatch race entirely. Also fix csa_usi_bridge.py: - Guard move_from_csa None/0 return to avoid 'moves None' USI error - Handle mid-game BEGIN Game_Summary by restarting game loop Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
csa_set_state now logs (from <function>) so PLAYING->LOGIN flips can be traced to their source. Also simplify csa_usi_bridge prefetch handling via _buf prepend instead of passing prefetch lists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KEB now sends KIOU_Sfen:<sfen> in Game_Summary so the bridge can
initialize its local board to the correct position without parsing
the multi-line CSA position format (which caused segfaults via
cshogi.CSA.Parser.parse_str).
Bridge reads KIOU_Sfen and calls board.set_position('sfen <sfen>').
If SFEN matches startpos the bridge uses 'startpos' for the engine
position command; otherwise passes 'sfen <sfen> moves ...' so
mid-game reconnects start from the correct board state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
INJECT_ANIMATION_DELAY_SEC was 0.40s — each engine move blocked the main thread for ~450ms via usleep(), causing visible UI stutters during rapid YaneuraOu engine play. Reduce to 0.15s. Animation typically completes in ~0.3s but the actual board commit is the authoritative state; a shorter delay trades visual smoothness for responsiveness. Bridge side: add 0.5s sleep after sending our move to give KIOU's main thread breathing room between back-to-back injections. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Board state commit (TryMakeMove) and animation are independent. Animation plays regardless; no need to block the main thread waiting for it. Eliminates the ~150-450ms main-thread stall on each inject. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On reconnect to a mid-game position the side to move may be white (-). Csa_GameInfo.m now reads the SFEN from GameController and sets To_Move:- when the SFEN's side token is 'w'. Bridge also now derives current_turn from KIOU_Sfen rather than To_Move, so it correctly waits for the opponent on reconnect when the position has white to move. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…chdog iOS killed KIOU with 0x8badf00d (scene-create watchdog transgression) during auto-rematch — main thread was blocked ~20s. Root cause: when OnMatchStart fires during scene transition, csa_send_game_summary calls SfenFromGameController which blocks on il2cpp locks until the new scene finishes initializing. Defer Game_Summary by 500ms via dispatch_after so the scene-create phase completes first. The re-check of g_csaState in the deferred block ensures we don't send Game_Summary if a LOGOUT raced us. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove: Observation+scoped injection, What you get, How it works, Layout, Versioning, Sibling tweaks, Plan documents, Scope of use - Install: align with KiouForge (JB / TrollStore / Patched IPA sections) - Compatibility: match KiouForge table structure, update iOS 15–26 - CSA wire table: split Direction into KEB→client / client→KEB - Add CSA v1.2 compatibility summary + KIOU_* extensions table - Badge: platform iOS 15–26, drop authorized-testing-only badge - Logs: update [WS]/[USI] tags to [CSA]/[CSA-ENG] - .gitignore: exclude scripts/*.py, scripts/*.sh, docs/plans/, docs/archive/ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s, docs/archive Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makefile: - Extract project variables to top (TWEAK_NAME, TARGET_PROCESS, DECRYPTED_IPA, etc.) - Use $(TWEAK_NAME) throughout instead of hardcoded strings - Rename KIOU_CLEAN_IPA -> DECRYPTED_IPA for consistency - Drop -Wl,-undefined,error from non-binpatch jailed path - Trim verbose comments from hook engine section deployment.yaml: - actions/checkout v4 -> v5, actions/cache v4 -> v5 - Add LDID_SHA256 pin - Add 'Verify control version matches tag' step - Add binpatch build + sanity check steps - Ship binpatch dylib as release asset alongside jailed integration.yaml: - actions/checkout v5, actions/cache v5, actions/upload-artifact v4 - Add LDID_SHA256 pin - Add binpatch flavor to build matrix - Merge jailed/binpatch sanity check into single parameterized step Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BinpatchDispatcher.m / Internal.h: - Add KIOU_BR_HOOK_GSTATE_SET_BLACK_PLAYER_INFO, KIOU_BR_HOOK_GSTATE_SET_WHITE_PLAYER_INFO, KIOU_BR_HOOK_GSTATE_NOTIFY_PIECE_MOVED to the hook id enum - Add corresponding dispatch_one cases calling HookGState* bodies - Declare HookGState* in Internal.h for the dispatcher Hook_GameStateStoreObserve.m: - Refactor: move HookGState* bodies above the #if KIOU_BINPATCH split so both JB and binpatch compile the same observation logic - Binpatch path: installer is a no-op (cave wires the hooks); JB path: MSHookFunction installer unchanged, now calls HookGState* - Fixes binpatch builds where CsaEngineOnMoveObserved was never called (all CSA move notifications were silently dropped) - Also fixes CsaOnPlayerInfoSet missing on binpatch (player identity for Game_Summary Name+/Name- fields on Online matches) recipes/kiouenginebridge.py: - Add three cave sites for the new GameStateStore hooks (28 total; verified to fit within 0x826A000..0x826C000 with 5840 bytes spare) Remove legacy USI / WebSocket code (migration complete): - Delete Sources/KiouEngineBridge/Server_WebSocket.m (was #if 0) - Delete Sources/KiouEngineBridge/Usi_Engine.m (was #if 0) - Delete Sources/KiouEngineBridge/Csa_Stubs.m (transition no-ops) - Makefile: drop Server_WebSocket.m / Usi_Engine.m exclusion filter - Internal.h: remove UsiEngine*, KEBWsServer*, kiou_ws_text_handler_t, usi_state_t declarations; keep usi_match_result_t (used by CSA path); update file header comment to reflect CSA architecture integration.yaml: actions/upload-artifact v4 -> v7 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- .gitmodules: rename submodule Sources/Common → Sources/Chinlan, update URL to IPA-Patch/Chinlan.git - Sources/Common removed, Sources/Chinlan added (pinned to HEAD) - Makefile: update -I flag, logging.m path, and comment to Sources/Chinlan - Sources/KiouEngineBridge/**: - file_log → IPALog (all call sites) - logging_init → IPALoggingInit (Tweak.m) - recipes/kiouenginebridge.py: add missing KIOU_BR_HOOK_GSTATE_* entries to _HOOK_IDS (ids 25–27); also drop UsiEngineOnMatchStart/End and UsiEngineOnMoveObserved call sites left over from the USI removal Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Server_CSA.m: - Make g_clientFd / g_pendingSends atomic (_Atomic int / _Atomic uint32_t) so concurrent reads from the recv queue and writes from the accept queue are race-free without a serial gate. - Fix header comment: drop policy is "drop new line", not "drop oldest". Csa_Engine.m: - Fix %TORYO result: the CSA engine IS the local player, so %TORYO means the local seat surrenders → #LOSE (was incorrectly #WIN). Pass lp to InjectResign instead of the engine-opposite seat. - Fix %KACHI: InjectNyugyokuDeclaration should take the local seat (lp), not the engine-opposite. - CsaEngineOnMatchEnd: skip result block when state is already GAME_OVER (%TORYO / %KACHI / %CHUDAN already sent it) to prevent duplicate or contradictory #WIN/#LOSE lines. Inject_Move.m: - Guard usleep with [NSThread isMainThread] check so the delay is skipped when inject_apply is called from the main thread (CSA path), avoiding a potential UI block if INJECT_ANIMATION_DELAY_SEC is ever raised above 0. docs/csa_compatibility.md: - Correct AGREE / START rows to reflect that KEB sends START immediately after Game_Summary without waiting for AGREE (KIOU's CPU can't wait). - Correct %TORYO row: result is #LOSE (engine/local seat surrenders), not #WIN. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR migrates KiouEngineBridge’s engine transport from the legacy USI-over-WebSocket model to a CSA v1.2 TCP server (:4081), adding conversion utilities, a CSA protocol state machine, and binpatch build support to survive iOS 18+ CSM constraints.
Changes:
- Replaces the WS/USI bridge with a CSA v1.2 TCP transport + CSA engine driver/state machine, including resign/declaration handling and
Game_Summarygeneration. - Introduces CSA conversion utilities and Python-pinned regression vectors for coordinate/piece/move/SFEN→CSA position conversions.
- Adds binpatch flavor infrastructure (dispatcher, recipe/tooling adjustments), plus CI/Makefile/devcontainer/docs updates to match the new workflow.
Reviewed changes
Copilot reviewed 50 out of 56 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_recipes_kiouenginebridge.py | Adds smoke tests to assert recipe exports/constants match the migration plan. |
| tests/test_csa_convert_expectations.py | Adds Python reference implementation + pinned vectors for CSA conversion behavior. |
| tests/init.py | Test package marker (present in PR context). |
| Sources/KiouEngineBridge/Usi_Engine.m | Removes the deprecated USI-over-WebSocket engine driver. |
| Sources/KiouEngineBridge/Tweak.m | Switches startup to CSA server + CSA engine install; adds binpatch conditional wiring and logging changes. |
| Sources/KiouEngineBridge/Server_CSA.m | New TCP line-oriented CSA transport (single-client, accept/recv queues, backlog cap). |
| Sources/KiouEngineBridge/Meta_Emitter.m | Adds binpatch no-op stubs and updates logging + WS push call site naming. |
| Sources/KiouEngineBridge/Inject_Resign.m | Adds %TORYO/%KACHI resign/declaration injection hooks. |
| Sources/KiouEngineBridge/Hook_OnlineObserve.m | Adds authoritative clock caching; renames hook entrypoints; binpatch conditional installer behavior. |
| Sources/KiouEngineBridge/Hook_LowLevelObserve.m | Renames exported helpers; adapts adapter hook to binpatch orig-calling model and defers logging. |
| Sources/KiouEngineBridge/Hook_GameStateStoreObserve.m | Centralizes move observation at NotifyPieceMoved; forwards to CSA engine + meta; adds binpatch/JB split install. |
| Sources/KiouEngineBridge/Hook_GameOrchestratorObserve.m | Renames hook entrypoint; disables installer on binpatch (driven by cave/SLOT). |
| Sources/KiouEngineBridge/Hook_AfkSuppress.m | Renames hook entrypoint; disables installer on binpatch (inline patch via recipe). |
| Sources/KiouEngineBridge/Csa_GameInfo.m | Builds CSA Game_Summary and result blocks from KIOU state (MatchConfig/PlayerInfo/TimeControl). |
| Sources/KiouEngineBridge/Csa_Engine.h | Declares CSA server-side state machine integration surface. |
| Sources/KiouEngineBridge/Csa_Convert.h | Declares CSA conversion helpers (square/piece/move/position + validators). |
| Sources/KiouEngineBridge/BinpatchDispatcher.m | Adds binpatch dispatcher publishing into UnityFramework SLOT and per-hook dispatching. |
| scripts/pre-commit | Adds local-only recipe<->dump cross-check hook. |
| recipes/kioukifexporter.py | Adds KiouKifExporter binpatch recipe (Phase 1.5). |
| recipes/init.py | Documents recipe module contract for patch tooling. |
| README.md | Rewrites docs for CSA TCP server model + build flavors and operator flow. |
| pyproject.toml | Adds Python tooling config for recipe/tests (pytest + ruff) and dependencies. |
| Makefile | Refactors targets, adds binpatch + ipa flow, and aligns build variables/conventions. |
| logs/.gitkeep | Ensures logs directory is tracked while keeping contents ignored. |
| docs/csa_protocol.md | Adds CSA wire contract and protocol/state machine documentation. |
| docs/csa_compatibility.md | Adds CSA feature/field compatibility matrix for KIOU↔CSA mapping. |
| assets/.gitkeep | Keeps assets directory tracked for local-only artifacts. |
| .gitmodules | Adds/updates submodules (shared, Sources/Chinlan). |
| .gitignore | Expands ignores, including local artifacts and repo-specific conventions. |
| .github/workflows/integration.yaml | Updates CI to include binpatch build, submodules, and toolchain/ldid setup; adds commitlint job. |
| .github/workflows/deployment.yaml | Updates release workflow to build all flavors, stage assets, and use CHANGELOG-derived notes. |
| .github/dependabot.yml | Enables Dependabot for GitHub Actions and submodules. |
| .gitattributes | Adds normalization rules and binary patterns. |
| .devcontainer/theos/Dockerfile | Adds devcontainer image for Theos/tooling. |
| .devcontainer/postCreateCommand.sh | Adds uv sync step for devcontainer setup. |
| .devcontainer/postAttachCommand.sh | Adds git config/setup conveniences on attach. |
| .devcontainer/Dockerfile | Adds broader RE + build tool devcontainer image. |
| .devcontainer/devcontainer.json | Adds devcontainer configuration/features/extensions. |
| .devcontainer/devcontainer-lock.json | Pins devcontainer feature digests. |
| .devcontainer/compose.yaml | Defines devcontainer compose service and venv volume. |
| .commitlintrc.yaml | Adds commitlint configuration. |
| .claude/.gitkeep | Tracks Claude directory placeholder. |
| _shared/kiou_logging.m | Removes legacy shared logging implementation (moved to submodule). |
| _shared/kiou_logging.h | Removes legacy shared logging header (moved to submodule). |
| _shared/kiou_il2cpp.h | Removes legacy shared il2cpp helpers (moved to submodule). |
| _shared/kiou_hookengine.h | Removes legacy hookengine shim (moved to submodule). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+72
to
+79
| static void csa_set_keepalive(int fd) { | ||
| int on = 1; | ||
| (void)setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)); | ||
| int idle = 5, intvl = 3, count = 3; | ||
| (void)setsockopt(fd, IPPROTO_TCP, TCP_KEEPALIVE, &idle, sizeof(idle)); | ||
| (void)setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl)); | ||
| (void)setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count)); | ||
| } |
Comment on lines
+53
to
+55
| - **Backpressure.** Outbound lines go through a serial GCD queue. If the | ||
| in-flight backlog exceeds 128 lines the oldest pending line is dropped | ||
| and a `[CSA]` warning is logged. |
Comment on lines
+116
to
+119
| | `<sign><from><to><PIECE>[,T<n>]` | Parse via `Csa_Convert::MoveBitsFromCsaText` → translate to USI → call `inject_apply`. The `,T<n>` suffix is logged but otherwise unused (KIOU runs its own clock). | | ||
| | `%TORYO` | Send `#RESIGN` + `#WIN`, advance to GAME_OVER, schedule `GameOrchestrator.RequestSurrender` on the main thread (see `Inject_Resign.m`). | | ||
| | `%KACHI` | Send `#JISHOGI` + `#WIN`, advance to GAME_OVER. No corresponding KIOU declaration API has been surfaced yet — see Task 7 of the migration plan. | | ||
| | `%CHUDAN` | Send `#CHUDAN`, advance to GAME_OVER. KIOU is not notified. | |
Comment on lines
+40
to
+42
| | Local seat | `Your_Turn:+` / `Your_Turn:-` | Mapped from KIOU's `_localPlayer`. Open-seat modes default to `+`. | ✅ | | ||
| | First to move | `To_Move:+` | Hard-coded `+` (KIOU always starts on Black's move). | ⚠️ no handicap-aware override yet | | ||
| | Max moves | `Max_Moves:<n>` | — | ⛔ KIOU does not expose a hard move cap. | |
Comment on lines
+20
to
+23
| label)`` cave-routed redirects. | ||
|
|
||
| See ``tools.recipes.kioukifexporter`` for a worked example. | ||
| """ |
Comment on lines
+6
to
+21
| // CSA fires %TORYO when the connected engine resigns — meaning the local | ||
| // KIOU side wins the match. KIOU's own surrender entry point is | ||
| // GameOrchestrator.RequestSurrender (RVA 0x594A91C, void method, no args) | ||
| // which kicks off the same end-of-match flow the in-game "投了" button | ||
| // triggers. | ||
| // | ||
| // Because the CSA engine is "the other player" we can't selectively resign | ||
| // the engine's seat — RequestSurrender always surrenders the LOCAL player. | ||
| // That's fine for the most common case (CPU vs engine), but in OnlinePvP | ||
| // with the user as black and an engine ghosting on the server side it | ||
| // would surrender the wrong seat. Until the CSA path is exercised against | ||
| // every match mode we accept that limitation; CSA's engine resignation in | ||
| // VsAI maps cleanly to "the user wins by the engine's resignation," which | ||
| // is what RequestSurrender produces if the user is the loser (it then ends | ||
| // up with the right outcome from CSA's perspective because we swap WIN / | ||
| // LOSE before sending the result block). |
2 of 3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
:4081Csa_Convertlibrary (square/piece/move/SFEN conversion) with 85 pinned Python test vectorsCsa_Enginestate machine,Server_CSAtransport,Inject_Resignfor%TORYOHook_GameStateStoreObservefor player info and move observationbinpatchbuild flavor for iOS 18 static-patch IPA pathSources/Commonsubmodule →Sources/ChinlanTest plan
nc) to:4081and verify LOGIN → Game_Summary → START → move exchange → result flow🤖 Generated with Claude Code