Compare commits

..

No commits in common. 'f31a8339d75ec435dbb35ea5d201aec3088c12c2' and '780e3aa52094aa099aa4251eafc576814a44cda9' have entirely different histories.

7
.gitignore vendored

@ -2,10 +2,3 @@
/platforms/web/*.wasm /platforms/web/*.wasm
/platforms/web/*.js /platforms/web/*.js
# Added by cargo
#
# already existing elements were commented out
#/target

1120
Cargo.lock generated

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 @@
00/a.entity

@ -0,0 +1 @@
name "Hello world"

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

@ -22,7 +22,6 @@ mkShell rec {
vulkan-loader # Rendering vulkan-loader # Rendering
xorg.libX11 xorg.libXcursor xorg.libXi xorg.libXrandr # To use the x11 feature xorg.libX11 xorg.libXcursor xorg.libXi xorg.libXrandr # To use the x11 feature
tmux # Sharing environemnt between editor and terminal tmux # Sharing environemnt between editor and terminal
git-bug
]; ];
LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;

@ -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,38 @@
#![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],
})
.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,460 @@
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),
}
///
/// 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();
while l.len() > 0 {
debug!("Line: {:?}", l);
if let Ok((rem, (_, s, _))) = tuple((
char::<&str, ()>('"'),
take_until("\""),
char::<&str, ()>('"'),
))(l)
{
debug!("Parsed string {:?}", s);
tokens.push(Token::Str(s.into()));
l = rem;
} else if let Ok((rem, num)) = float::<&str, ()>(l) {
debug!("Parsed float {:?}", num);
tokens.push(Token::Num(num.into()));
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;
}
}
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`
///
pub(crate) fn parse_save_transform(
tokens: &Vec<Token>,
) -> Result<Box<dyn Reflect>, SaveEntityParseError> {
// Tag(Transform),
// Tag(Translation), Number, Number, Number
// Tag(Rotation), Number, Number, Number, Number
// Tag(Scale), Number, Number, Number
// return Err(SaveEntityParseError::Transform);
todo!("parse_save_transform");
}
#[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 expected = 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),
};
assert!(expected
.clone_value()
.reflect_partial_eq(parsed.as_reflect())
.unwrap());
}
///
/// Returns a reflected `Name`
///
pub(crate) fn parse_save_name(
tokens: &Vec<Token>,
) -> Result<Box<dyn Reflect>, SaveEntityParseError> {
if let Some((Token::Tag(t), &[Token::Str(ref s)])) = tokens.split_first()
&& *t == String::from("name")
{
Ok(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 = Name::new("Van");
assert!(expected
.clone_value()
.reflect_partial_eq(parsed.as_reflect())
.unwrap());
}
{
let line = "name Big Mike";
let tokens = tokenize(line);
let parsed = parse_save_name(&tokens).unwrap();
let expected = Name::new("Big Mike");
assert!(expected
.clone_value()
.reflect_partial_eq(parsed.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<Box<dyn Reflect>, SaveEntityParseError> {
todo!("parse_save_model");
/*
Ok(SaveModel {
gltf_file: gltf_path.into(),
scene_name: scene_name.into(),
}
.clone_value())
*/
}
#[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 = SaveModel {
gltf_file: "models/foo.glb".into(),
scene_name: "My Scene".into(),
};
assert!(expected
.clone_value()
.reflect_partial_eq(parsed.as_reflect())
.unwrap());
}
#[derive(Debug, Default, PartialEq, Reflect, Clone)]
#[reflect_value(Component, Default, PartialEq)]
pub(crate) enum SaveCameraRenderTarget {
#[default]
Default,
Window(PathBuf),
}
impl Component for SaveCameraRenderTarget {
const STORAGE_TYPE: StorageType = StorageType::Table;
fn register_component_hooks(hooks: &mut ComponentHooks) {
todo!("Assign Render Target")
}
}
///
/// Returns a reflected `SaveCameraRenderTarget
///
pub(crate) fn parse_save_camera(
tokens: &Vec<Token>,
) -> Result<Box<dyn Reflect>, SaveEntityParseError> {
todo!("parse_save_camera");
// Nothing parsed well
// Err(SaveEntityParseError::Camera)
}
#[test]
fn test_parse_camera() {
{
let line = "camera";
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 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());
}
}
/// 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::take_until,
character::complete::{char, space0, alphanumeric1},
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,290 @@
use crate::prelude::*;
type ParseFn =
for<'a> fn(&'a Vec<Token>) -> Result<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<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);
// Run line against all parsers
for f in fns {
if let Ok(v) = f(&tokens) {
entity.components.push(v);
good = true;
}
}
if !good {
error!(
file = load_context.path().to_str().unwrap(),
line = line,
"failed to parse component",
);
}
});
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();
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(|component| {
e.insert_reflect(component.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);
});
}

@ -0,0 +1 @@
Wed Jul 31 09:09:04 PM PDT 2024 Ended work on T -> Box<dyn Reflect> parsing stuff
Loading…
Cancel
Save