From 76b9c5a0a89984abe83e59703a9cb02e0a9d73a6 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Wed, 1 Jul 2026 16:33:58 +0000 Subject: [PATCH 1/8] chore(submodule): bump KIOU-Hook to 2766e14 (master tip) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KiouForge's develop was referencing be4cc6a on an unmerged branch that has since been deleted along with the observer-slot fix landing on master (KIOU-Hook#9). Bump the pointer to the merged commit (2766e14 fix(slots): move HOOK_SLOT_RVA from __bss to __common) so the reference lives on a durable ref instead of a stray branch tip. No source or behaviour change here — the actual observer-slot fix already shipped in commit 76832ad (KiouForge#18); this only stabilises the submodule SHA. Co-Authored-By: Claude Opus 4.7 --- vendor/KIOU-Hook | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/KIOU-Hook b/vendor/KIOU-Hook index be4cc6a..2766e14 160000 --- a/vendor/KIOU-Hook +++ b/vendor/KIOU-Hook @@ -1 +1 @@ -Subproject commit be4cc6ae2a250687bf14d5622181d6cebc6a90a0 +Subproject commit 2766e1455d1521fa7e10c5fd0d49a731ed63ae77 From 0e1094019f1c712e013b75ad2222ab0cdfc49a67 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Wed, 1 Jul 2026 16:36:48 +0000 Subject: [PATCH 2/8] chore(submodule): bump KIOU-Hook to 0b6814a Co-Authored-By: Claude Opus 4.8 (1M context) --- vendor/KIOU-Hook | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/KIOU-Hook b/vendor/KIOU-Hook index 2766e14..0b6814a 160000 --- a/vendor/KIOU-Hook +++ b/vendor/KIOU-Hook @@ -1 +1 @@ -Subproject commit 2766e1455d1521fa7e10c5fd0d49a731ed63ae77 +Subproject commit 0b6814a0056bd4583e7c4418d4b8b90698058287 From a6ba15a2b5e5537da35958dcca0a4926f2b9c226 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Wed, 1 Jul 2026 16:45:49 +0000 Subject: [PATCH 3/8] chore(submodule): bump KIOU-Hook to 20271b3 Co-Authored-By: Claude Opus 4.8 (1M context) --- vendor/KIOU-Hook | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/KIOU-Hook b/vendor/KIOU-Hook index 0b6814a..20271b3 160000 --- a/vendor/KIOU-Hook +++ b/vendor/KIOU-Hook @@ -1 +1 @@ -Subproject commit 0b6814a0056bd4583e7c4418d4b8b90698058287 +Subproject commit 20271b3567c1cc6cccc1e95d36da2c1a11cbb61d From 5c7f4d7460123faefc8ae9f780c563288d224316 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Wed, 1 Jul 2026 16:48:23 +0000 Subject: [PATCH 4/8] chore(submodule): bump KIOU-Hook and use afk disable installer Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile | 1 - Sources/KiouForge/Tweak.m | 2 +- vendor/KIOU-Hook | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index b74fcb3..10cd2eb 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,6 @@ $(TWEAK_NAME)_FILES += vendor/KIOU-Hook/KIOUHook.m $(TWEAK_NAME)_FILES += vendor/KIOU-Hook/Account/Persistence.m $(TWEAK_NAME)_FILES += vendor/KIOU-Hook/Hook/AccountObserve.m $(TWEAK_NAME)_FILES += vendor/KIOU-Hook/Hook/GrpcLogging.m -$(TWEAK_NAME)_FILES += vendor/KIOU-Hook/Hook/AfkSuppress.m BUILD_COMMIT ?= $(shell git rev-parse --short=7 HEAD 2>/dev/null || echo unknown) diff --git a/Sources/KiouForge/Tweak.m b/Sources/KiouForge/Tweak.m index e2a8f90..c3e1fd0 100644 --- a/Sources/KiouForge/Tweak.m +++ b/Sources/KiouForge/Tweak.m @@ -55,7 +55,7 @@ static void installUnityHooks(uintptr_t unityBase, const char *unityName) { KIOUInstallKifuObserveHook(unityBase); KIOUInstallAccountObserveHook(unityBase); KIOUInstallGrpcLoggingHook(unityBase); - KIOUInstallAfkSuppressHook(unityBase); + KIOUAfkDisableAlwaysFalseInstall(unityBase); g_unityHooked = YES; IPALog(@"=== KiouForge: all hooks installed ==="); diff --git a/vendor/KIOU-Hook b/vendor/KIOU-Hook index 20271b3..bdcea4e 160000 --- a/vendor/KIOU-Hook +++ b/vendor/KIOU-Hook @@ -1 +1 @@ -Subproject commit 20271b3567c1cc6cccc1e95d36da2c1a11cbb61d +Subproject commit bdcea4eb2523926e4a653b9a6d7b1d4706afb20f From a28822a6d3c45a527a77f8392a2153d1ae9bd20d Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Wed, 1 Jul 2026 16:49:36 +0000 Subject: [PATCH 5/8] chore(submodule): bump KIOU-Hook to a60df60 (installer rename) Picks up the KIOU-Hook rename from KIOUAfkDisableAlwaysFalseInstall to KIOUInstallAfkSuppressHook so the AFK installer matches the rest of the KIOUInstall*Hook API surface. Tweak.m updated to call the new name. Co-Authored-By: Claude Opus 4.7 --- Sources/KiouForge/Tweak.m | 2 +- vendor/KIOU-Hook | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/KiouForge/Tweak.m b/Sources/KiouForge/Tweak.m index c3e1fd0..e2a8f90 100644 --- a/Sources/KiouForge/Tweak.m +++ b/Sources/KiouForge/Tweak.m @@ -55,7 +55,7 @@ static void installUnityHooks(uintptr_t unityBase, const char *unityName) { KIOUInstallKifuObserveHook(unityBase); KIOUInstallAccountObserveHook(unityBase); KIOUInstallGrpcLoggingHook(unityBase); - KIOUAfkDisableAlwaysFalseInstall(unityBase); + KIOUInstallAfkSuppressHook(unityBase); g_unityHooked = YES; IPALog(@"=== KiouForge: all hooks installed ==="); diff --git a/vendor/KIOU-Hook b/vendor/KIOU-Hook index bdcea4e..a60df60 160000 --- a/vendor/KIOU-Hook +++ b/vendor/KIOU-Hook @@ -1 +1 @@ -Subproject commit bdcea4eb2523926e4a653b9a6d7b1d4706afb20f +Subproject commit a60df609b2d282dae902130328eb9bafe41379b6 From 38e1f66b5dc99798e1f91c918d91355a3ae3c07a Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Wed, 1 Jul 2026 16:52:39 +0000 Subject: [PATCH 6/8] chore(submodule): bump KIOU-Hook to e51f508 Co-Authored-By: Claude Opus 4.8 (1M context) --- vendor/KIOU-Hook | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/KIOU-Hook b/vendor/KIOU-Hook index a60df60..e51f508 160000 --- a/vendor/KIOU-Hook +++ b/vendor/KIOU-Hook @@ -1 +1 @@ -Subproject commit a60df609b2d282dae902130328eb9bafe41379b6 +Subproject commit e51f5083482bee9fb04a7793cbf3a8ee796f4cc6 From 8003463dfdf3cb82eb1df18c89e3b79b0157ad12 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Wed, 1 Jul 2026 16:54:30 +0000 Subject: [PATCH 7/8] chore(submodule): bump KIOU-Hook and allow KiouForge hook ids Pass the KiouForge hook allow-list into IPA packaging so KiouEditor catalog rows are filtered out before cave allocation. Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile | 24 ++++++++++++++++++++++++ vendor/KIOU-Hook | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 10cd2eb..26d8be9 100644 --- a/Makefile +++ b/Makefile @@ -124,6 +124,29 @@ IPA_DYLIB := $(CURDIR)/packages/chinlan/$(TWEAK_NAME).dylib IPA_OUT := $(CURDIR)/packages/ipa/$(basename $(notdir $(DECRYPTED_IPA)))-patched.ipa +# KIOU-Hook site allow-list — KiouForge only ships 17 of the 34 catalog +# rows. Passing this to `recipes/__init__.py` (via env) filters out the +# KiouEditor entries whose caves would otherwise collide with the +# __oslogstring fragment past 0x826FFF8. +KIOU_HOOK_ID_ALLOW := \ + KIOU_HOOK_ID_SET_TARGET_FRAMERATE,\ + KIOU_HOOK_ID_NSS_SETHASHSIZE,\ + KIOU_HOOK_ID_NSS_SETSKILLEVEL,\ + KIOU_HOOK_ID_NSS_SEARCHFULL,\ + KIOU_HOOK_ID_ACCOUNT_EXISTS,\ + KIOU_HOOK_ID_LOGIN_ARGS_CREATE,\ + KIOU_HOOK_ID_REGISTER_USER_ARGS_CREATE,\ + KIOU_HOOK_ID_RUN_LOGIN_SEQ_MOVENEXT,\ + KIOU_HOOK_ID_GET_SELF_PROFILE_MOVENEXT,\ + KIOU_HOOK_ID_HTTPMSGINVOKER_SEND_ASYNC,\ + KIOU_HOOK_ID_KIFU_AI_END,\ + KIOU_HOOK_ID_KIFU_CPUSTREAM_END,\ + KIOU_HOOK_ID_KIFU_LOCAL_END,\ + KIOU_HOOK_ID_KIFU_ONLINE_END,\ + KIOU_HOOK_ID_KIFU_REPLAY_END,\ + KIOU_HOOK_ID_HEADER_PROVIDER_SET_OR_UPDATE_HEADER,\ + KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK + ipa:: chinlan @echo "==> assembling patched IPA from $(DECRYPTED_IPA) (v$(TARGET_VERSION))" @if [ ! -f "$(DECRYPTED_IPA)" ]; then \ @@ -132,6 +155,7 @@ ipa:: chinlan exit 1; \ fi @TARGET_VERSION="$(TARGET_VERSION)" \ + KIOU_HOOK_ID_ALLOW="$(strip $(KIOU_HOOK_ID_ALLOW))" \ PYTHONPATH="$(KIOU_HOOK_DIR):$$PYTHONPATH" \ ./shared/tools/build_patched_ipa.sh \ --recipe "$(IPA_RECIPE)" \ diff --git a/vendor/KIOU-Hook b/vendor/KIOU-Hook index e51f508..36a3ad1 160000 --- a/vendor/KIOU-Hook +++ b/vendor/KIOU-Hook @@ -1 +1 @@ -Subproject commit e51f5083482bee9fb04a7793cbf3a8ee796f4cc6 +Subproject commit 36a3ad15d5f67cdcc3b68e7374049771c8a960a7 From 0c0ed04df935ae30ae67edbb9e81d6b46b64e6a3 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Wed, 1 Jul 2026 17:03:36 +0000 Subject: [PATCH 8/8] docs: plan kifu sqlite storage and in-app viewer Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/plans/kifu-sqlite-storage.md | 283 ++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 docs/plans/kifu-sqlite-storage.md diff --git a/docs/plans/kifu-sqlite-storage.md b/docs/plans/kifu-sqlite-storage.md new file mode 100644 index 0000000..3b73892 --- /dev/null +++ b/docs/plans/kifu-sqlite-storage.md @@ -0,0 +1,283 @@ +# Kifu SQLite storage + in-app viewer + +Move .kif output away from loose files under `Documents/KiouForge/` to a +single SQLite database plus an in-app viewer sheet reachable from the +existing right-edge-swipe settings surface. + +## Motivation + +Current shape (`Sources/KiouForge/Kif/Writer.m::KIOUKifWriterEmit`): + +- On resign / match end, generate a KIF text via `KIOUKifTextFromGameController` + and write it as UTF-8 to `Documents/KiouForge/__vs_.kif`. +- Filenames encode metadata so the Files app view is somewhat browsable, + but the naming scheme is long, non-searchable, and locale-fragile + (Japanese names inside filenames survive APFS but confuse some + external viewers). + +Pain points: + +- No in-app browsing. Users must open Files, dig into the tweak's + Documents directory, and pick a file with a long name. +- No filtering by date / mode / opponent. +- No delete UI — pruning old kifu is a manual Files.app job. +- No share / export UI — moving a kifu to another shogi viewer app + needs Files.app copy dance. +- Filename length keeps growing as we add match metadata. + +Target shape: + +- Store the KIF blob plus metadata in `Documents/KiouForge/kifu.sqlite`. +- Add a "棋譜一覧" entry to the settings sheet that opens a list view + backed by the database. +- Detail view shows the KIF text with a share sheet handoff (so + PiyoShogi / Shogi Browser Q can still consume it). + +## Scope + +### In scope + +- SQLite schema + `KifuStorage` wrapper that owns opening, migrations, + insert-on-match-end, list, fetch-by-id, delete. +- `KifuListSheet` view controller (UITableView) + `KifuDetailSheet` + (UITextView + share button). +- One entry in the existing settings sheet routing into + `KifuListSheet`. +- Replace the file write in `KIOUKifWriterEmit` with a + `KifuStorage` insert. +- Migration: on first launch after upgrade, scan the existing + `Documents/KiouForge/*.kif`, insert each into the DB, delete the + source file. Guard with a "migrated" flag so it only runs once. + +### Out of scope (deferred) + +- Cloud sync / iCloud kifu library. +- Multi-user separation (one DB per KIOU account). +- Search over move sequences. +- Editing the KIF blob in-app. + +## Design + +### Storage layer — `Sources/KiouForge/Storage/KifuStorage.{h,m}` + +Uses the iOS-provided `sqlite3` C API — link with `-lsqlite3` +(add to Makefile `TWEAK_NAME_LDFLAGS`). No FMDB / GRDB / external +dependency; keeps the tweak self-contained. + +Public API: + +```objc +// Open/close (singleton, opens on first use, closes at dylib unload). +sqlite3 *KifuStorageDatabase(void); + +// Insert a game record. Returns 0 on success, sqlite errcode otherwise. +// Caller keeps ownership of NSStrings; the wrapper copies as needed. +int KifuStorageInsert(NSString *modeName, + NSString *blackName, + NSString *whiteName, + NSString *startposSeg, + NSString *kifText, + NSDate *playedAt); + +// Enumerate all games most-recent-first. Rows are lightweight +// dictionaries: id, played_at (NSDate), mode, black, white, startpos. +// KIF text is intentionally excluded — fetched lazily by detail view. +NSArray *KifuStorageList(NSUInteger limit, NSUInteger offset); + +// Fetch a single row including the KIF blob for the detail view. +NSDictionary *KifuStorageFetch(int64_t rowId); + +// Delete a row. +BOOL KifuStorageDelete(int64_t rowId); +``` + +Schema (SQL run at `KifuStorageDatabase` open time): + +```sql +CREATE TABLE IF NOT EXISTS games ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + played_at TEXT NOT NULL, -- ISO 8601 UTC + mode TEXT NOT NULL, + black TEXT NOT NULL, + white TEXT NOT NULL, + startpos TEXT NOT NULL, -- "startpos" | "sfen-" | "unknown" + kif_text TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) +); + +CREATE INDEX IF NOT EXISTS games_played_at_idx ON games(played_at DESC); +``` + +`kif_text` stays UTF-8 (the file version already is). No blob column — +text keeps the schema greppable via `sqlite3` CLI for debugging. + +Migration table for future schema bumps: + +```sql +CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT +); +``` + +Initial insert: `meta('schema_version', '1')`. Migrations bump this and +apply ALTER statements in sequence. + +Threading: everything runs on the caller's thread. Match-end fires on +the game's async continuation — the actual sqlite write completes in +microseconds so no queue needed. Detail view's fetch happens on the +main thread; already inside a UITableView cell tap handler which is +fine. + +### File → DB migration — one-shot at first launch + +`KifuStorageMigrateLegacyFiles()` called from `installUnityHooks` once +per session, guarded by `meta('legacy_files_migrated', '1')`: + +- Enumerate `Documents/KiouForge/*.kif` via `NSFileManager`. +- For each: parse filename `__vs_.kif` into + fields; read text via `stringWithContentsOfFile:`; call + `KifuStorageInsert`; delete source with `removeItemAtPath:`. +- Set the flag even on partial failure so a broken file doesn't keep + triggering migration attempts. + +If the filename doesn't parse cleanly, insert with best-effort +placeholders (`mode="unknown"` etc.) and log the discrepancy — losing +metadata is better than losing the kifu. + +### List sheet — `Sources/KiouForge/UI/KifuListSheet.{h,m}` + +```objc +@interface KifuListSheet : UIViewController ++ (void)presentFromViewController:(UIViewController *)parent; +@end +``` + +- `UITableView` with plain-style cells: + - Left: `played_at` formatted as `2026/07/02 01:56` (ja_JP locale). + - Center: `mode` short label ("CPU" / "AI" / "対局" / "リプレイ"). + - Right: ` vs ` truncated to 24 chars. + - Startpos badge (small chip) when non-standard. +- Swipe-to-delete calls `KifuStorageDelete` and reloads. +- Row tap pushes `KifuDetailSheet` with the row id. +- Empty state: friendly centered label + hint about resigning a match. +- Modal presentation via `UISheetPresentationController` with + `.medium()` and `.large()` detents so users can peek then expand. + +### Detail sheet — `Sources/KiouForge/UI/KifuDetailSheet.{h,m}` + +```objc +@interface KifuDetailSheet : UIViewController ++ (instancetype)detailForRowId:(int64_t)rowId; +@end +``` + +- `UITextView` (read-only, monospaced) showing the KIF blob. +- Nav bar right item: `UIActivityViewController` share sheet with the + KIF text — hands off to any registered UTI-aware app. +- Nav bar left item: close. +- Long-press on text → copy-all shortcut. + +Filename for external share stays `__vs.kif` (built +from row fields) so downstream apps see the familiar name even though +the local storage is DB-backed. + +### Settings entry — `Sources/KiouForge/Hook/SettingsUI.m` + +Add a row above the existing feature toggles: + +``` +┌─ 棋譜 ───────────────────────── +│ 棋譜一覧 ...................... › +└──────────────────────────────── +``` + +Tapping presents `KifuListSheet` as a sheet on top of the settings +modal. Nothing else in the settings surface changes. + +### Writer integration — `Sources/KiouForge/Kif/Writer.m` + +`KIOUKifWriterEmit` collapses to roughly: + +```objc +NSString *KIOUKifWriterEmit(void *gameCtrl, void *matchConfig, + void *stateStore, const char *matchModeTag) { + NSString *kif = KIOUKifTextFromGameController(gameCtrl, matchConfig, + stateStore, matchModeTag); + if (kif.length == 0) return nil; + + NSString *modeName = matchModeTag ? @(matchModeTag) : @"unknown"; + NSString *opponents = KIOUKifDescribeOpponents(matchConfig, stateStore); + NSString *startpos = KIOUKifDescribeStartpos(gameCtrl); + NSArray *sides = [opponents componentsSeparatedByString:@"vs"]; + NSString *black = sides.count > 0 ? sides[0] : @"unknown"; + NSString *white = sides.count > 1 ? sides[1] : @"unknown"; + + KifuStorageInsert(modeName, black, white, startpos, kif, [NSDate date]); + return nil; // caller only used the return value for a log line +} +``` + +Existing log lines stay for parity; the "wrote 204 bytes -> path" +message becomes "inserted rowId=X len=Y bytes". + +## File tree diff + +``` +Sources/KiouForge/ +├── Storage/ +│ ├── KifuStorage.h (new) +│ └── KifuStorage.m (new) +├── UI/ +│ ├── KifuListSheet.h (new) +│ ├── KifuListSheet.m (new) +│ ├── KifuDetailSheet.h (new) +│ └── KifuDetailSheet.m (new) +├── Kif/ +│ └── Writer.m (modified: file write → KifuStorageInsert) +├── Hook/ +│ └── SettingsUI.m (modified: add 棋譜一覧 row) +├── Internal.h (modified: forward-decls for the new modules) +└── Tweak.m (modified: KifuStorageMigrateLegacyFiles call) + +Makefile (modified: -lsqlite3 + new .m files) +``` + +## Estimate + +- ~300-400 lines total across the new/modified files. +- Objective-C UIKit boilerplate is the bulk (KifuListSheet + KifuDetailSheet). +- SQLite wrapper is thin — the sqlite3 C API is verbose but the actual + logic per call is 10-15 lines. +- No new external dependency. +- 1-2 focused sessions once implementation starts. + +## Risks + +- **SQLite corruption on abnormal exit.** Wrap inserts in + `BEGIN IMMEDIATE / COMMIT` and set `PRAGMA journal_mode=WAL` so a + mid-write crash leaves the DB consistent. +- **Migration losing files.** Only delete source `.kif` after the + insert commits successfully. Set the migration flag *inside* the + same transaction as the last insert so a partial run is retryable. +- **UIKit view controller lifetime on top of a modal Settings.** + Present via the current top-most view controller + (`UIApplication.keyWindow.rootViewController.presentedViewController`), + not the settings VC directly, so dismissing the outer sheet doesn't + yank the list sheet mid-interaction. +- **KIF text getting very large on long replays.** SQLite handles + megabyte TEXT columns fine; UITextView is the bottleneck. Cap the + detail view's `text` length at 100 KB and offer a "share full file" + button if truncated. Not expected to hit in normal play (average + kifu is ~200 bytes). + +## Open questions + +- Retention policy? Keep everything forever, or auto-purge past N + entries? Default: forever, add a manual "全削除" button in settings. +- Should the migration also copy `.kif` files to a backup subfolder + before deleting, for one release? Safer, but adds complexity. Vote: + no — the DB is the new source of truth, and files are trivially + recoverable via share sheet. +- Sheet presentation icon: `SF Symbols` `list.bullet.rectangle` vs + `doc.text`. Not blocking.