From 8b873f907e704b62e08c9995342dd1fd6a21f3b5 Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Fri, 30 Aug 2024 09:38:05 +0200 Subject: [PATCH 1/2] Add experimental runtime --- native/src/util.rs | 1 + native/src/util/async_support.rs | 586 +++++++++++++++++++++++++++++++ 2 files changed, 587 insertions(+) create mode 100644 native/src/util/async_support.rs diff --git a/native/src/util.rs b/native/src/util.rs index fcebfb2..2bed233 100644 --- a/native/src/util.rs +++ b/native/src/util.rs @@ -1,6 +1,7 @@ use godot::engine::{SceneTree, SceneTreeTimer}; use godot::obj::Gd; +pub mod async_support; pub mod logger; /// Create a new ingame one-shot timer in seconds. diff --git a/native/src/util/async_support.rs b/native/src/util/async_support.rs new file mode 100644 index 0000000..0a26ca3 --- /dev/null +++ b/native/src/util/async_support.rs @@ -0,0 +1,586 @@ +use std::any::type_name; +use std::cell::RefCell; +use std::fmt::{Debug, Display}; +use std::future::Future; +use std::marker::PhantomData; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll, Wake, Waker}; +use std::thread::{self, ThreadId}; + +use godot::builtin::{Callable, RustCallable, Signal, Variant}; +use godot::classes::object::ConnectFlags; +use godot::classes::Os; +use godot::meta::{FromGodot, ToGodot}; +use godot::obj::EngineEnum; + +pub fn godot_task(future: impl Future + 'static) -> TaskHandle { + let os = Os::singleton(); + + // Spawning new tasks is only allowed on the main thread for now. + // We can not accept Sync + Send futures since all object references (i.e. Gd) are not thread-safe. So a future has to remain on the + // same thread it was created on. Godots signals on the other hand can be emitted on any thread, so it can't be guaranteed on which thread + // a future will be polled. + // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`. + // + // Once thread-safe futures are possible the restriction can be lifted. + if os.get_thread_caller_id() != os.get_main_thread_id() { + panic!("godot_task can only be used on the main thread!"); + } + + let (task_handle, waker): (_, Waker) = ASYNC_RUNTIME.with_borrow_mut(move |rt| { + let task_handle = rt.add_task(Box::pin(future)); + let waker = Arc::new(GodotWaker::new( + task_handle.index, + task_handle.id, + thread::current().id(), + )) + .into(); + + (task_handle, waker) + }); + + waker.wake(); + task_handle +} + +thread_local! { pub(crate) static ASYNC_RUNTIME: RefCell = RefCell::new(AsyncRuntime::new()); } + +#[derive(Default)] +enum FutureSlotState { + /// Slot is currently empty. + #[default] + Empty, + /// Slot was previously occupied but the future has been canceled or the slot reused. + Gone, + /// Slot contains a pending future. + Pending(T), + /// slot contains a future which is currently being polled. + Polling, +} + +struct FutureSlot { + value: FutureSlotState, + id: u64, +} + +impl FutureSlot { + fn pending(id: u64, value: T) -> Self { + Self { + value: FutureSlotState::Pending(value), + id, + } + } + + fn is_empty(&self) -> bool { + matches!(self.value, FutureSlotState::Empty | FutureSlotState::Gone) + } + + fn clear(&mut self) { + self.value = FutureSlotState::Empty; + } + + fn cancel(&mut self) { + self.value = FutureSlotState::Gone; + } + + fn take(&mut self, id: u64) -> FutureSlotState { + match self.value { + FutureSlotState::Empty => FutureSlotState::Empty, + FutureSlotState::Polling => FutureSlotState::Polling, + FutureSlotState::Gone => FutureSlotState::Gone, + FutureSlotState::Pending(_) if self.id != id => FutureSlotState::Gone, + FutureSlotState::Pending(_) => { + std::mem::replace(&mut self.value, FutureSlotState::Polling) + } + } + } + + fn park(&mut self, value: T) { + match self.value { + FutureSlotState::Empty | FutureSlotState::Gone => { + panic!("Future slot is currently unoccupied, future can not be parked here!"); + } + FutureSlotState::Pending(_) => { + panic!("Future slot is already occupied by a different future!") + } + FutureSlotState::Polling => { + self.value = FutureSlotState::Pending(value); + } + } + } +} + +pub struct TaskHandle { + index: usize, + id: u64, + _pd: PhantomData<*const ()>, +} + +impl TaskHandle { + fn new(index: usize, id: u64) -> Self { + Self { + index, + id, + _pd: PhantomData, + } + } + + pub fn cancel(self) { + ASYNC_RUNTIME.with_borrow_mut(|rt| { + let Some(task) = rt.tasks.get(self.index) else { + return; + }; + + let alive = match task.value { + FutureSlotState::Empty | FutureSlotState::Gone => false, + FutureSlotState::Pending(_) => task.id == self.id, + FutureSlotState::Polling => panic!("Can not cancel future from inside it!"), + }; + + if !alive { + return; + } + + rt.cancel_task(self.index); + }) + } + + pub fn is_pending(&self) -> bool { + ASYNC_RUNTIME.with_borrow(|rt| { + let slot = rt.tasks.get(self.index).expect("Slot at index must exist!"); + + if slot.id != self.id { + return false; + } + + matches!(slot.value, FutureSlotState::Pending(_)) + }) + } +} + +#[derive(Default)] +pub(crate) struct AsyncRuntime { + tasks: Vec>>>>, + task_counter: u64, +} + +impl AsyncRuntime { + fn new() -> Self { + Self { + tasks: Vec::with_capacity(10), + task_counter: 0, + } + } + + fn next_id(&mut self) -> u64 { + let id = self.task_counter; + self.task_counter += 1; + id + } + + fn add_task + 'static>(&mut self, future: F) -> TaskHandle { + let id = self.next_id(); + let slot = self + .tasks + .iter_mut() + .enumerate() + .find(|(_, slot)| slot.is_empty()); + + let boxed = Box::pin(future); + + let index = match slot { + Some((index, slot)) => { + *slot = FutureSlot::pending(id, boxed); + index + } + None => { + self.tasks.push(FutureSlot::pending(id, boxed)); + self.tasks.len() - 1 + } + }; + + TaskHandle::new(index, id) + } + + fn get_task( + &mut self, + index: usize, + id: u64, + ) -> FutureSlotState + 'static>>> { + let slot = self.tasks.get_mut(index); + + slot.map(|inner| inner.take(id)).unwrap_or_default() + } + + fn clear_task(&mut self, index: usize) { + self.tasks[index].clear(); + } + + fn cancel_task(&mut self, index: usize) { + self.tasks[index].cancel(); + } + + fn park_task(&mut self, index: usize, future: Pin>>) { + self.tasks[index].park(future); + } +} + +struct GodotWaker { + runtime_index: usize, + task_id: u64, + thread_id: ThreadId, +} + +impl GodotWaker { + fn new(index: usize, task_id: u64, thread_id: ThreadId) -> Self { + Self { + runtime_index: index, + thread_id, + task_id, + } + } +} + +impl Wake for GodotWaker { + fn wake(self: std::sync::Arc) { + let callable = Callable::from_fn("GodotWaker::wake", move |_args| { + let current_thread = thread::current().id(); + + if self.thread_id != current_thread { + panic!("trying to poll future on a different thread!\nCurrent Thread: {:?}, Future Thread: {:?}", current_thread, self.thread_id); + } + + let waker: Waker = self.clone().into(); + let mut ctx = Context::from_waker(&waker); + + // take future out of the runtime. + let future = ASYNC_RUNTIME.with_borrow_mut(|rt| { + match rt.get_task(self.runtime_index, self.task_id) { + FutureSlotState::Empty => { + panic!("Future no longer exists when waking it! This is a bug!"); + }, + + FutureSlotState::Gone => { + None + } + + FutureSlotState::Polling => { + panic!("The same GodotWaker has been called recursively, this is not expected!"); + } + + FutureSlotState::Pending(future) => Some(future), + } + }); + + let Some(mut future) = future else { + // future has been canceled while the waker was already triggered. + return Ok(Variant::nil()); + }; + + let result = future.as_mut().poll(&mut ctx); + + // update runtime. + ASYNC_RUNTIME.with_borrow_mut(|rt| match result { + Poll::Pending => rt.park_task(self.runtime_index, future), + Poll::Ready(()) => rt.clear_task(self.runtime_index), + }); + + Ok(Variant::nil()) + }); + + // shedule waker to poll the future on the end of the frame. + callable.to_variant().call("call_deferred", &[]); + } +} + +pub struct SignalFuture { + state: Arc, Option)>>, + callable: Callable, + signal: Signal, +} + +impl SignalFuture { + fn new(signal: Signal) -> Self { + let state = Arc::new(Mutex::new((None, Option::::None))); + let callback_state = state.clone(); + + // the callable currently requires that the return value is Sync + Send + let callable = Callable::from_fn("async_task", move |args: &[&Variant]| { + let mut lock = callback_state.lock().unwrap(); + let waker = lock.1.take(); + + lock.0.replace(R::from_args(args)); + drop(lock); + + if let Some(waker) = waker { + waker.wake(); + } + + Ok(Variant::nil()) + }); + + signal.connect(callable.clone(), ConnectFlags::ONE_SHOT.ord() as i64); + + Self { + state, + callable, + signal, + } + } +} + +impl Future for SignalFuture { + type Output = R; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut lock = self.state.lock().unwrap(); + + if let Some(result) = lock.0.take() { + return Poll::Ready(result); + } + + lock.1.replace(cx.waker().clone()); + + Poll::Pending + } +} + +impl Drop for SignalFuture { + fn drop(&mut self) { + if !self.callable.is_valid() { + return; + } + + if self.signal.object().is_none() { + return; + } + + if self.signal.is_connected(self.callable.clone()) { + self.signal.disconnect(self.callable.clone()); + } + } +} + +struct GuaranteedSignalFutureWaker { + state: Arc, Option)>>, +} + +impl Clone for GuaranteedSignalFutureWaker { + fn clone(&self) -> Self { + Self { + state: self.state.clone(), + } + } +} + +impl GuaranteedSignalFutureWaker { + fn new(state: Arc, Option)>>) -> Self { + Self { state } + } +} + +impl std::hash::Hash for GuaranteedSignalFutureWaker { + fn hash(&self, state: &mut H) { + state.write_usize(Arc::as_ptr(&self.state) as usize); + } +} + +impl PartialEq for GuaranteedSignalFutureWaker { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.state, &other.state) + } +} + +impl RustCallable for GuaranteedSignalFutureWaker { + fn invoke(&mut self, args: &[&Variant]) -> Result { + let mut lock = self.state.lock().unwrap(); + let waker = lock.1.take(); + + lock.0 = GuaranteedSignalFutureState::Ready(R::from_args(args)); + drop(lock); + + if let Some(waker) = waker { + waker.wake(); + } + + Ok(Variant::nil()) + } +} + +impl Display for GuaranteedSignalFutureWaker { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SafeCallable::<{}>", type_name::()) + } +} + +impl Drop for GuaranteedSignalFutureWaker { + fn drop(&mut self) { + let mut lock = self.state.lock().unwrap(); + + if !matches!(lock.0, GuaranteedSignalFutureState::Pending) { + return; + } + + lock.0 = GuaranteedSignalFutureState::Dead; + + if let Some(ref waker) = lock.1 { + waker.wake_by_ref(); + } + } +} + +#[derive(Default)] +enum GuaranteedSignalFutureState { + #[default] + Pending, + Ready(T), + Dead, + Dropped, +} + +impl GuaranteedSignalFutureState { + fn take(&mut self) -> Self { + let new_value = match self { + Self::Pending => Self::Pending, + Self::Ready(_) | Self::Dead => Self::Dead, + Self::Dropped => Self::Dropped, + }; + + std::mem::replace(self, new_value) + } +} + +pub struct GuaranteedSignalFuture { + state: Arc, Option)>>, + callable: GuaranteedSignalFutureWaker, + signal: Signal, +} + +impl GuaranteedSignalFuture { + fn new(signal: Signal) -> Self { + let state = Arc::new(Mutex::new(( + GuaranteedSignalFutureState::Pending, + Option::::None, + ))); + + // the callable currently requires that the return value is Sync + Send + let callable = GuaranteedSignalFutureWaker::new(state.clone()); + + signal.connect( + Callable::from_custom(callable.clone()), + ConnectFlags::ONE_SHOT.ord() as i64, + ); + + Self { + state, + callable, + signal, + } + } +} + +impl Future for GuaranteedSignalFuture { + type Output = Option; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut lock = self.state.lock().unwrap(); + + lock.1.replace(cx.waker().clone()); + + let value = lock.0.take(); + + match value { + GuaranteedSignalFutureState::Pending => Poll::Pending, + GuaranteedSignalFutureState::Dropped => unreachable!(), + GuaranteedSignalFutureState::Dead => Poll::Ready(None), + GuaranteedSignalFutureState::Ready(value) => Poll::Ready(Some(value)), + } + } +} + +impl Drop for GuaranteedSignalFuture { + fn drop(&mut self) { + if self.signal.object().is_none() { + return; + } + + self.state.lock().unwrap().0 = GuaranteedSignalFutureState::Dropped; + + let gd_callable = Callable::from_custom(self.callable.clone()); + + if self.signal.is_connected(gd_callable.clone()) { + self.signal.disconnect(gd_callable); + } + } +} + +pub trait FromSignalArgs: Sync + Send + 'static { + fn from_args(args: &[&Variant]) -> Self; +} + +impl FromSignalArgs for R { + fn from_args(args: &[&Variant]) -> Self { + args.first() + .map(|arg| (*arg).to_owned()) + .unwrap_or_default() + .to() + } +} + +// more of these should be generated via macro to support more than two signal arguments +// impl FromSignalArgs +// for (R1, R2) +// { +// fn from_args(args: &[&Variant]) -> Self { +// (args[0].to(), args[0].to()) +// } +// } + +// Signal should implement IntoFuture for convenience. Keeping ToSignalFuture around might still be desirable, though. It allows to reuse i +// the same signal instance multiple times. +pub trait ToSignalFuture { + fn to_future(&self) -> SignalFuture; +} + +impl ToSignalFuture for Signal { + fn to_future(&self) -> SignalFuture { + SignalFuture::new(self.clone()) + } +} + +pub trait ToGuaranteedSignalFuture { + fn to_guaranteed_future(&self) -> GuaranteedSignalFuture; +} + +impl ToGuaranteedSignalFuture for Signal { + fn to_guaranteed_future(&self) -> GuaranteedSignalFuture { + GuaranteedSignalFuture::new(self.clone()) + } +} + +#[cfg(test)] +mod tests { + use std::{ + hash::{DefaultHasher, Hash, Hasher}, + sync::Arc, + }; + + use super::GuaranteedSignalFutureWaker; + + #[test] + fn guaranteed_future_waker_cloned_hash() { + let waker_a = GuaranteedSignalFutureWaker::::new(Arc::default()); + let waker_b = waker_a.clone(); + + let mut hasher = DefaultHasher::new(); + waker_a.hash(&mut hasher); + let hash_a = hasher.finish(); + + let mut hasher = DefaultHasher::new(); + waker_b.hash(&mut hasher); + let hash_b = hasher.finish(); + + assert_eq!(hash_a, hash_b); + } +} From 021c327d4c3425778b4233a3f77b6b0385368d54 Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Sat, 31 Aug 2024 00:18:51 +0200 Subject: [PATCH 2/2] Async level loading in rust --- native/Cargo.lock | 4 +- native/Cargo.toml | 2 +- native/src/scripts/world/buildings.rs | 217 ++++++++++++-------------- native/src/util/async_support.rs | 46 +++++- resources/main.tscn | 87 +++++------ src/HUD/HUDController.gd | 12 +- src/Objects/World/World.gd | 19 +-- 7 files changed, 192 insertions(+), 195 deletions(-) diff --git a/native/Cargo.lock b/native/Cargo.lock index f015138..f232010 100644 --- a/native/Cargo.lock +++ b/native/Cargo.lock @@ -283,7 +283,7 @@ dependencies = [ [[package]] name = "godot-rust-script" version = "0.1.0" -source = "git+https://github.com/titannano/godot-rust-script?rev=657a6b15d19b702b3aa6ad0fd23986ceb8598562#657a6b15d19b702b3aa6ad0fd23986ceb8598562" +source = "git+https://github.com/titannano/godot-rust-script?rev=ca371c120774412e4a222ba0362fdf4f5e844b6f#ca371c120774412e4a222ba0362fdf4f5e844b6f" dependencies = [ "const-str", "godot", @@ -298,7 +298,7 @@ dependencies = [ [[package]] name = "godot-rust-script-derive" version = "0.1.0" -source = "git+https://github.com/titannano/godot-rust-script?rev=657a6b15d19b702b3aa6ad0fd23986ceb8598562#657a6b15d19b702b3aa6ad0fd23986ceb8598562" +source = "git+https://github.com/titannano/godot-rust-script?rev=ca371c120774412e4a222ba0362fdf4f5e844b6f#ca371c120774412e4a222ba0362fdf4f5e844b6f" dependencies = [ "darling", "itertools", diff --git a/native/Cargo.toml b/native/Cargo.toml index 6974fca..a133840 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -21,4 +21,4 @@ rand = "0.8.5" pomsky-macro = "0.11.0" regex = "1.10.5" -godot-rust-script = { git = "https://github.com/titannano/godot-rust-script", rev = "657a6b15d19b702b3aa6ad0fd23986ceb8598562" } +godot-rust-script = { git = "https://github.com/titannano/godot-rust-script", rev = "ca371c120774412e4a222ba0362fdf4f5e844b6f" } diff --git a/native/src/scripts/world/buildings.rs b/native/src/scripts/world/buildings.rs index 68e996c..bca8aee 100644 --- a/native/src/scripts/world/buildings.rs +++ b/native/src/scripts/world/buildings.rs @@ -1,36 +1,31 @@ -use std::{ - collections::{BTreeMap, VecDeque}, - fmt::Debug, - ops::Not, -}; +use std::{collections::BTreeMap, ops::Not}; -use anyhow::Context; +use anyhow::Context as _; use derive_debug::Dbg; -use godot::{ - builtin::{meta::ToGodot, Array, Dictionary}, - engine::{Marker3D, Node, Node3D, Resource, Time}, - obj::{Gd, NewAlloc}, -}; -use godot_rust_script::{godot_script_impl, GodotScript, ScriptSignal, Signal}; - -use crate::{ - objects::scene_object_registry, - util::logger, - world::{ - city_coords_feature::CityCoordsFeature, - city_data::{self, TryFromDictionary}, - }, +use godot::builtin; +use godot::builtin::meta::ToGodot; +use godot::builtin::{Array, Dictionary}; +use godot::engine::{Marker3D, Node, Node3D, Resource, Time}; +use godot::obj::{Gd, NewAlloc}; +use godot_rust_script::{ + godot_script_impl, CastToScript, GodotScript, RsRef, ScriptSignal, Signal, }; -#[derive(GodotScript, Debug)] +use crate::objects::scene_object_registry; +use crate::util::async_support::{self, godot_task, GodotFuture, TaskHandle, ToSignalFuture}; +use crate::util::logger; +use crate::world::city_coords_feature::CityCoordsFeature; +use crate::world::city_data::{self, TryFromDictionary}; + +#[derive(GodotScript, Dbg)] #[script(base = Node)] struct Buildings { + #[dbg(skip)] + pending_build_tasks: Vec, + #[export] pub world_constants: Option>, - city_coords_feature: CityCoordsFeature, - job_runner: Option>, - /// tile_coords, size, altitude #[signal] pub spawn_point_encountered: Signal<(Array, u8, u32)>, @@ -38,24 +33,23 @@ struct Buildings { #[signal] pub loading_progress: Signal, - #[signal] - pub ready: Signal<()>, - base: Gd, } #[godot_script_impl] impl Buildings { + const TIME_BUDGET: u64 = 50; + pub fn _process(&mut self, _delta: f64) { - if let Some(mut job_runner) = self.job_runner.take() { - let progress = job_runner.poll(self); + self.pending_build_tasks.retain(|task| task.is_pending()); - self.job_runner = Some(job_runner); + let tasks = self.pending_build_tasks.len(); - match progress { - 0 => self.ready.emit(()), - progress => self.loading_progress.emit(progress), - } + if tasks > 0 { + logger::debug!( + "World Buildings Node: {} active tasks!", + self.pending_build_tasks.len() + ); } } @@ -65,49 +59,71 @@ impl Buildings { .expect("world_constants should be set!") } - pub fn build_async(&mut self, city: Dictionary) { - let city = match crate::world::city_data::City::try_from_dict(&city) - .context("Failed to deserialize city data") - { - Ok(v) => v, - Err(err) => { - logger::error!("{:?}", err); - return; - } - }; + pub fn build_async(&mut self, city: Dictionary) -> Gd { + let world_constants = self.world_constants().clone(); + let mut base = self.base.clone(); + let mut slf: RsRef = base.to_script(); + let tree = base.get_tree().expect("Node must be part of the tree!"); + let (resolve, godot_future) = async_support::godot_future(); + + let handle = godot_task(async move { + let next_tick = builtin::Signal::from_object_signal(&tree, "process_frame"); + let time = Time::singleton(); + + let city = match crate::world::city_data::City::try_from_dict(&city) + .context("Failed to deserialize city data") + { + Ok(v) => v, + Err(err) => { + logger::error!("{:?}", err); + return; + } + }; + + let sea_level = city.simulator_settings.sea_level; + let buildings = city.buildings; + let tiles = city.tilelist; + let city_coords_feature = CityCoordsFeature::new(world_constants, sea_level); + + logger::info!("starting to load buildings..."); - let sea_level = city.simulator_settings.sea_level; - let buildings = city.buildings; - let tiles = city.tilelist; + let mut count = 0; + let mut start = time.get_ticks_msec(); - self.city_coords_feature = - CityCoordsFeature::new(self.world_constants().to_owned(), sea_level); + for building in buildings.into_values() { + if (time.get_ticks_msec() - start) > Self::TIME_BUDGET { + slf.emit_progress(count); + count = 0; + start = time.get_ticks_msec(); - logger::info!("starting to load buildings..."); + let _: () = next_tick.to_future().await; + } + + count += 1; - let mut job_runner = LocalJobRunner::new( - move |host: &mut Self, building: city_data::Building| { if building.building_id == 0x00 { logger::info!("skipping empty building"); - return; + continue; } - host.insert_building(building, &tiles); - }, - 50, - ); + Self::insert_building(&mut base, building, &tiles, &city_coords_feature); + } - let buildings_array = buildings.into_values().collect(); + slf.emit_progress(count); - job_runner.tasks(buildings_array); + resolve(()); + }); - self.job_runner = Some(job_runner); + self.pending_build_tasks.push(handle); + godot_future } + /// Insert a new building into the world. fn insert_building( - &mut self, + base: &mut Gd, building: city_data::Building, tiles: &BTreeMap<(u32, u32), city_data::Tile>, + city_coords_feature: &CityCoordsFeature, ) { let building_size = building.size; let name = building.name.as_str(); @@ -137,12 +153,12 @@ impl Buildings { size: 2, }; - self.insert_building(spawn_building, tiles); - self.spawn_point_encountered.emit(( + Self::insert_building(base, spawn_building, tiles, city_coords_feature); + CastToScript::::to_script(base).emit_spawn_point_encountered( Array::from(&[tile_coords.0, tile_coords.1]), 2, altitude, - )); + ); } let (Some(mut instance), instance_time) = @@ -161,7 +177,7 @@ impl Buildings { instance.set("tile_coords_array".into(), array.to_variant()); } - let mut location = self.city_coords_feature.get_building_coords( + let mut location = city_coords_feature.get_building_coords( tile_coords.0, tile_coords.1, altitude, @@ -172,16 +188,12 @@ impl Buildings { location.y += 0.1; let (_, insert_time) = with_timing(|| { - self.get_sector(tile_coords) + Self::get_sector(base, tile_coords, city_coords_feature) .add_child_ex(instance.clone().upcast()) .force_readable_name(true) .done(); - let Some(root) = self - .base - .get_tree() - .and_then(|tree| tree.get_current_scene()) - else { + let Some(root) = base.get_tree().and_then(|tree| tree.get_current_scene()) else { logger::warn!("there is no active scene!"); return; }; @@ -206,7 +218,11 @@ impl Buildings { } /// sector coordinates are expected to align with a step of 10 - fn get_sector(&mut self, tile_coords: (u32, u32)) -> Gd { + fn get_sector( + base: &mut ::Base, + tile_coords: (u32, u32), + city_coords_feature: &CityCoordsFeature, + ) -> Gd { const SECTOR_SIZE: u32 = 32; let sector_coords = ( @@ -220,75 +236,36 @@ impl Buildings { format!("{}_{}", x, y) }; - self.base - .get_node_or_null(sector_name.to_godot().into()) + base.get_node_or_null(sector_name.to_godot().into()) .map(Gd::cast) .unwrap_or_else(|| { let mut sector: Gd = Marker3D::new_alloc().upcast(); sector.set_name(sector_name.to_godot()); - self.base.add_child(sector.clone().upcast()); + base.add_child(sector.clone().upcast()); - sector.translate(self.city_coords_feature.get_world_coords( + sector.translate(city_coords_feature.get_world_coords( sector_coords.0 + (SECTOR_SIZE / 2), sector_coords.1 + (SECTOR_SIZE / 2), 0, )); - if let Some(root) = self - .base - .get_tree() - .and_then(|tree| tree.get_current_scene()) - { + if let Some(root) = base.get_tree().and_then(|tree| tree.get_current_scene()) { sector.set_owner(root); }; sector }) } -} - -type LocalJob = Box; - -#[derive(Dbg)] -struct LocalJobRunner -where - T: Debug, -{ - budget: u64, - tasks: VecDeque, - #[dbg(skip)] - callback: LocalJob, -} - -impl LocalJobRunner { - fn new(callback: C, budget: u64) -> Self { - Self { - callback: Box::new(callback), - tasks: VecDeque::new(), - budget, - } - } - - fn poll(&mut self, host: &mut H) -> u32 { - let start = Time::singleton().get_ticks_msec(); - let mut count = 0; - - while Time::singleton().get_ticks_msec() - start < self.budget { - let Some(item) = self.tasks.remove(0) else { - return count; - }; - - (self.callback)(host, item); - count += 1; - } - count + pub fn emit_spawn_point_encountered(&self, tile_coords: Array, size: u8, altitide: u32) { + self.spawn_point_encountered + .emit((tile_coords, size, altitide)) } - fn tasks(&mut self, mut tasks: VecDeque) { - self.tasks.append(&mut tasks); + pub fn emit_progress(&self, new_building_count: u32) { + self.loading_progress.emit(new_building_count); } } diff --git a/native/src/util/async_support.rs b/native/src/util/async_support.rs index 0a26ca3..69802fa 100644 --- a/native/src/util/async_support.rs +++ b/native/src/util/async_support.rs @@ -12,7 +12,8 @@ use godot::builtin::{Callable, RustCallable, Signal, Variant}; use godot::classes::object::ConnectFlags; use godot::classes::Os; use godot::meta::{FromGodot, ToGodot}; -use godot::obj::EngineEnum; +use godot::obj::{EngineEnum, Gd, NewGd}; +use godot::prelude::{godot_api, GodotClass}; pub fn godot_task(future: impl Future + 'static) -> TaskHandle { let os = Os::singleton(); @@ -80,6 +81,7 @@ impl FutureSlot { self.value = FutureSlotState::Empty; } + #[allow(dead_code)] fn cancel(&mut self) { self.value = FutureSlotState::Gone; } @@ -126,6 +128,7 @@ impl TaskHandle { } } + #[allow(dead_code)] pub fn cancel(self) { ASYNC_RUNTIME.with_borrow_mut(|rt| { let Some(task) = rt.tasks.get(self.index) else { @@ -550,6 +553,7 @@ impl ToSignalFuture for Signal { } pub trait ToGuaranteedSignalFuture { + #[allow(dead_code)] fn to_guaranteed_future(&self) -> GuaranteedSignalFuture; } @@ -559,6 +563,46 @@ impl ToGuaranteedSignalFuture for Signal { } } +#[derive(GodotClass)] +#[class(base = RefCounted, init)] +pub struct GodotFuture; + +#[godot_api] +impl GodotFuture { + /// Returns an object which emits the completed signal once the asynchronus method has finished processing. + + /// Is emitted as soon as the async operation of the function has been completed. + #[signal] + fn completed(result: Variant); +} + +/// Creates a new GodotFuture that can be returned from a function which performs an async operation. This works similar to GdFunctionState. +/// +/// Example: +/// ```rs +/// fn async_do_task() -> Gd { +/// let (resolve, future) = godot_future(); +/// +/// godot_task(async move { +/// // do async operations +/// resolve(true); +/// }); +/// +/// future +/// } +/// ``` +pub fn godot_future() -> (impl Fn(R), Gd) { + let future = GodotFuture::new_gd(); + let sender = future.clone(); + + ( + move |value: R| { + Signal::from_object_signal(&sender, "completed").emit(&[value.to_variant()]) + }, + future, + ) +} + #[cfg(test)] mod tests { use std::{ diff --git a/resources/main.tscn b/resources/main.tscn index 0b0125f..391f837 100644 --- a/resources/main.tscn +++ b/resources/main.tscn @@ -1,11 +1,10 @@ -[gd_scene load_steps=20 format=3 uid="uid://cvh54xiw8586b"] +[gd_scene load_steps=18 format=3 uid="uid://cvh54xiw8586b"] [ext_resource type="Script" path="res://src/HUD/HUDController.gd" id="1"] [ext_resource type="Script" path="res://src/Objects/World/World.gd" id="3"] [ext_resource type="Script" path="res://src/ViewPort.gd" id="3_2c332"] [ext_resource type="Script" path="res://src/HUD/LoadingScreen.gd" id="4"] [ext_resource type="Script" path="res://src/Objects/World/Backdrop.gd" id="6"] -[ext_resource type="PackedScene" uid="uid://csuvlu656dwm4" path="res://resources/Objects/Buildings/hangar_2.tscn" id="7"] [ext_resource type="Environment" uid="uid://bl607wqoa882d" path="res://resources/Environments/WorldEnv.tres" id="8"] [ext_resource type="Script" path="res://src/Objects/Camera/CameraInterpolation.gd" id="10"] [ext_resource type="Script" path="res://src/Objects/Terrain/Terrain.gd" id="11"] @@ -23,9 +22,6 @@ dof_blur_far_enabled = true dof_blur_far_distance = 180.0 dof_blur_far_transition = -1.0 -[sub_resource type="StandardMaterial3D" id="1"] -albedo_color = Color(0, 0, 0, 1) - [node name="HUDController" type="Control"] layout_mode = 3 anchors_preset = 15 @@ -60,6 +56,7 @@ offset_bottom = 23.0 step = 1.0 [node name="SubViewportContainer" type="SubViewportContainer" parent="."] +visible = false layout_mode = 0 anchor_right = 1.0 anchor_bottom = 1.0 @@ -72,39 +69,15 @@ scaling_3d_mode = 1 audio_listener_enable_3d = true size = Vector2i(1152, 648) size_2d_override_stretch = true -render_target_update_mode = 4 +render_target_update_mode = 0 script = ExtResource("3_2c332") -current_camera_controller = NodePath("Schweizer_300/MainCameraAnchor") - -[node name="Schweizer_300" parent="SubViewportContainer/SubViewport" node_paths=PackedStringArray("child_camera", "child_main_camera", "child_debug_camera") groups=["player"] instance=ExtResource("16_e6k8r")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45.4393, -9.81614e-07, 17.9155) -child_camera = NodePath("MainCameraAnchor") -child_main_camera = NodePath("MainCameraAnchor") -child_debug_camera = NodePath("DebugCameraAnchor") - -[node name="MainCameraAnchor" type="Marker3D" parent="SubViewportContainer/SubViewport/Schweizer_300"] -process_mode = 3 -transform = Transform3D(1, 0, 0, 0, 0.847724, 0.530437, 0, -0.530437, 0.847724, 0, 5.669, 7.354) -script = ExtResource("10") -snap = true -track_y_axis = true -active = true - -[node name="DebugCameraAnchor" type="Marker3D" parent="SubViewportContainer/SubViewport/Schweizer_300"] -process_mode = 3 -transform = Transform3D(-0.0697565, 0.65051, -0.756287, 2.64427e-16, 0.758134, 0.652098, 0.997564, 0.0454881, -0.0528848, -34.7616, 32.649, -0.770037) -script = ExtResource("10") +current_camera_controller = NodePath("World/Schweizer_300/MainCameraAnchor") [node name="World" type="Node3D" parent="SubViewportContainer/SubViewport"] +process_mode = 4 script = ExtResource("3") world_constants = ExtResource("15") -[node name="Terrain" type="Node3D" parent="SubViewportContainer/SubViewport/World"] -script = ExtResource("11") -terrain_material = ExtResource("12") -ocean_material = ExtResource("13") -world_constants = ExtResource("15") - [node name="Environment" type="WorldEnvironment" parent="SubViewportContainer/SubViewport/World"] environment = ExtResource("8") camera_attributes = SubResource("CameraAttributesPractical_lhcij") @@ -141,6 +114,37 @@ directional_shadow_blend_splits = true directional_shadow_fade_start = 0.6 directional_shadow_max_distance = 400.0 +[node name="Camera" type="Camera3D" parent="SubViewportContainer/SubViewport/World"] +transform = Transform3D(1, 0, 0, 0, 0.847724, 0.530437, 0, -0.530437, 0.847724, -45.4393, 5.66891, 25.269) +doppler_tracking = 2 +current = true +near = 3.0 + +[node name="Schweizer_300" parent="SubViewportContainer/SubViewport/World" node_paths=PackedStringArray("child_camera", "child_main_camera", "child_debug_camera") groups=["player"] instance=ExtResource("16_e6k8r")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45.4393, -9.81614e-07, 17.9155) +child_camera = NodePath("MainCameraAnchor") +child_main_camera = NodePath("MainCameraAnchor") +child_debug_camera = NodePath("DebugCameraAnchor") + +[node name="MainCameraAnchor" type="Marker3D" parent="SubViewportContainer/SubViewport/World/Schweizer_300"] +process_mode = 3 +transform = Transform3D(1, 0, 0, 0, 0.847724, 0.530437, 0, -0.530437, 0.847724, 0, 5.669, 7.354) +script = ExtResource("10") +snap = true +track_y_axis = true +active = true + +[node name="DebugCameraAnchor" type="Marker3D" parent="SubViewportContainer/SubViewport/World/Schweizer_300"] +process_mode = 3 +transform = Transform3D(-0.0697565, 0.65051, -0.756287, 2.64427e-16, 0.758134, 0.652098, 0.997564, 0.0454881, -0.0528848, -34.7616, 32.649, -0.770037) +script = ExtResource("10") + +[node name="Terrain" type="Node3D" parent="SubViewportContainer/SubViewport/World"] +script = ExtResource("11") +terrain_material = ExtResource("12") +ocean_material = ExtResource("13") +world_constants = ExtResource("15") + [node name="Networks" type="Node" parent="SubViewportContainer/SubViewport/World"] script = ExtResource("14") world_constants = ExtResource("15") @@ -156,22 +160,3 @@ world_constants = ExtResource("15") [node name="Backdrop" type="Node" parent="SubViewportContainer/SubViewport/World"] script = ExtResource("6") - -[node name="Camera" type="Camera3D" parent="SubViewportContainer/SubViewport"] -transform = Transform3D(1, 0, 0, 0, 0.847724, 0.530437, 0, -0.530437, 0.847724, -45.4393, 5.66891, 25.269) -doppler_tracking = 2 -current = true -near = 3.0 - -[node name="Start" type="Node3D" parent="SubViewportContainer/SubViewport"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -50.5, 0, 0) -visible = false - -[node name="CSGPolygon3D" type="CSGPolygon3D" parent="SubViewportContainer/SubViewport/Start"] -transform = Transform3D(50, 0, 0, 0, -2.18557e-06, 1, 0, -50, -4.37114e-08, -24.9718, -0.122162, 25.0614) -depth = 0.001 -smooth_faces = true -material = SubResource("1") - -[node name="hangar" parent="SubViewportContainer/SubViewport/Start" instance=ExtResource("7")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.68345, 0, -4.04857) diff --git a/src/HUD/HUDController.gd b/src/HUD/HUDController.gd index 0bc526d..a04e446 100644 --- a/src/HUD/HUDController.gd +++ b/src/HUD/HUDController.gd @@ -3,8 +3,6 @@ extends Control const World := preload("res://src/Objects/World/World.gd") const LoadingScreen := preload("res://src/HUD/LoadingScreen.gd") -var game_ready := false - @onready var loading_screen: LoadingScreen = $LoadingScreen @onready var viewport: SubViewportContainer = $SubViewportContainer @onready var world: World = $SubViewportContainer/SubViewport/World @@ -20,10 +18,10 @@ func _ready(): window.position -= Vector2i(self.get_window().size / scale / 2) -func _process(_delta: float) -> void: - loading_screen.visible = !game_ready - viewport.visible = game_ready - +func game_ready() -> void: + loading_screen.visible = false + viewport.visible = true + world.process_mode = Node.PROCESS_MODE_PAUSABLE func _on_loading_scale(total: int): loading_screen.total_jobs = total @@ -33,4 +31,4 @@ func _on_loading_progress(new_progress: int): loading_screen.completed_jobs += new_progress if loading_screen.completed_jobs == loading_screen.total_jobs: - self.game_ready = true + self.game_ready() diff --git a/src/Objects/World/World.gd b/src/Objects/World/World.gd index fe467cc..2e0f5ee 100644 --- a/src/Objects/World/World.gd +++ b/src/Objects/World/World.gd @@ -27,14 +27,10 @@ func _ready(): assert(world_constants is WorldConstants, "world_constants is not of type WorldConstants") self.networks.loading_progress.connect(self._on_child_progress) - @warning_ignore("unsafe_property_access") - @warning_ignore("unsafe_method_access") self.buildings.spawn_point_encountered.connect(self._on_spawn_point_encountered) - @warning_ignore("unsafe_property_access") - @warning_ignore("unsafe_method_access") self.buildings.loading_progress.connect(self._on_child_progress) - call_deferred("_ready_deferred") + self._ready_deferred.call_deferred() func _ready_deferred(): @@ -63,10 +59,7 @@ func _on_spawn_point_encountered(tile_coords: Array[int], size: int, altitude: i func _load_map_async(city: Dictionary): await self.terrain.build_async(city) await self.networks.build_async(city) - @warning_ignore("unsafe_method_access") - self.buildings.build_async(city) - - await self.buildings.ready + await self.buildings.build_async(city).completed var city_size: int = city.get("city_size") @@ -94,11 +87,11 @@ func _create_snapshot() -> void: func _spawn_player() -> void: var spawns := get_tree().get_nodes_in_group("spawn") var players := get_tree().get_nodes_in_group("player") - var player: Helicopter = players[0] - var spawn: Node3D = spawns[0] + var player := players[0] as Helicopter + var spawn := spawns[0] as Node3D - player.global_transform.origin = spawn.global_transform.origin - player.force_update_transform() + player.global_transform.origin = spawn.global_transform.origin + Vector3(0, -0.1, 0) +# player.force_update_transform() player.snap_camera()