diff --git a/Android/README.md b/Android/README.md index 23c5d88..ac85ac5 100644 --- a/Android/README.md +++ b/Android/README.md @@ -1,5 +1,5 @@ - + # Android @@ -20,7 +20,7 @@ Chaque module `game-*` est une application Android indépendante ; il dépend du ## Toolchain de référence -Pour la baseline `0.1.0-0-pre.8` : +Pour la baseline `0.1.0-0-pre.9` : - Android Gradle Plugin `9.4.0` ; - Gradle `9.6.0` attendu par AGP 9.4 ; @@ -73,6 +73,21 @@ Le script extrait temporairement `libSDL3.so` de l'AAR uniquement pour fournir l ## Bridge JNI et tactile -À partir de `0.1.0-0-pre.8`, `SaseGameActivity` vérifie au démarrage la version du bridge Java/JNI chargé depuis la bibliothèque Rust. +À partir de `0.1.0-0-pre.9`, `SaseGameActivity` vérifie au démarrage la version du bridge Java/JNI chargé depuis la bibliothèque Rust. Les touch events standards restent traités via SDL3 et ne transitent pas par JNI. + +## Packaging des assets + +Les modules applications exécutent `stageSasedevAssets` avant `preBuild`. + +La tâche compose les sources : + +```text +assets/common/ -> assets/common/ +assets// -> assets/game/ +``` + +dans un répertoire généré sous `Android//build/generated/sasedevAssets/main/`. + +Aucune ressource source n'est copiée durablement dans un module Android ou une crate Rust. diff --git a/Android/game-reflex-poc/build.gradle b/Android/game-reflex-poc/build.gradle index 6548066..582b2b3 100644 --- a/Android/game-reflex-poc/build.gradle +++ b/Android/game-reflex-poc/build.gradle @@ -1,5 +1,5 @@ // file: Android/game-reflex-poc/build.gradle -// version: 6 +// version: 7 plugins { id 'com.android.application' @@ -16,7 +16,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.8' + versionName '0.1.0-0-pre.9' } compileOptions { @@ -29,3 +29,6 @@ dependencies { implementation files("../libs/${sdl3AarName}") implementation project(':common') } + +ext.sasedevGameAssetDirectory = "game-reflex-poc" +apply from: rootProject.file("gradle/sasedev-assets.gradle") diff --git a/Android/game-snake-poc/build.gradle b/Android/game-snake-poc/build.gradle index 3d1fcb6..9e25e3b 100644 --- a/Android/game-snake-poc/build.gradle +++ b/Android/game-snake-poc/build.gradle @@ -1,5 +1,5 @@ // file: Android/game-snake-poc/build.gradle -// version: 6 +// version: 7 plugins { id 'com.android.application' @@ -16,7 +16,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.8' + versionName '0.1.0-0-pre.9' } compileOptions { @@ -29,3 +29,6 @@ dependencies { implementation files("../libs/${sdl3AarName}") implementation project(':common') } + +ext.sasedevGameAssetDirectory = "game-snake-poc" +apply from: rootProject.file("gradle/sasedev-assets.gradle") diff --git a/Android/gradle/sasedev-assets.gradle b/Android/gradle/sasedev-assets.gradle new file mode 100644 index 0000000..b191233 --- /dev/null +++ b/Android/gradle/sasedev-assets.gradle @@ -0,0 +1,22 @@ +// file: Android/gradle/sasedev-assets.gradle +// version: 1 + +def generatedAssetsDirectory = layout.buildDirectory.dir("generated/sasedevAssets/main") +def sourceCommonAssets = rootProject.file("../assets/common") +def sourceGameAssets = rootProject.file("../assets/${project.ext.sasedevGameAssetDirectory}") + +tasks.register("stageSasedevAssets", Sync) { + into(generatedAssetsDirectory) + from(sourceCommonAssets) { + into("common") + } + from(sourceGameAssets) { + into("game") + } +} + +android.sourceSets.main.assets.srcDir(generatedAssetsDirectory) + +tasks.named("preBuild").configure { + dependsOn(tasks.named("stageSasedevAssets")) +} diff --git a/Cargo.toml b/Cargo.toml index 4081c6c..5da9746 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 16 +# version: 17 [workspace] resolver = "3" @@ -11,12 +11,13 @@ members = [ "crates/games/game-snake-poc", "crates/apps/game-reflex-poc-desktop", "crates/apps/game-snake-poc-desktop", + "crates/common/game-assets-lib", "crates/common/game-logging-lib", "crates/apps/game-android-entrypoint", ] [workspace.package] -version = "0.1.0-0-pre.8" +version = "0.1.0-0-pre.9" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/README.md b/README.md index 52d067c..c67c716 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - + # games.sasedev @@ -39,4 +39,4 @@ Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-sna ## Diagnostics et tests -Le socle transverse `crates/common/game-logging-lib` fournit le tracing commun. Les tests unitaires résident hors `src/` sous `unit_tests/`; les tests d’intégration/environnement résident sous `tests/`. +Les socles transverses `crates/common/game-assets-lib` et `crates/common/game-logging-lib` fournissent respectivement la résolution logique des assets et le tracing commun. Les tests unitaires résident hors `src/` sous `unit_tests/`; les tests d’intégration/environnement résident sous `tests/`. diff --git a/ROADMAP.md b/ROADMAP.md index 4588fcc..28f82eb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # Roadmap @@ -11,8 +11,8 @@ - [x] `0-pre.4` — architecture de tests hors `src/`, stratégie de tests ciblés et socle `tracing`/`tracing-subscriber`/`tracing-appender`. - [x] `0-pre.5` — intégration SDL3 Desktop réelle et premier rendu POC Reflex. - [x] `0-pre.6` — fondation Gradle Android, SDL3 AAR, Activity Java commune et politique de fenêtre Desktop redimensionnable. -- [ ] `0-pre.7` — compilation Rust Android `cdylib`, point d'entrée SDL Android et premier lancement APK sur appareil/émulateur. -- [ ] `0-pre.8` — bridge Java/JNI minimal et input tactile Android. +- [x] `0-pre.7` — compilation Rust Android `cdylib`, point d'entrée SDL Android et premier lancement APK sur appareil/émulateur. +- [x] `0-pre.8` — bridge Java/JNI minimal et input tactile Android. - [ ] `0-pre.9` — assets communs + spécifiques empaquetés sans copie dans les crates. - [ ] `0-pre.10` — POC Reflex jouable Desktop + Android. - [ ] `0-pre.11` — POC Snake jouable et validation de la réutilisation du moteur. diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 0000000..84b0b0f --- /dev/null +++ b/assets/README.md @@ -0,0 +1,31 @@ + + + +# Assets source + +Ce répertoire est l'unique source versionnée des ressources runtime. + +```text +assets/ +├── common/ +├── game-reflex-poc/ +└── game-snake-poc/ +``` + +Les crates Rust ne contiennent aucune copie source des assets. + +Le layout de package canonique est : + +```text +common/ +game/ +``` + +Ainsi : + +```text +common://data/runtime.json -> common/data/runtime.json +game://data/game.json -> game/data/game.json +``` + +Les répertoires de staging générés par Desktop ou Android ne sont pas des sources et ne doivent pas être commités. diff --git a/assets/common/data/runtime.json b/assets/common/data/runtime.json new file mode 100644 index 0000000..59d262e --- /dev/null +++ b/assets/common/data/runtime.json @@ -0,0 +1,5 @@ +{ + "schema": 1, + "scope": "common", + "engine_generation": 1 +} diff --git a/assets/game-reflex-poc/data/game.json b/assets/game-reflex-poc/data/game.json new file mode 100644 index 0000000..8533e83 --- /dev/null +++ b/assets/game-reflex-poc/data/game.json @@ -0,0 +1,5 @@ +{ + "schema": 1, + "game": "game-reflex-poc", + "kind": "reflex" +} diff --git a/assets/game-snake-poc/data/game.json b/assets/game-snake-poc/data/game.json new file mode 100644 index 0000000..9951165 --- /dev/null +++ b/assets/game-snake-poc/data/game.json @@ -0,0 +1,5 @@ +{ + "schema": 1, + "game": "game-snake-poc", + "kind": "snake" +} diff --git a/crates/common/game-assets-lib/Cargo.toml b/crates/common/game-assets-lib/Cargo.toml new file mode 100644 index 0000000..f51dbda --- /dev/null +++ b/crates/common/game-assets-lib/Cargo.toml @@ -0,0 +1,17 @@ +# file: crates/common/game-assets-lib/Cargo.toml +# version: 1 + +[package] +name = "game-assets-lib" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +publish.workspace = true + +[dev-dependencies] +game-logging-lib = { path = "../game-logging-lib" } + +[lints] +workspace = true diff --git a/crates/common/game-assets-lib/src/error.rs b/crates/common/game-assets-lib/src/error.rs new file mode 100644 index 0000000..5243287 --- /dev/null +++ b/crates/common/game-assets-lib/src/error.rs @@ -0,0 +1,25 @@ +// file: crates/common/game-assets-lib/src/error.rs +// version: 1 + +/// Asset URI validation or resolution failure. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum AssetError { + /// The URI does not use a supported logical asset namespace. + UnsupportedScheme, + /// The URI contains no relative asset path. + EmptyPath, + /// The URI attempts to escape its logical asset namespace. + InvalidPath, +} + +impl core::fmt::Display for AssetError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + return match self { + crate::AssetError::UnsupportedScheme => formatter.write_str("unsupported asset URI scheme"), + crate::AssetError::EmptyPath => formatter.write_str("asset URI path is empty"), + crate::AssetError::InvalidPath => formatter.write_str("asset URI path is invalid"), + }; + } +} + +impl std::error::Error for AssetError {} diff --git a/crates/common/game-assets-lib/src/lib.rs b/crates/common/game-assets-lib/src/lib.rs new file mode 100644 index 0000000..8957b59 --- /dev/null +++ b/crates/common/game-assets-lib/src/lib.rs @@ -0,0 +1,16 @@ +// file: crates/common/game-assets-lib/src/lib.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Logical asset URI validation and source-root resolution for games.sasedev. + +mod error; +mod resolver; + +/// Re-export of asset URI and resolution failures. +pub use self::error::AssetError; +/// Re-export of the source-root asset resolver. +pub use self::resolver::AssetResolver; diff --git a/crates/common/game-assets-lib/src/resolver.rs b/crates/common/game-assets-lib/src/resolver.rs new file mode 100644 index 0000000..89cace1 --- /dev/null +++ b/crates/common/game-assets-lib/src/resolver.rs @@ -0,0 +1,45 @@ +// file: crates/common/game-assets-lib/src/resolver.rs +// version: 1 + +/// Resolves logical asset URIs against separate common and game roots. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetResolver { + common_root: std::path::PathBuf, + game_root: std::path::PathBuf, +} + +impl AssetResolver { + /// Creates a resolver from physical common and game asset roots. + #[must_use] + pub fn new(common_root: impl Into, game_root: impl Into) -> Self { + return Self { common_root: common_root.into(), game_root: game_root.into() }; + } + + /// Resolves `common://` or `game://` URI into a physical path without touching the filesystem. + pub fn resolve(&self, uri: &str) -> Result { + let (root, relative) = if let Some(relative) = uri.strip_prefix("common://") { + (&self.common_root, relative) + } else if let Some(relative) = uri.strip_prefix("game://") { + (&self.game_root, relative) + } else { + return Err(crate::AssetError::UnsupportedScheme); + }; + if relative.is_empty() { + return Err(crate::AssetError::EmptyPath); + } + let relative_path = std::path::Path::new(relative); + if relative_path.is_absolute() || relative.contains('\\') { + return Err(crate::AssetError::InvalidPath); + } + for component in relative_path.components() { + if !matches!(component, std::path::Component::Normal(_)) { + return Err(crate::AssetError::InvalidPath); + } + } + return Ok(root.join(relative_path)); + } +} + +#[cfg(test)] +#[path = "../unit_tests/resolver.rs"] +mod tests; diff --git a/crates/common/game-assets-lib/unit_tests/resolver.rs b/crates/common/game-assets-lib/unit_tests/resolver.rs new file mode 100644 index 0000000..3ca5256 --- /dev/null +++ b/crates/common/game-assets-lib/unit_tests/resolver.rs @@ -0,0 +1,21 @@ +// file: crates/common/game-assets-lib/unit_tests/resolver.rs +// version: 1 + +#[test] +fn logical_namespaces_resolve_to_separate_roots() { + game_logging_lib::with_test_tracing(|| { + let resolver = crate::AssetResolver::new("assets/common", "assets/game-reflex-poc"); + assert_eq!(resolver.resolve("common://data/runtime.json"), Ok(std::path::PathBuf::from("assets/common/data/runtime.json"))); + assert_eq!(resolver.resolve("game://data/game.json"), Ok(std::path::PathBuf::from("assets/game-reflex-poc/data/game.json"))); + }); +} + +#[test] +fn traversal_and_unknown_schemes_are_rejected() { + game_logging_lib::with_test_tracing(|| { + let resolver = crate::AssetResolver::new("assets/common", "assets/game-reflex-poc"); + assert_eq!(resolver.resolve("game://../secret"), Err(crate::AssetError::InvalidPath)); + assert_eq!(resolver.resolve("file://data/game.json"), Err(crate::AssetError::UnsupportedScheme)); + assert_eq!(resolver.resolve("common://"), Err(crate::AssetError::EmptyPath)); + }); +} diff --git a/deltas/0.1.0/0-pre.9.md b/deltas/0.1.0/0-pre.9.md new file mode 100644 index 0000000..ae543cf --- /dev/null +++ b/deltas/0.1.0/0-pre.9.md @@ -0,0 +1,147 @@ + + + +# Delta 0.1.0-0-pre.9 + +## Base + +Base validée : `0.1.0-0-pre.8`. + +## Objectif + +Stabiliser la source, les namespaces, la résolution et le packaging multi-plateforme des assets avant le premier gameplay visuel complet. + +## Source unique + +Les ressources versionnées restent exclusivement sous : + +```text +assets/common/ +assets/game-reflex-poc/ +assets/game-snake-poc/ +``` + +Aucune crate Rust et aucun module Android ne reçoit de copie source durable. + +## game-assets-lib + +Nouveau crate transverse : + +```text +crates/common/game-assets-lib +``` + +Il fournit `AssetResolver` et `AssetError`. + +Namespaces canoniques : + +```text +common://... +game://... +``` + +Le resolver rejette les chemins vides, absolus, traversants et les schemes inconnus. + +## Ressources de validation + +Premiers assets réels : + +```text +assets/common/data/runtime.json +assets/game-reflex-poc/data/game.json +assets/game-snake-poc/data/game.json +``` + +Ils servent de contrats de packaging, pas de contenu graphique définitif. + +## Desktop + +```bash +python3 scripts/stage_game_assets.py reflex +python3 scripts/stage_game_assets.py snake +``` + +Le layout généré est : + +```text +common/ +game/ +``` + +sous `../builds/sasedev-games/assets//` par défaut. + +## Android + +Chaque application utilise la tâche Gradle commune `stageSasedevAssets`. + +Le package final doit contenir : + +```text +assets/common/data/runtime.json +assets/game/data/game.json +``` + +sans inclure les assets de l'autre jeu. + +## Validation Rust + +```bash +cargo fmt --all +cargo fmt --all -- --check + +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android deltas history + +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings + +cargo test -p game-assets-lib --all-targets --all-features +``` + +## Validation staging Desktop + +```bash +python3 scripts/stage_game_assets.py reflex +python3 scripts/stage_game_assets.py snake + +test -f ../builds/sasedev-games/assets/game-reflex-poc/common/data/runtime.json +test -f ../builds/sasedev-games/assets/game-reflex-poc/game/data/game.json +test -f ../builds/sasedev-games/assets/game-snake-poc/common/data/runtime.json +test -f ../builds/sasedev-games/assets/game-snake-poc/game/data/game.json +``` + +## Validation Android + +Les bibliothèques natives de `0-pre.8` peuvent être réutilisées car ce delta ne modifie pas le code natif Android. + +```bash +cd Android + +gradle :game-reflex-poc:assembleDebug +gradle :game-snake-poc:assembleDebug + +cd .. +``` + +Vérifier le contenu : + +```bash +unzip -l Android/game-reflex-poc/build/outputs/apk/debug/game-reflex-poc-debug.apk | grep 'assets/common/data/runtime.json' +unzip -l Android/game-reflex-poc/build/outputs/apk/debug/game-reflex-poc-debug.apk | grep 'assets/game/data/game.json' + +unzip -l Android/game-snake-poc/build/outputs/apk/debug/game-snake-poc-debug.apk | grep 'assets/common/data/runtime.json' +unzip -l Android/game-snake-poc/build/outputs/apk/debug/game-snake-poc-debug.apk | grep 'assets/game/data/game.json' +``` + +Vérifier aussi qu'aucun package n'embarque le namespace source de l'autre jeu : + +```bash +! unzip -l Android/game-reflex-poc/build/outputs/apk/debug/game-reflex-poc-debug.apk | grep -q 'game-snake-poc' +! unzip -l Android/game-snake-poc/build/outputs/apk/debug/game-snake-poc-debug.apk | grep -q 'game-reflex-poc' +``` + +## Règle de transition + +Si ces gates sont propres, passer automatiquement à `0.1.0-0-pre.10` pour rendre le POC Reflex réellement jouable Desktop + Android. + +En cas d'échec imputable au projet, produire `0.1.0-0-pre.9.fix.1`. diff --git a/docs/000-README.md b/docs/000-README.md index 67bd0ba..9310092 100644 --- a/docs/000-README.md +++ b/docs/000-README.md @@ -1,5 +1,5 @@ - + # Documentation games.sasedev diff --git a/docs/architecture/001-WORKSPACE_ARCHITECTURE.md b/docs/architecture/001-WORKSPACE_ARCHITECTURE.md index 68f728f..d5ae7b9 100644 --- a/docs/architecture/001-WORKSPACE_ARCHITECTURE.md +++ b/docs/architecture/001-WORKSPACE_ARCHITECTURE.md @@ -1,5 +1,5 @@ - + # Architecture du workspace @@ -8,6 +8,9 @@ ```text games.sasedev/ ├── crates/ +│ ├── common/ +│ │ ├── game-assets-lib/ +│ │ └── game-logging-lib/ │ ├── engines/ │ │ ├── engine-v1-common/ │ │ ├── engine-v1-platform-api/ @@ -36,7 +39,9 @@ games.sasedev/ Une génération `engine-vN` est une ligne de compatibilité. Un jeu peut rester sur `engine-v1` pendant qu'un nouveau jeu expérimente `engine-v2`. Les anciens jeux sont ensuite migrés individuellement. -Les subdivisions sont exprimées par crates afin de ne pas forcer un jeu à dépendre de capacités inutiles. La baseline sépare déjà : +Les capacités transverses réutilisables sont séparées des générations moteur. `game-assets-lib` porte la résolution logique des assets et `game-logging-lib` le tracing commun. + +Les subdivisions moteur sont exprimées par crates afin de ne pas forcer un jeu à dépendre de capacités inutiles. La baseline sépare déjà : - `engine-v1-common` : types et comportements génériques indépendants des plateformes ; - `engine-v1-platform-api` : contrats abstraits des services plateforme ; diff --git a/docs/architecture/005-ASSET_ARCHITECTURE.md b/docs/architecture/005-ASSET_ARCHITECTURE.md index 0a7d53f..4f986ae 100644 --- a/docs/architecture/005-ASSET_ARCHITECTURE.md +++ b/docs/architecture/005-ASSET_ARCHITECTURE.md @@ -1,5 +1,5 @@ - + # Architecture des assets @@ -47,3 +47,48 @@ Desktop, Android et Web peuvent utiliser des mécanismes de packaging différent ## Évolution Un AssetManager commun pourra ultérieurement prendre en charge cache, loaders, textures, audio, fonts, données, erreurs, hot-reload de développement et éventuellement bundles. Ces capacités ne doivent être ajoutées qu'au rythme des besoins réels des jeux. + +## Contrat V1 concret + +À partir de `0.1.0-0-pre.9`, les URI logiques canoniques sont : + +```text +common:// +game:// +``` + +`game-assets-lib` valide ces URI et les résout à partir de deux racines physiques explicites. Il interdit les chemins absolus, les traversées `..` et les séparateurs Windows injectés dans une URI logique. + +Le layout runtime généré est identique sur toutes les plateformes : + +```text +common/ +game/ +``` + +## Desktop + +Le script `scripts/stage_game_assets.py` compose un package généré hors des sources. + +Par défaut : + +```text +../builds/sasedev-games/assets// +├── common/ +└── game/ +``` + +Le développement peut aussi résoudre directement les deux racines source sans staging. + +## Android + +Une tâche Gradle `stageSasedevAssets` compose les assets sous `build/generated/` avant `preBuild`. + +Le contenu final de l'APK conserve les namespaces : + +```text +assets/common/... +assets/game/... +``` + +Les répertoires `build/generated/` restent des artefacts jetables. diff --git a/history/0.1.0/0-pre.8.md b/history/0.1.0/0-pre.8.md new file mode 100644 index 0000000..56743f2 --- /dev/null +++ b/history/0.1.0/0-pre.8.md @@ -0,0 +1,38 @@ + + + +# Historique 0.1.0-0-pre.8 + +## Statut + +Validé par l'utilisateur le 2026-09-16. + +## Gates validées + +Après `cargo clean` : + +- `cargo fmt --all` ; +- `cargo fmt --all -- --check` ; +- audits Rust/workspace et Markdown ; +- `cargo check --workspace` ; +- Clippy global strict ; +- tests ciblés `engine-v1-common`, `engine-v1-sdl` et `game-android-entrypoint` ; +- builds Rust Android Reflex et Snake ; +- builds APK Reflex et Snake. + +## Vérification native + +`readelf -Ws` confirme dans les deux bibliothèques Android : + +```text +SDL_main +Java_com_sasedev_games_common_NativeBridge_nativeContractVersion +``` + +## Limite + +Aucun appareil ou émulateur n'était attaché à `adb` ; le smoke runtime Android reste donc non exécuté. + +## Portée durable + +Ce jalon valide `PointerState`, le mapping tactile SDL3, le contrat Java/JNI V1 et son export natif dans les deux applications. diff --git a/scripts/stage_game_assets.py b/scripts/stage_game_assets.py new file mode 100644 index 0000000..eab7ba5 --- /dev/null +++ b/scripts/stage_game_assets.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# file: scripts/stage_game_assets.py +# version: 1 + +"""Stage common and game-specific source assets into the canonical runtime layout.""" + +import argparse +import pathlib +import shutil + +GAME_DIRECTORIES = { + "reflex": "game-reflex-poc", + "snake": "game-snake-poc", +} + +def copy_tree(source: pathlib.Path, destination: pathlib.Path) -> None: + """Copy one source asset tree into one generated destination tree.""" + if destination.exists(): + shutil.rmtree(destination) + shutil.copytree(source, destination) + +def main() -> int: + """Stage one game's assets and print the generated root.""" + parser = argparse.ArgumentParser() + parser.add_argument("game", choices=sorted(GAME_DIRECTORIES)) + parser.add_argument("--output") + arguments = parser.parse_args() + root = pathlib.Path(__file__).resolve().parent.parent + game_directory = GAME_DIRECTORIES[arguments.game] + output = pathlib.Path(arguments.output).resolve() if arguments.output else (root.parent / "builds" / "sasedev-games" / "assets" / game_directory) + if output.exists(): + shutil.rmtree(output) + output.mkdir(parents=True, exist_ok=True) + copy_tree(root / "assets" / "common", output / "common") + copy_tree(root / "assets" / game_directory, output / "game") + print(output) + return 0 + +if __name__ == "__main__": + raise SystemExit(main())