#![allow(deprecated)]
use std::time::Duration;
use bevy::{
asset::RenderAssetUsages,
diagnostic::LogDiagnosticsPlugin,
image::ImageSampler,
log::LogPlugin,
prelude::*,
reflect::Reflect,
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
window::{PrimaryWindow, WindowResized},
};
use bevy_console::{AddConsoleCommand, ConsoleCommand, ConsoleOpen, ConsolePlugin, make_layer};
use bevy_mod_scripting::prelude::*;
use bevy_mod_scripting_bindings::AllocatorDiagnosticPlugin;
use clap::Parser;
fn console_app(app: &mut App) -> &mut App {
app.add_plugins((
DefaultPlugins.set(LogPlugin {
level: bevy::log::Level::INFO,
filter: "error,game_of_life=info".to_owned(),
custom_layer: make_layer,
..Default::default()
}),
ConsolePlugin,
))
.add_console_command::<GameOfLifeCommand, _>(run_script_cmd)
.add_systems(Startup, |mut open: ResMut<ConsoleOpen>| {
open.open = true;
})
}
fn run_script_cmd(
mut log: ConsoleCommand<GameOfLifeCommand>,
mut commands: Commands,
asset_server: Res<AssetServer>,
script_comps: Query<(Entity, &ScriptComponent)>,
mut static_scripts: Local<Vec<Handle<ScriptAsset>>>,
) {
if let Some(Ok(command)) = log.take() {
match command {
GameOfLifeCommand::Start {
language,
use_static_script,
} => {
bevy::log::info!("Using game of life script game_of_life.{}", language);
let script_path = format!("scripts/game_of_life.{language}");
if !use_static_script {
bevy::log::info!("Spawning an entity with ScriptComponent");
commands.spawn(ScriptComponent::new(vec![asset_server.load(script_path)]));
} else {
bevy::log::info!("Using static script instead of spawning an entity");
let handle = asset_server.load(script_path);
static_scripts.push(handle.clone());
if language == "lua" {
commands.queue(AttachScript::<LuaScriptingPlugin>::new(
ScriptAttachment::StaticScript(handle),
));
} else {
commands.queue(AttachScript::<RhaiScriptingPlugin>::new(
ScriptAttachment::StaticScript(handle),
))
}
}
}
GameOfLifeCommand::Stop => {
bevy::log::info!(
"Stopping game of life by detaching each script and static script"
);
for (id, _) in &script_comps {
commands.entity(id).despawn();
}
for script in static_scripts.iter() {
commands.queue(DetachScript::<LuaScriptingPlugin>::new(
ScriptAttachment::StaticScript(script.clone()),
));
commands.queue(DetachScript::<RhaiScriptingPlugin>::new(
ScriptAttachment::StaticScript(script.clone()),
));
}
}
}
}
}
#[derive(Parser, ConsoleCommand)]
#[command(name = "gol")]
pub enum GameOfLifeCommand {
Start {
#[clap(default_value = "lua")]
language: String,
#[clap(short, long, default_value = "false")]
use_static_script: bool,
},
Stop,
}
fn game_of_life_app(app: &mut App) -> &mut App {
app.insert_resource(Time::<Fixed>::from_seconds(UPDATE_FREQUENCY.into()))
.add_plugins(BMSPlugin)
.register_type::<LifeState>()
.register_type::<Settings>()
.init_resource::<Settings>()
.add_systems(Startup, init_game_of_life_state)
.add_systems(Update, (sync_window_size, send_on_click))
.add_systems(
FixedUpdate,
(
update_rendered_state.after(sync_window_size),
send_on_update.after(update_rendered_state),
(
event_handler::<OnUpdate, LuaScriptingPlugin>,
event_handler::<OnUpdate, RhaiScriptingPlugin>,
event_handler::<OnClick, LuaScriptingPlugin>,
event_handler::<OnClick, RhaiScriptingPlugin>,
)
.after(send_on_update),
),
);
register_script_functions(app)
}
#[derive(Debug, Default, Clone, Reflect, Component)]
#[reflect(Component, Default)]
pub struct LifeState {
pub cells: Vec<u8>,
}
#[derive(Reflect, Resource)]
#[reflect(Resource)]
pub struct Settings {
physical_grid_dimensions: (u32, u32),
display_grid_dimensions: (u32, u32),
border_thickness: u32,
live_color: u8,
dead_color: u8,
}
impl Default for Settings {
fn default() -> Self {
Self {
border_thickness: 1,
live_color: 255u8,
dead_color: 0u8,
physical_grid_dimensions: (88, 50),
display_grid_dimensions: (0, 0),
}
}
}
pub fn register_script_functions(app: &mut App) -> &mut App {
let world = app.world_mut();
NamespaceBuilder::<GlobalNamespace>::new_unregistered(world)
.register("info", |s: String| {
bevy::log::info!(s);
})
.register("rand", rand::random::<f32>);
app
}
pub fn init_game_of_life_state(
mut commands: Commands,
mut assets: ResMut<Assets<Image>>,
settings: Res<Settings>,
) {
let mut image = Image::new_fill(
Extent3d {
width: settings.physical_grid_dimensions.0,
height: settings.physical_grid_dimensions.1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&[0u8],
TextureFormat::R8Unorm,
RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD,
);
image.sampler = ImageSampler::nearest();
let handle = assets.add(image);
commands.spawn(Camera2d);
commands
.spawn(Sprite::from_image(handle))
.insert(LifeState {
cells: vec![
0u8;
(settings.physical_grid_dimensions.0 * settings.physical_grid_dimensions.1)
as usize
],
});
bevy::log::info!("Game of life was initialized. use `gol start` to start the game!");
bevy::log::info!("Type `help gol` for more commands.");
}
pub fn sync_window_size(
mut resize_event: MessageReader<WindowResized>,
mut settings: ResMut<Settings>,
mut query: Query<&mut Sprite, With<LifeState>>,
primary_windows: Query<&Window, With<PrimaryWindow>>,
) {
if let Some(e) = resize_event
.read()
.filter(|e| primary_windows.get(e.window).is_ok())
.last()
{
let primary_window = primary_windows.get(e.window).unwrap();
settings.display_grid_dimensions = (
primary_window.physical_width(),
primary_window.physical_height(),
);
for mut sprite in query.iter_mut() {
let scale = if settings.physical_grid_dimensions.0 > settings.physical_grid_dimensions.1
{
settings.display_grid_dimensions.1 as f32
/ settings.physical_grid_dimensions.1 as f32
} else {
settings.display_grid_dimensions.0 as f32
/ settings.physical_grid_dimensions.0 as f32
};
sprite.custom_size = Some(Vec2::new(
(settings.physical_grid_dimensions.0 as f32) * scale,
(settings.physical_grid_dimensions.1 as f32) * scale,
));
}
}
}
pub fn update_rendered_state(
mut assets: ResMut<Assets<Image>>,
query: Query<(&LifeState, &Sprite)>,
) {
for (new_state, old_rendered_state) in query.iter() {
let old_rendered_state = assets
.get_mut(&old_rendered_state.image)
.expect("World is not setup correctly");
old_rendered_state.data = Some(new_state.cells.clone());
}
}
callback_labels!(
OnUpdate => "on_update",
OnClick => "on_click"
);
pub fn send_on_update(mut events: MessageWriter<ScriptCallbackEvent>) {
events.write(ScriptCallbackEvent::new_for_all_scripts(OnUpdate, vec![]));
}
pub fn send_on_click(
buttons: Res<ButtonInput<MouseButton>>,
q_windows: Query<&Window, With<PrimaryWindow>>,
mut events: MessageWriter<ScriptCallbackEvent>,
) {
if buttons.just_pressed(MouseButton::Left) {
let window = q_windows.single();
let pos = window.unwrap().cursor_position().unwrap_or_default();
let x = pos.x as u32;
let y = pos.y as u32;
events.write(ScriptCallbackEvent::new_for_all_scripts(
OnClick,
vec![
ScriptValue::Integer(x as i64),
ScriptValue::Integer(y as i64),
],
));
}
}
const UPDATE_FREQUENCY: f32 = 1.0 / 60.0;
fn main() -> std::io::Result<()> {
let mut app = App::new();
console_app(&mut app);
game_of_life_app(&mut app);
app.add_plugins((
AllocatorDiagnosticPlugin,
LogDiagnosticsPlugin {
wait_duration: Duration::from_secs(60),
..Default::default()
},
));
app.run();
Ok(())
}