Skip to content

feat(csa): CSA migration — USI/WS → CSA v1.2 TCP server - #1

Merged
tkgstrator merged 51 commits into
masterfrom
feat/csa-migration
Jun 17, 2026
Merged

tkgstrator merged 51 commits into
masterfrom
feat/csa-migration

Conversation

@tkgstrator

Copy link
Copy Markdown
Contributor

Summary

  • Replace USI-over-WebSocket transport with a CSA v1.2 TCP server on :4081
  • Implement full CSA protocol state machine (LOGIN → Game_Summary → START → per-move exchange → result)
  • Add Csa_Convert library (square/piece/move/SFEN conversion) with 85 pinned Python test vectors
  • Add Csa_Engine state machine, Server_CSA transport, Inject_Resign for %TORYO
  • Add Hook_GameStateStoreObserve for player info and move observation
  • Add binpatch build flavor for iOS 18 static-patch IPA path
  • Rewrite README and docs to reflect CSA wire contract
  • Rename Sources/Common submodule → Sources/Chinlan
  • Align Makefile and CI workflows with KiouForge conventions
  • Address Copilot review comments (atomic globals, %TORYO result, duplicate match-end block, usleep guard)

Test plan

  • CI builds all three flavors (JB / jailed / binpatch) clean
  • Sanity check: jailed and binpatch dylibs have no external hook-engine dependency
  • Connect a CSA engine (YaneuraOu or nc) to :4081 and verify LOGIN → Game_Summary → START → move exchange → result flow

🤖 Generated with Claude Code

tkgstrator and others added 30 commits June 15, 2026 22:22
…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.
- 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>
tkgstrator and others added 21 commits June 17, 2026 01:21
…-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>
Copilot AI review requested due to automatic review settings June 17, 2026 11:55
@tkgstrator
tkgstrator merged commit 8e992f7 into master Jun 17, 2026
2 of 10 checks passed

Copilot AI 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.

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_Summary generation.
  • 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 thread docs/csa_protocol.md
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 thread docs/csa_protocol.md
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 thread docs/csa_compatibility.md
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 thread recipes/__init__.py
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).
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.

2 participants