diff --git a/gameupdate/GameUpdate_linux.sh b/gameupdate/GameUpdate_linux.sh index f3a5b47..49bc370 100755 --- a/gameupdate/GameUpdate_linux.sh +++ b/gameupdate/GameUpdate_linux.sh @@ -3,12 +3,44 @@ # Enable error handling set -e -# Same layout as GameUpdate.bat: patch assets live under ./gameupdate/ next to this file. -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -GAME_ROOT="$SCRIPT_DIR" -PATCH_DIR="$SCRIPT_DIR/gameupdate" +# Re-exec from a temp copy so applying the patch zip cannot overwrite/truncate +# this launcher while bash is still reading it (same idea as patch.sh -> patch2.sh). +if [ -z "${GAMEUPDATE_LINUX_REEXEC:-}" ]; then + SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")" + GAME_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + TMP_SELF="$(mktemp)" + cp -f "$SELF" "$TMP_SELF" + chmod +x "$TMP_SELF" + export GAMEUPDATE_LINUX_REEXEC=1 + # shellcheck disable=SC2093 + exec bash "$TMP_SELF" "$GAME_ROOT" +fi + +GAME_ROOT="$1" +PATCH_DIR="$GAME_ROOT/gameupdate" +REEXEC_PATH="$0" + +cleanup_reexec() { + rm -f "$REEXEC_PATH" +} +trap cleanup_reexec EXIT cd "$GAME_ROOT" cp "$PATCH_DIR/patch.sh" "$PATCH_DIR/patch2.sh" +set +e bash "$PATCH_DIR/patch2.sh" -rm "$PATCH_DIR/patch2.sh" +patch_exit=$? +set -e +rm -f "$PATCH_DIR/patch2.sh" + +if [ "$patch_exit" -ne 0 ]; then + echo "GameUpdate failed (exit $patch_exit)." +fi + +# Match Windows patch.ps1 pause when launched interactively (double-click / terminal). +if [ -t 0 ] && [ -t 1 ]; then + echo + read -r -p "Press Enter to close..." +fi + +exit "$patch_exit" diff --git a/gameupdate/gameupdate/patch.sh b/gameupdate/gameupdate/patch.sh index 02991a1..e3c2a47 100755 --- a/gameupdate/gameupdate/patch.sh +++ b/gameupdate/gameupdate/patch.sh @@ -15,6 +15,25 @@ check_dependency() { fi } +# Run a Windows .exe. On Linux always go through wine (never rely on binfmt, +# which can hang waiting on wineserver / GUI). On Windows / Git Bash, exec directly. +run_windows_exe() { + local exe="$1" + shift + case "$(uname -s)" in + Linux*) + if ! command -v wine > /dev/null 2>&1; then + echo "ERROR: wine is required to run $(basename "$exe") on Linux." + return 1 + fi + ( cd "$ROOT_DIR" && WINEDEBUG=-all wine "$exe" "$@" ) + ;; + *) + ( cd "$ROOT_DIR" && "$exe" "$@" ) + ;; + esac +} + # Check for jq, unzip, and curl check_dependency jq check_dependency unzip @@ -263,12 +282,7 @@ invoke_wolf_pre_setup() { fi echo "[Pre-Setup] Unpacking Data.wolf with UberWolfCli (one-time)..." - # Native .exe works under Windows / Git Bash; wine covers Linux hosts. - if ( cd "$ROOT_DIR" && "$cli" -o "$data_wolf" ); then - : - elif command -v wine > /dev/null 2>&1 && ( cd "$ROOT_DIR" && wine "$cli" -o "$data_wolf" ); then - : - else + if ! run_windows_exe "$cli" -o "$data_wolf"; then echo "[Pre-Setup] ERROR: UberWolfCli unpack failed." return 0 fi @@ -315,7 +329,7 @@ if [ -f "$ROOT_DIR/data.dts" ]; then if [ ! -d "$ROOT_DIR/data" ] || [ ! -f "$ROOT_DIR/data/project.dat" ]; then if [ -f "$ROOT_DIR/data.dts" ]; then echo "[Pre-Setup] Step 1: Unpacking data.dts -> data" - ( cd "$ROOT_DIR" && "$UNPACKER" -o data data.dts ) || echo "[Pre-Setup] ERROR: Unpack failed." + run_windows_exe "$UNPACKER" -o data data.dts || echo "[Pre-Setup] ERROR: Unpack failed." else echo "[Pre-Setup] Step 1: Skipping unpack (no data folder and no data.dts found)." fi @@ -327,7 +341,7 @@ if [ -f "$ROOT_DIR/data.dts" ]; then if [ ! -d "$ROOT_DIR/patch" ]; then if [ -f "$ROOT_DIR/data/project.dat" ]; then echo "[Pre-Setup] Step 2: Creating patch from data/project.dat" - ( cd "$ROOT_DIR" && "$UNPACKER" ./data/project.dat -c ) || echo "[Pre-Setup] ERROR: Create Patch failed." + run_windows_exe "$UNPACKER" ./data/project.dat -c || echo "[Pre-Setup] ERROR: Create Patch failed." else echo "[Pre-Setup] Step 2: Skipping create patch (data/project.dat not found)." fi @@ -385,20 +399,17 @@ download_extract() { invoke_wolf_pre_setup echo "Applying patch..." - if ! cp -r "$inner"/* "$ROOT_DIR/"; then + if ! cp -rf "$inner"/* "$ROOT_DIR/"; then echo "Patch application failed!" rm -rf "$TMP_EX" rm -f "$ROOT_DIR/repo.zip" return 1 fi - rm -rf "$TMP_EX" - - echo "Cleaning up..." - rm -f "$ROOT_DIR/repo.zip" - # -------------------------------------------------------- # POST-APPLY: Run Steps 3 and 4 after patch files are merged + # (Must finish before "Cleaning up" so a slow wine/SRPG step is not + # mistaken for a hung cleanup - matches patch.ps1 order.) # -------------------------------------------------------- UNPACKER="$ROOT_DIR/SRPG_Unpacker.exe" if [ -f "$ROOT_DIR/data.dts" ]; then @@ -407,14 +418,14 @@ download_extract() { if [ -f "$ROOT_DIR/data/project.dat" ]; then echo "Step 3: Applying patch to data/project.dat" - ( cd "$ROOT_DIR" && "$UNPACKER" ./data/project.dat -a ) || echo "ERROR: Apply Patch failed." + run_windows_exe "$UNPACKER" ./data/project.dat -a || echo "ERROR: Apply Patch failed." else echo "ERROR: data/project.dat not found; cannot apply patch." fi if [ -d "$ROOT_DIR/data" ]; then echo "Step 4: Packing data -> data.dts" - ( cd "$ROOT_DIR" && "$UNPACKER" -o data.dts data ) || echo "WARNING: Pack failed." + run_windows_exe "$UNPACKER" -o data.dts data || echo "WARNING: Pack failed." else echo "Step 4: Skipping pack (data folder not found)." fi @@ -426,7 +437,19 @@ download_extract() { # Retire Data.wolf if loose Data/ is ready after the patch copy. invoke_wolf_pre_setup + echo "Cleaning up..." + rm -rf "$TMP_EX" + rm -f "$ROOT_DIR/repo.zip" + # Legacy / accidental leftovers from older patchers. + rm -rf "$ROOT_DIR/_patch_extract_tmp" + shopt -s nullglob + for stale in "$ROOT_DIR"/dazedmtl_patch_*.zip; do + rm -f "$stale" + done + shopt -u nullglob + echo "$latest_patch_sha" > "$STATE_FILE" + echo "Done." } # Check if previous_patch_sha.txt exists in gameupdate diff --git a/gui/translation_tab.py b/gui/translation_tab.py index d38f614..f13850f 100644 --- a/gui/translation_tab.py +++ b/gui/translation_tab.py @@ -74,6 +74,89 @@ BATCH_COLLECT_LIVE_CHARGE_NOTE = ( ) +class _ShimLabel: + """Plain stand-in for QLabel used by Files-tab row helpers (no real Qt widget).""" + + def __init__(self, text=""): + self._text = text + self._style = "" + self._visible = False + self._tooltip = "" + + def setText(self, text): + self._text = text or "" + + def text(self): + return self._text + + def setStyleSheet(self, style): + self._style = style or "" + + def setVisible(self, visible): + self._visible = bool(visible) + + def setToolTip(self, tip): + self._tooltip = tip or "" + + def toolTip(self): + return self._tooltip + + +class _ShimCheckBox: + def __init__(self): + self._checked = False + self._enabled = False + self._visible = False + + def setChecked(self, checked): + self._checked = bool(checked) + + def isChecked(self): + return self._checked + + def setEnabled(self, enabled): + self._enabled = bool(enabled) + + def setVisible(self, visible): + self._visible = bool(visible) + + +class _ShimProgressBar: + def __init__(self): + self._value = 0 + self._maximum = 100 + self._visible = False + self._text_visible = True + self._style = "" + + def setValue(self, value): + self._value = int(value) + + def setMaximum(self, maximum): + self._maximum = int(maximum) + + def setVisible(self, visible): + self._visible = bool(visible) + + def setTextVisible(self, visible): + self._text_visible = bool(visible) + + def setStyleSheet(self, style): + self._style = style or "" + + +class _ShimWidget: + def __init__(self): + self._visible = False + self._tooltip = "" + + def setVisible(self, visible): + self._visible = bool(visible) + + def setToolTip(self, tip): + self._tooltip = tip or "" + + class TranslationWorker(QThread): """Worker thread for running translations without blocking the UI.""" @@ -1162,31 +1245,12 @@ class TranslationTab(QWidget): self.batch_pipeline_stack.setStyleSheet("background: transparent;") self.batch_pipeline_stack.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.progress_list = QListWidget() - self.progress_list.setMinimumHeight(120) - self.progress_list.setSelectionMode(QListWidget.NoSelection) - self.progress_list.setFocusPolicy(Qt.NoFocus) - self.progress_list.setSpacing(1) # Minimal spacing between items - self.progress_list.setStyleSheet(""" - QListWidget { - outline: none; - border: 1px solid #555555; - border-radius: 3px; - background-color: #1e1e1e; - color: white; - font-size: 13px; - } - QListWidget::item { - padding: 0px; - border-bottom: 1px solid #333333; - } - QListWidget::item:last { - border-bottom: none; - } - """) - # Prefer a Batch-history-style table for per-file status; keep the list - # widget as a non-visible compatibility shim for any leftover references. + # Prefer a Batch-history-style table for per-file status. Keep a hidden, + # parented list as a compatibility shim so clear()/legacy refs stay safe + # without creating an orphan top-level window (minimize/restore glitches). + self.progress_list = QListWidget(self) self.progress_list.setVisible(False) + self.progress_list.setMaximumSize(0, 0) self.progress_files_summary = QLabel("No files in this run.") self.progress_files_summary.setStyleSheet("color:#9d9d9d;font-size:12px;padding:2px 0;") @@ -1785,6 +1849,16 @@ class TranslationTab(QWidget): self.files_tab_btn.setText("Files") self._batch_tab_index = -1 self.progress_content_stack.setCurrentIndex(1) + # Per-file Time is useful for live translate; for batch, Anthropic owns + # the wait and local write time is negligible. + table = getattr(self, "progress_table", None) + if table is not None and table.columnCount() > 5: + table.setColumnHidden(5, bool(batch_mode)) + try: + if hasattr(self, "totals_time_label"): + self.totals_time_label.setVisible(not batch_mode) + except Exception: + pass def _on_batch_submit_yes(self): if hasattr(self, "translation_worker") and self.translation_worker: @@ -2456,11 +2530,10 @@ class TranslationTab(QWidget): self._refresh_files_summary() def create_progress_item(self, filename): - """Add a Files-tab table row for a file and return a placeholder widget.""" + """Add a Files-tab table row for a file and return a placeholder shim.""" table = getattr(self, "progress_table", None) if table is None: - # Fallback empty widget if table is unavailable - return QWidget() + return _ShimWidget() row = table.rowCount() table.insertRow(row) @@ -2473,25 +2546,16 @@ class TranslationTab(QWidget): item.setData(Qt.UserRole, filename) table.setItem(row, col, item) - # Keep a small compatibility widget so older helpers that expect - # checkbox/label/progress_bar keys do not crash. - checkbox = QCheckBox() + # Non-Qt shims keep older helpers working without spawning orphan windows. + checkbox = _ShimCheckBox() checkbox.setEnabled(False) - checkbox.setVisible(False) - progress_label = QLabel("Waiting...") - progress_label.setVisible(False) - progress_bar = QProgressBar() - progress_bar.setVisible(False) - tokens_label = QLabel("") - tokens_label.setVisible(False) - cost_label = QLabel("") - cost_label.setVisible(False) - time_label = QLabel("") - time_label.setVisible(False) - status_label = QLabel("") - status_label.setVisible(False) - shim = QWidget() - shim.setVisible(False) + progress_label = _ShimLabel("Waiting...") + progress_bar = _ShimProgressBar() + tokens_label = _ShimLabel("") + cost_label = _ShimLabel("") + time_label = _ShimLabel("") + status_label = _ShimLabel("") + shim = _ShimWidget() self.file_progress_items[filename] = { "row": row, @@ -2650,7 +2714,7 @@ class TranslationTab(QWidget): filename, tokens=tokens or None, cost=cost or None, - time_s=time_s or None, + time_s=None if getattr(self, "_batch_active", False) else (time_s or None), ) def reset_to_file_view(self): @@ -3083,7 +3147,7 @@ class TranslationTab(QWidget): filename, tokens=tokens_text, cost=cost_text, - time_s=time_text, + time_s=None if getattr(self, "_batch_active", False) else time_text, ) except Exception: pass