0.1.0-0-pre.9

This commit is contained in:
2026-09-16 10:01:13 +02:00
parent 00808ea5a6
commit 4fcbdf316d
22 changed files with 507 additions and 18 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: Android/README.md --> <!-- file: Android/README.md -->
<!-- version: 5 --> <!-- version: 6 -->
# Android # Android
@@ -20,7 +20,7 @@ Chaque module `game-*` est une application Android indépendante ; il dépend du
## Toolchain de référence ## 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` ; - Android Gradle Plugin `9.4.0` ;
- Gradle `9.6.0` attendu par AGP 9.4 ; - 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 ## 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. 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/<game>/ -> assets/game/
```
dans un répertoire généré sous `Android/<module>/build/generated/sasedevAssets/main/`.
Aucune ressource source n'est copiée durablement dans un module Android ou une crate Rust.

View File

@@ -1,5 +1,5 @@
// file: Android/game-reflex-poc/build.gradle // file: Android/game-reflex-poc/build.gradle
// version: 6 // version: 7
plugins { plugins {
id 'com.android.application' id 'com.android.application'
@@ -16,7 +16,7 @@ android {
minSdk 21 minSdk 21
targetSdk 36 targetSdk 36
versionCode 1 versionCode 1
versionName '0.1.0-0-pre.8' versionName '0.1.0-0-pre.9'
} }
compileOptions { compileOptions {
@@ -29,3 +29,6 @@ dependencies {
implementation files("../libs/${sdl3AarName}") implementation files("../libs/${sdl3AarName}")
implementation project(':common') implementation project(':common')
} }
ext.sasedevGameAssetDirectory = "game-reflex-poc"
apply from: rootProject.file("gradle/sasedev-assets.gradle")

View File

@@ -1,5 +1,5 @@
// file: Android/game-snake-poc/build.gradle // file: Android/game-snake-poc/build.gradle
// version: 6 // version: 7
plugins { plugins {
id 'com.android.application' id 'com.android.application'
@@ -16,7 +16,7 @@ android {
minSdk 21 minSdk 21
targetSdk 36 targetSdk 36
versionCode 1 versionCode 1
versionName '0.1.0-0-pre.8' versionName '0.1.0-0-pre.9'
} }
compileOptions { compileOptions {
@@ -29,3 +29,6 @@ dependencies {
implementation files("../libs/${sdl3AarName}") implementation files("../libs/${sdl3AarName}")
implementation project(':common') implementation project(':common')
} }
ext.sasedevGameAssetDirectory = "game-snake-poc"
apply from: rootProject.file("gradle/sasedev-assets.gradle")

View File

@@ -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"))
}

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml # file: Cargo.toml
# version: 16 # version: 17
[workspace] [workspace]
resolver = "3" resolver = "3"
@@ -11,12 +11,13 @@ members = [
"crates/games/game-snake-poc", "crates/games/game-snake-poc",
"crates/apps/game-reflex-poc-desktop", "crates/apps/game-reflex-poc-desktop",
"crates/apps/game-snake-poc-desktop", "crates/apps/game-snake-poc-desktop",
"crates/common/game-assets-lib",
"crates/common/game-logging-lib", "crates/common/game-logging-lib",
"crates/apps/game-android-entrypoint", "crates/apps/game-android-entrypoint",
] ]
[workspace.package] [workspace.package]
version = "0.1.0-0-pre.8" version = "0.1.0-0-pre.9"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/games" repository = "https://git.sasedev.com/Sasedev/games"

View File

@@ -1,5 +1,5 @@
<!-- file: README.md --> <!-- file: README.md -->
<!-- version: 4 --> <!-- version: 5 -->
# games.sasedev # games.sasedev
@@ -39,4 +39,4 @@ Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-sna
## Diagnostics et tests ## 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 dinté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 dintégration/environnement résident sous `tests/`.

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 6 --> <!-- version: 7 -->
# Roadmap # 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.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.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. - [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. - [x] `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.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.9` — assets communs + spécifiques empaquetés sans copie dans les crates.
- [ ] `0-pre.10` — POC Reflex jouable Desktop + Android. - [ ] `0-pre.10` — POC Reflex jouable Desktop + Android.
- [ ] `0-pre.11` — POC Snake jouable et validation de la réutilisation du moteur. - [ ] `0-pre.11` — POC Snake jouable et validation de la réutilisation du moteur.

31
assets/README.md Normal file
View File

@@ -0,0 +1,31 @@
<!-- file: assets/README.md -->
<!-- version: 1 -->
# 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/<relative-path>
game/<relative-path>
```
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.

View File

@@ -0,0 +1,5 @@
{
"schema": 1,
"scope": "common",
"engine_generation": 1
}

View File

@@ -0,0 +1,5 @@
{
"schema": 1,
"game": "game-reflex-poc",
"kind": "reflex"
}

View File

@@ -0,0 +1,5 @@
{
"schema": 1,
"game": "game-snake-poc",
"kind": "snake"
}

View File

@@ -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

View File

@@ -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 {}

View File

@@ -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;

View File

@@ -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<std::path::PathBuf>, game_root: impl Into<std::path::PathBuf>) -> 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<std::path::PathBuf, crate::AssetError> {
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;

View File

@@ -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));
});
}

147
deltas/0.1.0/0-pre.9.md Normal file
View File

@@ -0,0 +1,147 @@
<!-- file: deltas/0.1.0/0-pre.9.md -->
<!-- version: 1 -->
# 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/<game>/` 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`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/000-README.md --> <!-- file: docs/000-README.md -->
<!-- version: 10 --> <!-- version: 11 -->
# Documentation games.sasedev # Documentation games.sasedev

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/001-WORKSPACE_ARCHITECTURE.md --> <!-- file: docs/architecture/001-WORKSPACE_ARCHITECTURE.md -->
<!-- version: 2 --> <!-- version: 3 -->
# Architecture du workspace # Architecture du workspace
@@ -8,6 +8,9 @@
```text ```text
games.sasedev/ games.sasedev/
├── crates/ ├── crates/
│ ├── common/
│ │ ├── game-assets-lib/
│ │ └── game-logging-lib/
│ ├── engines/ │ ├── engines/
│ │ ├── engine-v1-common/ │ │ ├── engine-v1-common/
│ │ ├── engine-v1-platform-api/ │ │ ├── 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. 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-common` : types et comportements génériques indépendants des plateformes ;
- `engine-v1-platform-api` : contrats abstraits des services plateforme ; - `engine-v1-platform-api` : contrats abstraits des services plateforme ;

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/005-ASSET_ARCHITECTURE.md --> <!-- file: docs/architecture/005-ASSET_ARCHITECTURE.md -->
<!-- version: 1 --> <!-- version: 2 -->
# Architecture des assets # Architecture des assets
@@ -47,3 +47,48 @@ Desktop, Android et Web peuvent utiliser des mécanismes de packaging différent
## Évolution ## É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. 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://<relative-path>
game://<relative-path>
```
`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/<relative-path>
game/<relative-path>
```
## 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/<game>/
├── 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.

38
history/0.1.0/0-pre.8.md Normal file
View File

@@ -0,0 +1,38 @@
<!-- file: history/0.1.0/0-pre.8.md -->
<!-- version: 1 -->
# 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.

View File

@@ -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())