From 4114cfec93297ab606f33cdb68b3aa9583564cf9 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Fri, 3 Jul 2026 15:24:19 -0500 Subject: [PATCH] Some fixes to workflow wolf --- README.md | 4 +- gui/wolf_workflow_tab.py | 304 +++++++++++++++++++++++++++++++++++++-- gui/workflow_tab.py | 39 +++-- util/vocab.py | 47 ++++++ 4 files changed, 359 insertions(+), 35 deletions(-) create mode 100644 util/vocab.py diff --git a/README.md b/README.md index 97f0af4..7e7da00 100644 --- a/README.md +++ b/README.md @@ -324,8 +324,8 @@ Open the **Workflow** tab and choose **Wolf RPG (WolfDawn)** from the engine sel | Step | Action | |------|--------| -| **0 Project** | Select the game folder, **Unpack** the `.wolf` archives into a loose `Data/` folder, then **Extract text** (maps, common events, databases, `Game.dat`, external event text, and the project-wide name glossary). Everything is staged into `files/` for translation. | -| **1 Glossary** | Translate the name glossary (`names.json`) first. WOLF references item/skill/enemy names by value across many files, so doing names first keeps them consistent. | +| **0 Project** | Select the game folder, **Unpack** the `.wolf` archives into a loose `Data/` folder, then **Extract text** (maps, common events, databases, `Game.dat`, external event text, and the project-wide name glossary). Everything is staged into `files/` for translation. Finally, **Set up git tracking** by copying the `gameupdate/` folder (updater scripts, `patch.sh`/`patch.ps1`, and a `.gitignore` that excludes the original game files) into the game root, so the translation project can be tracked with git and later shipped as a git-based patch players apply themselves. | +| **1 Glossary** | Build both glossaries before translating. **1a - Character & Worldbuilding Glossary (`vocab.txt`):** copy the WOLF-tailored prompt into Cursor/Copilot with the extracted `files/` JSON, let it discover character names, speech registers, and lore terms, then paste the result into the in-tab editor and save (the shared `vocab.txt` is used by every translation batch to keep names and voice consistent). **1b - Name Value Glossary (`names.json`):** translate the name glossary first, since WOLF references item/skill/enemy names by value across many files. | | **2 Translate** | Run the `Wolf RPG (WolfDawn)` module over `files/`. Only the `text` fields are filled in; `source` is preserved so injection can verify each line. | | **3 Inject** | Write the translations back into the `Data/` binaries. The name glossary is applied consistently across every file. Toggle `--en-punct` (convert Japanese punctuation to ASCII) and `--allow-code-drift` (relax the inline-code guard) as needed. Use **Check name consistency** to catch names translated differently across files. | | **4 Package** | Either run from the loose `Data/` folder (backs up `Data.wolf` → `Data.wolf.bak`), or **Repack** a fresh `Data.wolf`, inheriting the original archive's encryption. | diff --git a/gui/wolf_workflow_tab.py b/gui/wolf_workflow_tab.py index ec88d97..eff5c8a 100644 --- a/gui/wolf_workflow_tab.py +++ b/gui/wolf_workflow_tab.py @@ -5,8 +5,10 @@ Mirrors the RPGMaker WorkflowTab, driven by the vendored WolfDawn ``wolf`` CLI (see util/wolfdawn): Step 0 Project - select game folder, unpack .wolf archives, extract text - (strings-extract per file + names-extract) into files/ - Step 1 Glossary - translate the project-wide name glossary first + (strings-extract per file + names-extract) into files/, and + copy the gameupdate/ patch files (incl. .gitignore) for git tracking + Step 1 Glossary - build vocab.txt (characters/terms) and translate the + project-wide name-value glossary first Step 2 Translate - run the "Wolf RPG (WolfDawn)" module over files/ Step 3 Inject - inject translations + names back into the Data/ binaries Step 4 Package - run from a loose Data/ folder, or repack Data.wolf @@ -26,7 +28,9 @@ from pathlib import Path from PyQt5.QtCore import Qt, QSettings, QThread, QTimer, pyqtSignal from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( + QApplication, QCheckBox, + QComboBox, QFileDialog, QHBoxLayout, QLabel, @@ -41,13 +45,109 @@ from PyQt5.QtWidgets import ( QWidget, ) +from gui.translation_tab import ( + BATCH_COLLECT_LIVE_CHARGE_NOTE, + BATCH_MODE_BENEFIT_NOTE, + BATCH_MODE_LABEL, +) from gui.workflow_tab import _make_btn, _make_hr, _make_section_label from util.project_scanner import detect_wolf_layout +from util.vocab import read_game_vocab, write_game_vocab + +# Workflow-level label for the non-batch (live) translation path. The Translation +# tab's own mode is called "Translate"; _workflow_mode_text() maps to that. +_TL_NORMAL_LABEL = "Normal Translate" MANIFEST_NAME = "manifest.json" NAMES_JSON = "names.json" WORK_DIR_NAME = "wolf_json" +# Glossary-discovery prompt for Copilot / Cursor, tailored to WolfDawn's extracted +# JSON (source/text pairs staged in files/). Produces the same two-section format +# the shared vocab.txt expects, so the AI output pastes straight into the editor. +_WOLF_GLOSSARY_PROMPT = ( + "You are an expert Japanese WOLF RPG Editor game analyst building a translation glossary.\n" + "\n" + "\n" + "Extract named characters and lore-specific worldbuilding terms from this game's extracted " + "text. Produce a structured glossary in the exact format specified below. It will be loaded " + "directly into a translation tool, so strict format compliance is required.\n" + "\n" + "\n" + "--- attach the extracted JSON in files/ here before continuing ---\n" + "\n" + "\n" + "The files/ folder holds WolfDawn extractions as JSON. Each file is a list of entries with a\n" + "'source' field (the original Japanese) and a 'text' field (initially empty/identical).\n" + "Analyse the 'source' fields only. The files are:\n" + " - DataBase.project.json, CDataBase.project.json, SysDatabase.project.json — databases:\n" + " richest, small source of character/actor names, classes, factions, and lore titles.\n" + " - CommonEvent.dat.json — common events: dialogue and system text.\n" + " - Game.dat.json — game/system strings (title, terms).\n" + " - .mps.json — per-map events: the main story dialogue (can be large).\n" + " - Evtext.json — external event text, when present.\n" + " - names.json — item/skill/enemy value names (the tool handles these; do NOT list them).\n" + "\n" + "\n" + "\n" + "Map files can be extremely large. Do NOT read them sequentially — you will hit context " + "limits. Instead:\n" + "1. Read the DataBase.project.json files IN FULL first — small and the best source of names.\n" + "2. For large map files, SEARCH (grep) the 'source' fields instead of reading sequentially. " + "Prioritise dialogue because it is the best evidence for character voice:\n" + " - Speaker patterns such as 【Name】, [Name], Name「…」, Name:, and at the start of a line\n" + " - Katakana clusters or kanji compound proper nouns that recur across dialogue\n" + " Scan the lowest-numbered maps first — early maps have the most story content.\n" + "3. Stop once you stop finding new names or terms. Do not pad the output.\n" + "\n" + "\n" + "\n" + "Apply to both sections:\n" + "- Separator: use a plain hyphen-minus (-). Never use an em dash or en dash. " + "The translation tool only recognises the plain hyphen.\n" + "- Descriptions must be entirely in English. Refer to other characters by English name only.\n" + "- Never give two spelling options (e.g. 'Sylfia / Sylphia' is wrong). Commit to one translation.\n" + "\n" + "# Game Characters — rules:\n" + "- Discover named characters from the databases and dialogue. Skip unnamed NPCs, generic " + "enemies, and narration-only entries.\n" + "- For each character include: gender, role, speech register, and personality. Note if the " + "name is player-chosen (a hero/actor whose name comes from a variable or input prompt).\n" + "\n" + "# Worldbuilding Terms — rules:\n" + "- Include: faction/organisation names, locations mentioned in dialogue but not on maps, " + "unique magic systems, lore titles, recurring in-universe concepts.\n" + "- Exclude: skill names, item names, weapon/armour names (the tool handles those via " + "names.json). Skip generic RPG words. Do not repeat character names here.\n" + "\n" + "\n" + "\n" + "Return the ENTIRE glossary inside ONE fenced code block (a ``` block), with nothing before " + "or after it, so it can be copied in a single click and pasted straight into the tool.\n" + "Inside the code block, output EXACTLY two sections with these headers. Do not add any " + "preamble, explanation, or text outside the entries.\n" + "\n" + "# Game Characters\n" + "# Worldbuilding Terms\n" + "\n" + "Entry format: Japanese (English) - description\n" + "\n" + "\n" + "# Game Characters\n" + "シロ (Shiro) - Female; protagonist; player-named; speaks in a flustered, cute register " + "with feminine speech markers\n" + "ゼクス (Zex) - Male; antagonist; cold and commanding; uses an archaic formal register\n" + "カナエ (Kanae) - Female; NPC shopkeeper; warm and motherly; ends sentences with わね\n" + "\n" + "# Worldbuilding Terms\n" + "虚無の穴 (Void Rift) - Dimensional tear referenced repeatedly in late-game dialogue; " + "not a named map location\n" + "裁定者 (Arbiter) - Title held by the ruling council; lore-specific rank with no " + "real-world equivalent\n" + "\n" + "\n" +) + class _WolfTaskWorker(QThread): """Run a blocking WolfDawn task callable off the UI thread. @@ -270,6 +370,13 @@ class WolfWorkflowTab(QWidget): lbl.setStyleSheet("color:#9d9d9d;font-size:12px;background:transparent;") return lbl + def _subheading(self, text: str) -> QLabel: + lbl = QLabel(text) + lbl.setStyleSheet( + "color:#4ec9b0;font-size:13px;font-weight:bold;background:transparent;padding-top:4px;" + ) + return lbl + def _register(self, btn: QPushButton) -> QPushButton: self._buttons.append(btn) return btn @@ -375,6 +482,17 @@ class WolfWorkflowTab(QWidget): "project-wide name glossary, then imports them into files/ for translation." )) + gu_btn = self._register(_make_btn("③ Set up git tracking (copy gameupdate/ files)", "#3a3a3a")) + gu_btn.clicked.connect(self._apply_gameupdate) + layout.addWidget(gu_btn) + layout.addWidget(self._desc( + "Copies the tool's gameupdate/ folder into the game's root folder: the updater " + "scripts (GameUpdate.bat / GameUpdate_linux.sh), the patch scripts (patch.sh / " + "patch.ps1), and a .gitignore that excludes the original game files. Do this early so " + "you can track the translation project with git and later ship it as a git-based patch " + "players apply themselves. Existing files are overwritten." + )) + def _browse_folder(self): start = self.folder_edit.text() or str(Path.home()) folder = QFileDialog.getExistingDirectory(self, "Select WOLF Game Folder", start) @@ -547,20 +665,91 @@ class WolfWorkflowTab(QWidget): # ── Step 1: Glossary / names ─────────────────────────────────────────────── def _build_step1_glossary(self, layout: QVBoxLayout): - layout.addWidget(_make_section_label("Step 1 · Name Glossary (recommended first)")) + layout.addWidget(_make_section_label("Step 1 · Glossary (build before translating)")) + layout.addWidget(self._desc( + "Two glossaries keep the translation consistent. Build them before Step 2 so the AI " + "already knows every character and term while translating." + )) + + # ── 1a: Character & worldbuilding glossary (vocab.txt) ── + layout.addWidget(self._subheading("1a · Character & Worldbuilding Glossary (vocab.txt)")) + layout.addWidget(self._desc( + "vocab.txt is the project-wide glossary used by every translation batch to keep " + "character names, speech register, honorifics, and lore terms consistent. Copy the " + "prompt below into Cursor or Copilot Chat with the extracted files/ JSON open, let it " + "analyse the game, then paste its output into the editor and save." + )) + prompt_btn = _make_btn("📋 Copy glossary prompt for Copilot / Cursor", "#5a3a7a") + prompt_btn.clicked.connect(self._copy_wolf_glossary_prompt) + layout.addWidget(prompt_btn) + + fmt = QLabel( + "Format: Japanese (English) - description\n" + "Example: シロ (Shiro) - Female; protagonist; flustered, cute register" + ) + fmt.setFont(QFont("Consolas", 9)) + fmt.setWordWrap(True) + fmt.setStyleSheet( + "color:#569cd6;background-color:#1a1e2a;border:1px solid #2a3a5a;" + "padding:5px 10px;border-radius:4px;font-size:12px;border-left:3px solid #2a6a9a;" + ) + layout.addWidget(fmt) + + self.vocab_editor = QTextEdit() + self.vocab_editor.setMinimumHeight(120) + self.vocab_editor.setFont(QFont("Consolas", 9)) + self.vocab_editor.setStyleSheet( + "QTextEdit{background-color:#252526;color:#d4d4d4;border:1px solid #3c3c3c;" + "border-radius:4px;padding:8px;selection-background-color:#264f78;}" + ) + self._reload_vocab() + layout.addWidget(self.vocab_editor, 1) + + vrow = QHBoxLayout() + save_btn = _make_btn("💾 Save vocab.txt", "#3a7a3a") + save_btn.clicked.connect(self._save_vocab) + vrow.addWidget(save_btn) + reload_btn = _make_btn("↺ Reload", "#555") + reload_btn.clicked.connect(self._reload_vocab) + vrow.addWidget(reload_btn) + vrow.addStretch() + layout.addLayout(vrow) + + layout.addWidget(_make_hr()) + + # ── 1b: Name-value glossary (names.json) ── + layout.addWidget(self._subheading("1b · Name Value Glossary (item / skill / enemy names)")) layout.addWidget(self._desc( "WOLF databases reference item/skill/enemy names by value across many files, so " - "translating the glossary first keeps them consistent. This translates only " - f"{NAMES_JSON} using the Wolf RPG (WolfDawn) module, then you can inject it in Step 3." + f"translating {NAMES_JSON} first keeps those consistent. This translates only " + f"{NAMES_JSON} using the Wolf RPG (WolfDawn) module; it is injected in Step 3. " + "You can also translate it together with everything else in Step 2, but doing it " + "first is the higher-quality path. It uses the translation mode (Normal / Batch) " + "selected in Step 2." )) btn = self._register(_make_btn("Translate name glossary now", "#007acc")) btn.clicked.connect(lambda: self._navigate_to_translation(only=NAMES_JSON, auto_start=True)) layout.addWidget(btn) - layout.addWidget(_make_hr()) - layout.addWidget(self._desc( - "Optional: you can also open the glossary in Step 2 together with everything else. " - "Doing names first is just the higher-quality path." - )) + + def _copy_wolf_glossary_prompt(self): + try: + QApplication.clipboard().setText(_WOLF_GLOSSARY_PROMPT) + self._log("📋 Glossary prompt copied. Paste it into Cursor/Copilot with files/ open.") + except Exception as exc: + self._log(f"❌ Could not copy glossary prompt: {exc}") + + def _reload_vocab(self): + try: + self.vocab_editor.setPlainText(read_game_vocab()) + except Exception as exc: + self._log(f"❌ Could not load vocab.txt: {exc}") + + def _save_vocab(self): + try: + write_game_vocab(self.vocab_editor.toPlainText()) + self._log("✅ vocab.txt saved (base terms from vocab_base.txt appended).") + except Exception as exc: + self._log(f"❌ Could not save vocab.txt: {exc}") # ── Step 2: Translate ────────────────────────────────────────────────────── @@ -571,6 +760,9 @@ class WolfWorkflowTab(QWidget): "Wolf RPG (WolfDawn) module and starts translating. Only the 'text' fields are " "filled in; 'source' is preserved so injection can verify each line." )) + + self._add_tl_mode_selector(layout) + btn = self._register(_make_btn("Translate all files now", "#00a86b")) btn.clicked.connect(lambda: self._navigate_to_translation(auto_start=True)) layout.addWidget(btn) @@ -579,6 +771,48 @@ class WolfWorkflowTab(QWidget): open_btn.clicked.connect(lambda: self._navigate_to_translation(auto_start=False)) layout.addWidget(open_btn) + def _add_tl_mode_selector(self, layout: QVBoxLayout): + """Normal vs Batch selector; applies to the name glossary (1b) and full runs.""" + row = QHBoxLayout() + lbl = QLabel("Translation mode:") + lbl.setStyleSheet("color:#cccccc;font-size:12px;font-weight:bold;background:transparent;") + self._tl_mode_combo = QComboBox() + self._tl_mode_combo.addItem(_TL_NORMAL_LABEL) + self._tl_mode_combo.addItem(BATCH_MODE_LABEL) + self._tl_mode_combo.setFixedWidth(220) + self._tl_mode_combo.setToolTip( + "Applies to both the Step 1 name glossary and the full run here.\n" + "Normal translates live; Batch uses the Anthropic Batches API (~50% cheaper, Claude only)." + ) + saved = self._setting("tl_mode", BATCH_MODE_LABEL) or BATCH_MODE_LABEL + idx = self._tl_mode_combo.findText(str(saved)) + if idx >= 0: + self._tl_mode_combo.setCurrentIndex(idx) + self._tl_mode_combo.currentTextChanged.connect(self._on_tl_mode_changed) + row.addWidget(lbl) + row.addWidget(self._tl_mode_combo) + row.addStretch() + layout.addLayout(row) + + self._batch_note = QLabel(BATCH_MODE_BENEFIT_NOTE + "\n" + BATCH_COLLECT_LIVE_CHARGE_NOTE) + self._batch_note.setWordWrap(True) + self._batch_note.setStyleSheet("color:#8fbc8f;font-size:11px;background:transparent;") + layout.addWidget(self._batch_note) + self._on_tl_mode_changed(self._tl_mode_combo.currentText()) + + def _on_tl_mode_changed(self, mode_text: str): + is_batch = mode_text == BATCH_MODE_LABEL + if hasattr(self, "_batch_note"): + self._batch_note.setVisible(is_batch) + self._save_setting("tl_mode", mode_text) + + def _workflow_mode_text(self) -> str: + """Map the selector to the Translation tab's own mode label.""" + combo = getattr(self, "_tl_mode_combo", None) + if combo is not None and combo.currentText() == BATCH_MODE_LABEL: + return BATCH_MODE_LABEL + return "Translate" + def _navigate_to_translation(self, only: str | None = None, auto_start: bool = False): """Switch to the Translation tab, select the WolfDawn module, and check files.""" pw = self.parent_window @@ -595,6 +829,16 @@ class WolfWorkflowTab(QWidget): except Exception: pass + # Apply the chosen translation mode (after the module, since changing the + # module can refresh the mode list). + try: + mode_combo = tt.mode_combo + mode_idx = mode_combo.findText(self._workflow_mode_text()) + if mode_idx >= 0: + mode_combo.setCurrentIndex(mode_idx) + except Exception: + pass + try: tt.refresh_file_lists() fl = tt.file_list @@ -769,6 +1013,46 @@ class WolfWorkflowTab(QWidget): "archive's encryption where possible (--like the backed-up original)." )) + def _apply_gameupdate(self): + if not self._require_root(): + return + from util.paths import PROJECT_ROOT + + src = PROJECT_ROOT / "gameupdate" + dst = Path(self._game_root) + if not src.is_dir(): + QMessageBox.warning( + self, "gameupdate", + f"The tool's gameupdate/ folder was not found at {src}.", + ) + return + + def task(log): + import shutil + + copied = 0 + errors: list[str] = [] + log(f"Copying gameupdate patch files: {src} → {dst} …") + for fp in sorted(src.rglob("*")): + if not fp.is_file(): + continue + rel = fp.relative_to(src) + target = dst / rel + try: + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(fp, target) + copied += 1 + log(f" copied {rel}") + except Exception as exc: + errors.append(f"{rel}: {exc}") + for e in errors: + log(f" ⚠ {e}") + if errors: + return False, f"Copied {copied} file(s); {len(errors)} failed (see log)." + return True, f"Copied {copied} gameupdate file(s) into {dst.name}." + + self._run_task(task) + def _package_loose(self): if not self._require_root(): return diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index 6148b2f..34f9e5f 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -22,7 +22,9 @@ import sys import threading from pathlib import Path -from util.paths import VOCAB_BASE_PATH, VOCAB_PATH +from util.paths import VOCAB_PATH +from util.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR +from util.vocab import read_game_vocab, write_game_vocab import jsbeautifier @@ -1075,8 +1077,10 @@ class WorkflowTab(QWidget): "\n" "\n" "\n" - "Output EXACTLY two sections with these headers. Do not add any preamble, explanation, " - "or text outside the entries.\n" + "Return the ENTIRE glossary inside ONE fenced code block (a ``` block), with nothing before " + "or after it, so it can be copied in a single click and pasted straight into the tool.\n" + "Inside the code block, output EXACTLY two sections with these headers. Do not add any " + "preamble, explanation, or text outside the entries.\n" "\n" "# Game Characters\n" "# Worldbuilding Terms\n" @@ -1194,10 +1198,11 @@ class WorkflowTab(QWidget): "\n" "\n" "\n" - "Provide the translated file as a complete replacement of js/plugins.js. Only change the " + "Provide the translated file as a complete replacement of js/plugins.js, inside a single " + "fenced code block (```js ... ```) so it can be copied in one click. Only change the " "string values identified above. Preserve all original formatting, indentation, comments, and structure.\n" "\n" - "After the file, output a summary:\n" + "After the code block, output a summary:\n" "\n" "### Translations Made\n" " Plugin: \n" @@ -1268,8 +1273,9 @@ class WorkflowTab(QWidget): "\n" "\n" "\n" - "For each .rb file that needed changes, provide the full translated file content. " - "Only change string values identified as safe. " + "For each .rb file that needed changes, provide the full translated file content inside its " + "own fenced code block (```ruby ... ```), preceded by the filename, so each file can be " + "copied in one click. Only change string values identified as safe. " "Preserve all Ruby syntax, indentation, comments, and structure exactly.\n" "\n" "After all files, output a summary:\n" @@ -3555,7 +3561,7 @@ class WorkflowTab(QWidget): # Step 1 – Vocab # ───────────────────────────────────────────────────────────────────────── - _BASE_SEPARATOR = "# ── Base Vocabulary (auto-appended from vocab_base.txt — do not edit below) ──\n" + _BASE_SEPARATOR = _SHARED_BASE_SEPARATOR def _copy_glossary_prompt(self): """Copy the glossary prompt to clipboard, injecting known speakers from vocab.txt.""" @@ -3608,27 +3614,14 @@ class WorkflowTab(QWidget): return results def _reload_vocab(self): - vocab_path = VOCAB_PATH try: - if vocab_path.exists(): - text = vocab_path.read_text(encoding="utf-8") - # Strip the auto-appended base section so editor shows only game-specific content - sep_idx = text.find(self._BASE_SEPARATOR) - if sep_idx != -1: - text = text[:sep_idx].rstrip("\n") - self.vocab_editor.setPlainText(text) - else: - self.vocab_editor.setPlainText("# Add character glossary entries here\n") + self.vocab_editor.setPlainText(read_game_vocab()) except Exception as exc: self._log(f"❌ Could not load vocab.txt: {exc}") def _save_vocab(self): try: - game_text = self.vocab_editor.toPlainText().rstrip("\n") - base_path = VOCAB_BASE_PATH - base_text = base_path.read_text(encoding="utf-8") if base_path.exists() else "" - combined = game_text + "\n\n" + self._BASE_SEPARATOR + base_text - VOCAB_PATH.write_text(combined, encoding="utf-8") + write_game_vocab(self.vocab_editor.toPlainText()) self._log("✅ vocab.txt saved (base terms from vocab_base.txt appended).") except Exception as exc: self._log(f"❌ Could not save vocab.txt: {exc}") diff --git a/util/vocab.py b/util/vocab.py new file mode 100644 index 0000000..640232c --- /dev/null +++ b/util/vocab.py @@ -0,0 +1,47 @@ +"""Shared helpers for the game-specific translation glossary (``data/vocab.txt``). + +``vocab.txt`` is loaded by the shared translation layer (:mod:`util.translation`) +and applied to every engine, so a good glossary keeps character names, honorifics, +and worldbuilding terms consistent across the whole translation. + +The file has two parts: + +* the game-specific entries (characters, worldbuilding terms) edited per project, +* a base vocabulary that is auto-appended from ``data/vocab_base.txt``. + +``BASE_SEPARATOR`` marks where the auto-appended base section begins so the +workflow editors can show and save only the game-specific portion. It must stay +byte-identical to what has already been written into users' ``vocab.txt`` files, +otherwise the base section would not be stripped on reload. +""" + +from __future__ import annotations + +from util.paths import VOCAB_BASE_PATH, VOCAB_PATH + +BASE_SEPARATOR = ( + "# ── Base Vocabulary (auto-appended from vocab_base.txt — do not edit below) ──\n" +) + +_EMPTY_PLACEHOLDER = "# Add character glossary entries here\n" + + +def read_game_vocab() -> str: + """Return the game-specific portion of ``vocab.txt`` (base section stripped).""" + if VOCAB_PATH.is_file(): + text = VOCAB_PATH.read_text(encoding="utf-8") + idx = text.find(BASE_SEPARATOR) + if idx != -1: + text = text[:idx].rstrip("\n") + return text + return _EMPTY_PLACEHOLDER + + +def write_game_vocab(game_text: str) -> None: + """Write the game-specific vocab and re-append the base vocabulary.""" + game_text = (game_text or "").rstrip("\n") + base_text = ( + VOCAB_BASE_PATH.read_text(encoding="utf-8") if VOCAB_BASE_PATH.is_file() else "" + ) + combined = game_text + "\n\n" + BASE_SEPARATOR + base_text + VOCAB_PATH.write_text(combined, encoding="utf-8")