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 -->
<!-- version: 5 -->
<!-- version: 6 -->
# 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/<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
// 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")

View File

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

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
# 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"

View File

@@ -1,5 +1,5 @@
<!-- file: README.md -->
<!-- version: 4 -->
<!-- version: 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 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 -->
<!-- version: 6 -->
<!-- version: 7 -->
# 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.

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 -->
<!-- version: 10 -->
<!-- version: 11 -->
# Documentation games.sasedev

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/001-WORKSPACE_ARCHITECTURE.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# 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 ;

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/005-ASSET_ARCHITECTURE.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# 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://<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())