chore: checkpoint before render engine vendoring
Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -7247,6 +7247,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bevy",
|
"bevy",
|
||||||
|
"bytes",
|
||||||
"inventory",
|
"inventory",
|
||||||
"libmarathon",
|
"libmarathon",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ rusqlite = "0.37.0"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
toml = "0.9"
|
toml = "0.9"
|
||||||
rkyv = { version = "0.8", features = ["uuid-1"] }
|
rkyv = { version = "0.8", features = ["uuid-1", "bytes-1"] }
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
|
|||||||
@@ -33,20 +33,21 @@ rand = "0.8"
|
|||||||
iroh = { version = "0.95", features = ["discovery-local-network"] }
|
iroh = { version = "0.95", features = ["discovery-local-network"] }
|
||||||
iroh-gossip = "0.95"
|
iroh-gossip = "0.95"
|
||||||
futures-lite = "2.0"
|
futures-lite = "2.0"
|
||||||
bincode = "1.3"
|
rkyv = { workspace = true }
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
crossbeam-channel = "0.5.15"
|
crossbeam-channel = "0.5.15"
|
||||||
|
|
||||||
[target.'cfg(target_os = "ios")'.dependencies]
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
objc = "0.2"
|
objc = "0.2"
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
|
tracing-oslog = "0.3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
iroh = { version = "0.95", features = ["discovery-local-network"] }
|
iroh = { version = "0.95", features = ["discovery-local-network"] }
|
||||||
iroh-gossip = "0.95"
|
iroh-gossip = "0.95"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
futures-lite = "2.0"
|
futures-lite = "2.0"
|
||||||
bincode = "1.3"
|
rkyv = { workspace = true }
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ fn screen_to_world_ray(
|
|||||||
screen_pos: glam::Vec2,
|
screen_pos: glam::Vec2,
|
||||||
camera: &Camera,
|
camera: &Camera,
|
||||||
camera_transform: &GlobalTransform,
|
camera_transform: &GlobalTransform,
|
||||||
window: &Window,
|
_window: &Window,
|
||||||
) -> Option<Ray> {
|
) -> Option<Ray> {
|
||||||
// Convert screen position to viewport position (0..1 range)
|
// Convert screen position to viewport position (0..1 range)
|
||||||
let viewport_pos = Vec2::new(screen_pos.x, screen_pos.y);
|
let viewport_pos = Vec2::new(screen_pos.x, screen_pos.y);
|
||||||
|
|||||||
@@ -9,5 +9,4 @@
|
|||||||
pub mod event_buffer;
|
pub mod event_buffer;
|
||||||
pub mod input_handler;
|
pub mod input_handler;
|
||||||
|
|
||||||
pub use event_buffer::InputEventBuffer;
|
|
||||||
pub use input_handler::InputHandlerPlugin;
|
pub use input_handler::InputHandlerPlugin;
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use libmarathon::{
|
use libmarathon::{
|
||||||
engine::{EngineBridge, EngineCore},
|
engine::{
|
||||||
|
EngineBridge,
|
||||||
|
EngineCore,
|
||||||
|
},
|
||||||
persistence::PersistenceConfig,
|
persistence::PersistenceConfig,
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
mod camera;
|
mod camera;
|
||||||
mod cube;
|
mod cube;
|
||||||
@@ -32,56 +34,102 @@ use session::*;
|
|||||||
use session_ui::*;
|
use session_ui::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// Note: eprintln doesn't work on iOS, but tracing-oslog will once initialized
|
||||||
|
eprintln!(">>> RUST ENTRY: main() started");
|
||||||
|
|
||||||
// Initialize logging
|
// Initialize logging
|
||||||
tracing_subscriber::fmt()
|
eprintln!(">>> Initializing tracing_subscriber");
|
||||||
.with_env_filter(
|
|
||||||
tracing_subscriber::EnvFilter::from_default_env()
|
#[cfg(target_os = "ios")]
|
||||||
.add_directive("wgpu=error".parse().unwrap())
|
{
|
||||||
.add_directive("naga=warn".parse().unwrap()),
|
use tracing_subscriber::prelude::*;
|
||||||
)
|
|
||||||
.init();
|
let filter = tracing_subscriber::EnvFilter::builder()
|
||||||
|
.with_default_directive(tracing::Level::DEBUG.into())
|
||||||
|
.from_env_lossy()
|
||||||
|
.add_directive("wgpu=error".parse().unwrap())
|
||||||
|
.add_directive("naga=warn".parse().unwrap())
|
||||||
|
.add_directive("winit=error".parse().unwrap());
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter)
|
||||||
|
.with(tracing_oslog::OsLogger::new("io.r3t.aspen", "default"))
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!("OSLog initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "ios"))]
|
||||||
|
{
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
tracing_subscriber::EnvFilter::from_default_env()
|
||||||
|
.add_directive("wgpu=error".parse().unwrap())
|
||||||
|
.add_directive("naga=warn".parse().unwrap()),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!(">>> Tracing subscriber initialized");
|
||||||
|
|
||||||
// Application configuration
|
// Application configuration
|
||||||
const APP_NAME: &str = "Aspen";
|
const APP_NAME: &str = "Aspen";
|
||||||
|
|
||||||
// Get platform-appropriate database path
|
// Get platform-appropriate database path
|
||||||
|
eprintln!(">>> Getting database path");
|
||||||
let db_path = libmarathon::platform::get_database_path(APP_NAME);
|
let db_path = libmarathon::platform::get_database_path(APP_NAME);
|
||||||
let db_path_str = db_path.to_str().unwrap().to_string();
|
let db_path_str = db_path.to_str().unwrap().to_string();
|
||||||
info!("Database path: {}", db_path_str);
|
info!("Database path: {}", db_path_str);
|
||||||
|
eprintln!(">>> Database path: {}", db_path_str);
|
||||||
|
|
||||||
// Create EngineBridge (for communication between Bevy and EngineCore)
|
// Create EngineBridge (for communication between Bevy and EngineCore)
|
||||||
|
eprintln!(">>> Creating EngineBridge");
|
||||||
let (engine_bridge, engine_handle) = EngineBridge::new();
|
let (engine_bridge, engine_handle) = EngineBridge::new();
|
||||||
info!("EngineBridge created");
|
info!("EngineBridge created");
|
||||||
|
eprintln!(">>> EngineBridge created");
|
||||||
|
|
||||||
// Spawn EngineCore on tokio runtime (runs in background thread)
|
// Spawn EngineCore on tokio runtime (runs in background thread)
|
||||||
|
eprintln!(">>> Spawning EngineCore background thread");
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
eprintln!(">>> [EngineCore thread] Thread started");
|
||||||
info!("Starting EngineCore on tokio runtime...");
|
info!("Starting EngineCore on tokio runtime...");
|
||||||
|
eprintln!(">>> [EngineCore thread] Creating tokio runtime");
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
eprintln!(">>> [EngineCore thread] Tokio runtime created");
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
|
eprintln!(">>> [EngineCore thread] Creating EngineCore");
|
||||||
let core = EngineCore::new(engine_handle, &db_path_str);
|
let core = EngineCore::new(engine_handle, &db_path_str);
|
||||||
|
eprintln!(">>> [EngineCore thread] Running EngineCore");
|
||||||
core.run().await;
|
core.run().await;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
info!("EngineCore spawned in background");
|
info!("EngineCore spawned in background");
|
||||||
|
eprintln!(">>> EngineCore thread spawned");
|
||||||
|
|
||||||
// Create Bevy app (without winit - we own the event loop)
|
// Create Bevy app (without winit - we own the event loop)
|
||||||
|
eprintln!(">>> Creating Bevy App");
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
eprintln!(">>> Bevy App created");
|
||||||
|
|
||||||
// Insert EngineBridge as a resource for Bevy systems to use
|
// Insert EngineBridge as a resource for Bevy systems to use
|
||||||
|
eprintln!(">>> Inserting EngineBridge resource");
|
||||||
app.insert_resource(engine_bridge);
|
app.insert_resource(engine_bridge);
|
||||||
|
|
||||||
// Use DefaultPlugins but disable winit/window/input (we own those)
|
// Use DefaultPlugins but disable winit/window/input (we own those)
|
||||||
|
eprintln!(">>> Adding DefaultPlugins");
|
||||||
app.add_plugins(
|
app.add_plugins(
|
||||||
DefaultPlugins
|
DefaultPlugins
|
||||||
.build()
|
.build()
|
||||||
.disable::<bevy::log::LogPlugin>() // Using tracing-subscriber
|
.disable::<bevy::log::LogPlugin>() // Using tracing-subscriber
|
||||||
.disable::<bevy::winit::WinitPlugin>() // We own winit
|
.disable::<bevy::winit::WinitPlugin>() // We own winit
|
||||||
.disable::<bevy::window::WindowPlugin>() // We own the window
|
.disable::<bevy::window::WindowPlugin>() // We own the window
|
||||||
.disable::<bevy::input::InputPlugin>() // We provide InputEvents directly
|
.disable::<bevy::input::InputPlugin>() // We provide InputEvents directly
|
||||||
.disable::<bevy::gilrs::GilrsPlugin>() // We handle gamepad input ourselves
|
.disable::<bevy::gilrs::GilrsPlugin>(), // We handle gamepad input ourselves
|
||||||
);
|
);
|
||||||
|
eprintln!(">>> DefaultPlugins added");
|
||||||
|
|
||||||
// Marathon core plugins (networking, debug UI, persistence)
|
// Marathon core plugins (networking, debug UI, persistence)
|
||||||
|
eprintln!(">>> Adding MarathonPlugin");
|
||||||
app.add_plugins(libmarathon::MarathonPlugin::new(
|
app.add_plugins(libmarathon::MarathonPlugin::new(
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
PersistenceConfig {
|
PersistenceConfig {
|
||||||
@@ -91,8 +139,10 @@ fn main() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
eprintln!(">>> MarathonPlugin added");
|
||||||
|
|
||||||
// App-specific bridge for polling engine events
|
// App-specific bridge for polling engine events
|
||||||
|
eprintln!(">>> Adding app plugins");
|
||||||
app.add_plugins(EngineBridgePlugin);
|
app.add_plugins(EngineBridgePlugin);
|
||||||
app.add_plugins(CameraPlugin);
|
app.add_plugins(CameraPlugin);
|
||||||
app.add_plugins(RenderingPlugin);
|
app.add_plugins(RenderingPlugin);
|
||||||
@@ -102,6 +152,9 @@ fn main() {
|
|||||||
app.add_plugins(DebugUiPlugin);
|
app.add_plugins(DebugUiPlugin);
|
||||||
app.add_plugins(SessionUiPlugin);
|
app.add_plugins(SessionUiPlugin);
|
||||||
app.add_systems(Startup, initialize_offline_resources);
|
app.add_systems(Startup, initialize_offline_resources);
|
||||||
|
eprintln!(">>> All plugins added");
|
||||||
|
|
||||||
|
eprintln!(">>> Running executor");
|
||||||
libmarathon::platform::run_executor(app).expect("Failed to run executor");
|
libmarathon::platform::run_executor(app).expect("Failed to run executor");
|
||||||
|
eprintln!(">>> Executor returned (should never reach here)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ fn spawn_bridge_tasks(
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(msg) = bridge_out.try_recv_outgoing() {
|
if let Some(msg) = bridge_out.try_recv_outgoing() {
|
||||||
if let Ok(bytes) = bincode::serialize(&msg) {
|
if let Ok(bytes) = rkyv::to_bytes::<rkyv::rancor::Failure>(&msg).map(|b| b.to_vec()) {
|
||||||
if let Err(e) = sender.broadcast(Bytes::from(bytes)).await {
|
if let Err(e) = sender.broadcast(Bytes::from(bytes)).await {
|
||||||
error!("[Node {}] Broadcast failed: {}", node_id, e);
|
error!("[Node {}] Broadcast failed: {}", node_id, e);
|
||||||
}
|
}
|
||||||
@@ -303,7 +303,7 @@ fn spawn_bridge_tasks(
|
|||||||
| Ok(Some(Ok(event))) => {
|
| Ok(Some(Ok(event))) => {
|
||||||
if let iroh_gossip::api::Event::Received(msg) = event {
|
if let iroh_gossip::api::Event::Received(msg) = event {
|
||||||
if let Ok(versioned_msg) =
|
if let Ok(versioned_msg) =
|
||||||
bincode::deserialize::<VersionedMessage>(&msg.content)
|
rkyv::from_bytes::<VersionedMessage, rkyv::rancor::Failure>(&msg.content)
|
||||||
{
|
{
|
||||||
if let Err(e) = bridge_in.push_incoming(versioned_msg) {
|
if let Err(e) = bridge_in.push_incoming(versioned_msg) {
|
||||||
error!("[Node {}] Push incoming failed: {}", node_id, e);
|
error!("[Node {}] Push incoming failed: {}", node_id, e);
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ mod test_utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Count entities with CubeMarker component
|
/// Count entities with CubeMarker component
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn count_cubes(world: &mut World) -> usize {
|
pub fn count_cubes(world: &mut World) -> usize {
|
||||||
let mut query = world.query::<&CubeMarker>();
|
let mut query = world.query::<&CubeMarker>();
|
||||||
query.iter(world).count()
|
query.iter(world).count()
|
||||||
@@ -308,7 +309,7 @@ mod test_utils {
|
|||||||
"[Node {}] Sending message #{} via gossip",
|
"[Node {}] Sending message #{} via gossip",
|
||||||
node_id, msg_count
|
node_id, msg_count
|
||||||
);
|
);
|
||||||
match bincode::serialize(&versioned_msg) {
|
match rkyv::to_bytes::<rkyv::rancor::Failure>(&versioned_msg).map(|b| b.to_vec()) {
|
||||||
| Ok(bytes) => {
|
| Ok(bytes) => {
|
||||||
if let Err(e) = sender.broadcast(Bytes::from(bytes)).await {
|
if let Err(e) = sender.broadcast(Bytes::from(bytes)).await {
|
||||||
eprintln!("[Node {}] Failed to broadcast message: {}", node_id, e);
|
eprintln!("[Node {}] Failed to broadcast message: {}", node_id, e);
|
||||||
@@ -349,7 +350,7 @@ mod test_utils {
|
|||||||
"[Node {}] Received message #{} from gossip",
|
"[Node {}] Received message #{} from gossip",
|
||||||
node_id, msg_count
|
node_id, msg_count
|
||||||
);
|
);
|
||||||
match bincode::deserialize::<VersionedMessage>(&msg.content) {
|
match rkyv::from_bytes::<VersionedMessage, rkyv::rancor::Failure>(&msg.content) {
|
||||||
| Ok(versioned_msg) => {
|
| Ok(versioned_msg) => {
|
||||||
if let Err(e) = bridge_in.push_incoming(versioned_msg) {
|
if let Err(e) = bridge_in.push_incoming(versioned_msg) {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
|||||||
11
crates/xtask/Cargo.toml
Normal file
11
crates/xtask/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
94
crates/xtask/README.md
Normal file
94
crates/xtask/README.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# xtask - Build Automation for Aspen
|
||||||
|
|
||||||
|
This crate provides build automation tasks for the Aspen workspace using the [cargo-xtask pattern](https://github.com/matklad/cargo-xtask).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
All commands are run from the workspace root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for iOS Simulator (release mode)
|
||||||
|
cargo xtask ios-build
|
||||||
|
|
||||||
|
# Build for iOS Simulator (debug mode)
|
||||||
|
cargo xtask ios-build --debug
|
||||||
|
|
||||||
|
# Deploy to iOS Simulator
|
||||||
|
cargo xtask ios-deploy
|
||||||
|
|
||||||
|
# Deploy to a specific device
|
||||||
|
cargo xtask ios-deploy --device "iPad Air (5th generation)"
|
||||||
|
|
||||||
|
# Build, deploy, and stream logs (most common)
|
||||||
|
cargo xtask ios-run
|
||||||
|
|
||||||
|
# Build, deploy, and stream logs (debug mode)
|
||||||
|
cargo xtask ios-run --debug
|
||||||
|
|
||||||
|
# Build, deploy, and stream logs (specific device)
|
||||||
|
cargo xtask ios-run --device "iPhone 15 Pro"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
- **`ios-build`**: Builds the app for iOS Simulator (aarch64-apple-ios-sim)
|
||||||
|
- `--debug`: Build in debug mode (default is release)
|
||||||
|
|
||||||
|
- **`ios-deploy`**: Deploys the app to iOS Simulator
|
||||||
|
- `--device`: Device name (default: "iPad Pro 12.9-inch M2")
|
||||||
|
|
||||||
|
- **`ios-run`**: Builds, deploys, and streams logs (all-in-one)
|
||||||
|
- `--debug`: Build in debug mode
|
||||||
|
- `--device`: Device name (default: "iPad Pro 12.9-inch M2")
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The xtask pattern creates a new binary target in your workspace that contains build automation code. This is better than shell scripts because:
|
||||||
|
|
||||||
|
1. **Cross-platform**: Pure Rust, works everywhere Rust works
|
||||||
|
2. **Type-safe**: Configuration and arguments are type-checked
|
||||||
|
3. **Maintainable**: Can use the same error handling and libraries as your main code
|
||||||
|
4. **Fast**: Compiled Rust is much faster than shell scripts
|
||||||
|
5. **Integrated**: Can share code with your workspace
|
||||||
|
|
||||||
|
## Adding New Tasks
|
||||||
|
|
||||||
|
To add a new task:
|
||||||
|
|
||||||
|
1. Add a new variant to the `Commands` enum in `src/main.rs`
|
||||||
|
2. Implement the corresponding function
|
||||||
|
3. Add the function call in the `match` statement in `main()`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
// ... existing commands
|
||||||
|
|
||||||
|
/// Run tests on iOS Simulator
|
||||||
|
IosTest {
|
||||||
|
/// Device name
|
||||||
|
#[arg(long, default_value = "iPad Pro 12.9-inch M2")]
|
||||||
|
device: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ios_test(device_name: &str) -> Result<()> {
|
||||||
|
// Implementation here
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
match cli.command {
|
||||||
|
// ... existing matches
|
||||||
|
Commands::IosTest { device } => ios_test(&device),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `anyhow`: Error handling
|
||||||
|
- `clap`: Command-line argument parsing
|
||||||
448
crates/xtask/src/main.rs
Normal file
448
crates/xtask/src/main.rs
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::fs;
|
||||||
|
use tracing::{info, warn, error};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "xtask")]
|
||||||
|
#[command(about = "Build automation for Aspen", long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Build for iOS Simulator
|
||||||
|
IosBuild {
|
||||||
|
/// Build in debug mode instead of release
|
||||||
|
#[arg(long)]
|
||||||
|
debug: bool,
|
||||||
|
},
|
||||||
|
/// Deploy to iOS Simulator
|
||||||
|
IosDeploy {
|
||||||
|
/// Device name (defaults to "iPad Pro 12.9-inch M2")
|
||||||
|
#[arg(long, default_value = "iPad Pro 12.9-inch M2")]
|
||||||
|
device: String,
|
||||||
|
/// Use debug build instead of release
|
||||||
|
#[arg(long)]
|
||||||
|
debug: bool,
|
||||||
|
},
|
||||||
|
/// Build, deploy, and stream logs from iOS Simulator
|
||||||
|
IosRun {
|
||||||
|
/// Device name (defaults to "iPad Pro 12.9-inch M2")
|
||||||
|
#[arg(long, default_value = "iPad Pro 12.9-inch M2")]
|
||||||
|
device: String,
|
||||||
|
/// Build in debug mode instead of release
|
||||||
|
#[arg(long)]
|
||||||
|
debug: bool,
|
||||||
|
},
|
||||||
|
/// Build, deploy, and stream logs from physical iOS device
|
||||||
|
IosDevice {
|
||||||
|
/// Build in debug mode instead of release
|
||||||
|
#[arg(long)]
|
||||||
|
debug: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
// Initialize tracing
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
tracing_subscriber::EnvFilter::from_default_env()
|
||||||
|
.add_directive("xtask=info".parse().unwrap()),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::IosBuild { debug } => ios_build(debug),
|
||||||
|
Commands::IosDeploy { device, debug } => ios_deploy(&device, debug),
|
||||||
|
Commands::IosRun { device, debug } => ios_run(&device, debug),
|
||||||
|
Commands::IosDevice { debug } => ios_device(debug),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_root() -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.ancestors()
|
||||||
|
.nth(2)
|
||||||
|
.expect("Failed to find project root")
|
||||||
|
.to_path_buf()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn package_app_bundle(profile: &str, target: &str) -> Result<()> {
|
||||||
|
let root = project_root();
|
||||||
|
let app_name = "Aspen";
|
||||||
|
|
||||||
|
let binary_path = root.join(format!("target/{}/{}/app", target, profile));
|
||||||
|
let bundle_path = root.join(format!("target/{}/{}/{}.app", target, profile, app_name));
|
||||||
|
let info_plist_src = root.join("scripts/ios/Info.plist");
|
||||||
|
|
||||||
|
info!("Packaging app bundle");
|
||||||
|
info!(" Binary: {}", binary_path.display());
|
||||||
|
info!(" Bundle: {}", bundle_path.display());
|
||||||
|
|
||||||
|
// Verify binary exists
|
||||||
|
if !binary_path.exists() {
|
||||||
|
anyhow::bail!("Binary not found at {}", binary_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old bundle if it exists
|
||||||
|
if bundle_path.exists() {
|
||||||
|
fs::remove_dir_all(&bundle_path)
|
||||||
|
.context("Failed to remove old app bundle")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create bundle directory
|
||||||
|
fs::create_dir_all(&bundle_path)
|
||||||
|
.context("Failed to create app bundle directory")?;
|
||||||
|
|
||||||
|
// Copy binary
|
||||||
|
let bundle_binary = bundle_path.join("app");
|
||||||
|
fs::copy(&binary_path, &bundle_binary)
|
||||||
|
.context("Failed to copy binary to bundle")?;
|
||||||
|
|
||||||
|
// Set executable permissions
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(&bundle_binary)?.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(&bundle_binary, perms)
|
||||||
|
.context("Failed to set executable permissions")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy Info.plist
|
||||||
|
fs::copy(&info_plist_src, bundle_path.join("Info.plist"))
|
||||||
|
.context("Failed to copy Info.plist")?;
|
||||||
|
|
||||||
|
// Create PkgInfo
|
||||||
|
fs::write(bundle_path.join("PkgInfo"), b"APPL????")
|
||||||
|
.context("Failed to create PkgInfo")?;
|
||||||
|
|
||||||
|
info!("App bundle packaged successfully");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ios_build(debug: bool) -> Result<()> {
|
||||||
|
let root = project_root();
|
||||||
|
let profile = if debug { "debug" } else { "release" };
|
||||||
|
|
||||||
|
info!("Building for iOS Simulator");
|
||||||
|
info!("Project root: {}", root.display());
|
||||||
|
|
||||||
|
let mut cmd = Command::new("cargo");
|
||||||
|
cmd.current_dir(&root)
|
||||||
|
.arg("build")
|
||||||
|
.arg("--package")
|
||||||
|
.arg("app")
|
||||||
|
.arg("--target")
|
||||||
|
.arg("aarch64-apple-ios-sim");
|
||||||
|
|
||||||
|
if !debug {
|
||||||
|
cmd.arg("--release");
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = cmd.status().context("Failed to run cargo build")?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!("Build failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package the app bundle
|
||||||
|
package_app_bundle(profile, "aarch64-apple-ios-sim")?;
|
||||||
|
|
||||||
|
info!("Build complete: target/aarch64-apple-ios-sim/{}/Aspen.app", profile);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ios_deploy(device_name: &str, debug: bool) -> Result<()> {
|
||||||
|
let root = project_root();
|
||||||
|
let profile = if debug { "debug" } else { "release" };
|
||||||
|
let bundle_path = root.join(format!("target/aarch64-apple-ios-sim/{}/Aspen.app", profile));
|
||||||
|
|
||||||
|
info!("Deploying to iOS Simulator");
|
||||||
|
info!("Device: {}", device_name);
|
||||||
|
info!("Bundle: {}", bundle_path.display());
|
||||||
|
|
||||||
|
// Find device UUID
|
||||||
|
info!("Finding device UUID");
|
||||||
|
let output = Command::new("xcrun")
|
||||||
|
.args(["simctl", "list", "devices", "available"])
|
||||||
|
.output()
|
||||||
|
.context("Failed to list devices")?;
|
||||||
|
|
||||||
|
let devices_list = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let device_uuid = devices_list
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.contains(device_name))
|
||||||
|
.and_then(|line| {
|
||||||
|
line.split('(')
|
||||||
|
.nth(1)?
|
||||||
|
.split(')')
|
||||||
|
.next()
|
||||||
|
})
|
||||||
|
.context("Device not found")?;
|
||||||
|
|
||||||
|
info!("Found device: {}", device_uuid);
|
||||||
|
|
||||||
|
// Boot simulator
|
||||||
|
info!("Booting simulator");
|
||||||
|
let boot_result = Command::new("xcrun")
|
||||||
|
.args(["simctl", "boot", device_uuid])
|
||||||
|
.status();
|
||||||
|
|
||||||
|
if let Ok(status) = boot_result {
|
||||||
|
if !status.success() {
|
||||||
|
warn!("Simulator boot returned non-zero (device may already be booted)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open Simulator.app
|
||||||
|
info!("Opening Simulator.app");
|
||||||
|
Command::new("open")
|
||||||
|
.args(["-a", "Simulator"])
|
||||||
|
.status()
|
||||||
|
.context("Failed to open Simulator")?;
|
||||||
|
|
||||||
|
// Uninstall old version
|
||||||
|
info!("Uninstalling old version");
|
||||||
|
let uninstall_result = Command::new("xcrun")
|
||||||
|
.args(["simctl", "uninstall", device_uuid, "G872CZV7WG.aspen"])
|
||||||
|
.status();
|
||||||
|
|
||||||
|
if let Ok(status) = uninstall_result {
|
||||||
|
if !status.success() {
|
||||||
|
warn!("Uninstall returned non-zero (app may not have been installed)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install app
|
||||||
|
info!("Installing app");
|
||||||
|
let status = Command::new("xcrun")
|
||||||
|
.args(["simctl", "install", device_uuid])
|
||||||
|
.arg(&bundle_path)
|
||||||
|
.status()
|
||||||
|
.context("Failed to install app")?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!("Installation failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch app
|
||||||
|
info!("Launching app");
|
||||||
|
let output = Command::new("xcrun")
|
||||||
|
.args(["simctl", "launch", device_uuid, "G872CZV7WG.aspen"])
|
||||||
|
.output()
|
||||||
|
.context("Failed to launch app")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
error!("Launch failed: {}", stderr);
|
||||||
|
anyhow::bail!("Launch failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("App launched successfully");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ios_run(device_name: &str, debug: bool) -> Result<()> {
|
||||||
|
// Build
|
||||||
|
ios_build(debug)?;
|
||||||
|
|
||||||
|
// Deploy
|
||||||
|
ios_deploy(device_name, debug)?;
|
||||||
|
|
||||||
|
// Stream logs
|
||||||
|
info!("Streaming logs (Ctrl+C to stop)");
|
||||||
|
|
||||||
|
// Find device UUID again
|
||||||
|
let output = Command::new("xcrun")
|
||||||
|
.args(["simctl", "list", "devices", "available"])
|
||||||
|
.output()
|
||||||
|
.context("Failed to list devices")?;
|
||||||
|
|
||||||
|
let devices_list = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let device_uuid = devices_list
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.contains(device_name))
|
||||||
|
.and_then(|line| {
|
||||||
|
line.split('(')
|
||||||
|
.nth(1)?
|
||||||
|
.split(')')
|
||||||
|
.next()
|
||||||
|
})
|
||||||
|
.context("Device not found")?;
|
||||||
|
|
||||||
|
// Stream logs using simctl spawn
|
||||||
|
// Note: Process name is "app" (CFBundleExecutable), not "Aspen" (display name)
|
||||||
|
let mut log_cmd = Command::new("xcrun");
|
||||||
|
log_cmd.args([
|
||||||
|
"simctl",
|
||||||
|
"spawn",
|
||||||
|
device_uuid,
|
||||||
|
"log",
|
||||||
|
"stream",
|
||||||
|
"--predicate",
|
||||||
|
"process == \"app\"",
|
||||||
|
"--style",
|
||||||
|
"compact",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// When debug flag is set, show all log levels (info, debug, default, error, fault)
|
||||||
|
if debug {
|
||||||
|
log_cmd.args(["--info", "--debug"]);
|
||||||
|
info!("Streaming all log levels (info, debug, default, error, fault)");
|
||||||
|
} else {
|
||||||
|
info!("Streaming default log levels (default, error, fault)");
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = log_cmd.status().context("Failed to start log stream")?;
|
||||||
|
|
||||||
|
// Log streaming can exit for legitimate reasons:
|
||||||
|
// - User quit the simulator
|
||||||
|
// - App was killed
|
||||||
|
// - User hit Ctrl+C
|
||||||
|
// Don't treat these as errors
|
||||||
|
match status.code() {
|
||||||
|
Some(0) => info!("Log streaming ended normally"),
|
||||||
|
Some(code) => info!("Log streaming ended with code {}", code),
|
||||||
|
None => info!("Log streaming ended (killed by signal)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ios_device(debug: bool) -> Result<()> {
|
||||||
|
let root = project_root();
|
||||||
|
let profile = if debug { "debug" } else { "release" };
|
||||||
|
let target = "aarch64-apple-ios";
|
||||||
|
|
||||||
|
info!("Building for physical iOS device");
|
||||||
|
info!("Project root: {}", root.display());
|
||||||
|
|
||||||
|
// Build
|
||||||
|
let mut cmd = Command::new("cargo");
|
||||||
|
cmd.current_dir(&root)
|
||||||
|
.arg("build")
|
||||||
|
.arg("--package")
|
||||||
|
.arg("app")
|
||||||
|
.arg("--target")
|
||||||
|
.arg(target);
|
||||||
|
|
||||||
|
if !debug {
|
||||||
|
cmd.arg("--release");
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = cmd.status().context("Failed to run cargo build")?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!("Build failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package the app bundle
|
||||||
|
package_app_bundle(profile, target)?;
|
||||||
|
|
||||||
|
let bundle_path = root.join(format!("target/{}/{}/Aspen.app", target, profile));
|
||||||
|
info!("Build complete: {}", bundle_path.display());
|
||||||
|
|
||||||
|
// Code sign with Apple Development certificate
|
||||||
|
info!("Signing app with Apple Development certificate");
|
||||||
|
let status = Command::new("codesign")
|
||||||
|
.args(["--force", "--sign", "Apple Development: sienna@r3t.io (6A6PF29R8A)"])
|
||||||
|
.arg(&bundle_path)
|
||||||
|
.status()
|
||||||
|
.context("Failed to code sign app")?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!("Code signing failed. Make sure the certificate is trusted in Keychain Access.");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("App signed successfully");
|
||||||
|
|
||||||
|
// Get connected device
|
||||||
|
info!("Finding connected device");
|
||||||
|
let output = Command::new("xcrun")
|
||||||
|
.args(["devicectl", "list", "devices"])
|
||||||
|
.output()
|
||||||
|
.context("Failed to list devices")?;
|
||||||
|
|
||||||
|
let devices_list = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let device_id = devices_list
|
||||||
|
.lines()
|
||||||
|
.find(|line| (line.contains("connected") || line.contains("available")) && line.contains("iPad"))
|
||||||
|
.and_then(|line| {
|
||||||
|
// Extract device ID from the line
|
||||||
|
// Format: "Name domain.coredevice.local ID connected/available Model"
|
||||||
|
line.split_whitespace().nth(2)
|
||||||
|
})
|
||||||
|
.context("No connected iPad found. Make sure your iPad is connected and trusted.")?;
|
||||||
|
|
||||||
|
info!("Found device: {}", device_id);
|
||||||
|
|
||||||
|
// Install app
|
||||||
|
info!("Installing app to device");
|
||||||
|
let status = Command::new("xcrun")
|
||||||
|
.args(["devicectl", "device", "install", "app"])
|
||||||
|
.arg("--device")
|
||||||
|
.arg(device_id)
|
||||||
|
.arg(&bundle_path)
|
||||||
|
.status()
|
||||||
|
.context("Failed to install app")?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!("Installation failed. Make sure Developer Mode is enabled on your iPad.");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("App installed successfully");
|
||||||
|
|
||||||
|
// Launch app
|
||||||
|
info!("Launching app");
|
||||||
|
let status = Command::new("xcrun")
|
||||||
|
.args(["devicectl", "device", "process", "launch"])
|
||||||
|
.arg("--device")
|
||||||
|
.arg(device_id)
|
||||||
|
.arg("G872CZV7WG.aspen")
|
||||||
|
.status()
|
||||||
|
.context("Failed to launch app")?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!("Launch failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("App launched successfully");
|
||||||
|
|
||||||
|
// Stream logs
|
||||||
|
info!("Streaming device logs (Ctrl+C to stop)");
|
||||||
|
info!("Note: Device logs may be delayed. Use Console.app for real-time logs.");
|
||||||
|
|
||||||
|
let mut log_cmd = Command::new("xcrun");
|
||||||
|
log_cmd.args([
|
||||||
|
"devicectl",
|
||||||
|
"device",
|
||||||
|
"stream",
|
||||||
|
"log",
|
||||||
|
"--device",
|
||||||
|
device_id,
|
||||||
|
"--predicate",
|
||||||
|
"process == \"app\"",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
info!("Streaming all log levels");
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = log_cmd.status().context("Failed to start log stream")?;
|
||||||
|
|
||||||
|
match status.code() {
|
||||||
|
Some(0) => info!("Log streaming ended normally"),
|
||||||
|
Some(code) => info!("Log streaming ended with code {}", code),
|
||||||
|
None => info!("Log streaming ended (killed by signal)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
159
docs/ESTIMATION.md
Normal file
159
docs/ESTIMATION.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# Estimation and Prioritization
|
||||||
|
|
||||||
|
This document defines how r3t Studios sizes, estimates, and prioritizes work. Our approach is grounded in **Lean Software Development** principles, adapted for indie game development.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lean Principles as Evaluation Questions
|
||||||
|
|
||||||
|
Every sizing and prioritization decision should be evaluated through these questions:
|
||||||
|
|
||||||
|
| Principle | Evaluation Questions |
|
||||||
|
|-----------|---------------------|
|
||||||
|
| **Eliminate Waste** | Does this reduce waste? Is this the simplest solution? Are we building something nobody needs? |
|
||||||
|
| **Amplify Learning** | What will we learn from this? Does this reduce uncertainty? Should we prototype first? |
|
||||||
|
| **Decide as Late as Possible** | Do we have enough information to commit? Can this decision be deferred responsibly? |
|
||||||
|
| **Deliver as Fast as Possible** | What's the smallest increment that delivers value? What's blocking flow? |
|
||||||
|
| **Empower the Team** | Do I have what I need to do this? Who else should be involved in this decision? |
|
||||||
|
| **Build Integrity In** | Are we building quality in from the start? Will this create technical debt? |
|
||||||
|
| **Optimize the Whole** | Does this improve the whole system or just one part? Are we sub-optimizing? |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sizing
|
||||||
|
|
||||||
|
We use **powers of 2** for effort estimation: `1, 2, 4, 8, 16, 32`
|
||||||
|
|
||||||
|
This scale is intentionally coarse. Precision in estimation is waste—we optimize for fast, good-enough decisions.
|
||||||
|
|
||||||
|
| Size | Points | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| **XS** | 1 | Trivial change. A few lines, minimal risk. Less than 2 hours. |
|
||||||
|
| **S** | 2 | Small task. Well-understood, limited scope. Half a day. |
|
||||||
|
| **M** | 4 | Medium task. Some complexity or unknowns. About 1 day. |
|
||||||
|
| **L** | 8 | Large task. Multiple components or significant complexity. 2-3 days. |
|
||||||
|
| **XL** | 16 | Very large. Should probably be an epic. ~1 week. |
|
||||||
|
| **XXL** | 32 | Epic-scale work. Multi-phase refactoring or major features. 2-4 weeks. |
|
||||||
|
|
||||||
|
### Sizing Guidelines
|
||||||
|
|
||||||
|
**Before assigning a size, ask:**
|
||||||
|
|
||||||
|
- *Eliminate Waste*: Is this the smallest scope that delivers value?
|
||||||
|
- *Deliver Fast*: Can we break this down further?
|
||||||
|
- *Amplify Learning*: Is there uncertainty that inflates the estimate? Should we prototype/spike first?
|
||||||
|
- *Optimize the Whole*: Does the estimate account for integration, testing, and platform testing (iOS + macOS)?
|
||||||
|
|
||||||
|
**If the size is XL (16) or larger:**
|
||||||
|
|
||||||
|
This is epic territory. Break it into phases or sub-tasks. An XL/XXL indicates the work is not well-understood or needs careful sequencing. Consider writing an RFC for XXL work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority
|
||||||
|
|
||||||
|
Priority is based on **Cost of Delay**—the impact of not doing this work now.
|
||||||
|
|
||||||
|
| Label | Priority | Cost of Delay Profile | Description |
|
||||||
|
|-------|----------|----------------------|-------------|
|
||||||
|
| **P0** | Critical | Expedite | Game-breaking bug, crash on launch, data loss, or blocks release. Drop everything. Every hour matters. |
|
||||||
|
| **P1** | High | Fixed Date / High Value | Release blocker, community-visible bug, or deadline-driven (demo, event, content creator access). Do this sprint. |
|
||||||
|
| **P2** | Medium | Standard | Normal feature work, engine improvements, content additions. Standard backlog work—prioritize by WSJF. |
|
||||||
|
| **P3** | Low | Intangible | Nice-to-have, polish, research, experiments. Do when capacity allows or inspiration strikes. |
|
||||||
|
|
||||||
|
### Priority Guidelines
|
||||||
|
|
||||||
|
**Before assigning a priority, ask:**
|
||||||
|
|
||||||
|
- *Eliminate Waste*: Should we do this at all? What happens if we never do it?
|
||||||
|
- *Amplify Learning*: Does doing this first reduce uncertainty for other work?
|
||||||
|
- *Decide Late*: Do we need to commit now, or can we learn more first? (Especially important for game design decisions)
|
||||||
|
- *Optimize the Whole*: Does this unblock other high-value work? Does it improve both engine (Marathon) and game (Aspen)?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WSJF (Weighted Shortest Job First)
|
||||||
|
|
||||||
|
For backlog ordering within a priority tier, we use WSJF:
|
||||||
|
|
||||||
|
```
|
||||||
|
WSJF = Cost of Delay / Job Size
|
||||||
|
```
|
||||||
|
|
||||||
|
Where **Cost of Delay** considers:
|
||||||
|
|
||||||
|
- **Player Value**: Does this improve player experience, immersion, or enjoyment?
|
||||||
|
- **Time Criticality**: Content creator previews, demo deadlines, platform submission windows
|
||||||
|
- **Risk Reduction**: Tech debt payoff, engine stability, enables future content/features
|
||||||
|
|
||||||
|
Higher WSJF = do it first.
|
||||||
|
|
||||||
|
### Example (Game Dev Context)
|
||||||
|
|
||||||
|
| Item | Player Value | Time Criticality | Risk Reduction | CoD | Size | WSJF |
|
||||||
|
|------|--------------|------------------|----------------|-----|------|------|
|
||||||
|
| Spatial Audio | 8 | 6 | 4 | 18 | 8 | 2.25 |
|
||||||
|
| Agent AI Polish | 6 | 2 | 2 | 10 | 16 | 0.63 |
|
||||||
|
| Fix iOS Crash | 10 | 10 | 8 | 28 | 4 | 7.0 |
|
||||||
|
| Low-Poly Tree Assets | 4 | 2 | 1 | 7 | 2 | 3.5 |
|
||||||
|
|
||||||
|
**Order: iOS Crash (7.0), Tree Assets (3.5), Spatial Audio (2.25), AI Polish (0.63)**
|
||||||
|
|
||||||
|
The crash gets fixed first (high value, small effort), then quick content wins, then larger features.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Area Labels and Work Types
|
||||||
|
|
||||||
|
Our `area/` labels map to different types of work:
|
||||||
|
|
||||||
|
| Area | Typical Work | Considerations |
|
||||||
|
|------|--------------|----------------|
|
||||||
|
| **area/core** | Engine foundation | High risk—test thoroughly, affects everything |
|
||||||
|
| **area/rendering** | Graphics, PBR, cameras | Test on iOS + macOS, check DPI scaling |
|
||||||
|
| **area/audio** | Spatial audio, sound | Requires device testing, earbuds vs speakers |
|
||||||
|
| **area/networking** | P2P, CRDT sync | Test with poor connections, edge cases |
|
||||||
|
| **area/platform** | iOS/macOS specific | Platform-specific testing required |
|
||||||
|
| **area/simulation** | Agent behaviors, AI | Needs playtesting, emergent behavior testing |
|
||||||
|
| **area/content** | Art, audio assets, dialogue | Artistic review, asset pipeline testing |
|
||||||
|
| **area/ui-ux** | Menus, HUD, accessibility | Needs user testing, various screen sizes |
|
||||||
|
| **area/tooling** | Build, CI/CD, dev tools | Verify on fresh checkout |
|
||||||
|
| **area/docs** | Technical specs, RFCs | Keep updated as code evolves |
|
||||||
|
| **area/infrastructure** | Deployment, hosting | Test in staging before production |
|
||||||
|
| **area/rfc** | Design proposals | Gather feedback before committing |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### When Sizing
|
||||||
|
|
||||||
|
1. Compare to recent similar work
|
||||||
|
2. Use the Lean questions to validate scope
|
||||||
|
3. If XL or larger, consider making it an epic with sub-tasks
|
||||||
|
4. Bias toward smaller—deliver fast, learn, iterate
|
||||||
|
5. Remember: estimates are for ordering work, not commitments
|
||||||
|
|
||||||
|
### When Prioritizing
|
||||||
|
|
||||||
|
1. Is it game-breaking (crash/data loss)? If yes, **P0**—do it now
|
||||||
|
2. Is there a deadline (demo/release)? If yes, **P1**—this sprint
|
||||||
|
3. Otherwise, score WSJF and order the backlog as **P2**
|
||||||
|
4. Nice-to-haves and experiments go to **P3**
|
||||||
|
5. Re-evaluate priorities weekly—inspiration and context change
|
||||||
|
|
||||||
|
### For Solo Work
|
||||||
|
|
||||||
|
- **Batching**: Group similar work (all rendering tasks, all iOS fixes) to minimize context switching
|
||||||
|
- **Flow**: Protect deep work time—don't interrupt XL tasks with small P3 items
|
||||||
|
- **Momentum**: Sometimes doing a quick XS/S task builds momentum for harder work
|
||||||
|
- **Inspiration**: If you're excited about a P3 item, that's valuable—creative energy matters in game dev
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Poppendieck, Mary & Tom. *Lean Software Development: An Agile Toolkit*. Addison-Wesley, 2003.
|
||||||
|
- Reinertsen, Donald G. *The Principles of Product Development Flow*. Celeritas Publishing, 2009.
|
||||||
|
- [Cost of Delay - Black Swan Farming](https://blackswanfarming.com/cost-of-delay/)
|
||||||
|
- [WSJF - Scaled Agile Framework](https://framework.scaledagile.com/wsjf/)
|
||||||
14
scripts/ios/Entitlements.plist
Normal file
14
scripts/ios/Entitlements.plist
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>application-identifier</key>
|
||||||
|
<string>G872CZV7WG.G872CZV7WG.aspen</string>
|
||||||
|
<key>get-task-allow</key>
|
||||||
|
<true/>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>G872CZV7WG.*</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>app</string>
|
<string>app</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>io.r3t.aspen</string>
|
<string>G872CZV7WG.aspen</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
APP_NAME="Aspen"
|
APP_NAME="Aspen"
|
||||||
BUNDLE_ID="io.r3t.aspen"
|
BUNDLE_ID="G872CZV7WG.aspen"
|
||||||
TARGET="aarch64-apple-ios-sim"
|
TARGET="aarch64-apple-ios-sim"
|
||||||
BUILD_MODE="release"
|
BUILD_MODE="release"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user