Compare commits
2 commits
3645db1f57
...
3311d81645
| Author | SHA1 | Date | |
|---|---|---|---|
| 3311d81645 | |||
| 2b825a02f3 |
9 changed files with 2315 additions and 137 deletions
|
|
@ -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
|
||||
30
gui/main.py
30
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -2395,10 +2406,11 @@ class WorkflowTab(QWidget):
|
|||
|
||||
self._pt_forge_hotkey_edit = QLineEdit("F10")
|
||||
self._pt_forge_hotkey_edit.setFixedWidth(_PT_FIELD_W)
|
||||
self._pt_forge_hotkey_edit.setPlaceholderText("F10")
|
||||
self._pt_forge_hotkey_edit.setPlaceholderText("F8")
|
||||
self._pt_forge_hotkey_edit.setToolTip(
|
||||
"Key to open Forge (e.g. F10, F6, Ctrl+Shift+F).\n"
|
||||
"Under Wine/Linux, F10 is often stolen by the window menu - use F6/F8 instead.\n"
|
||||
"Key to open Forge (e.g. F8, F6, F10).\n"
|
||||
"MV uses the legacy plugin; MZ uses the modern one.\n"
|
||||
"Under Wine/Linux, F10 is often stolen by the window menu - prefer F8.\n"
|
||||
"Click Apply to game after changing."
|
||||
)
|
||||
hotkey_row.addWidget(self._pt_forge_hotkey_edit)
|
||||
|
|
@ -4280,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
|
||||
|
|
@ -4315,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()
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,10 @@
|
|||
"""Forge plugin build — install-time hotkey and UI scale injection.
|
||||
|
||||
Vanilla Forge_MV.js / Forge_MZ.js from len's repo stay untouched on disk.
|
||||
Vanilla Forge_MV.js / Forge_MZ.js stay untouched on disk.
|
||||
Patches are applied in memory when installing or applying settings to a game.
|
||||
|
||||
MV uses the pre-rewrite legacy plugin (old NW.js / Chrome ~65).
|
||||
MZ uses the rewritten unified forge.js bundle.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
"""Download / update Forge plugins from len's upstream repo (gitgud.io).
|
||||
|
||||
Upstream: https://gitgud.io/zero64801/forge-mvmz
|
||||
CI builds a unified forge.js plugin (master branch artifacts).
|
||||
CI builds a unified forge.js plugin (master branch artifacts) for MZ.
|
||||
|
||||
RPG Maker MV ships with NW.js / Chrome ~65, which cannot run the rewritten
|
||||
Svelte Forge bundle. MV therefore keeps the pre-rewrite legacy plugin
|
||||
(``Forge_MV.js`` / ``upstream/Forge_MV.js`` / ``legacy/Forge_MV.js``).
|
||||
|
||||
Offline copies: util/forge/upstream/
|
||||
Active plugins: util/forge/Forge_MZ.js, util/forge/Forge_MV.js
|
||||
Active plugins: util/forge/Forge_MZ.js (modern), util/forge/Forge_MV.js (legacy)
|
||||
|
||||
End users receive curated copies shipped with DazedMTLTool updates.
|
||||
Upstream fetches are maintainer-only (``--refresh-offline`` or ``--force``).
|
||||
Upstream fetches are maintainer-only (``--refresh-offline`` or ``--force``)
|
||||
and only refresh the MZ modern plugin.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -120,12 +126,11 @@ def _fetch_plugins(log_fn) -> bytes:
|
|||
return _download_bytes(_artifact_url("forge.js"))
|
||||
|
||||
|
||||
def _install_plugin_bytes(data: bytes) -> None:
|
||||
"""Write forge.js into both engine plugin paths and the offline bundle."""
|
||||
def _install_modern_mz_bytes(data: bytes) -> None:
|
||||
"""Write modern forge.js into MZ active + offline paths only (never MV)."""
|
||||
UPSTREAM_DIR.mkdir(parents=True, exist_ok=True)
|
||||
for engine in PLUGIN_BY_ENGINE:
|
||||
bundled_plugin_path(engine).write_bytes(data)
|
||||
upstream_plugin_path(engine).write_bytes(data)
|
||||
bundled_plugin_path("MZ").write_bytes(data)
|
||||
upstream_plugin_path("MZ").write_bytes(data)
|
||||
|
||||
|
||||
def _download_bytes(url: str) -> bytes:
|
||||
|
|
@ -135,7 +140,7 @@ def _download_bytes(url: str) -> bytes:
|
|||
|
||||
|
||||
def refresh_forge_plugins(log_fn=print) -> bool:
|
||||
"""Download len's latest forge.js and install Forge_MV/MZ copies."""
|
||||
"""Download len's latest forge.js and refresh the MZ modern plugin only."""
|
||||
try:
|
||||
commit = _upstream_commit()
|
||||
except Exception as exc:
|
||||
|
|
@ -152,13 +157,17 @@ def refresh_forge_plugins(log_fn=print) -> bool:
|
|||
_log("ERROR: forge.js download did not look like a plugin file.", log_fn)
|
||||
return False
|
||||
|
||||
_install_plugin_bytes(data)
|
||||
_install_modern_mz_bytes(data)
|
||||
|
||||
versions = _load_versions()
|
||||
versions["commit"] = commit
|
||||
versions["branch"] = FORGE_BRANCH
|
||||
_save_versions(versions)
|
||||
_log(f"Forge plugins updated ({commit[:12]})", log_fn)
|
||||
_log(
|
||||
f"Forge MZ plugin updated ({commit[:12]}). "
|
||||
"MV keeps the legacy Chrome-65-compatible plugin.",
|
||||
log_fn,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue