diff --git a/.install/OpenQ4.icns b/.install/OpenQ4.icns deleted file mode 100644 index 0db67215..00000000 Binary files a/.install/OpenQ4.icns and /dev/null differ diff --git a/.install/OpenQ4.ico b/.install/OpenQ4.ico deleted file mode 100644 index e74c677f..00000000 Binary files a/.install/OpenQ4.ico and /dev/null differ diff --git a/.install/openQ4.icns b/.install/openQ4.icns deleted file mode 100644 index 0db67215..00000000 Binary files a/.install/openQ4.icns and /dev/null differ diff --git a/.install/openQ4.ico b/.install/openQ4.ico deleted file mode 100644 index e74c677f..00000000 Binary files a/.install/openQ4.ico and /dev/null differ diff --git a/docs/dev/release-completion.md b/docs/dev/release-completion.md index 0c686f0d..e05f104c 100644 --- a/docs/dev/release-completion.md +++ b/docs/dev/release-completion.md @@ -243,6 +243,7 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] Listen-server multiplayer clients now receive short-lived hit, impact, and explosion presentation events more reliably when PVS data is not available at the exact emit frame, and first-person view-weapon visuals update on the final client prediction step to reduce loopback weapon jitter. - [x] Content-only mods are easier to run: when a selected mod does not ship its own SP/MP game module, openQ4 now falls back to the matching `baseoq4` module instead of requiring copied dynamic libraries in every mod folder. - [x] Release packages now avoid debug-build performance costs on every platform: Windows release jobs moved from Meson `debug` to the optimized diagnostics profile already used by Linux and experimental macOS, release builds disable runtime asserts with `b_ndebug=true`, and symbol artifacts remain available through Windows PDBs plus Linux debug-symbol archives. +- [x] Linux packages no longer carry obsolete staged icon copies whose names differ only by case, allowing the archives to extract on Windows and other case-insensitive filesystems; staged validation now rejects case-insensitive path collisions on every platform before CI uploads an artifact. - [x] Follow-up Strogg flyer/fighter destruction fix: map-authored `func_fx` explosions now service newly submitted BSE effects immediately like retail Quake 4, so one-shot explosion, smoke, sound, and debris bursts are no longer skipped before their first visible frame. - [x] Single-player tram combat is more stable in debug and local validation builds: long straight AI projectile aim probes are split into collision-safe segments, preventing the `game/tram1b` tram-rail fight from tripping the debug `Clip.cpp` huge-translation assertion while keeping the collision guard active for invalid oversized sweeps. - [x] Validation and release tooling now rejects more unsafe source inputs before build/package work starts: source roots, GameLibs roots, Meson-discovered source files, Windows runtime staging paths, OpenAL overrides, staged GameLibs manifests, macOS signing files, and documentation links/sources fail closed on symlinks, missing files, or root escapes; legacy Linux shell helpers also pass broad `bash -n` syntax checks. diff --git a/docs/dev/releases/v0.12.0.md b/docs/dev/releases/v0.12.0.md index a8634e40..f03d4971 100644 --- a/docs/dev/releases/v0.12.0.md +++ b/docs/dev/releases/v0.12.0.md @@ -25,6 +25,7 @@ - Replace the complete openQ4 package for your platform so the engine, renderer modules, `game_sp`, `game_mp`, and bundled openQ4 data all come from 0.12.0. Do not mix modules from older releases. - Mac users should replace the complete package to receive the bundled OpenAL Soft runtime; no separate OpenAL installation is needed. The package includes its corresponding licence notices and source offer. +- Linux packages no longer contain duplicate root icons whose names differ only by letter case, so the same archive can be extracted safely on Windows and other case-insensitive filesystems. - Existing settings and compatible saves do not need to be reset. As a precaution, retain a copy of important saves before replacing an older installation. - The exact older broad shadow-filter defaults are upgraded once to the new balanced contact-shadow values; customized shadow profiles are left untouched. `r_shadowMapCasterCulling 2` remains the recommended value but now means topology-aware automatic culling rather than far-shell storage. Use `0` for an always-two-sided comparison or `1` to force near-shell culling. - Level-load data under the active `fs_savepath` `generated/` directory is private and disposable. `com_levelLoadModernization` defaults to `0` and overrides older archived cache settings, so ordinary play uses the classic source path. Set it to `1` only for focused cache evaluation. Never delete the original retail or mod assets. @@ -66,6 +67,7 @@ - Added complete Polish and Russian UI tables, Unicode code-point drawing, CP1250 conversion support, and stricter generated-font checksum validation. - Restored the marine hovertank engine and hover-pad loops with null-safe sound-emitter teardown. - Bundled checksum-pinned OpenAL Soft 1.25.1 in macOS release and validation packages, retained Apple's OpenAL framework only as an explicit compatibility-build option, and made backend buffer allocation or upload failures fail gracefully rather than terminating the session. +- Removed obsolete tracked copies of generated package icons and extended staged-payload validation to reject case-insensitive path collisions on every platform before an artifact is uploaded. - Prevented partial attachment restoration from tearing down incompletely restored entities twice. - Restored Arena Campaign countdown and scoring progression when ready-up is disabled. - Added multiplayer bot controls and validation to the server setup menu. diff --git a/tools/tests/validation_hardening.py b/tools/tests/validation_hardening.py index 0f88ac8c..c50b6e26 100644 --- a/tools/tests/validation_hardening.py +++ b/tools/tests/validation_hardening.py @@ -281,6 +281,25 @@ def validate_recursive_non_runtime_scan() -> None: ) +def validate_staged_casefold_collision_guard() -> None: + collision = VALIDATOR.first_staged_casefold_path_collision( + ["baseoq4/pak0.pk4", "OpenQ4.icns", "openQ4.icns"] + ) + if collision != ("OpenQ4.icns", "openQ4.icns"): + raise AssertionError(f"unexpected casefold collision result: {collision!r}") + + root = WORK / "casefold-payload" + install_root = root / ".install" + write_file(install_root / "OpenQ4.icns") + if not (install_root / "openQ4.icns").exists(): + write_file(install_root / "openQ4.icns") + expect_validation_error( + lambda: VALIDATOR.validate_no_staged_casefold_path_collisions(root, install_root), + "case-insensitive duplicate paths", + "staged casefold collision guard", + ) + + def validate_engine_architecture_mismatch() -> None: root = WORK / "engine-arch" install_root = root / ".install" @@ -598,6 +617,7 @@ def validate_validation_wiring() -> None: "validate_game_libs_repo_path", "validate_build_dir", "validate_no_staged_symlinks", + "validate_no_staged_casefold_path_collisions", "validate_staged_architecture_set", "validate_distinct_game_modules", "validate_linux_client_runtime_dependencies", @@ -694,6 +714,7 @@ def main() -> None: validate_game_libs_repo_guards() validate_staged_symlink_guard() validate_recursive_non_runtime_scan() + validate_staged_casefold_collision_guard() validate_engine_architecture_mismatch() validate_game_module_suffix_guard() validate_game_module_architecture_match() diff --git a/tools/validation/openq4_validate.py b/tools/validation/openq4_validate.py index 303c4aa2..059fea42 100644 --- a/tools/validation/openq4_validate.py +++ b/tools/validation/openq4_validate.py @@ -777,23 +777,57 @@ def validate_no_macos_non_runtime_artifacts(root: Path, install_root: Path, game ) -def macos_casefold_path_key(path: str) -> str: +def staged_casefold_path_key(path: str) -> str: return unicodedata.normalize("NFC", path).casefold() -def validate_no_macos_casefold_path_collisions(root: Path, install_root: Path) -> None: +def first_staged_casefold_path_collision(paths: list[str]) -> tuple[str, str] | None: seen_paths: dict[str, str] = {} - for path in sorted(install_root.rglob("*")): - relative = path.relative_to(install_root).as_posix() - key = macos_casefold_path_key(relative) + for relative in sorted(paths): + key = staged_casefold_path_key(relative) previous = seen_paths.get(key) if previous is not None and previous != relative: - raise ValidationError( - "macOS staged payload contains case-insensitive duplicate paths:\n" - f" - {rel(install_root / previous, root)}\n" - f" - {rel(path, root)}" - ) + return previous, relative seen_paths[key] = relative + return None + + +def validate_no_staged_casefold_path_collisions(root: Path, install_root: Path) -> None: + relative_paths = [ + path.relative_to(install_root).as_posix() + for path in install_root.rglob("*") + ] + collision = first_staged_casefold_path_collision(relative_paths) + if collision is None: + return + + previous, relative = collision + raise ValidationError( + "Staged payload contains case-insensitive duplicate paths:\n" + f" - {rel(install_root / previous, root)}\n" + f" - {rel(install_root / relative, root)}" + ) + + +def macos_casefold_path_key(path: str) -> str: + return staged_casefold_path_key(path) + + +def validate_no_macos_casefold_path_collisions(root: Path, install_root: Path) -> None: + relative_paths = [ + path.relative_to(install_root).as_posix() + for path in install_root.rglob("*") + ] + collision = first_staged_casefold_path_collision(relative_paths) + if collision is None: + return + + previous, relative = collision + raise ValidationError( + "macOS staged payload contains case-insensitive duplicate paths:\n" + f" - {rel(install_root / previous, root)}\n" + f" - {rel(install_root / relative, root)}" + ) def validate_no_staged_symlinks(root: Path, install_root: Path) -> None: @@ -1413,6 +1447,7 @@ def validate_staged_payload(root: Path, *, dry_run: bool, build_dir: Path | None raise ValidationError(f"Staged game directory must not be a symlink: {game_dir}") if not game_dir.is_dir(): raise ValidationError(f"Staged game directory is missing: {game_dir}") + validate_no_staged_casefold_path_collisions(root, install_root) client_candidates = find_engine_executables(install_root, "openQ4-client") dedicated_candidates = find_engine_executables(install_root, "openQ4-ded")