diff --git a/gameupdate/gameupdate/vocab.txt b/gameupdate/gameupdate/vocab.txt deleted file mode 100644 index e2e1f59..0000000 --- a/gameupdate/gameupdate/vocab.txt +++ /dev/null @@ -1,65 +0,0 @@ -# Game Characters -エル (El) - Female; inn adventurer tied to the Light Plain plotline; not player-chosen; speaks casually and informally; energetic and direct -流浪の求道者 (Seeker) - Male; roaming challenge NPC; not player-chosen; speaks in a grave, formal register; stoic and trial-focused -シスターエキュリー (Sister Ecurie) - Female; church sister of the Amol faith; not player-chosen; polite religious register; gentle and dutiful -ホエミ (Hoemi) - Female; friend in the northern district; not player-chosen; casual friendly speech; warm and approachable -ロクサヌ (Roxanu) - Female; northern district student; not player-chosen; youthful casual speech; sociable and ordinary -ミント (Mint) - Female; northern district student; not player-chosen; youthful casual speech; lively and informal -ヒルデ (Hilde) - Female; apartment resident; not player-chosen; plain civilian speech; practical and grounded -管理人ナミサ (Manager Namisa) - Female; apartment manager; not player-chosen; polite caretaker speech; responsible and matter-of-fact -デフィ (Defi) - Female; soldier's wife living in the apartment; not player-chosen; domestic casual speech; reserved and ordinary -ラム (Ram) - Female; arena warrior; not player-chosen; confident fighter's speech; bold and competitive -アルシェ (Arche) - Female; arena fan; not player-chosen; enthusiastic casual speech; excitable and friendly -バーニア (Barnia) - Female; student associated with the arena; not player-chosen; youthful casual speech; curious and energetic -フォサ (Fossa) - Female; arena-side NPC; not player-chosen; casual town speech; socially active -キャナリ (Canari) - Female; cafe worker or cafe patron; not player-chosen; light casual speech; cheerful and sociable -エロン (Elon) - Male; cafe NPC; not player-chosen; casual adult speech; relaxed and conversational -チコニャ (Ciconya) - Female; cafe NPC; not player-chosen; casual service speech; friendly and talkative -ルートル (Rootle) - Male; cafe NPC; not player-chosen; casual adult speech; easygoing -シャモー (Chameau) - Male; cafe NPC; not player-chosen; casual adult speech; relaxed and ordinary -コション (Cochon) - Male; cafe NPC; not player-chosen; casual adult speech; informal and sociable -アレニエ (Areignee) - Female; trading-company NPC; not player-chosen; businesslike polite speech; composed and practical -ミルバト (Mirbat) - Male; trading-company NPC; not player-chosen; businesslike speech; practical and restrained -受付アンヌ (Receptionist Anne) - Female; theater receptionist; not player-chosen; polite service register; courteous and professional -支配人アドルフ (Manager Adolf) - Male; theater manager; not player-chosen; formal managerial speech; authoritative and composed -アンリ (Henri) - Male; theater-related named NPC; not player-chosen; polite stage-world speech; refined and reserved -カトリーヌ (Catherine) - Female; theater performer or staff member; not player-chosen; elegant feminine speech; theatrical and poised -シエル (Ciel) - Female; theater performer or staff member; not player-chosen; polite feminine speech; graceful and composed -グラティア (Gratia) - Female; theater NPC; not player-chosen; refined speech; elegant and expressive -オレイユ (Oreille) - Female; theater NPC; not player-chosen; refined speech; attentive and polished -実況 (Commentator) - Male; arena or event commentator; not player-chosen; energetic announcing register; loud and performative -ウミ (Umi) - Female; named NPC in Albol Forest; not player-chosen; casual rural speech; practical and outdoorsy -ネリネ (Nerine) - Female; named NPC at Albol Camp; not player-chosen; casual camp speech; warm and grounded -ジャスミン (Jasmine) - Female; named NPC at Albol Camp; not player-chosen; casual feminine speech; friendly and composed -アーディ (Ardi) - Male; named NPC in the Testa settlement; not player-chosen; casual local speech; practical and direct -クラレット (Claret) - Female; named NPC in Viola Village; not player-chosen; casual village speech; grounded and approachable -ステノ (Steno) - Female; hot spring inn NPC; not player-chosen; polite hospitality speech; calm and service-minded - -# Worldbuilding Terms -淫催都市ヒュプノズム (Lewd Hypnosis City Hypnosm) - Game title and central city setting; a city shaped by the Incitation curse and its social control -ヒュプノズム (Hypnosm) - Main city and hub area; divided into districts such as the southern district, slums, upper district, church, guild, theater, and lord's manor -淫催 (Incitation) - The central hypnosis-like curse or state that alters obedience, desire, and social values throughout Hypnosm -淫催の呪い (Incitation Curse) - Story term for the magical influence affecting the city and its residents -淫催の守護者 (Guardian of Incitation) - Enemy or boss title connected to the Incitation system -アモル教 (Amol Faith) - City religion imposed on residents; tied to marriage, sexuality, and the social norms of Hypnosm -光の平原 (Light Plain) - Major plot location where magical power gathers and expands; associated with danger, monsters, and the Divine King's awakening -神王覚醒 (Divine King's Awakening) - Historical event said to have created or transformed the Light Plain around twenty years earlier -血胤 (Bloodline) - Ruby-tagged lore term read as Bloodseed; unique hereditary magic tied to absorbing traits from other beings -血胤魔術 (Bloodline Magic) - Keimi's unique magic system derived from the Bloodline lineage -ブラッドシード家 (Bloodseed Family) - Fallen noble family associated with Bloodline magic -フィガロ家 (Figaro Family) - Prestigious family famous for swords and magic swords -魔剣 (Magic Sword) - Lore term for special enchanted swords, especially those connected to the Figaro family -フィガロの魔剣 (Figaro Magic Sword) - Named magic sword from the Figaro family -魔剣活性化 (Magic Sword Activation) - Story process or quest concept for making a magic sword usable -冒険者ランク (Adventurer Rank) - Guild status system; raising it unlocks citizenship and marriage-related progression -住民権 (Citizenship) - Legal status required to access restricted districts and marry within Hypnosm -ギルド (Guild) - Adventurer organization that handles rank progression and quest reporting -マナ (Mana) - Recurring magical resource gained from exploration and used in city systems -マナ屋 (Mana Shop) - City facility or service related to exchanging mana -領主の館 (Lord's Manor) - Plot location tied to the city's ruler and later story investigation -地下道 (Underground Passage) - Story route used during the Hypnosm investigation arc -魔人の塔 (Demon Tower) - Major dungeon or travel destination referenced in progression data -氷結の迷宮 (Frozen Labyrinth) - Major dungeon or travel destination referenced in progression data -宵闇のエージェント (Agent of Twilight) - Named quest title with story significance -極寒のプレリュード (Frigid Prelude) - Named quest title with story significance -性奴隷 (Sex Slave) - In-universe class/status used by event progression and relationship systems \ No newline at end of file diff --git a/gui/main.py b/gui/main.py index 5436c97..6f2fa69 100644 --- a/gui/main.py +++ b/gui/main.py @@ -74,8 +74,18 @@ class UpdateThread(QThread): ARCHIVE_ROOT = "dazed-mtl-tool" SHA_FILE = str(LAST_UPDATE_SHA_PATH) - # Paths (relative, top-level) that should never be touched during update - PROTECTED = {".env", "venv", "log", "files", "translated", "data"} + # Top-level paths that should never be touched during update + PROTECTED_TOP = {".env", "venv", "log", "files", "translated"} + + # User-local files under data/ that must not be overwritten. + # Shipped defaults (translation_contexts.json, skills/*.md, vocab_base.txt, …) + # are intentionally updated so tool releases refresh prompts and contexts. + PROTECTED_DATA_FILES = frozenset({ + "data/vocab.txt", + "data/last_update_sha.txt", + "data/wolf_speakers.json", + "data/wolf_safe_notes.json", + }) # GitLab zip archives do not preserve Unix execute bits; restore after apply. EXECUTABLE_SUFFIXES = {".sh", ".desktop"} @@ -88,6 +98,18 @@ class UpdateThread(QThread): def __init__(self, parent=None): super().__init__(parent) + @classmethod + def should_install(cls, rel: Path) -> bool: + """Return True when *rel* (path relative to archive root) may be copied.""" + parts = rel.parts + if not parts: + return False + if parts[0] in cls.PROTECTED_TOP: + return False + if rel.as_posix() in cls.PROTECTED_DATA_FILES: + return False + return True + @staticmethod def _fmt_bytes(num: int) -> str: if num >= 1024 * 1024: @@ -188,9 +210,7 @@ class UpdateThread(QThread): install_files = [ src for src in extracted.rglob("*") - if src.is_file() - and src.relative_to(extracted).parts - and src.relative_to(extracted).parts[0] not in self.PROTECTED + if src.is_file() and self.should_install(src.relative_to(extracted)) ] total_files = max(len(install_files), 1) diff --git a/gui/wolf_workflow_tab.py b/gui/wolf_workflow_tab.py index 73e15ad..f365686 100644 --- a/gui/wolf_workflow_tab.py +++ b/gui/wolf_workflow_tab.py @@ -85,6 +85,7 @@ from gui.translation_tab import ( BATCH_MODE_LABEL, ) from gui.workflow_tab import ( + _GAMEUPDATE_COPY_SKIP_NAMES, _FileCopyWorker, _ImportWorker, _make_btn, @@ -1396,7 +1397,7 @@ class WolfWorkflowTab(QWidget): if not Path(src).is_dir(): self._log(f"⚠ gameupdate folder not found: {src}") return - w = _FileCopyWorker(src, dst) + w = _FileCopyWorker(src, dst, skip_names=_GAMEUPDATE_COPY_SKIP_NAMES) w.log.connect(self._log) w.done.connect(self._on_gameupdate_done) self._import_worker = w # reuse slot; not WolfTaskWorker @@ -1445,6 +1446,9 @@ class WolfWorkflowTab(QWidget): for fp in sorted(Path(src).rglob("*")): if not fp.is_file(): continue + if fp.name in _GAMEUPDATE_COPY_SKIP_NAMES: + log(f" skipped {fp.relative_to(src)}") + continue rel = fp.relative_to(src) target = Path(dst) / rel try: diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index 8736887..cd3ffaf 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -312,15 +312,23 @@ class _JsonFormatWorker(QThread): self.done.emit(False, f"dazedformat error: {exc}") +# Never copy these into a game root when applying gameupdate/ (local updater state +# or stray translator assets that must not overwrite a live install). +_GAMEUPDATE_COPY_SKIP_NAMES = frozenset({ + "previous_patch_sha.txt", +}) + + class _FileCopyWorker(QThread): """Recursively copy a source folder into a destination folder.""" done = pyqtSignal(int, list) # count_copied, errors log = pyqtSignal(str) - def __init__(self, src: str, dst: str): + def __init__(self, src: str, dst: str, skip_names: frozenset[str] | None = None): super().__init__() self.src = src self.dst = dst + self.skip_names = skip_names or frozenset() def run(self): import shutil @@ -336,6 +344,9 @@ class _FileCopyWorker(QThread): for fp in src.rglob("*"): if not fp.is_file(): continue + if fp.name in self.skip_names: + self.log.emit(f" skipped {fp.relative_to(src)}") + continue rel = fp.relative_to(src) target = dst / rel try: @@ -4281,7 +4292,7 @@ class WorkflowTab(QWidget): if not Path(src).is_dir(): self._log(f"⚠ gameupdate folder not found: {src}") return - w = _FileCopyWorker(src, dst) + w = _FileCopyWorker(src, dst, skip_names=_GAMEUPDATE_COPY_SKIP_NAMES) w.log.connect(self._log) w.done.connect(self._on_gameupdate_done) self._worker = w @@ -4316,7 +4327,14 @@ class WorkflowTab(QWidget): self._log(f" ⏭ Skipped: B (format plugins.js): not found ({plugins_js or 'not set'})") if gameupdate_src and Path(gameupdate_src).is_dir() and game_root_dst: - queue.append(("[C] gameupdate copy", _FileCopyWorker(gameupdate_src, game_root_dst))) + queue.append(( + "[C] gameupdate copy", + _FileCopyWorker( + gameupdate_src, + game_root_dst, + skip_names=_GAMEUPDATE_COPY_SKIP_NAMES, + ), + )) else: reason = (f"source not found ({gameupdate_src or 'not set'})" if not gameupdate_src or not Path(gameupdate_src).is_dir() diff --git a/tests/test_bundled_updates.py b/tests/test_bundled_updates.py index ca003cc..ab335bf 100644 --- a/tests/test_bundled_updates.py +++ b/tests/test_bundled_updates.py @@ -15,6 +15,42 @@ from unittest.mock import patch from util.wolfdawn import WolfDawnError, bundled_binary_path, ensure_wolf_binary +class UpdateThreadInstallFilterTests(unittest.TestCase): + """Tool update must refresh shipped data/ assets while protecting user state.""" + + def test_installs_translation_contexts_and_skills(self): + from gui.main import UpdateThread + + for rel in ( + "data/translation_contexts.json", + "data/skills/system.md", + "data/skills/project_setup.md", + "data/vocab_base.txt", + "gui/main.py", + "gameupdate/GameUpdate.bat", + "gameupdate/gameupdate/patch.ps1", + ): + with self.subTest(rel=rel): + self.assertTrue(UpdateThread.should_install(Path(rel))) + + def test_skips_user_local_data_and_workdir(self): + from gui.main import UpdateThread + + for rel in ( + "data/vocab.txt", + "data/last_update_sha.txt", + "data/wolf_speakers.json", + "data/wolf_safe_notes.json", + ".env", + "venv/pyvenv.cfg", + "log/translations.txt", + "files/Map001.json", + "translated/Map001.json", + ): + with self.subTest(rel=rel): + self.assertFalse(UpdateThread.should_install(Path(rel))) + + class CheckToolUpdateTests(unittest.TestCase): """Tool update check compares remote SHA to data/last_update_sha.txt only."""