Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .install/OpenQ4.icns
Binary file not shown.
Binary file removed .install/OpenQ4.ico
Binary file not shown.
Binary file removed .install/openQ4.icns
Binary file not shown.
Binary file removed .install/openQ4.ico
Binary file not shown.
1 change: 1 addition & 0 deletions docs/dev/release-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs/dev/releases/v0.12.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
21 changes: 21 additions & 0 deletions tools/tests/validation_hardening.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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()
Expand Down
55 changes: 45 additions & 10 deletions tools/validation/openq4_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent failed collision checks from uploading the payload

When this check detects a collision in a push-verification job, validation fails but the same invalid .install tree is still published because both staged-payload upload steps in .github/workflows/push-verification.yml use if: always() (lines 613-618 and 689-694). Thus a Linux-produced artifact can still contain the case-colliding paths and remain impossible to extract on Windows—the exact failure this change intends to prevent—so the payload upload must be gated on successful validation or explicitly skipped after this error.

Useful? React with 👍 / 👎.


client_candidates = find_engine_executables(install_root, "openQ4-client")
dedicated_candidates = find_engine_executables(install_root, "openQ4-ded")
Expand Down
Loading