diff --git a/.gitattributes b/.gitattributes index cd6be071488041d9713c05cbcd87cd2590dc4426..0195febc1afb06c207c447060ec4079a37055bf4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,4 @@ *.png filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text *.ttf filter=lfs diff=lfs merge=lfs -text +*.apack filter=lfs diff=lfs merge=lfs -text diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c13b95f1d7f3f6d8943ee16ace1873e696c558c9..0867f1bd693312f180c44299939675f09317500c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,30 +52,30 @@ build-linux: only: - trunk -build-arm64: - image: "r.lcr.gr/microhacks/bevy-builder:arm64" - tags: - - arm64 - stage: build - before_script: - - export CARGO_HOME="${CI_PROJECT_DIR}/.cargo" - - export PATH="${CI_PROJECT_DIR}/.cargo/bin:$PATH" - cache: - key: build-cache-arm64 - paths: - - .cargo/registry/cache - - .cargo/registry/index - - .cargo/git/db - - .cargo/bin/ - - target/ - script: - - cargo build --release -p ${BINARY_FOLDER} --target aarch64-unknown-linux-gnu - artifacts: - expire_in: 1 day - paths: - - target/aarch64-unknown-linux-gnu/release/game_core - only: - - trunk +#build-arm64: +# image: "r.lcr.gr/microhacks/bevy-builder:arm64" +# tags: +# - arm64 +# stage: build +# before_script: +# - export CARGO_HOME="${CI_PROJECT_DIR}/.cargo" +# - export PATH="${CI_PROJECT_DIR}/.cargo/bin:$PATH" +# cache: +# key: build-cache-arm64 +# paths: +# - .cargo/registry/cache +# - .cargo/registry/index +# - .cargo/git/db +# - .cargo/bin/ +# - target/ +# script: +# - cargo build --release -p ${BINARY_FOLDER} --target aarch64-unknown-linux-gnu +# artifacts: +# expire_in: 1 day +# paths: +# - target/aarch64-unknown-linux-gnu/release/game_core +# only: +# - trunk build-web: stage: build @@ -112,16 +112,16 @@ package-all: - cp -r assets dist/assets - cp target/x86_64-unknown-linux-gnu/release/${BINARY_NAME} "dist/${BINARY_NAME}" - cp target/x86_64-pc-windows-gnu/release/${BINARY_NAME}.exe "dist/${BINARY_NAME}.exe" - - cp target/aarch64-unknown-linux-gnu/release/${BINARY_NAME} "dist/${BINARY_NAME}.arm64" +# - cp target/aarch64-unknown-linux-gnu/release/${BINARY_NAME} "dist/${BINARY_NAME}.arm64" - cd "${CI_PROJECT_DIR}/dist" && zip -r "windows.zip" "./${BINARY_NAME}.exe" ./assets - cd "${CI_PROJECT_DIR}/dist" && zip -r "linux.x86.zip" "./${BINARY_NAME}" ./assets - - cd "${CI_PROJECT_DIR}/dist" && zip -r "linux.arm64.zip" "./${BINARY_NAME}.arm64" ./assets +# - cd "${CI_PROJECT_DIR}/dist" && zip -r "linux.arm64.zip" "./${BINARY_NAME}.arm64" ./assets - cd "${CI_PROJECT_DIR}/${BINARY_FOLDER}/dist" && zip -r "web.zip" ./* - cd "${CI_PROJECT_DIR}" && mv "${CI_PROJECT_DIR}/game_core/dist/web.zip" "${CI_PROJECT_DIR}/dist/web.zip" dependencies: - build-windows - build-linux - - build-arm64 +# - build-arm64 - build-web artifacts: expire_in: 7 days @@ -129,7 +129,7 @@ package-all: - dist/web.zip - dist/windows.zip - dist/linux.x86.zip - - dist/linux.arm64.zip +# - dist/linux.arm64.zip only: - trunk diff --git a/CREDITS.toml b/CREDITS.toml new file mode 100644 index 0000000000000000000000000000000000000000..c43b422d4cea1d907c8cf6e5a334319f42710b12 --- /dev/null +++ b/CREDITS.toml @@ -0,0 +1,5 @@ +[[font]] +name = "compass_pro.ttf" +author = "Eeve Somepx" +website = "https://somepx.itch.io/humble-fonts-free" +usage = "Main game font" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 2fc492e4d4e279a28300936cfae5e5155a20f15f..c94aed1c5b0af214129004fb50fd2737cf7971d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -734,6 +734,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bevy_tweening" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d38b2abf5518df10c3c32ee57a54b9ba7067f3bd1c137b912e286d573962145" +dependencies = [ + "bevy", + "interpolation", +] + [[package]] name = "bevy_ui" version = "0.9.1" @@ -1431,14 +1441,17 @@ version = "0.1.0" dependencies = [ "anyhow", "bevy", + "bevy_tweening", "fastrand", "iyes_loopless", "log", + "micro_asset_io", "micro_banimate", "micro_musicbox", "serde", "serde_json", "thiserror", + "toml", "web-sys", ] @@ -1699,6 +1712,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "interpolation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7357d2bbc5ee92f8e899ab645233e43d21407573cceb37fed8bc3dede2c02" + [[package]] name = "itoa" version = "1.0.4" @@ -1845,6 +1864,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "mach" version = "0.3.2" @@ -1910,6 +1940,23 @@ dependencies = [ "objc", ] +[[package]] +name = "micro_asset_io" +version = "0.1.0" +dependencies = [ + "anyhow", + "bevy_app", + "bevy_asset", + "bevy_ecs", + "bevy_reflect", + "bevy_tasks", + "flate2", + "futures-lite", + "log", + "tar", + "xz2", +] + [[package]] name = "micro_banimate" version = "0.2.1" @@ -2801,6 +2848,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -3461,6 +3519,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + [[package]] name = "xcursor" version = "0.3.4" @@ -3481,3 +3548,12 @@ name = "xml-rs" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] diff --git a/Cargo.toml b/Cargo.toml index a1651997580825f71a14895ee1f3d613f49b352c..1ae6fca738a1d73d133d7756e3b707c5adaceb7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "game_core", + "micro_asset_io", ] [workspace.dependencies] @@ -18,6 +19,9 @@ iyes_loopless = "0.9.1" micro_musicbox = { version = "0.5.0", features = ["mp3"] } micro_banimate = "0.2.1" +kayak_ui = { rev = "220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7", git = "https://github.com/StarArawn/kayak_ui" } +kayak_font = { rev = "220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7", git = "https://github.com/StarArawn/kayak_ui.git" } + [workspace.dependencies.bevy] version = "0.9.0" default-features = false diff --git a/Makefile b/Makefile index 2dc4e111f5fabb10dc0a0a26fea2a3e955504ff9..3d8c38951e82535b0e1a6890b832a3297a239fb8 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,9 @@ run-web: check: cargo check --release --features "bevy/dynamic" -p game_core +pak: + cd raw_assets && tar -cJf ../assets/resources.apack ./ + build-windows: clean_dist top_tail docker run --rm --name "${PROJECT_NAME}-build-windows" -v "$(CURRENT_DIRECTORY):/app" -w /app --user $(shell id -u):$(shell id -g) r.lcr.gr/microhacks/bevy-builder \ cargo build --release -p game_core --target x86_64-pc-windows-gnu diff --git a/assets/maps/overworld.ldtk b/assets/maps/overworld.ldtk new file mode 100644 index 0000000000000000000000000000000000000000..deca56318dc78664be698711b94a41cd1b7cafd0 --- /dev/null +++ b/assets/maps/overworld.ldtk @@ -0,0 +1,63 @@ +{ + "__header__": { + "fileType": "LDtk Project JSON", + "app": "LDtk", + "doc": "https://ldtk.io/json", + "schema": "https://ldtk.io/files/JSON_SCHEMA.json", + "appAuthor": "Sebastien 'deepnight' Benard", + "appVersion": "1.1.3", + "url": "https://ldtk.io" + }, + "jsonVersion": "1.1.3", + "appBuildId": 458364, + "nextUid": 1, + "identifierStyle": "Capitalize", + "worldLayout": "Free", + "worldGridWidth": 256, + "worldGridHeight": 256, + "defaultLevelWidth": 256, + "defaultLevelHeight": 256, + "defaultPivotX": 0, + "defaultPivotY": 0, + "defaultGridSize": 16, + "bgColor": "#40465B", + "defaultLevelBgColor": "#696A79", + "minifyJson": false, + "externalLevels": false, + "exportTiled": false, + "simplifiedExport": false, + "imageExportMode": "None", + "pngFilePattern": null, + "backupOnSave": false, + "backupLimit": 10, + "levelNamePattern": "Level_%idx", + "tutorialDesc": null, + "flags": [], + "defs": { "layers": [], "entities": [], "tilesets": [], "enums": [], "externalEnums": [], "levelFields": [] }, + "levels": [ + { + "identifier": "Level_0", + "iid": "bb560c20-5110-11ed-89b7-477fe10d896e", + "uid": 0, + "worldX": 0, + "worldY": 0, + "worldDepth": 0, + "pxWid": 256, + "pxHei": 256, + "__bgColor": "#696A79", + "bgColor": null, + "useAutoIdentifier": true, + "bgRelPath": null, + "bgPos": null, + "bgPivotX": 0.5, + "bgPivotY": 0.5, + "__smartColor": "#ADADB5", + "__bgPos": null, + "externalRelPath": null, + "fieldInstances": [], + "layerInstances": [], + "__neighbours": [] + } + ], + "worlds": [] +} \ No newline at end of file diff --git a/assets/resources.apack b/assets/resources.apack new file mode 100644 index 0000000000000000000000000000000000000000..b3121861cf1be79f9eaebfdcd0074d351e85a440 --- /dev/null +++ b/assets/resources.apack @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14b68833d9afa61d753201afec2aa717ed8266344799c5a46bdb03892288331a +size 44828 diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml index e29da139023946621c2256a673f544644dd2be83..af78c8b55d9699089c7d2dd1150694471dc23aa3 100644 --- a/game_core/Cargo.toml +++ b/game_core/Cargo.toml @@ -19,6 +19,11 @@ iyes_loopless.workspace = true micro_banimate.workspace = true micro_musicbox.workspace = true +micro_asset_io = { path = "../micro_asset_io" } + +bevy_tweening = "0.6.0" +toml = "0.5.9" + #remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"} [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/game_core/src/assets/apack_handler.rs b/game_core/src/assets/apack_handler.rs new file mode 100644 index 0000000000000000000000000000000000000000..ea3e271e2ac612e24c7003c90ff3f171c37dbc24 --- /dev/null +++ b/game_core/src/assets/apack_handler.rs @@ -0,0 +1,226 @@ +/// This module is a cut down version of the currently WIP APack loader plugin that will +/// accompany micro_asset_io. +/// +/// As such, it is missing many features and has been put together by cutting bits from various +/// files. Have a peek for curiosity, but try not to learn from this particular file +/// +use std::ffi::OsStr; +use std::path::PathBuf; + +use bevy::asset::Asset; +use bevy::ecs::system::{SystemParam, SystemParamFetch, SystemState}; +use bevy::math::vec2; +use bevy::prelude::*; +use bevy::render::texture::{CompressedImageFormats, ImageType}; +use micro_asset_io::{APack, APackProcessingComplete}; +use serde::{Deserialize, Serialize}; + +use crate::assets::AssetHandles; + +fn file_prefix(path: &PathBuf) -> Option<String> { + path.file_name().and_then(|ss: &OsStr| { + let lossy = ss.to_string_lossy(); + let mut parts = lossy.split("."); + parts.next().map(String::from) + }) +} + +trait BuildType { + fn build_from_bytes(bytes: &Vec<u8>) -> Self; +} + +impl BuildType for Image { + fn build_from_bytes(bytes: &Vec<u8>) -> Self { + Image::from_buffer( + bytes.as_slice(), + ImageType::Extension("png"), + CompressedImageFormats::all(), + true, + ) + .expect("Invalid PNG") + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ManifestGenericAsset { + pub name: String, + pub path: String, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ManifestImage { + pub name: String, + pub path: String, + pub format: String, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ManifestSpriteSheetDescriptor { + pub size: usize, + pub rows: usize, + pub columns: usize, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ManifestSpriteSheet { + pub name: String, + pub path: String, + pub tiles: ManifestSpriteSheetDescriptor, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct APackManifest { + #[serde(default = "Vec::new")] + pub images: Vec<ManifestImage>, + #[serde(default = "Vec::new")] + pub spritesheets: Vec<ManifestSpriteSheet>, + #[serde(default = "Vec::new")] + pub fonts: Vec<ManifestGenericAsset>, +} + +fn in_world<Param: SystemParam + 'static>( + world: &mut World, + cb: impl FnOnce(&mut <Param::Fetch as SystemParamFetch>::Item), +) { + let mut state: SystemState<Param> = SystemState::new(world); + let mut params = state.get_mut(world); + cb(&mut params); +} + +fn create_asset_type<Type: Asset + BuildType>( + world: &mut World, + bytes: &Vec<u8>, + cb: impl FnOnce(Handle<Type>, &mut ResMut<AssetHandles>), +) { + let mut state: SystemState<(ResMut<Assets<Type>>, ResMut<AssetHandles>)> = + SystemState::new(world); + let (mut assets, mut handles) = state.get_mut(world); + + let value = Type::build_from_bytes(bytes); + let handle = assets.add(value); + + cb(handle, &mut handles); +} + +pub fn handle_apack_process_events(world: &mut World) { + let events: Vec<APackProcessingComplete> = { + let mut state: SystemState<ResMut<Events<APackProcessingComplete>>> = + SystemState::new(world); + let mut events = state.get_mut(world); + + events.drain().collect() + }; + + for event in events { + let pack = { + let mut state: SystemState<ResMut<Assets<APack>>> = SystemState::new(world); + let mut packs = state.get_mut(world); + if let Some(pack) = packs.get_mut(&event.0) { + if pack.processed { + std::mem::take(&mut pack.vfs) + } else { + continue; + } + } else { + continue; + } + }; + + if let Some(raw) = pack.get("./manifest.toml") { + let manifest: APackManifest = + toml::from_slice(raw.as_slice()).expect("Badly formatted apack manifest"); + + for image in &manifest.images { + let asset = pack + .get(&format!("./{}", &image.path)) + .expect("Missing asset"); + + if let Ok(image_data) = Image::from_buffer( + asset.as_slice(), + ImageType::Extension(&image.format), + CompressedImageFormats::all(), + true, + ) { + in_world::<ParamSet<(ResMut<Assets<Image>>, ResMut<AssetHandles>)>>( + world, + move |params: &mut ParamSet<( + ResMut<Assets<Image>>, + ResMut<AssetHandles>, + )>| { + let handle = params.p0().add(image_data); + params.p1().images.insert(image.name.clone(), handle); + }, + ); + } else { + log::warn!("Malformed image {}", &image.path); + } + } + + for font in &manifest.fonts { + let asset = pack + .get(&format!("./{}", &font.path)) + .expect("Missing asset"); + + if let Ok(font_data) = Font::try_from_bytes(asset.to_vec()) { + in_world::<ParamSet<(ResMut<Assets<Font>>, ResMut<AssetHandles>)>>( + world, + move |params: &mut ParamSet<( + ResMut<Assets<Font>>, + ResMut<AssetHandles>, + )>| { + let handle = params.p0().add(font_data); + params.p1().fonts.insert(font.name.clone(), handle); + }, + ); + } else { + log::warn!("Malformed font {}", font.path); + } + } + + for spritesheet in &manifest.spritesheets { + let asset = pack + .get(&format!("./{}", &spritesheet.path)) + .expect("Missing asset"); + + if let Ok(image_data) = Image::from_buffer( + asset.as_slice(), + ImageType::Extension("png"), + CompressedImageFormats::all(), + true, + ) { + in_world::< + ParamSet<( + ResMut<Assets<Image>>, + ResMut<Assets<TextureAtlas>>, + ResMut<AssetHandles>, + )>, + >( + world, + move |params: &mut ParamSet<( + ResMut<Assets<Image>>, + ResMut<Assets<TextureAtlas>>, + ResMut<AssetHandles>, + )>| { + let image_handle = params.p0().add(image_data); + let texture_atlas = TextureAtlas::from_grid( + image_handle.clone_weak(), + Vec2::splat(spritesheet.tiles.size as f32), + spritesheet.tiles.columns, + spritesheet.tiles.rows, + None, + None, + ); + + let atlas_handle = params.p1().add(texture_atlas); + let mut assets = params.p2(); + assets.images.insert(spritesheet.name.clone(), image_handle); + assets.atlas.insert(spritesheet.name.clone(), atlas_handle); + }, + ); + } else { + log::warn!("Malformed spritesheet {}", &spritesheet.path); + } + } + } + } +} diff --git a/game_core/src/assets/loader.rs b/game_core/src/assets/loader.rs index a66028a4ec6e26ca17a5f6e7530751c1aba16cb1..e725fbbb70a446d7c4c8ac3a5bcf9f6ace166c8d 100644 --- a/game_core/src/assets/loader.rs +++ b/game_core/src/assets/loader.rs @@ -4,6 +4,7 @@ use bevy::asset::LoadState; use bevy::ecs::system::SystemParam; use bevy::prelude::*; use bevy::reflect::TypeUuid; +use micro_asset_io::APack; use micro_musicbox::prelude::AudioSource; use crate::assets::{AssetHandles, FixedAssetNameMapping, SpriteSheetConfig}; @@ -55,6 +56,7 @@ impl<'w, 's> AssetTypeLoader<'w, 's> { load_basic_type!(load_images, Image => images); load_basic_type!(load_audio, AudioSource => sounds); load_basic_type!(load_font, Font => fonts); + load_basic_type!(load_apack, APack => apacks); pub fn load_spritesheet( &mut self, diff --git a/game_core/src/assets/mod.rs b/game_core/src/assets/mod.rs index 5fc030efb503352aec0631efe091a2e49b9a2124..37ce6f9abbe8fee11a395aedeaf126468ec8f68b 100644 --- a/game_core/src/assets/mod.rs +++ b/game_core/src/assets/mod.rs @@ -1,3 +1,4 @@ +mod apack_handler; mod loader; mod resources; mod startup; @@ -8,6 +9,7 @@ use iyes_loopless::prelude::AppLooplessStateExt; pub use loader::AssetTypeLoader; pub use resources::{AssetHandles, AssetNameMapping, FixedAssetNameMapping, SpriteSheetConfig}; +use crate::assets::apack_handler::handle_apack_process_events; use crate::system::flow::AppState; pub struct AssetsPlugin; @@ -16,6 +18,13 @@ impl Plugin for AssetsPlugin { app.init_resource::<AssetHandles>() .add_enter_system(AppState::Preload, startup::start_preload_resources) .add_enter_system(AppState::Preload, startup::start_load_resources) + .add_system(handle_apack_process_events) + .add_enter_system( + AppState::Menu, + |assets: bevy::prelude::Res<AssetHandles>| { + log::info!("{:?}", assets.images); + }, + ) .add_system_set( ConditionSet::new() .run_in_state(AppState::Setup) diff --git a/game_core/src/assets/resources.rs b/game_core/src/assets/resources.rs index 0800d966e3cc927da713f7956904cd95297e6eb3..3b317e0475cd6e75bf4334f37db84a36a12772e6 100644 --- a/game_core/src/assets/resources.rs +++ b/game_core/src/assets/resources.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; use bevy::utils::HashMap; +use micro_asset_io::APack; use micro_musicbox::prelude::AudioSource; use micro_musicbox::utilities::{SuppliesAudio, TrackType}; @@ -37,6 +38,7 @@ pub struct AssetHandles { pub atlas: HashMap<String, Handle<TextureAtlas>>, pub sounds: HashMap<String, Handle<AudioSource>>, pub fonts: HashMap<String, Handle<Font>>, + pub apacks: HashMap<String, Handle<APack>>, } macro_rules! fetch_wrapper { @@ -66,6 +68,7 @@ impl AssetHandles { fetch_wrapper!(atlas, TextureAtlas => atlas); fetch_wrapper!(sound, AudioSource => sounds); fetch_wrapper!(font, Font => fonts); + fetch_wrapper!(apack, APack => apacks); } impl SuppliesAudio for AssetHandles { diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs index cb6d71ed2de4f8479c254b106a5effea69d0b012..18641088458aee209dc9716d4f0420b1b0b0df9f 100644 --- a/game_core/src/assets/startup.rs +++ b/game_core/src/assets/startup.rs @@ -14,6 +14,7 @@ pub fn start_preload_resources(mut commands: Commands) { pub fn start_load_resources(mut loader: AssetTypeLoader) { loader.load_images(&[("splash.png", "splash")]); loader.load_audio(&[("splash_sting.mp3", "splash_sting")]); + loader.load_apack(&[("resources.apack", "resources")]); } pub fn check_load_resources(mut commands: Commands, loader: AssetTypeLoader) { diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs index e0acb2fac262674fb983e9880091120b5ad2a646..cd6d8dcfd7d64c2e9d4066f4b458ed62d3343301 100644 --- a/game_core/src/lib.rs +++ b/game_core/src/lib.rs @@ -1,4 +1,7 @@ +extern crate core; + pub mod assets; pub mod multiplayer; pub mod splash_screen; +pub mod states; pub mod system; diff --git a/game_core/src/main.rs b/game_core/src/main.rs index 1eda1ba63ea6e8a0e5938db49a1bd2b4321b7369..ba118436d60bf522ed2e16438c5a429e1ff02229 100644 --- a/game_core/src/main.rs +++ b/game_core/src/main.rs @@ -4,7 +4,6 @@ use game_core::system::flow::AppState; use game_core::system::resources::InitAppPlugins; use iyes_loopless::prelude::AppLooplessStateExt; use micro_musicbox::CombinedAudioPlugins; -// use remote_events::RemoteEventPlugin; fn main() { App::new() @@ -14,9 +13,8 @@ fn main() { .add_plugins(CombinedAudioPlugins::<AssetHandles>::new()) .add_plugin(game_core::splash_screen::SplashScreenPlugin) .add_plugin(game_core::system::camera::CameraManagementPlugin) - // .add_plugin(RemoteEventPlugin::< - // game_core::multiplayer::OutgoingEvent, - // game_core::multiplayer::IncomingEvent, - // >::new()) + .add_plugin(game_core::states::StatesPlugin) + .add_plugin(micro_asset_io::MicroAssetIOPlugin) + .add_plugin(bevy_tweening::TweeningPlugin) .run(); } diff --git a/game_core/src/states/menu_state.rs b/game_core/src/states/menu_state.rs new file mode 100644 index 0000000000000000000000000000000000000000..e0638347a92677d9bbc5b4df9598f7b8e2c5b7cf --- /dev/null +++ b/game_core/src/states/menu_state.rs @@ -0,0 +1,84 @@ +use std::time::Duration; + +use bevy::prelude::*; +use bevy_tweening::lens::TextColorLens; +use bevy_tweening::{Animator, EaseFunction, RepeatCount, RepeatStrategy, Tween}; + +use crate::assets::AssetHandles; + +#[derive(Component)] +pub struct MenuStateEntity; + +pub fn spawn_menu_entities(mut commands: Commands, assets: Res<AssetHandles>) { + commands.spawn(( + SpriteBundle { + texture: assets.image("menu_background"), + ..Default::default() + }, + MenuStateEntity, + )); + + commands + .spawn(NodeBundle { + style: Style { + size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + ..Default::default() + }, + ..Default::default() + }) + .with_children(|commands| { + commands.spawn(TextBundle { + text: Text::from_section( + "Trader Tales", + TextStyle { + font_size: 72.0, + font: assets.font("default"), + color: Color::ANTIQUE_WHITE, + }, + ), + style: Style { + margin: UiRect::top(Val::Percent(20.0)), + ..Default::default() + }, + ..Default::default() + }); + commands.spawn(( + TextBundle { + text: Text::from_section( + "> Get Trading <", + TextStyle { + font_size: 48.0, + font: assets.font("default"), + color: Color::ANTIQUE_WHITE, + }, + ), + style: Style { + margin: UiRect::top(Val::Px(50.0)), + ..Default::default() + }, + ..Default::default() + }, + Animator::new( + Tween::new( + EaseFunction::QuadraticInOut, + Duration::from_secs(1), + TextColorLens { + start: Color::ANTIQUE_WHITE, + end: *Color::ANTIQUE_WHITE.set_a(0.0), + section: 0, + }, + ) + .with_repeat_count(RepeatCount::Infinite) + .with_repeat_strategy(RepeatStrategy::MirroredRepeat), + ), + )); + }); +} + +pub fn despawn_menu_entities(mut commands: Commands, query: Query<Entity, With<MenuStateEntity>>) { + for entity in &query { + commands.entity(entity).despawn_recursive(); + } +} diff --git a/game_core/src/states/mod.rs b/game_core/src/states/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..20d5cbde4e5d2574926702faf92e26d62847f9d0 --- /dev/null +++ b/game_core/src/states/mod.rs @@ -0,0 +1,14 @@ +use bevy::app::{App, Plugin}; +use iyes_loopless::prelude::AppLooplessStateExt; + +use crate::system::flow::AppState; + +mod menu_state; + +pub struct StatesPlugin; +impl Plugin for StatesPlugin { + fn build(&self, app: &mut App) { + app.add_enter_system(AppState::Menu, menu_state::spawn_menu_entities) + .add_exit_system(AppState::Menu, menu_state::despawn_menu_entities); + } +} diff --git a/game_core/src/system/load_config.rs b/game_core/src/system/load_config.rs index fa15cb9108edc2b0c32fbd78aa290d8c293c2ae5..a989503b5a093d3b463381b4d121f5efc29be91f 100644 --- a/game_core/src/system/load_config.rs +++ b/game_core/src/system/load_config.rs @@ -13,7 +13,7 @@ mod setup { (1280.0, 720.0) } pub fn virtual_size() -> (f32, f32) { - (1280.0, 720.0) + (640.0, 360.0) } } @@ -23,7 +23,7 @@ mod setup { String::from("assets") } pub fn virtual_size() -> (f32, f32) { - (1280.0, 720.0) + (640.0, 360.0) } #[cfg(feature = "no_aspect")] pub fn initial_size() -> (f32, f32) { diff --git a/micro_asset_io/Cargo.toml b/micro_asset_io/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..49f7081c5160e5aed6d9c3476035b065cc8a83bf --- /dev/null +++ b/micro_asset_io/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "micro_asset_io" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bevy_asset = "0.9.1" +bevy_reflect = "0.9.1" +bevy_ecs = "0.9.1" +bevy_tasks = "0.9.1" +bevy_app = "0.9.1" + +anyhow = "1.0.66" +flate2 = { version = "1.0.25", features = ["rust_backend"] } +tar = "0.4.38" +log = "0.4.17" +futures-lite = "1.12.0" +xz2 = "0.1.7" \ No newline at end of file diff --git a/micro_asset_io/src/apack_loader.rs b/micro_asset_io/src/apack_loader.rs new file mode 100644 index 0000000000000000000000000000000000000000..2417fc45c3e15d9a4b579f81394589b2cab170ef --- /dev/null +++ b/micro_asset_io/src/apack_loader.rs @@ -0,0 +1,248 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use std::io::Read; +use std::ops::{Deref, DerefMut, Index, IndexMut}; + +use bevy_asset::{AssetEvent, AssetLoader, Assets, BoxedFuture, Handle, LoadContext, LoadedAsset}; +use bevy_ecs::component::Component; +use bevy_ecs::entity::Entity; +use bevy_ecs::event::{EventReader, EventWriter}; +use bevy_ecs::system::{Commands, Query, Res, ResMut}; +use bevy_reflect::TypeUuid; +use bevy_tasks::{AsyncComputeTaskPool, Task}; +use futures_lite::future; +use xz2::read::XzDecoder; +// use bevy::prelude::*; + +#[derive(Default)] +pub struct APackLoader; + +#[derive(TypeUuid)] +#[uuid = "bce5dd7a-726c-11ed-b53f-071b386b4896"] +pub struct APack { + pub processed: bool, + pub compressed_bytes: Vec<u8>, + pub vfs: HashMap<String, Vec<u8>>, +} + +struct DebugVfs<'a>(&'a HashMap<String, Vec<u8>>); +impl<'a> Debug for DebugVfs<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_map() + .entries(self.0.iter().map(|(key, value)| { + let key = key.clone(); + let value = format!("len({})", value.len()); + + (key, value) + })) + .finish() + } +} + +impl Debug for APack { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("APack") + .field("processed", &self.processed) + .field( + "compressed_bytes", + &format!("len({})", self.compressed_bytes.len()), + ) + .field("vfs", &DebugVfs(&self.vfs)) + .finish() + } +} + +pub struct ClonableAPack(pub APack); +impl Deref for ClonableAPack { + type Target = APack; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for ClonableAPack { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl Clone for ClonableAPack { + fn clone(&self) -> Self { + ClonableAPack(APack { + processed: self.processed, + compressed_bytes: self.compressed_bytes.clone(), + vfs: self.vfs.clone(), + }) + } +} +impl Into<APack> for ClonableAPack { + fn into(self) -> APack { + self.0 + } +} + +impl APack { + pub fn new(bytes: Vec<u8>) -> APack { + APack { + processed: false, + compressed_bytes: bytes, + vfs: Default::default(), + } + } + + /// Cloning an APack can be _very_ expensive, depending on the + /// contents. To clone an APack, you've got to jump through some hoops + pub fn into_clone(self) -> ClonableAPack { + ClonableAPack(self) + } + + pub fn clone_ref(apack: &APack) -> APack { + APack { + processed: apack.processed, + compressed_bytes: apack.compressed_bytes.clone(), + vfs: apack.vfs.clone(), + } + } + + /// An APack doesn't need to hold onto the raw bytes after it has been processed. + /// This method clears the raw buffer + pub fn drop_raw_bytes(&mut self) { + self.compressed_bytes = Vec::new(); + } + + pub fn set(&mut self, key: impl ToString, value: Vec<u8>) { + self.vfs.insert(key.to_string(), value); + } +} + +impl Index<&String> for APack { + type Output = Vec<u8>; + + fn index(&self, index: &String) -> &Self::Output { + &self.vfs[index] + } +} +impl IndexMut<&String> for APack { + fn index_mut(&mut self, index: &String) -> &mut Self::Output { + self.vfs + .get_mut(index) + .expect(&*format!("Missing key {}", index)) + } +} + +impl AssetLoader for APackLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, anyhow::Result<(), anyhow::Error>> { + Box::pin(async move { + load_context.set_default_asset(LoadedAsset::new(APack::new(bytes.to_vec()))); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["apack"] + } +} + +type TaskOutput = (APack, Handle<APack>); +#[derive(Component)] +pub struct ProcessAPack(Task<TaskOutput>); + +fn create_task_body( + mut pack: APack, + handle_ref: Handle<APack>, +) -> impl futures_lite::Future<Output = TaskOutput> { + async move { + let mut pack = pack; + + let raw_bytes = std::mem::take(&mut pack.compressed_bytes); + let decoder = XzDecoder::new(raw_bytes.as_slice()); + let mut tarboy = tar::Archive::new(decoder); + + if let Ok(entries) = tarboy.entries() { + for entry in entries.flatten() { + let path = &entry + .path() + .ok() + .and_then(|v| v.to_str().map(String::from)) + .expect("Malformed path"); + + let file = entry.bytes().flatten().collect::<Vec<u8>>(); + pack.set(path, file); + } + } + + pack.processed = true; + pack.drop_raw_bytes(); + (pack, handle_ref) + } +} + +pub fn process_loaded_apacks( + mut commands: Commands, + mut events: EventReader<AssetEvent<APack>>, + packs: Res<Assets<APack>>, +) { + if !events.is_empty() { + let pool = AsyncComputeTaskPool::get(); + + for event in events.iter() { + match event { + AssetEvent::Created { handle } => { + if let Some(pack) = packs.get(handle) { + let copy = APack::clone_ref(pack); + let handle_ref = handle.clone_weak(); + let task = pool.spawn(create_task_body(copy, handle_ref)); + + commands.spawn(ProcessAPack(task)); + } else { + log::warn!("Received a 'created' event for an apack that is not part of the Assets<APack> resource"); + } + } + AssetEvent::Modified { handle } => { + if let Some(pack) = packs.get(handle) { + if !pack.processed { + log::debug!( + "Handling hot-reload of APack by spawning new processing task" + ); + + let copy = APack::clone_ref(pack); + let handle_ref = handle.clone_weak(); + let task = pool.spawn(create_task_body(copy, handle_ref)); + + commands.spawn(ProcessAPack(task)); + } else { + log::debug!("APack was changed, but will not notify because it has already been processed") + } + } else { + log::warn!("Received a 'modified' event for an apack that is not part of the Assets<APack> resource"); + } + } + AssetEvent::Removed { .. } => { + log::debug!("Removed an APack. Handles already extracted from this APack may not be removed"); + } + } + } + } +} + +pub struct APackProcessingComplete(pub Handle<APack>); + +pub fn poll_apack_loading_tasks( + mut commands: Commands, + mut query: Query<(Entity, &mut ProcessAPack)>, + mut packs: ResMut<Assets<APack>>, + mut events: EventWriter<APackProcessingComplete>, +) { + for (entity, mut task) in &mut query { + if let Some((pack, handle)) = future::block_on(future::poll_once(&mut task.0)) { + if let Some(existing) = packs.get_mut(&handle) { + *existing = pack; + } + events.send(APackProcessingComplete(handle.clone_weak())); + commands.entity(entity).despawn(); + } + } +} diff --git a/micro_asset_io/src/lib.rs b/micro_asset_io/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..33b995857d8c68433e10ea3fdf410d7c6abf731d --- /dev/null +++ b/micro_asset_io/src/lib.rs @@ -0,0 +1,20 @@ +mod apack_loader; + +mod micro_asset_io { + use bevy_app::{App, Plugin}; + use bevy_asset::AddAsset; + + pub struct MicroAssetIOPlugin; + impl Plugin for MicroAssetIOPlugin { + fn build(&self, app: &mut App) { + app.add_event::<super::apack_loader::APackProcessingComplete>() + .add_asset::<super::apack_loader::APack>() + .add_asset_loader(super::apack_loader::APackLoader) + .add_system(super::apack_loader::process_loaded_apacks) + .add_system(super::apack_loader::poll_apack_loading_tasks); + } + } +} + +pub use apack_loader::{APack, APackLoader, APackProcessingComplete, ClonableAPack, ProcessAPack}; +pub use micro_asset_io::MicroAssetIOPlugin; diff --git a/raw_assets/.gitignore b/raw_assets/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..fbeaca80e3ce81fa39959a7c162c6f3e2bccd7ff --- /dev/null +++ b/raw_assets/.gitignore @@ -0,0 +1,6 @@ +backgrounds/ +fonts/ +sprites/ + +!.gitignore +!manifest.toml \ No newline at end of file diff --git a/raw_assets/manifest.toml b/raw_assets/manifest.toml new file mode 100644 index 0000000000000000000000000000000000000000..c6536724a339df51bbfcb7d77dcea613885082c0 --- /dev/null +++ b/raw_assets/manifest.toml @@ -0,0 +1,13 @@ +[[spritesheets]] +path = "sprites/overworld.png" +name = "overworld" +tiles = { size = 4, columns = 32, rows = 64 } + +[[images]] +path = "backgrounds/main_menu.png" +name = "menu_background" +format = "png" + +[[fonts]] +path = "fonts/CompassPro.ttf" +name = "default" \ No newline at end of file