Compare commits
No commits in common. 'main' and 'attempt/001' have entirely different histories.
main
...
attempt/00
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,34 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "acts-of-gods"
|
name = "acts-of-gods"
|
||||||
version = "0.0.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = "0.14"
|
bevy = { version = "0.14", features = ["file_watcher", "dynamic_linking"] }
|
||||||
bevy_common_assets = "0.11"
|
bevy_mod_picking = "0.20"
|
||||||
|
thiserror = "1"
|
||||||
|
nom = "7"
|
||||||
|
# Entity Uuid parsing
|
||||||
|
# TODO: Remove this
|
||||||
|
uuid = "1.7.0"
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
chrono = { version = "0.4" }
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = "thin"
|
||||||
|
|
||||||
|
[profile.wasm-release]
|
||||||
|
inherits = "release"
|
||||||
|
# opt-level = "z"
|
||||||
|
opt-level = "s"
|
||||||
|
lto = "fat"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = "debuginfo"
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Assets
|
||||||
|
|
||||||
|
## Custom Assets
|
||||||
|
|
||||||
|
### `.entity` files
|
||||||
|
|
||||||
|
Files ending in `.entity` store entity information.
|
||||||
|
|
||||||
|
Note: This data is not auto-magically de/serialized by Bevy, we write our own parser using `nom`.
|
||||||
|
This custom reading/writing can be found in `src/save.rs`.
|
||||||
|
|
||||||
|
All `.entity` files list one component per line.
|
||||||
|
Components include:
|
||||||
|
* `name <string>` Human readable name for entity
|
||||||
|
* `uuid <UUID string>` Globally unique ID for entity
|
||||||
|
* `transform translation <f32> <f32> <f32> rotation <f32> <f32> <f32> <f32> scale <f32> <f32> <f32>` 3D Location, Rotation, and Scale for entity
|
||||||
|
* `model "<path string>" "<string>"` Declares the entity's 3d model
|
||||||
|
* `camera` Marks the entity as a camera
|
||||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,2 @@
|
|||||||
|
00/camera.entity
|
||||||
|
00/van.entity
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
camera
|
||||||
|
transform translation 2.0 2.0 2.0 ...
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
; spatial3d
|
||||||
|
; transform ...
|
||||||
|
; pointLight color #ffffff intensity 800
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
transform ...
|
||||||
|
visible
|
||||||
|
model "models/van.glb" "Scene"
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=ActsOfGods
|
||||||
|
Exec=ActsOfGods
|
||||||
|
Icon=ActsOfGods
|
||||||
|
Type=Application
|
||||||
|
Categories=Game;
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
<?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>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Acts of Gods</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>ActsOfGods</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>ActsOfGods.icns</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>your.domain.bevy-game</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>ActsOfGods</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.games</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.9</string>
|
||||||
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
|
<array>
|
||||||
|
<string>MacOSX</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<body style="margin: 0px;">
|
||||||
|
<script type="module">
|
||||||
|
import init from './acts-of-gods.js'
|
||||||
|
|
||||||
|
init().catch((error) => {
|
||||||
|
if (!error.message.startsWith("Using exceptions for control flow, don't mind me. This isn't actually an error!")) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
|
components = [
|
||||||
|
"clippy",
|
||||||
|
"llvm-tools-preview",
|
||||||
|
"rls",
|
||||||
|
"rustc-dev",
|
||||||
|
"rustfmt",
|
||||||
|
# "wasm-bindgen-cli",
|
||||||
|
# "wasm-server-runner",
|
||||||
|
]
|
||||||
|
targets = [
|
||||||
|
# "aarch64-apple-darwin",
|
||||||
|
# "wasm32-unknown-unknown",
|
||||||
|
# "x86_64-apple-darwin",
|
||||||
|
# "x86_64-unknown-linux-gnu",
|
||||||
|
"x86_64-pc-windows-msvc",
|
||||||
|
]
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
TARGET="x86_64-unknown-linux-gnu"
|
||||||
|
|
||||||
|
cargo build --release --target $TARGET
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
AARCH_TARGET="aarch64-apple-darwin"
|
||||||
|
X64_TARGET="x86_64-apple-darwin"
|
||||||
|
|
||||||
|
# Generate x86 and arm builds
|
||||||
|
cargo build --release --target $AARCH_TARGET
|
||||||
|
cargo build --release --target $X64_TARGET
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
BUILD_FILE="./target/$TARGET/release/$NAME.wasm"
|
||||||
|
OUT_DIR="./platforms/web/"
|
||||||
|
NAME="acts-of-gods"
|
||||||
|
TARGET="wasm32-unknown-unknown"
|
||||||
|
|
||||||
|
cargo build --profile wasm-release --target $TARGET
|
||||||
|
|
||||||
|
wasm-bindgen --target web \
|
||||||
|
--out-dir $OUT_DIR \
|
||||||
|
--out-name $NAME \
|
||||||
|
$BUILD_FILE
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
# Build x86_64 release
|
||||||
|
cargo build --release --target x86_64-pc-windows-msvc
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
cp media/ActsOfGods.png platforms/linux/ActsOfGods.AppDir/ActsOfGods.png
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# https://gist.github.com/ansarizafar/6fa64f44aa933794c4d6638eec32b9aa
|
||||||
|
|
||||||
|
BASE="media/ActsOfGods.png"
|
||||||
|
OUT_DIR="media/ActsOfGods.iconset"
|
||||||
|
mkdir -p $OUT_DIR
|
||||||
|
|
||||||
|
# 1024x1024
|
||||||
|
sips "$BASE" -Z 1024 -o $OUT_DIR/icon_512x512@2.png
|
||||||
|
|
||||||
|
# 512x512
|
||||||
|
sips "$BASE" -Z 512 -o $OUT_DIR/icon_512x512.png
|
||||||
|
sips "$BASE" -Z 512 -o $OUT_DIR/icon_256x256@2.png
|
||||||
|
|
||||||
|
# 256x256
|
||||||
|
sips "$BASE" -Z 256 -o $OUT_DIR/icon_256x256.png
|
||||||
|
sips "$BASE" -Z 256 -o $OUT_DIR/icon_128x128@2.png
|
||||||
|
|
||||||
|
# 128x128
|
||||||
|
sips "$BASE" -Z 128 -o $OUT_DIR/icon_128x128.png
|
||||||
|
sips "$BASE" -Z 128 -o $OUT_DIR/icon_64x64@2.png
|
||||||
|
|
||||||
|
# 64x64
|
||||||
|
sips "$BASE" -Z 64 -o $OUT_DIR/icon_64x64.png
|
||||||
|
sips "$BASE" -Z 64 -o $OUT_DIR/icon_32x32@2.png
|
||||||
|
|
||||||
|
# 32x32
|
||||||
|
sips "$BASE" -Z 32 -o $OUT_DIR/icon_32x32.png
|
||||||
|
sips "$BASE" -Z 32 -o $OUT_DIR/icon_16x16@2.png
|
||||||
|
|
||||||
|
# 16x16
|
||||||
|
sips "$BASE" -Z 16 -o $OUT_DIR/icon_16x16.png
|
||||||
|
|
||||||
|
iconutil -c icns $OUT_DIR
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
NAME="acts-of-gods"
|
||||||
|
PACKAGE_NAME="ActsOfGods-alpha-linux"
|
||||||
|
|
||||||
|
# Variables for copying binary file
|
||||||
|
TARGET="x86_64-unknown-linux-gnu"
|
||||||
|
APP_DIR="platforms/linux/ActsOfGods.AppDir/"
|
||||||
|
TARGET_DIR="target/$TARGET/release/"
|
||||||
|
BUILD_BIN="$TARGET_DIR/$NAME"
|
||||||
|
PACKAGE_BIN="$APP_DIR/AppRun"
|
||||||
|
|
||||||
|
# Vars for dynamic libraries
|
||||||
|
PACKAGE_LIB_DIR="$APP_DIR/usr/lib/"
|
||||||
|
LIB_DIR="lib/linux/"
|
||||||
|
|
||||||
|
# Assets
|
||||||
|
ASSETS_DIR="assets"
|
||||||
|
|
||||||
|
# Package variables
|
||||||
|
PACKAGE_FILE="$PACKAGE_NAME.AppImage"
|
||||||
|
DEST_FILE="packages/$PACKAGE_FILE"
|
||||||
|
|
||||||
|
########################################
|
||||||
|
|
||||||
|
# Copy binary to build dir
|
||||||
|
cp -f $BUILD_BIN $PACKAGE_BIN
|
||||||
|
|
||||||
|
# Copy dynamic libraries
|
||||||
|
mkdir -p $PACKAGE_LIB_DIR
|
||||||
|
cp -f $LIB_DIR/* $PACKAGE_LIB_DIR/
|
||||||
|
|
||||||
|
# Copy assets
|
||||||
|
cp -rf $ASSET_DIR $APP_DIR/
|
||||||
|
|
||||||
|
# Build AppImage file
|
||||||
|
appimagetool $APP_DIR $DEST_FILE
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# https://github.com/create-dmg/create-dmg/tree/master?tab=readme-ov-file#create-dmg
|
||||||
|
|
||||||
|
NAME='Acts of Gods.app'
|
||||||
|
APP="platforms/macos/$NAME"
|
||||||
|
CONTENTS="$APP/Contents"
|
||||||
|
GAME="$CONTENTS/MacOS"
|
||||||
|
RESOURCES="$CONTENTS/Resources"
|
||||||
|
FRAMEWORKS="$CONTENTS/Frameworks"
|
||||||
|
|
||||||
|
mkdir -p "$APP"
|
||||||
|
mkdir -p "$GAME"
|
||||||
|
mkdir -p "$RESOURCES"
|
||||||
|
mkdir -p "$FRAMEWORKS"
|
||||||
|
|
||||||
|
# Copy icons to package
|
||||||
|
cp -f media/ActsOfGods.icns "$RESOURCES"
|
||||||
|
|
||||||
|
# Generate cross-architecture binary
|
||||||
|
rm -f "$GAME/ActsOfGods"
|
||||||
|
lipo "target/x86_64-apple-darwin/release/acts-of-gods" \
|
||||||
|
"target/aarch64-apple-darwin/release/acts-of-gods" \
|
||||||
|
-create -output "$GAME/ActsOfGods"
|
||||||
|
|
||||||
|
# Copy assets
|
||||||
|
rm -rf "$GAME/assets"
|
||||||
|
cp -r assets "$GAME/"
|
||||||
|
|
||||||
|
# Copy fmod libraries
|
||||||
|
cp -f lib/macos/libfmod.dylib "$FRAMEWORKS/"
|
||||||
|
cp -f lib/macos/libfmodstudio.dylib "$FRAMEWORKS/"
|
||||||
|
|
||||||
|
# Update dynamic linking search paths
|
||||||
|
install_name_tool -change @rpath/libfmod.dylib "@loader_path/../Frameworks/libfmod.dylib" "$GAME/"
|
||||||
|
install_name_tool -change @rpath/libfmodstudio.dylib "@loader_path/../Frameworks/libfmodstudio.dylib" "$GAME/ActsOfGods"
|
||||||
|
|
||||||
|
# Build dmg file
|
||||||
|
PACKAGE_FILE="packages/ActsOfGods-alpha-macos.dmg"
|
||||||
|
rm -f $PACKAGE_FILE
|
||||||
|
create-dmg \
|
||||||
|
--volname "Acts of Gods" \
|
||||||
|
--volicon "media/ActsOfGods.icns" \
|
||||||
|
--window-pos 200 120 \
|
||||||
|
--window-size 800 400 \
|
||||||
|
--app-drop-link 600 200 \
|
||||||
|
--icon-size 100 \
|
||||||
|
--icon "$NAME" 200 190 \
|
||||||
|
--hide-extension "$NAME" \
|
||||||
|
--app-drop-link 600 185 \
|
||||||
|
$PACKAGE_FILE \
|
||||||
|
./platforms/macos/
|
||||||
|
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Just zip platforms/web
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
Copy-Item -Force "./target/x86_64-pc-windows-msvc/release/acts-of-gods.exe" "./platforms/windows/ActsOfGods.exe"
|
||||||
|
Copy-Item -Force "./lib/windows/*" "./platforms/windows/"
|
||||||
|
Copy-Item -Force -Recurse "./assets" "./platforms/windows"
|
||||||
|
|
||||||
|
$compress = @{
|
||||||
|
Path = "./platforms/windows/*"
|
||||||
|
CompressionLevel = "Optimal"
|
||||||
|
DestinationPath = "./packages/ActsOfGods-alpha-windows.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
Compress-Archive -Force @compress
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Menu Plugin; empty struct for Plugin impl
|
||||||
|
pub(crate) struct CameraPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CameraPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
move_editor_fly_camera.run_if(any_with_component::<FlyCamera>),
|
||||||
|
);
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
rotate_editor_fly_camera.run_if(any_with_component::<FlyCamera>),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub(crate) struct FlyCamera;
|
||||||
|
|
||||||
|
/// Fly camera system for moving around like a drone
|
||||||
|
/// TODO: Only if key is pressed!
|
||||||
|
fn move_editor_fly_camera(
|
||||||
|
mut cameras: Query<(&Camera, &mut Transform), With<FlyCamera>>,
|
||||||
|
windows: Query<&Window>,
|
||||||
|
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
(keys.any_pressed([
|
||||||
|
KeyCode::KeyW,
|
||||||
|
KeyCode::KeyS,
|
||||||
|
KeyCode::KeyA,
|
||||||
|
KeyCode::KeyD,
|
||||||
|
KeyCode::KeyQ,
|
||||||
|
KeyCode::KeyE,
|
||||||
|
]))
|
||||||
|
.then(|| {
|
||||||
|
// Iterate over all cameras
|
||||||
|
cameras.iter_mut().for_each(|(c, mut t)| {
|
||||||
|
// Determine which window this camera is attached to
|
||||||
|
let target_window = match c.target {
|
||||||
|
RenderTarget::Window(wr) => match wr {
|
||||||
|
WindowRef::Entity(e) => Some(e),
|
||||||
|
WindowRef::Primary => Some(primary_window.get_single().unwrap()),
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let window = windows.get(target_window.unwrap()).unwrap();
|
||||||
|
|
||||||
|
// If the target window is focused
|
||||||
|
window.focused.then(|| {
|
||||||
|
let move_speed = if keys.pressed(KeyCode::ShiftLeft) {
|
||||||
|
16.0
|
||||||
|
} else {
|
||||||
|
4.0
|
||||||
|
};
|
||||||
|
let mut delta = Vec3::ZERO;
|
||||||
|
if keys.pressed(KeyCode::KeyW) {
|
||||||
|
delta += t.forward() * move_speed * time.delta_seconds()
|
||||||
|
}
|
||||||
|
if keys.pressed(KeyCode::KeyS) {
|
||||||
|
delta += t.back() * move_speed * time.delta_seconds()
|
||||||
|
}
|
||||||
|
if keys.pressed(KeyCode::KeyA) {
|
||||||
|
delta += t.left() * move_speed * time.delta_seconds()
|
||||||
|
}
|
||||||
|
if keys.pressed(KeyCode::KeyD) {
|
||||||
|
delta += t.right() * move_speed * time.delta_seconds()
|
||||||
|
}
|
||||||
|
if keys.pressed(KeyCode::KeyE) {
|
||||||
|
delta += Vec3::Y * move_speed * time.delta_seconds()
|
||||||
|
}
|
||||||
|
if keys.pressed(KeyCode::KeyQ) {
|
||||||
|
delta += Vec3::NEG_Y * move_speed * time.delta_seconds()
|
||||||
|
}
|
||||||
|
t.translation += delta;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate_editor_fly_camera(
|
||||||
|
mut cameras: Query<(&Camera, &mut Transform), With<FlyCamera>>,
|
||||||
|
windows: Query<&Window>,
|
||||||
|
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||||
|
mouse: Res<ButtonInput<MouseButton>>,
|
||||||
|
mut cursor_events: EventReader<CursorMoved>,
|
||||||
|
) {
|
||||||
|
(!cursor_events.is_empty()).then(|| {
|
||||||
|
// Iterate over all cameras
|
||||||
|
cameras.iter_mut().for_each(|(c, mut t)| {
|
||||||
|
// Determine which window this camera is attached to
|
||||||
|
let target_window = match c.target {
|
||||||
|
RenderTarget::Window(wr) => match wr {
|
||||||
|
WindowRef::Entity(e) => Some(e),
|
||||||
|
WindowRef::Primary => Some(primary_window.get_single().unwrap()),
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let window = windows.get(target_window.unwrap()).unwrap();
|
||||||
|
if mouse.pressed(MouseButton::Middle) {
|
||||||
|
cursor_events
|
||||||
|
.read()
|
||||||
|
.filter_map(|CursorMoved { delta, window, .. }| {
|
||||||
|
(*window == target_window.unwrap()).then_some(delta)
|
||||||
|
})
|
||||||
|
.for_each(|delta| {
|
||||||
|
if let Some(Vec2 { x, y }) = delta {
|
||||||
|
// Cribbing from bevy_flycam
|
||||||
|
// Link: https://github.com/sburris0/bevy_flycam/blob/baffe50e0961ad1491d467fa6ab5551f9f21db8f/src/lib.rs#L145-L151
|
||||||
|
let (mut yaw, mut pitch, _) = t.rotation.to_euler(EulerRot::YXZ);
|
||||||
|
let window_scale = window.height().min(window.width());
|
||||||
|
let sensitivity = 0.00012;
|
||||||
|
pitch -= (sensitivity * y * window_scale).to_radians();
|
||||||
|
yaw -= (sensitivity * x * window_scale).to_radians();
|
||||||
|
t.rotation = Quat::from_axis_angle(Vec3::Y, yaw)
|
||||||
|
* Quat::from_axis_angle(Vec3::X, pitch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cursor_events.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// ECS scheduler active when an entity has a given component added
|
||||||
|
///
|
||||||
|
pub(crate) fn any_component_added<C: Component>(q: Query<Entity, Added<C>>) -> bool {
|
||||||
|
!q.is_empty()
|
||||||
|
}
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub(crate) struct EditorPlugin;
|
||||||
|
|
||||||
|
impl Plugin for EditorPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_state::<EditorState>()
|
||||||
|
.add_systems(OnEnter(EditorState::Open), spawn_editor)
|
||||||
|
.add_systems(OnExit(EditorState::Open), despawn_editor)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
toggle_editor_state.run_if(input_just_pressed(KeyCode::F3)),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
handle_window_close.run_if(on_event::<WindowCloseRequested>()),
|
||||||
|
plane_gizmos,
|
||||||
|
)
|
||||||
|
.run_if(in_state(EditorState::Open)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracking the open/closed state of the editor
|
||||||
|
#[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default, Component)]
|
||||||
|
pub(crate) enum EditorState {
|
||||||
|
/// The editor is closed => the editor window is disabled
|
||||||
|
#[default]
|
||||||
|
Closed,
|
||||||
|
/// The editor is open => the editor window is visible
|
||||||
|
Open,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Not for &EditorState {
|
||||||
|
type Output = EditorState;
|
||||||
|
|
||||||
|
fn not(self) -> Self::Output {
|
||||||
|
match self {
|
||||||
|
EditorState::Open => EditorState::Closed,
|
||||||
|
EditorState::Closed => EditorState::Open,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<&EditorState> for bool {
|
||||||
|
fn from(value: &EditorState) -> bool {
|
||||||
|
match value {
|
||||||
|
EditorState::Open => true,
|
||||||
|
EditorState::Closed => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tag component for editor entities
|
||||||
|
#[derive(Component, Reflect, Debug, Default)]
|
||||||
|
#[reflect(Component, Default)]
|
||||||
|
pub(crate) struct EditorTag;
|
||||||
|
|
||||||
|
fn toggle_editor_state(
|
||||||
|
curr_state: Res<State<EditorState>>,
|
||||||
|
mut next_state: ResMut<NextState<EditorState>>,
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut catch: Local<bool>,
|
||||||
|
) {
|
||||||
|
if keys.pressed(KeyCode::F3) && !*catch {
|
||||||
|
*catch = true;
|
||||||
|
match curr_state.get() {
|
||||||
|
EditorState::Open => next_state.set(EditorState::Closed),
|
||||||
|
EditorState::Closed => next_state.set(EditorState::Open),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*catch = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_editor(mut _commands: Commands) {
|
||||||
|
todo!("Spawn editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn despawn_editor(mut _comands: Commands) {
|
||||||
|
todo!("Despawn editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles window close requests which are only weird because of the editor
|
||||||
|
/// When the editor window closes, just make it invisible and change the editor state to "Closed".
|
||||||
|
fn handle_window_close(
|
||||||
|
mut events: EventReader<WindowCloseRequested>,
|
||||||
|
editor_windows: Query<Entity, With<EditorTag>>,
|
||||||
|
mut editor_state: ResMut<NextState<EditorState>>,
|
||||||
|
) {
|
||||||
|
events
|
||||||
|
.read()
|
||||||
|
// Filter events down to jus those affecting the editor window
|
||||||
|
.filter_map(|WindowCloseRequested { window }| editor_windows.get(*window).ok())
|
||||||
|
// For each related close event, send out a "Close the editor" state transition
|
||||||
|
.for_each(|_| {
|
||||||
|
editor_state.set(EditorState::Closed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plane_gizmos(mut gizmos: Gizmos) {
|
||||||
|
let r = 20.0;
|
||||||
|
let start = -r as i8;
|
||||||
|
let end = r as i8;
|
||||||
|
(start..=end).for_each(|n| {
|
||||||
|
{
|
||||||
|
let offset = Vec3::Z * (n as f32);
|
||||||
|
gizmos.line(Vec3::NEG_X * r + offset, Vec3::X * r + offset, GRAY);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let offset = Vec3::X * (n as f32);
|
||||||
|
gizmos.line(Vec3::NEG_Z * r + offset, Vec3::Z * r + offset, GRAY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// World origin arrows
|
||||||
|
{
|
||||||
|
gizmos.arrow(Vec3::ZERO, Vec3::X, RED);
|
||||||
|
gizmos.arrow(Vec3::ZERO, Vec3::Y, DARK_GREEN);
|
||||||
|
gizmos.arrow(Vec3::ZERO, Vec3::Z, BLUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,44 @@
|
|||||||
|
#![feature(let_chains)]
|
||||||
|
|
||||||
|
/// Camera controller
|
||||||
|
pub(crate) mod camera;
|
||||||
|
/// ECS scheduling `run_if` conditions
|
||||||
|
pub(crate) mod conditions;
|
||||||
|
/// Editor: debugging and run-time modifications to the game
|
||||||
|
pub(crate) mod editor;
|
||||||
|
/// Menu: Main and otherwise
|
||||||
|
pub(crate) mod menu;
|
||||||
|
/// Save file parsing
|
||||||
|
pub(crate) mod parser;
|
||||||
|
/// Helper module containing common imports across the project
|
||||||
|
pub(crate) mod prelude;
|
||||||
|
/// Level saving/loading logic
|
||||||
|
pub(crate) mod save;
|
||||||
|
/// Window handling
|
||||||
|
pub(crate) mod window;
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!");
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
||||||
|
// handled in crate::window::handle_window_close and crate::editor::handle_window_close
|
||||||
|
close_when_requested: false,
|
||||||
|
exit_condition: bevy::window::ExitCondition::OnPrimaryClosed,
|
||||||
|
..default()
|
||||||
|
}))
|
||||||
|
.add_plugins(camera::CameraPlugin)
|
||||||
|
// .add_plugins(editor::EditorPlugin)
|
||||||
|
// .add_plugins(menu::MenuPlugin)
|
||||||
|
// .add_plugins(window::WindowPlugin)
|
||||||
|
.add_plugins(save::SavePlugin {
|
||||||
|
fns: vec![
|
||||||
|
parse_save_name,
|
||||||
|
parse_save_camera,
|
||||||
|
parse_save_visibility,
|
||||||
|
parse_save_transform,
|
||||||
|
parse_save_model,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.run();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub(crate) struct MenuPlugin;
|
||||||
|
|
||||||
|
impl Plugin for MenuPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Startup, init_menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_menu(mut commands: Commands) {
|
||||||
|
// commands.spawn(Camera3dBundle { ..default() });
|
||||||
|
commands
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
max_width: Val::Percent(60.0),
|
||||||
|
max_height: Val::Percent(100.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn(TextBundle {
|
||||||
|
style: Style { ..default() },
|
||||||
|
text: Text {
|
||||||
|
justify: JustifyText::Center,
|
||||||
|
sections: vec![
|
||||||
|
TextSection::new(
|
||||||
|
"ACTS",
|
||||||
|
TextStyle {
|
||||||
|
font_size: 384.0,
|
||||||
|
color: Color::WHITE,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextSection::new(
|
||||||
|
"of\n",
|
||||||
|
TextStyle {
|
||||||
|
font_size: 16.0,
|
||||||
|
color: Color::WHITE,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextSection::new(
|
||||||
|
"GODS",
|
||||||
|
TextStyle {
|
||||||
|
font_size: 384.0,
|
||||||
|
color: Color::WHITE,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,669 @@
|
|||||||
|
use bevy::{
|
||||||
|
core_pipeline::{
|
||||||
|
core_3d::graph::Core3d,
|
||||||
|
tonemapping::{DebandDither, Tonemapping},
|
||||||
|
},
|
||||||
|
render::{
|
||||||
|
camera::{CameraMainTextureUsages, CameraRenderGraph, Exposure},
|
||||||
|
primitives::Frustum,
|
||||||
|
view::{ColorGrading, VisibleEntities},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub(crate) enum SaveEntityParseError {
|
||||||
|
#[error("Failed to parse `{0}`")]
|
||||||
|
Component(String),
|
||||||
|
#[error("Failed to parse Entity")]
|
||||||
|
Nom(nom::Err<nom::error::Error<Box<str>>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Nom error to parse error
|
||||||
|
// https://stackoverflow.com/a/77974858/3096574
|
||||||
|
impl From<nom::Err<nom::error::Error<&str>>> for SaveEntityParseError {
|
||||||
|
fn from(err: nom::Err<nom::error::Error<&str>>) -> Self {
|
||||||
|
SaveEntityParseError::Nom(err.map_input(|input| input.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub(crate) enum Token {
|
||||||
|
Tag(String),
|
||||||
|
Str(String),
|
||||||
|
Num(f32),
|
||||||
|
Comment(String),
|
||||||
|
Etc,
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Simple function to tokenize a string into an array of Token values
|
||||||
|
///
|
||||||
|
pub(crate) fn tokenize(line: &str) -> Vec<Token> {
|
||||||
|
let mut l = line;
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
|
||||||
|
// Check for comment
|
||||||
|
if let Ok((_, (_start, content))) = tuple((char::<&str, ()>(';'), not_line_ending))(line) {
|
||||||
|
tokens.push(Token::Comment(content.strip_prefix(" ").unwrap().into()));
|
||||||
|
} else {
|
||||||
|
// Check for all other token types in a loop
|
||||||
|
while l.len() > 0 {
|
||||||
|
if let Ok((rem, (_, _, s, _, _))) = tuple((
|
||||||
|
space0,
|
||||||
|
char::<&str, ()>('"'),
|
||||||
|
take_until("\""),
|
||||||
|
char::<&str, ()>('"'),
|
||||||
|
space0,
|
||||||
|
))(l)
|
||||||
|
{
|
||||||
|
debug!("Parsed string {:?}", s);
|
||||||
|
tokens.push(Token::Str(s.into()));
|
||||||
|
l = rem;
|
||||||
|
} else if let Ok((rem, (_, num, _))) = tuple((space0, float::<&str, ()>, space0))(l) {
|
||||||
|
debug!("Parsed float {:?}", num);
|
||||||
|
tokens.push(Token::Num(num.into()));
|
||||||
|
l = rem;
|
||||||
|
} else if let Ok((rem, (_, etc, _))) =
|
||||||
|
tuple((space0, tag::<&str, &str, ()>("..."), space0))(l)
|
||||||
|
{
|
||||||
|
debug!("Parsed etc. {:?}", etc);
|
||||||
|
tokens.push(Token::Etc);
|
||||||
|
l = rem;
|
||||||
|
} else if let Ok((rem, (_, tag, _))) =
|
||||||
|
tuple((space0, alphanumeric1::<&str, ()>, space0))(l)
|
||||||
|
{
|
||||||
|
debug!("Parsed tag {:?}", tag);
|
||||||
|
tokens.push(Token::Tag(tag.into()));
|
||||||
|
l = rem;
|
||||||
|
} else {
|
||||||
|
debug!("Breaking loop");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("Parsed tokens: {:?}", tokens);
|
||||||
|
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tokenize() {
|
||||||
|
let line = "foo \"bar\" 1.23 baz -3.45 \"asdf\" \"multi word string\" etc";
|
||||||
|
assert_eq!(
|
||||||
|
tokenize(line),
|
||||||
|
vec![
|
||||||
|
Token::Tag("foo".into()),
|
||||||
|
Token::Str("bar".into()),
|
||||||
|
Token::Num(1.23),
|
||||||
|
Token::Tag("baz".into()),
|
||||||
|
Token::Num(-3.45),
|
||||||
|
Token::Str("asdf".into()),
|
||||||
|
Token::Str("multi word string".into()),
|
||||||
|
Token::Tag("etc".into())
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Returns reflected `Transform`
|
||||||
|
///
|
||||||
|
/// A fairly complicated parse function because Transform is sort of 3 components in one
|
||||||
|
/// (translation, rotation, scale).
|
||||||
|
///
|
||||||
|
/// It might get more complicated as I add more ways to express rotation!
|
||||||
|
///
|
||||||
|
pub(crate) fn parse_save_transform(
|
||||||
|
tokens: &Vec<Token>,
|
||||||
|
) -> Result<Vec<Box<dyn Reflect>>, SaveEntityParseError> {
|
||||||
|
if tokens.get(0) == Some(&Token::Tag("transform".into())) {
|
||||||
|
let mut t = Transform::default();
|
||||||
|
let mut idx = 1;
|
||||||
|
while idx < tokens.len() {
|
||||||
|
match tokens.get(idx) {
|
||||||
|
Some(Token::Tag(attr)) => {
|
||||||
|
idx += 1;
|
||||||
|
match attr.as_str() {
|
||||||
|
"translation" => {
|
||||||
|
if let Token::Num(x) = tokens[idx] {
|
||||||
|
t.translation.x = x;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
if let Token::Num(y) = tokens[idx] {
|
||||||
|
t.translation.y = y;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
if let Token::Num(z) = tokens[idx] {
|
||||||
|
t.translation.z = z;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
info!("{:?}", t.translation);
|
||||||
|
}
|
||||||
|
"scale" => {
|
||||||
|
if let Token::Num(x) = tokens[idx] {
|
||||||
|
t.scale.x = x;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
if let Token::Num(y) = tokens[idx] {
|
||||||
|
t.scale.y = y;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
if let Token::Num(z) = tokens[idx] {
|
||||||
|
t.scale.z = z;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"rotation" => {
|
||||||
|
let x = match tokens.get(idx) {
|
||||||
|
Some(Token::Num(x)) => {
|
||||||
|
idx += 1;
|
||||||
|
*x
|
||||||
|
}
|
||||||
|
_ => 0.0,
|
||||||
|
};
|
||||||
|
let y = match tokens.get(idx) {
|
||||||
|
Some(Token::Num(y)) => {
|
||||||
|
idx += 1;
|
||||||
|
*y
|
||||||
|
}
|
||||||
|
_ => 0.0,
|
||||||
|
};
|
||||||
|
let z = match tokens.get(idx) {
|
||||||
|
Some(Token::Num(z)) => {
|
||||||
|
idx += 1;
|
||||||
|
*z
|
||||||
|
}
|
||||||
|
_ => 0.0,
|
||||||
|
};
|
||||||
|
let w = match tokens.get(idx) {
|
||||||
|
Some(Token::Num(w)) => {
|
||||||
|
idx += 1;
|
||||||
|
*w
|
||||||
|
}
|
||||||
|
_ => 1.0,
|
||||||
|
};
|
||||||
|
t.rotation = Quat::from_xyzw(x, y, z, w);
|
||||||
|
}
|
||||||
|
_ => idx += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => idx += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Parsed transform: {:?}", t);
|
||||||
|
Ok(vec![
|
||||||
|
t.clone_value(),
|
||||||
|
GlobalTransform::default().clone_value(),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
return Err(SaveEntityParseError::Component("Transform".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_transform() {
|
||||||
|
{
|
||||||
|
let line = "transform translation 1.0 2.0 3.0 rotation 0.1 0.2 0.3 1.0 scale 1.1 1.2 1.3";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_transform(&tokens).unwrap();
|
||||||
|
let t = Transform {
|
||||||
|
translation: Vec3::new(1.0, 2.0, 3.0),
|
||||||
|
rotation: Quat::from_xyzw(0.1, 0.2, 0.3, 1.0),
|
||||||
|
scale: Vec3::new(1.1, 1.2, 1.3),
|
||||||
|
};
|
||||||
|
let gt = GlobalTransform::default();
|
||||||
|
let expected = vec![t.clone_value(), gt.clone_value()];
|
||||||
|
|
||||||
|
parsed.iter().zip(expected).for_each(|(p, e)| {
|
||||||
|
assert!(e.clone_value().reflect_partial_eq(p.as_reflect()).unwrap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let line = "transform translation ... rotation ... scale ...";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_transform(&tokens).unwrap();
|
||||||
|
let t = Transform::default();
|
||||||
|
let gt = GlobalTransform::default();
|
||||||
|
let expected = vec![t.clone_value(), gt.clone_value()];
|
||||||
|
|
||||||
|
parsed.iter().zip(expected).for_each(|(p, e)| {
|
||||||
|
assert!(e.clone_value().reflect_partial_eq(p.as_reflect()).unwrap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let line = "transform ...";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_transform(&tokens).unwrap();
|
||||||
|
let t = Transform::default();
|
||||||
|
let gt = GlobalTransform::default();
|
||||||
|
let expected = vec![t.clone_value(), gt.clone_value()];
|
||||||
|
|
||||||
|
parsed.iter().zip(expected).for_each(|(p, e)| {
|
||||||
|
assert!(e.clone_value().reflect_partial_eq(p.as_reflect()).unwrap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let line = "transform translation 1.0 ... rotation 2.0 ... scale 3.0 ...";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_transform(&tokens).unwrap();
|
||||||
|
let t = Transform {
|
||||||
|
translation: Vec3::new(1.0, 0.0, 0.0),
|
||||||
|
rotation: Quat::from_xyzw(2.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: Vec3::new(3.0, 1.0, 1.0),
|
||||||
|
};
|
||||||
|
let gt = GlobalTransform::default();
|
||||||
|
let expected = vec![t.clone_value(), gt.clone_value()];
|
||||||
|
|
||||||
|
parsed.iter().zip(expected).for_each(|(p, e)| {
|
||||||
|
assert!(e.clone_value().reflect_partial_eq(p.as_reflect()).unwrap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Returns a reflected `Name`
|
||||||
|
///
|
||||||
|
pub(crate) fn parse_save_name(
|
||||||
|
tokens: &Vec<Token>,
|
||||||
|
) -> Result<Vec<Box<dyn Reflect>>, SaveEntityParseError> {
|
||||||
|
if let Some((Token::Tag(t), &[Token::Str(ref s)])) = tokens.split_first()
|
||||||
|
&& *t == String::from("name")
|
||||||
|
{
|
||||||
|
Ok(vec![Name::new(s.clone()).clone_value()])
|
||||||
|
} else {
|
||||||
|
Err(SaveEntityParseError::Component("Name".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_name() {
|
||||||
|
{
|
||||||
|
let line = "name Van";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_name(&tokens).unwrap();
|
||||||
|
let expected = vec![Name::new("Van").clone_value()];
|
||||||
|
|
||||||
|
parsed.iter().zip(expected).for_each(|(p, e)| {
|
||||||
|
assert!(e.clone_value().reflect_partial_eq(p.as_reflect()).unwrap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let line = "name Big Mike";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_name(&tokens).unwrap();
|
||||||
|
let expected = vec![Name::new("Big Mike").clone_value()];
|
||||||
|
|
||||||
|
parsed.iter().zip(expected).for_each(|(p, e)| {
|
||||||
|
assert!(e.clone_value().reflect_partial_eq(p.as_reflect()).unwrap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Reflect, PartialEq)]
|
||||||
|
#[reflect(Component, PartialEq)]
|
||||||
|
pub(crate) struct SaveModel {
|
||||||
|
gltf_file: PathBuf,
|
||||||
|
scene_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for SaveModel {
|
||||||
|
const STORAGE_TYPE: StorageType = StorageType::Table;
|
||||||
|
|
||||||
|
fn register_component_hooks(hooks: &mut ComponentHooks) {
|
||||||
|
todo!("Assign Scene Handle for SaveModel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Returns a reflected `SaveModel`
|
||||||
|
///
|
||||||
|
pub(crate) fn parse_save_model(
|
||||||
|
tokens: &Vec<Token>,
|
||||||
|
) -> Result<Vec<Box<dyn Reflect>>, SaveEntityParseError> {
|
||||||
|
if tokens.get(0) == Some(&Token::Tag("model".into())) {
|
||||||
|
if let Token::Str(gltf_file) = tokens.get(1).expect("model requires gltf file") {
|
||||||
|
if let Token::Str(scene_name) = tokens.get(2).expect("model requires scene name") {
|
||||||
|
Ok(vec![SaveModel {
|
||||||
|
gltf_file: gltf_file.into(),
|
||||||
|
scene_name: scene_name.clone(),
|
||||||
|
}
|
||||||
|
.clone_value()])
|
||||||
|
} else {
|
||||||
|
Err(SaveEntityParseError::Component("Model".into()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(SaveEntityParseError::Component("Model".into()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(SaveEntityParseError::Component("Model".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_model() {
|
||||||
|
let line = "model \"models/foo.glb\" \"My Scene\"";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_model(&tokens).unwrap();
|
||||||
|
let expected = vec![SaveModel {
|
||||||
|
gltf_file: "models/foo.glb".into(),
|
||||||
|
scene_name: "My Scene".into(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
parsed.iter().zip(expected).for_each(|(p, e)| {
|
||||||
|
assert!(e.clone_value().reflect_partial_eq(p.as_reflect()).unwrap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Default, PartialEq, Reflect, Clone)]
|
||||||
|
#[reflect_value(Component, Default, PartialEq)]
|
||||||
|
pub(crate) enum SaveCameraRenderTarget {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Window(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Returns a reflected `Camera3d`
|
||||||
|
///
|
||||||
|
pub(crate) fn parse_save_camera(
|
||||||
|
tokens: &Vec<Token>,
|
||||||
|
) -> Result<Vec<Box<dyn Reflect>>, SaveEntityParseError> {
|
||||||
|
if tokens[0] == Token::Tag("camera".into()) {
|
||||||
|
Ok(vec![
|
||||||
|
Camera::default().clone_value(),
|
||||||
|
CameraRenderGraph::new(Core3d).clone_value(),
|
||||||
|
Projection::default().clone_value(),
|
||||||
|
VisibleEntities::default().clone_value(),
|
||||||
|
Frustum::default().clone_value(),
|
||||||
|
Camera3d::default().clone_value(),
|
||||||
|
Tonemapping::default().clone_value(),
|
||||||
|
DebandDither::default().clone_value(),
|
||||||
|
ColorGrading::default().clone_value(),
|
||||||
|
Exposure::default().clone_value(),
|
||||||
|
CameraMainTextureUsages::default().clone_value(),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
Err(SaveEntityParseError::Component("camera".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_camera() {
|
||||||
|
{
|
||||||
|
let line = "camera";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_camera(&tokens).unwrap();
|
||||||
|
let expected = vec![
|
||||||
|
Camera::default().clone_value(),
|
||||||
|
CameraRenderGraph::new(Core3d).clone_value(),
|
||||||
|
Projection::default().clone_value(),
|
||||||
|
VisibleEntities::default().clone_value(),
|
||||||
|
Frustum::default().clone_value(),
|
||||||
|
Camera3d::default().clone_value(),
|
||||||
|
Tonemapping::default().clone_value(),
|
||||||
|
DebandDither::default().clone_value(),
|
||||||
|
ColorGrading::default().clone_value(),
|
||||||
|
Exposure::default().clone_value(),
|
||||||
|
CameraMainTextureUsages::default().clone_value(),
|
||||||
|
];
|
||||||
|
parsed
|
||||||
|
.iter()
|
||||||
|
.zip(expected)
|
||||||
|
.for_each(|(p, e)| match e.reflect_partial_eq(p.as_reflect()) {
|
||||||
|
Some(r) => assert!(r),
|
||||||
|
None => warn!(
|
||||||
|
"Type {:?} does not support reflection",
|
||||||
|
e.get_represented_type_info().unwrap()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// let line = "camera target window";
|
||||||
|
// let tokens = tokenize(line);
|
||||||
|
// let parsed = parse_save_camera(&tokens).unwrap();
|
||||||
|
// let expected = SaveCameraRenderTarget::Default;
|
||||||
|
// assert!(expected
|
||||||
|
// .clone_value()
|
||||||
|
// .reflect_partial_eq(parsed.as_reflect())
|
||||||
|
// .unwrap());
|
||||||
|
// }
|
||||||
|
// {
|
||||||
|
// let line = "camera target";
|
||||||
|
// let tokens = tokenize(line);
|
||||||
|
// let parsed = parse_save_camera(&tokens);
|
||||||
|
// assert!(parsed.is_err());
|
||||||
|
// }
|
||||||
|
// {
|
||||||
|
// let line = "camera target window \"some.entity\"";
|
||||||
|
// let tokens = tokenize(line);
|
||||||
|
// let parsed = parse_save_camera(&tokens).unwrap();
|
||||||
|
// let expected = SaveCameraRenderTarget::Window("some.entity".into());
|
||||||
|
// assert!(expected
|
||||||
|
// .clone_value()
|
||||||
|
// .reflect_partial_eq(parsed.as_reflect())
|
||||||
|
// .unwrap());
|
||||||
|
// }
|
||||||
|
{
|
||||||
|
let line = "notcamera";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_camera(&tokens);
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// encapsulates the spatial 3d bits of an entity
|
||||||
|
pub(crate) fn parse_save_visibility(
|
||||||
|
tokens: &Vec<Token>,
|
||||||
|
) -> Result<Vec<Box<dyn Reflect>>, SaveEntityParseError> {
|
||||||
|
if tokens[0] == Token::Tag("visible".into()) {
|
||||||
|
Ok(vec![
|
||||||
|
Visibility::default().clone_value(),
|
||||||
|
InheritedVisibility::default().clone_value(),
|
||||||
|
ViewVisibility::default().clone_value(),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
Err(SaveEntityParseError::Component("visibility".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_visibility() {
|
||||||
|
let line = "visible";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_visibility(&tokens).unwrap();
|
||||||
|
let expected = vec![
|
||||||
|
Visibility::default().clone_value(),
|
||||||
|
InheritedVisibility::default().clone_value(),
|
||||||
|
ViewVisibility::default().clone_value(),
|
||||||
|
];
|
||||||
|
parsed.iter().zip(expected).for_each(|(p, e)| {
|
||||||
|
assert!(e.clone_value().reflect_partial_eq(p.as_reflect()).unwrap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SaveParent entity which is a reference to which entity this is a child of
|
||||||
|
/// A run-time system converts this to a bevy Parent component
|
||||||
|
#[derive(PartialEq, Debug, Reflect, Clone)]
|
||||||
|
#[reflect_value(Component, PartialEq)]
|
||||||
|
pub(crate) struct SaveParent(String);
|
||||||
|
|
||||||
|
impl Component for SaveParent {
|
||||||
|
const STORAGE_TYPE: StorageType = StorageType::Table;
|
||||||
|
|
||||||
|
fn register_component_hooks(hooks: &mut ComponentHooks) {
|
||||||
|
todo!("Assign parent for entity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Parses a parent entity with this format:
|
||||||
|
/// ```text
|
||||||
|
/// parent some_other_file.entity
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Returns a reflected `SaveParent`
|
||||||
|
///
|
||||||
|
pub(crate) fn parse_save_parent(
|
||||||
|
tokens: &Vec<Token>,
|
||||||
|
) -> Result<Box<dyn Reflect>, SaveEntityParseError> {
|
||||||
|
todo!("parse_save_parent");
|
||||||
|
|
||||||
|
// Ok(SaveParent(parent_file.into()).clone_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_parent() {
|
||||||
|
let line = "parent \"some_other_file.entity\"";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_parent(&tokens).unwrap();
|
||||||
|
let expected = SaveParent("some_other_file.entity".into());
|
||||||
|
assert!(expected
|
||||||
|
.clone_value()
|
||||||
|
.reflect_partial_eq(parsed.as_reflect())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Parse the Window component (which is very big!)
|
||||||
|
/// We only sparsely define the bits that we want to edit (for now)
|
||||||
|
///
|
||||||
|
/// Returns a reflected `Window`
|
||||||
|
///
|
||||||
|
pub(crate) fn parse_save_window(
|
||||||
|
tokens: &Vec<Token>,
|
||||||
|
) -> Result<Box<dyn Reflect>, SaveEntityParseError> {
|
||||||
|
todo!("parse_save_window");
|
||||||
|
|
||||||
|
/*
|
||||||
|
Ok(Window {
|
||||||
|
title: window_title.into(),
|
||||||
|
visible: visibility,
|
||||||
|
..default()
|
||||||
|
}
|
||||||
|
.clone_value())
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_window() {
|
||||||
|
let line = "window \"Editor\" visible false";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_window(&tokens).unwrap();
|
||||||
|
let expected = Window {
|
||||||
|
visible: false,
|
||||||
|
title: "Editor".into(),
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
assert!(expected
|
||||||
|
.clone_value()
|
||||||
|
.reflect_partial_eq(parsed.as_reflect())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// The UI Text bundle specified as a sparse subset of a bundle
|
||||||
|
/// ```text
|
||||||
|
/// uiText "This is the text" color #abc123 size 12.34
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Returns a reflected `Text`
|
||||||
|
///
|
||||||
|
pub(crate) fn parse_save_ui_text(
|
||||||
|
tokens: &Vec<Token>,
|
||||||
|
) -> Result<Box<dyn Reflect>, SaveEntityParseError> {
|
||||||
|
todo!("parse_save_ui_text");
|
||||||
|
|
||||||
|
/*
|
||||||
|
let style = TextStyle {
|
||||||
|
color: Color::Srgba(Srgba::hex(hex_color).unwrap()),
|
||||||
|
font_size,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
Ok(Text::from_section(text, style).clone_value())
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_save_ui_text() {
|
||||||
|
let line = "uiText \"This is the text\" color #caffee size 14.6";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_ui_text(&tokens).unwrap();
|
||||||
|
let expected = Text::from_section(
|
||||||
|
"This is the text",
|
||||||
|
TextStyle {
|
||||||
|
color: Srgba::hex("#caffee").unwrap().into(),
|
||||||
|
font_size: 14.6,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(expected
|
||||||
|
.clone_value()
|
||||||
|
.reflect_partial_eq(parsed.as_reflect())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Returns a parser function for a basic stand-alone tag
|
||||||
|
/// e.g., "editor_tag" or "ui_node"
|
||||||
|
///
|
||||||
|
pub(crate) fn parse_save_tag<T: Component + Reflect + Default>(
|
||||||
|
t: &str,
|
||||||
|
) -> impl FnOnce(&Vec<Token>) -> Result<Box<dyn Reflect>, SaveEntityParseError> + '_ {
|
||||||
|
move |tokens: &Vec<Token>| -> Result<Box<dyn Reflect>, SaveEntityParseError> {
|
||||||
|
todo!("parse_save_tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Reflect, PartialEq, Debug, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
struct TestingTag;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_save_tag() {
|
||||||
|
let line = "testing";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_tag::<TestingTag>("testing")(&tokens).unwrap();
|
||||||
|
let expected = TestingTag;
|
||||||
|
assert!(expected
|
||||||
|
.clone_value()
|
||||||
|
.reflect_partial_eq(parsed.as_reflect())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Reflect, PartialEq)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub(crate) struct SaveTargetCamera(String);
|
||||||
|
|
||||||
|
impl Component for SaveTargetCamera {
|
||||||
|
const STORAGE_TYPE: StorageType = StorageType::Table;
|
||||||
|
|
||||||
|
fn register_component_hooks(hooks: &mut ComponentHooks) {
|
||||||
|
todo!("Assign target camera")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the a SaveTargetCamera which at runtime is converted to a TargetCamera
|
||||||
|
/// ```text
|
||||||
|
/// targetCamera "./level-camera.entity"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Returns reflected `SaveTargetCamera`
|
||||||
|
pub(crate) fn parse_save_target_camera(
|
||||||
|
tokens: &Vec<Token>,
|
||||||
|
) -> Result<Box<dyn Reflect>, SaveEntityParseError> {
|
||||||
|
todo!("parse_save_target_camera")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_target_camera() {
|
||||||
|
let line = "targetCamera \"./level-camera.entity\"";
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
let parsed = parse_save_target_camera(&tokens).unwrap();
|
||||||
|
let expected = SaveTargetCamera("./level-camera.entity".into());
|
||||||
|
|
||||||
|
assert!(expected
|
||||||
|
.clone_value()
|
||||||
|
.reflect_partial_eq(parsed.as_reflect())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
pub(crate) use bevy::ecs::reflect::ReflectCommandExt;
|
||||||
|
pub(crate) use bevy::{
|
||||||
|
app::AppExit,
|
||||||
|
asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext},
|
||||||
|
color::palettes::css::{BLUE, DARK_GREEN, GRAY, RED},
|
||||||
|
gltf::Gltf,
|
||||||
|
input::common_conditions::input_just_pressed,
|
||||||
|
prelude::*,
|
||||||
|
render::camera::RenderTarget,
|
||||||
|
window::{PrimaryWindow, WindowCloseRequested, WindowRef},
|
||||||
|
};
|
||||||
|
pub(crate) use nom::{
|
||||||
|
bytes::complete::{tag, take_until},
|
||||||
|
character::complete::{alphanumeric1, char, not_line_ending, space0},
|
||||||
|
number::complete::float,
|
||||||
|
sequence::tuple,
|
||||||
|
};
|
||||||
|
pub(crate) use std::path::PathBuf;
|
||||||
|
pub(crate) use thiserror::Error;
|
||||||
|
|
||||||
|
pub(crate) use crate::{conditions::*, parser::*};
|
||||||
|
|
||||||
|
pub(crate) use bevy::ecs::component::{ComponentHooks, StorageType};
|
||||||
@ -0,0 +1,369 @@
|
|||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
type ParseFn =
|
||||||
|
for<'a> fn(&'a Vec<Token>) -> Result<Vec<Box<(dyn Reflect + 'static)>>, SaveEntityParseError>;
|
||||||
|
|
||||||
|
/// Menu Plugin; contains parser functions
|
||||||
|
pub(crate) struct SavePlugin {
|
||||||
|
pub fns: Vec<ParseFn>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for SavePlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.register_type::<SaveCameraRenderTarget>()
|
||||||
|
.register_type::<GltfScene>()
|
||||||
|
.register_type::<SaveModel>()
|
||||||
|
.init_asset::<SaveEntity>()
|
||||||
|
.register_asset_loader(SaveEntityLoader {
|
||||||
|
fns: self.fns.clone(),
|
||||||
|
})
|
||||||
|
.init_asset::<SaveScene>()
|
||||||
|
.init_asset_loader::<SaveSceneLoader>()
|
||||||
|
.add_systems(Startup, load_scenes)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
spawn_scenes.run_if(on_event::<AssetEvent<SaveScene>>()),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
sync_entities
|
||||||
|
.run_if(on_event::<AssetEvent<SaveEntity>>())
|
||||||
|
.after(spawn_scenes),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
spawn_entities
|
||||||
|
.run_if(any_component_added::<Handle<SaveEntity>>)
|
||||||
|
.after(sync_entities),
|
||||||
|
)
|
||||||
|
.add_systems(Update, debug_entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Asset, TypePath, Default)]
|
||||||
|
pub(crate) struct SaveScene {
|
||||||
|
entities: Vec<Handle<SaveEntity>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: SCALABILITY: SaveEntity should maybe be a HashSet or Vec of Box<into Bundle>
|
||||||
|
// Then we tell the parse "For each line, run through each of these concrete parsers"
|
||||||
|
// If it matches, add it to the set of Box<Bundle>
|
||||||
|
// Ironically we basically want DynamicEntity: https://docs.rs/bevy/latest/bevy/scene/struct.DynamicEntity.html
|
||||||
|
#[derive(Asset, TypePath, Default)]
|
||||||
|
pub(crate) struct SaveEntity {
|
||||||
|
components: Vec<SaveEntityComponent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SaveEntityComponent {
|
||||||
|
type_id: TypeId,
|
||||||
|
data: Box<dyn Reflect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SaveEntity {
|
||||||
|
fn parse(
|
||||||
|
text: &str,
|
||||||
|
load_context: &mut LoadContext,
|
||||||
|
fns: &Vec<ParseFn>,
|
||||||
|
) -> Result<SaveEntity, SaveEntityParseError> {
|
||||||
|
let lines = text.split('\n');
|
||||||
|
let mut entity = SaveEntity { ..default() };
|
||||||
|
lines
|
||||||
|
.into_iter()
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.for_each(|line| {
|
||||||
|
// track if this line matched any components
|
||||||
|
let mut good = false;
|
||||||
|
|
||||||
|
// Tokenize the line
|
||||||
|
let tokens = tokenize(line);
|
||||||
|
|
||||||
|
if matches!(tokens[0], Token::Comment(..)) {
|
||||||
|
debug!("Skipping parsing comment line {:?}", tokens);
|
||||||
|
} else {
|
||||||
|
// Run line against all parsers
|
||||||
|
for f in fns {
|
||||||
|
if let Ok(p) = f(&tokens) {
|
||||||
|
p.iter().for_each(|c| {
|
||||||
|
// Bundle the Type ID with this entry for auditing purposes
|
||||||
|
let t = c.get_represented_type_info().unwrap().type_id();
|
||||||
|
entity.components.push(SaveEntityComponent {
|
||||||
|
type_id: t,
|
||||||
|
data: c.clone_value(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
good = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !good {
|
||||||
|
error!(
|
||||||
|
file = load_context.path().to_str().unwrap(),
|
||||||
|
line = line,
|
||||||
|
"failed to parse component",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Check for duplicate component types and emit an error
|
||||||
|
// TODO: It would be nice if this emitted a line refernece instead of a parsed struct!
|
||||||
|
let l = entity.components.len();
|
||||||
|
(0..l).for_each(|i| {
|
||||||
|
(i..l).for_each(|j| {
|
||||||
|
if i != j {
|
||||||
|
let c1 = &entity.components[i];
|
||||||
|
let c2 = &entity.components[j];
|
||||||
|
let t1 = c1.type_id;
|
||||||
|
let t2 = c2.type_id;
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
debug_assert!(
|
||||||
|
t1 != t2,
|
||||||
|
"Duplicate components in {:?}: \n\t{:?}\n\t{:?}",
|
||||||
|
load_context.asset_path(),
|
||||||
|
c1.data,
|
||||||
|
c2.data
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"Duplicate components in {:?}: \n\t{:?}\n\t{:?}",
|
||||||
|
load_context.asset_path(),
|
||||||
|
c1.data,
|
||||||
|
c2.data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
struct GltfScene {
|
||||||
|
gltf: Handle<Gltf>,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct SaveEntityLoader {
|
||||||
|
fns: Vec<ParseFn>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
enum SaveEntityLoaderError {
|
||||||
|
#[error("Could not load asset: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("Could not parse entity: {0}")]
|
||||||
|
Parse(#[from] SaveEntityParseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetLoader for SaveEntityLoader {
|
||||||
|
type Asset = SaveEntity;
|
||||||
|
type Settings = ();
|
||||||
|
type Error = SaveEntityLoaderError;
|
||||||
|
|
||||||
|
async fn load<'a>(
|
||||||
|
&'a self,
|
||||||
|
reader: &'a mut Reader<'_>,
|
||||||
|
_settings: &'a (),
|
||||||
|
load_context: &'a mut LoadContext<'_>,
|
||||||
|
) -> Result<Self::Asset, Self::Error> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
reader.read_to_end(&mut bytes).await?;
|
||||||
|
|
||||||
|
let s = std::str::from_utf8(bytes.as_slice()).unwrap();
|
||||||
|
let save_entity = SaveEntity::parse(s, load_context, &self.fns)?;
|
||||||
|
Ok(save_entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extensions(&self) -> &[&str] {
|
||||||
|
&["entity"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct SaveSceneLoader;
|
||||||
|
|
||||||
|
impl AssetLoader for SaveSceneLoader {
|
||||||
|
type Asset = SaveScene;
|
||||||
|
type Settings = ();
|
||||||
|
type Error = SaveEntityLoaderError;
|
||||||
|
|
||||||
|
async fn load<'a>(
|
||||||
|
&'a self,
|
||||||
|
reader: &'a mut Reader<'_>,
|
||||||
|
_settings: &'a (),
|
||||||
|
load_context: &'a mut LoadContext<'_>,
|
||||||
|
) -> Result<Self::Asset, Self::Error> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
reader.read_to_end(&mut bytes).await?;
|
||||||
|
|
||||||
|
let s = std::str::from_utf8(bytes.as_slice()).unwrap();
|
||||||
|
|
||||||
|
let asset_path = load_context.path().to_path_buf();
|
||||||
|
let parent_dir = asset_path.parent().unwrap();
|
||||||
|
let entities: Vec<Handle<SaveEntity>> = s
|
||||||
|
.lines()
|
||||||
|
.map(|line| parent_dir.join(line))
|
||||||
|
.map(|path| load_context.load(path))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Assert there are no duplicate entities in the file
|
||||||
|
{
|
||||||
|
let l = entities.len();
|
||||||
|
(0..l).for_each(|i| {
|
||||||
|
(i..l).for_each(|j| {
|
||||||
|
if i != j {
|
||||||
|
let e1 = &entities[i];
|
||||||
|
let e2 = &entities[j];
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
debug_assert!(
|
||||||
|
e1 != e2,
|
||||||
|
"Duplicate entities in scene {:?}:\n\t{:?}\n\t{:?}",
|
||||||
|
load_context.asset_path(),
|
||||||
|
e1,
|
||||||
|
e2
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if e1 == e2 {
|
||||||
|
error!(
|
||||||
|
"Duplicate entities in scene {:?}:\n\t{:?}\n\t{:?}",
|
||||||
|
load_context.asset_path(),
|
||||||
|
e1,
|
||||||
|
e2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SaveScene { entities })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extensions(&self) -> &[&str] {
|
||||||
|
&["scene"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Testing system for loading a specific scene
|
||||||
|
fn load_scenes(loader: Res<AssetServer>, mut commands: Commands) {
|
||||||
|
let handle: Handle<SaveScene> = loader.load("scenes/00.scene");
|
||||||
|
info!("Loading scene {:?}", handle);
|
||||||
|
commands.spawn(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns scenes with the Handle<Scene> marker
|
||||||
|
fn spawn_scenes(
|
||||||
|
query: Query<(Entity, &Handle<SaveScene>)>,
|
||||||
|
mut events: EventReader<AssetEvent<SaveScene>>,
|
||||||
|
save_scenes: Res<Assets<SaveScene>>,
|
||||||
|
server: Res<AssetServer>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
events.read().for_each(|event| {
|
||||||
|
if let AssetEvent::LoadedWithDependencies { id } = event {
|
||||||
|
debug!("Spawning scene {:?}", server.get_id_handle(*id).unwrap());
|
||||||
|
query
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, handle)| handle.id() == *id)
|
||||||
|
.for_each(|(entity, handle)| {
|
||||||
|
let scene = save_scenes.get(handle).unwrap();
|
||||||
|
|
||||||
|
// Get entity with SaveEntity handle
|
||||||
|
let mut e = commands.entity(entity);
|
||||||
|
|
||||||
|
// Clear the entity of descendants
|
||||||
|
debug!("Despawning scene descendants");
|
||||||
|
e.despawn_descendants();
|
||||||
|
|
||||||
|
debug!("Populating children in scene");
|
||||||
|
// Populate with entities
|
||||||
|
e.with_children(|parent| {
|
||||||
|
scene.entities.iter().for_each(|this| {
|
||||||
|
parent.spawn(this.clone());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns entities with the Handle<SaveEntity> component
|
||||||
|
fn spawn_entities(
|
||||||
|
events: Query<
|
||||||
|
(Entity, &Handle<SaveEntity>),
|
||||||
|
Or<(Added<Handle<SaveEntity>>, Changed<Handle<SaveEntity>>)>,
|
||||||
|
>,
|
||||||
|
save_entities: Res<Assets<SaveEntity>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
events.iter().for_each(|(entity, handle)| {
|
||||||
|
// Get a handle on the
|
||||||
|
let mut e = commands.entity(entity);
|
||||||
|
|
||||||
|
debug!("Despawning entity descendants {:?}", entity);
|
||||||
|
e.despawn_descendants();
|
||||||
|
|
||||||
|
debug!("Clearing components on entity {:?}", entity);
|
||||||
|
// Clear any existing components on the entity;
|
||||||
|
e.retain::<Handle<SaveEntity>>();
|
||||||
|
|
||||||
|
// Get the entity asset containing reflected component
|
||||||
|
let save_entity = save_entities.get(handle).unwrap();
|
||||||
|
|
||||||
|
// Add each component to the entity
|
||||||
|
debug!("Populating entity with components {:?}", entity);
|
||||||
|
save_entity.components.iter().for_each(|item| {
|
||||||
|
e.insert_reflect(item.data.clone_value());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When an entity asset is updated, re-spawn it
|
||||||
|
/// NOTE: This should only be enabled in development mode(?)
|
||||||
|
fn sync_entities(
|
||||||
|
query: Query<(Entity, &Handle<SaveEntity>)>,
|
||||||
|
server: Res<AssetServer>,
|
||||||
|
mut events: EventReader<AssetEvent<SaveEntity>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
// Any time a SaveEntity asset is updated
|
||||||
|
events.read().for_each(|event| {
|
||||||
|
match event {
|
||||||
|
AssetEvent::LoadedWithDependencies { id } => {
|
||||||
|
debug!(
|
||||||
|
"SaveEnity loaded {:?}",
|
||||||
|
server.get_id_handle(*id).unwrap().path().unwrap()
|
||||||
|
);
|
||||||
|
// Find any entities with this Handle<SaveEntity>
|
||||||
|
query
|
||||||
|
.iter()
|
||||||
|
.filter(|(_entity, handle)| handle.id() == *id)
|
||||||
|
.for_each(|(entity, handle)| {
|
||||||
|
debug!("Found entity with same ID, updating");
|
||||||
|
|
||||||
|
// Get this entity with SaveEntity handle
|
||||||
|
let mut e = commands.entity(entity);
|
||||||
|
|
||||||
|
// Re-Insert the SaveEntity handle
|
||||||
|
e.remove::<Handle<SaveEntity>>().insert(handle.clone());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => debug!("Skipping SaveEntity event {:?}", event),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_entities(
|
||||||
|
events: Query<Entity, Or<(Changed<Handle<SaveEntity>>, Added<Handle<SaveEntity>>)>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
events.iter().for_each(|e| {
|
||||||
|
commands.entity(e).log_components();
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub(crate) struct UiPlugin;
|
||||||
|
|
||||||
|
impl Plugin for UiPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Startup, init_ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_ui(
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub(crate) struct WindowPlugin;
|
||||||
|
|
||||||
|
impl Plugin for WindowPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
setup_primary.run_if(any_component_added::<PrimaryWindow>),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
handle_window_close.run_if(on_event::<WindowCloseRequested>()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_primary(mut events: Query<&mut Window, Added<PrimaryWindow>>) {
|
||||||
|
events.iter_mut().for_each(|mut window| {
|
||||||
|
window.title = "Acts of Gods".into();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles window close requests which are only weird because of the editor
|
||||||
|
/// When the primary window closes shut down the game.
|
||||||
|
fn handle_window_close(
|
||||||
|
mut events: EventReader<WindowCloseRequested>,
|
||||||
|
primary: Query<Entity, With<PrimaryWindow>>,
|
||||||
|
mut exit: EventWriter<AppExit>,
|
||||||
|
) {
|
||||||
|
events
|
||||||
|
.read()
|
||||||
|
// FilterMap this entity to the primary window
|
||||||
|
// Meaning if we get Some(entity) the event was for the primary
|
||||||
|
// If we get None it was for a different window so we skip the for_each
|
||||||
|
.filter_map(|WindowCloseRequested { window }| primary.get(*window).ok())
|
||||||
|
// If this was the primary window, send an AppExit
|
||||||
|
.for_each(|_| {
|
||||||
|
exit.send(AppExit::Success);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue