diff --git a/jong/src/game.rs b/jong/src/game.rs index 759b0ee..02cbbef 100644 --- a/jong/src/game.rs +++ b/jong/src/game.rs @@ -1,6 +1,20 @@ -use bevy::prelude::*; -use bevy_spacetimedb::StdbPlugin; +#![allow(unused)] +use bevy::prelude::*; +use bevy_spacetimedb::{ + ReadInsertUpdateMessage, ReadStdbConnectedMessage, ReadStdbDisconnectedMessage, StdbPlugin, + TableMessages, +}; +use spacetimedb::Identity; +use spacetimedb_sdk::credentials; + +use crate::{ + SpacetimeDB, creds_store, + stdb::{ + self, DbConnection, LobbyTableAccess, PlayerTableAccess, RemoteTables, add_bot, + join_or_create_lobby, login_or_add_player, + }, +}; use crate::{ game::{ hand::{Hand, Pond}, @@ -8,7 +22,6 @@ use crate::{ round::{TurnState, Wind}, wall::Wall, }, - stdb::DbConnection, tile::{self}, }; @@ -24,6 +37,7 @@ pub enum GameState { Setup, Deal, Play, + Exit, } #[derive(Message)] @@ -33,77 +47,115 @@ pub enum GameMessage { Called { player: Entity, calltype: Entity }, } -impl GameMessage { - pub(crate) fn is_called(&self) -> bool { - match self { - GameMessage::Called { .. } => true, - _ => false, - } - } -} - pub struct Riichi; impl Plugin for Riichi { fn build(&self, app: &mut App) { - // app.add_plugins( - // StdbPlugin::default() - // .with_uri("http://localhost:3000") - // .with_module_name("jongline") - // .with_run_fn(DbConnection::run_threaded), - // ); - app - // start stopper + let mut plugins = StdbPlugin::default() + .with_uri("http://localhost:3000") + .with_module_name("jongline") + .with_run_fn(DbConnection::run_threaded) + // TODO change to partial no update or a subscription? + .add_table(RemoteTables::player); + let plugins = + if let Some(token) = creds_store().load().expect("i/o error loading credentials") { + // FIXME patch plugin so this takes Option? + plugins.with_token(&token) + } else { + plugins + }; + + app.add_plugins(plugins) .init_state::() - .add_sub_state::() - .init_resource::() - .init_resource::() - .add_message::() + // .add_sub_state::() + // .init_resource::() + // .init_resource::() + // .add_message::() .add_systems(Startup, tile::init_tiles) - .add_systems(OnEnter(GameState::Setup), setup) - .add_systems(OnEnter(GameState::Deal), hand::shuffle_deal) - .add_systems(Update, hand::sort_hands.run_if(in_state(GameState::Play))) - .add_systems(OnEnter(TurnState::Tsumo), round::tsumo) - .add_systems(OnEnter(TurnState::Menzen), round::menzen) - .add_systems(Update, round::riichi_kan.run_if(in_state(TurnState::RiichiKan))) - .add_systems(Update, round::discard.run_if(in_state(TurnState::Discard))) - .add_systems(OnEnter(TurnState::RonChiiPonKan), round::notify_callable) - .add_systems(Update, round::ron_chi_pon_kan.run_if(in_state(TurnState::RonChiiPonKan)).after(round::notify_callable)) - .add_systems(OnEnter(TurnState::End), round::end) + // .add_systems(OnEnter(GameState::Setup), setup) + // .add_systems(OnEnter(GameState::Deal), hand::shuffle_deal) + // .add_systems(Update, hand::sort_hands.run_if(in_state(GameState::Play))) + // .add_systems(OnEnter(TurnState::Tsumo), round::tsumo) + // .add_systems(OnEnter(TurnState::Menzen), round::menzen) + // .add_systems(Update, round::riichi_kan.run_if(in_state(TurnState::RiichiKan))) + // .add_systems(Update, round::discard.run_if(in_state(TurnState::Discard))) + // .add_systems(OnEnter(TurnState::RonChiiPonKan), round::notify_callable) + // .add_systems(Update, round::ron_chi_pon_kan.run_if(in_state(TurnState::RonChiiPonKan)).after(round::notify_callable)) + // .add_systems(OnEnter(TurnState::End), round::end) + // stdb + .add_systems(Update, on_connect) + .add_systems(Update, on_disconnect) + .add_systems(Update, on_player_insert_update) + .add_systems(OnEnter(GameState::Setup), join_or_create_lobby) + .add_systems(OnEnter(GameState::Deal), ||todo!()) // semicolon stopper ; } } -pub(crate) fn setup( +#[derive(Resource, Deref)] +struct Player(stdb::Player); + +// TODO or reconnect? +fn on_connect(stdb: SpacetimeDB, mut messages: ReadStdbConnectedMessage) { + for msg in messages.read() { + info!("you're now jongline"); + debug!("with identity: {}", stdb.identity()); + creds_store() + .save(&msg.access_token) + .expect("i/o error saving token"); + + // FIXME delet this in future + stdb.subscription_builder().subscribe_to_all_tables(); + } +} + +fn on_disconnect(stdb: SpacetimeDB, mut messages: ReadStdbDisconnectedMessage) { + for msg in messages.read() { + warn!("lost connection: {:#?}", msg.err); + } +} + +fn on_player_insert_update( + stdb: SpacetimeDB, + mut messages: ReadInsertUpdateMessage, mut commands: Commands, - matchsettings: Res, - // mut compass: ResMut - // tiles: Query>, - mut next_gamestate: ResMut>, + player: Option>, ) { - for i in 1..=matchsettings.player_count { - let player = player::Player { - name: format!("Player {}", i), - }; - let points = player::Points(matchsettings.starting_points); - - let bundle = ( - player, - points, - Hand, - Pond, - Wind::from_repr((i - 1) as usize).unwrap(), - ); - - if i == 1 { - let player = commands.spawn((bundle, MainPlayer, CurrentPlayer)).id(); - // commands.insert_resource(CurrentPlayer(player)); + for msg in messages.read() { + if player.is_some() { + if msg.new.lobby_id != 0 { + info!("joined lobby {}", msg.new.lobby_id); + for _ in 0..3 { + stdb.reducers().add_bot(msg.new.lobby_id).unwrap() + } + } + } else if let Some(player) = stdb.db().player().identity().find(&stdb.identity()) { + commands.insert_resource(Player(player)); + trace!("logged in"); } else { - commands.spawn(bundle); + debug!( + "received player insert update msg: {:?} -> {:?}", + msg.old, msg.new + ) } } +} - commands.spawn(Wall); +fn join_or_create_lobby( + stdb: SpacetimeDB, + player: Res, + mut next_gamestate: ResMut>, +) { + if player.lobby_id == 0 { + stdb.reducers().join_or_create_lobby(None).unwrap(); + stdb.subscription_builder() + .on_applied(|_| trace!("subbed to lobby table")) + .on_error(|_, err| error!("sub to lobby table failed: {}", err)) + .subscribe(["SELECT l.* FROM lobby l JOIN player p ON l.host_id = p.id"]); + } else { + debug!("already in lobby") + } + // TODO how can this trigger with a reducer or automatically after all players join/ready next_gamestate.set(GameState::Deal); } diff --git a/jong/src/lib.rs b/jong/src/lib.rs index 8e711a7..8981acd 100644 --- a/jong/src/lib.rs +++ b/jong/src/lib.rs @@ -2,6 +2,7 @@ use bevy::prelude::*; use bevy_spacetimedb::StdbConnection; +use spacetimedb_sdk::credentials; pub mod game; pub mod tile; @@ -14,3 +15,7 @@ trait EnumNextCycle { } pub type SpacetimeDB<'a> = Res<'a, StdbConnection>; + +fn creds_store() -> credentials::File { + credentials::File::new("jongline") +} diff --git a/jong/src/stdb/add_bot_reducer.rs b/jong/src/stdb/add_bot_reducer.rs new file mode 100644 index 0000000..eccf63a --- /dev/null +++ b/jong/src/stdb/add_bot_reducer.rs @@ -0,0 +1,104 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub(super) struct AddBotArgs { + pub lobby_id: u32, +} + +impl From for super::Reducer { + fn from(args: AddBotArgs) -> Self { + Self::AddBot { + lobby_id: args.lobby_id, + } + } +} + +impl __sdk::InModule for AddBotArgs { + type Module = super::RemoteModule; +} + +pub struct AddBotCallbackId(__sdk::CallbackId); + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `add_bot`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait add_bot { + /// Request that the remote module invoke the reducer `add_bot` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed by listening for [`Self::on_add_bot`] callbacks. + fn add_bot(&self, lobby_id: u32) -> __sdk::Result<()>; + /// Register a callback to run whenever we are notified of an invocation of the reducer `add_bot`. + /// + /// Callbacks should inspect the [`__sdk::ReducerEvent`] contained in the [`super::ReducerEventContext`] + /// to determine the reducer's status. + /// + /// The returned [`AddBotCallbackId`] can be passed to [`Self::remove_on_add_bot`] + /// to cancel the callback. + fn on_add_bot( + &self, + callback: impl FnMut(&super::ReducerEventContext, &u32) + Send + 'static, + ) -> AddBotCallbackId; + /// Cancel a callback previously registered by [`Self::on_add_bot`], + /// causing it not to run in the future. + fn remove_on_add_bot(&self, callback: AddBotCallbackId); +} + +impl add_bot for super::RemoteReducers { + fn add_bot(&self, lobby_id: u32) -> __sdk::Result<()> { + self.imp.call_reducer("add_bot", AddBotArgs { lobby_id }) + } + fn on_add_bot( + &self, + mut callback: impl FnMut(&super::ReducerEventContext, &u32) + Send + 'static, + ) -> AddBotCallbackId { + AddBotCallbackId(self.imp.on_reducer( + "add_bot", + Box::new(move |ctx: &super::ReducerEventContext| { + #[allow(irrefutable_let_patterns)] + let super::ReducerEventContext { + event: + __sdk::ReducerEvent { + reducer: super::Reducer::AddBot { lobby_id }, + .. + }, + .. + } = ctx + else { + unreachable!() + }; + callback(ctx, lobby_id) + }), + )) + } + fn remove_on_add_bot(&self, callback: AddBotCallbackId) { + self.imp.remove_on_reducer("add_bot", callback.0) + } +} + +#[allow(non_camel_case_types)] +#[doc(hidden)] +/// Extension trait for setting the call-flags for the reducer `add_bot`. +/// +/// Implemented for [`super::SetReducerFlags`]. +/// +/// This type is currently unstable and may be removed without a major version bump. +pub trait set_flags_for_add_bot { + /// Set the call-reducer flags for the reducer `add_bot` to `flags`. + /// + /// This type is currently unstable and may be removed without a major version bump. + fn add_bot(&self, flags: __ws::CallReducerFlags); +} + +impl set_flags_for_add_bot for super::SetReducerFlags { + fn add_bot(&self, flags: __ws::CallReducerFlags) { + self.imp.set_call_reducer_flags("add_bot", flags); + } +} diff --git a/jong/src/stdb/bot_table.rs b/jong/src/stdb/bot_table.rs new file mode 100644 index 0000000..9e4c080 --- /dev/null +++ b/jong/src/stdb/bot_table.rs @@ -0,0 +1,142 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::bot_type::Bot; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `bot`. +/// +/// Obtain a handle from the [`BotTableAccess::bot`] method on [`super::RemoteTables`], +/// like `ctx.db.bot()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bot().on_insert(...)`. +pub struct BotTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `bot`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BotTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BotTableHandle`], which mediates access to the table `bot`. + fn bot(&self) -> BotTableHandle<'_>; +} + +impl BotTableAccess for super::RemoteTables { + fn bot(&self) -> BotTableHandle<'_> { + BotTableHandle { + imp: self.imp.get_table::("bot"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BotInsertCallbackId(__sdk::CallbackId); +pub struct BotDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BotTableHandle<'ctx> { + type Row = Bot; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = BotInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BotInsertCallbackId { + BotInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BotInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BotDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BotDeleteCallbackId { + BotDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BotDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("bot"); + _table.add_unique_constraint::("id", |row| &row.id); +} +pub struct BotUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BotTableHandle<'ctx> { + type UpdateCallbackId = BotUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BotUpdateCallbackId { + BotUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BotUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::TableUpdate<__ws::BsatnFormat>, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +/// Access to the `id` unique index on the table `bot`, +/// which allows point queries on the field of the same name +/// via the [`BotIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bot().id().find(...)`. +pub struct BotIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> BotTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `bot`. + pub fn id(&self) -> BotIdUnique<'ctx> { + BotIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> BotIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u32) -> Option { + self.imp.find(col_val) + } +} diff --git a/jong/src/stdb/bot_type.rs b/jong/src/stdb/bot_type.rs new file mode 100644 index 0000000..ba15e53 --- /dev/null +++ b/jong/src/stdb/bot_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Bot { + pub id: u32, + pub lobby_id: u32, +} + +impl __sdk::InModule for Bot { + type Module = super::RemoteModule; +} diff --git a/jong/src/stdb/lobby_table.rs b/jong/src/stdb/lobby_table.rs index 53f11a7..62a0aeb 100644 --- a/jong/src/stdb/lobby_table.rs +++ b/jong/src/stdb/lobby_table.rs @@ -82,6 +82,7 @@ impl<'ctx> __sdk::Table for LobbyTableHandle<'ctx> { pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { let _table = client_cache.get_or_make_table::("lobby"); _table.add_unique_constraint::("id", |row| &row.id); + _table.add_unique_constraint::("host_id", |row| &row.host_id); } pub struct LobbyUpdateCallbackId(__sdk::CallbackId); @@ -140,3 +141,33 @@ impl<'ctx> LobbyIdUnique<'ctx> { self.imp.find(col_val) } } + +/// Access to the `host_id` unique index on the table `lobby`, +/// which allows point queries on the field of the same name +/// via the [`LobbyHostIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.lobby().host_id().find(...)`. +pub struct LobbyHostIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> LobbyTableHandle<'ctx> { + /// Get a handle on the `host_id` unique index on the table `lobby`. + pub fn host_id(&self) -> LobbyHostIdUnique<'ctx> { + LobbyHostIdUnique { + imp: self.imp.get_unique_constraint::("host_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> LobbyHostIdUnique<'ctx> { + /// Find the subscribed row whose `host_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u32) -> Option { + self.imp.find(col_val) + } +} diff --git a/jong/src/stdb/lobby_type.rs b/jong/src/stdb/lobby_type.rs index fcb5045..6117233 100644 --- a/jong/src/stdb/lobby_type.rs +++ b/jong/src/stdb/lobby_type.rs @@ -8,7 +8,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; #[sats(crate = __lib)] pub struct Lobby { pub id: u32, - pub host: __sdk::Identity, + pub host_id: u32, } impl __sdk::InModule for Lobby { diff --git a/jong/src/stdb/login_or_add_player_reducer.rs b/jong/src/stdb/login_or_add_player_reducer.rs new file mode 100644 index 0000000..f94c20f --- /dev/null +++ b/jong/src/stdb/login_or_add_player_reducer.rs @@ -0,0 +1,103 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub(super) struct LoginOrAddPlayerArgs {} + +impl From for super::Reducer { + fn from(args: LoginOrAddPlayerArgs) -> Self { + Self::LoginOrAddPlayer + } +} + +impl __sdk::InModule for LoginOrAddPlayerArgs { + type Module = super::RemoteModule; +} + +pub struct LoginOrAddPlayerCallbackId(__sdk::CallbackId); + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `login_or_add_player`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait login_or_add_player { + /// Request that the remote module invoke the reducer `login_or_add_player` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed by listening for [`Self::on_login_or_add_player`] callbacks. + fn login_or_add_player(&self) -> __sdk::Result<()>; + /// Register a callback to run whenever we are notified of an invocation of the reducer `login_or_add_player`. + /// + /// Callbacks should inspect the [`__sdk::ReducerEvent`] contained in the [`super::ReducerEventContext`] + /// to determine the reducer's status. + /// + /// The returned [`LoginOrAddPlayerCallbackId`] can be passed to [`Self::remove_on_login_or_add_player`] + /// to cancel the callback. + fn on_login_or_add_player( + &self, + callback: impl FnMut(&super::ReducerEventContext) + Send + 'static, + ) -> LoginOrAddPlayerCallbackId; + /// Cancel a callback previously registered by [`Self::on_login_or_add_player`], + /// causing it not to run in the future. + fn remove_on_login_or_add_player(&self, callback: LoginOrAddPlayerCallbackId); +} + +impl login_or_add_player for super::RemoteReducers { + fn login_or_add_player(&self) -> __sdk::Result<()> { + self.imp + .call_reducer("login_or_add_player", LoginOrAddPlayerArgs {}) + } + fn on_login_or_add_player( + &self, + mut callback: impl FnMut(&super::ReducerEventContext) + Send + 'static, + ) -> LoginOrAddPlayerCallbackId { + LoginOrAddPlayerCallbackId(self.imp.on_reducer( + "login_or_add_player", + Box::new(move |ctx: &super::ReducerEventContext| { + #[allow(irrefutable_let_patterns)] + let super::ReducerEventContext { + event: + __sdk::ReducerEvent { + reducer: super::Reducer::LoginOrAddPlayer {}, + .. + }, + .. + } = ctx + else { + unreachable!() + }; + callback(ctx) + }), + )) + } + fn remove_on_login_or_add_player(&self, callback: LoginOrAddPlayerCallbackId) { + self.imp + .remove_on_reducer("login_or_add_player", callback.0) + } +} + +#[allow(non_camel_case_types)] +#[doc(hidden)] +/// Extension trait for setting the call-flags for the reducer `login_or_add_player`. +/// +/// Implemented for [`super::SetReducerFlags`]. +/// +/// This type is currently unstable and may be removed without a major version bump. +pub trait set_flags_for_login_or_add_player { + /// Set the call-reducer flags for the reducer `login_or_add_player` to `flags`. + /// + /// This type is currently unstable and may be removed without a major version bump. + fn login_or_add_player(&self, flags: __ws::CallReducerFlags); +} + +impl set_flags_for_login_or_add_player for super::SetReducerFlags { + fn login_or_add_player(&self, flags: __ws::CallReducerFlags) { + self.imp + .set_call_reducer_flags("login_or_add_player", flags); + } +} diff --git a/jong/src/stdb/mod.rs b/jong/src/stdb/mod.rs index e6d9a19..3061c2c 100644 --- a/jong/src/stdb/mod.rs +++ b/jong/src/stdb/mod.rs @@ -6,21 +6,22 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; -pub mod add_player_reducer; +pub mod add_bot_reducer; +pub mod bot_table; +pub mod bot_type; pub mod deal_hands_reducer; pub mod dragon_type; pub mod hand_table; pub mod hand_type; -pub mod insert_wall_reducer; pub mod join_or_create_lobby_reducer; pub mod lobby_table; pub mod lobby_type; +pub mod login_or_add_player_reducer; pub mod player_table; pub mod player_type; pub mod pond_table; pub mod pond_type; pub mod rank_type; -pub mod set_name_reducer; pub mod shuffle_wall_reducer; pub mod sort_hand_reducer; pub mod suit_type; @@ -29,23 +30,26 @@ pub mod wall_table; pub mod wall_type; pub mod wind_type; -pub use add_player_reducer::{add_player, set_flags_for_add_player, AddPlayerCallbackId}; +pub use add_bot_reducer::{add_bot, set_flags_for_add_bot, AddBotCallbackId}; +pub use bot_table::*; +pub use bot_type::Bot; pub use deal_hands_reducer::{deal_hands, set_flags_for_deal_hands, DealHandsCallbackId}; pub use dragon_type::Dragon; pub use hand_table::*; pub use hand_type::Hand; -pub use insert_wall_reducer::{insert_wall, set_flags_for_insert_wall, InsertWallCallbackId}; pub use join_or_create_lobby_reducer::{ join_or_create_lobby, set_flags_for_join_or_create_lobby, JoinOrCreateLobbyCallbackId, }; pub use lobby_table::*; pub use lobby_type::Lobby; +pub use login_or_add_player_reducer::{ + login_or_add_player, set_flags_for_login_or_add_player, LoginOrAddPlayerCallbackId, +}; pub use player_table::*; pub use player_type::Player; pub use pond_table::*; pub use pond_type::Pond; pub use rank_type::Rank; -pub use set_name_reducer::{set_flags_for_set_name, set_name, SetNameCallbackId}; pub use shuffle_wall_reducer::{set_flags_for_shuffle_wall, shuffle_wall, ShuffleWallCallbackId}; pub use sort_hand_reducer::{set_flags_for_sort_hand, sort_hand, SortHandCallbackId}; pub use suit_type::Suit; @@ -62,11 +66,10 @@ pub use wind_type::Wind; /// to indicate which reducer caused the event. pub enum Reducer { - AddPlayer { name: Option }, + AddBot { lobby_id: u32 }, DealHands, - InsertWall { player_ids: Vec }, JoinOrCreateLobby { lobby: Option }, - SetName { name: String }, + LoginOrAddPlayer, ShuffleWall, SortHand, } @@ -78,11 +81,10 @@ impl __sdk::InModule for Reducer { impl __sdk::Reducer for Reducer { fn reducer_name(&self) -> &'static str { match self { - Reducer::AddPlayer { .. } => "add_player", + Reducer::AddBot { .. } => "add_bot", Reducer::DealHands => "deal_hands", - Reducer::InsertWall { .. } => "insert_wall", Reducer::JoinOrCreateLobby { .. } => "join_or_create_lobby", - Reducer::SetName { .. } => "set_name", + Reducer::LoginOrAddPlayer => "login_or_add_player", Reducer::ShuffleWall => "shuffle_wall", Reducer::SortHand => "sort_hand", _ => unreachable!(), @@ -93,13 +95,11 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { type Error = __sdk::Error; fn try_from(value: __ws::ReducerCallInfo<__ws::BsatnFormat>) -> __sdk::Result { match &value.reducer_name[..] { - "add_player" => Ok( - __sdk::parse_reducer_args::( - "add_player", - &value.args, - )? - .into(), - ), + "add_bot" => Ok(__sdk::parse_reducer_args::( + "add_bot", + &value.args, + )? + .into()), "deal_hands" => Ok( __sdk::parse_reducer_args::( "deal_hands", @@ -107,21 +107,13 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { )? .into(), ), - "insert_wall" => Ok( - __sdk::parse_reducer_args::( - "insert_wall", - &value.args, - )? - .into(), - ), "join_or_create_lobby" => Ok(__sdk::parse_reducer_args::< join_or_create_lobby_reducer::JoinOrCreateLobbyArgs, >("join_or_create_lobby", &value.args)? .into()), - "set_name" => Ok(__sdk::parse_reducer_args::( - "set_name", - &value.args, - )? + "login_or_add_player" => Ok(__sdk::parse_reducer_args::< + login_or_add_player_reducer::LoginOrAddPlayerArgs, + >("login_or_add_player", &value.args)? .into()), "shuffle_wall" => Ok( __sdk::parse_reducer_args::( @@ -151,6 +143,7 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { #[allow(non_snake_case)] #[doc(hidden)] pub struct DbUpdate { + bot: __sdk::TableUpdate, hand: __sdk::TableUpdate, lobby: __sdk::TableUpdate, player: __sdk::TableUpdate, @@ -164,6 +157,9 @@ impl TryFrom<__ws::DatabaseUpdate<__ws::BsatnFormat>> for DbUpdate { let mut db_update = DbUpdate::default(); for table_update in raw.tables { match &table_update.table_name[..] { + "bot" => db_update + .bot + .append(bot_table::parse_table_update(table_update)?), "hand" => db_update .hand .append(hand_table::parse_table_update(table_update)?), @@ -205,6 +201,9 @@ impl __sdk::DbUpdate for DbUpdate { ) -> AppliedDiff<'_> { let mut diff = AppliedDiff::default(); + diff.bot = cache + .apply_diff_to_table::("bot", &self.bot) + .with_updates_by_pk(|row| &row.id); diff.hand = cache .apply_diff_to_table::("hand", &self.hand) .with_updates_by_pk(|row| &row.player_ident); @@ -227,6 +226,7 @@ impl __sdk::DbUpdate for DbUpdate { #[allow(non_snake_case)] #[doc(hidden)] pub struct AppliedDiff<'r> { + bot: __sdk::TableAppliedDiff<'r, Bot>, hand: __sdk::TableAppliedDiff<'r, Hand>, lobby: __sdk::TableAppliedDiff<'r, Lobby>, player: __sdk::TableAppliedDiff<'r, Player>, @@ -245,6 +245,7 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { event: &EventContext, callbacks: &mut __sdk::DbCallbacks, ) { + callbacks.invoke_table_row_callbacks::("bot", &self.bot, event); callbacks.invoke_table_row_callbacks::("hand", &self.hand, event); callbacks.invoke_table_row_callbacks::("lobby", &self.lobby, event); callbacks.invoke_table_row_callbacks::("player", &self.player, event); @@ -969,6 +970,7 @@ impl __sdk::SpacetimeModule for RemoteModule { type SubscriptionHandle = SubscriptionHandle; fn register_tables(client_cache: &mut __sdk::ClientCache) { + bot_table::register_table(client_cache); hand_table::register_table(client_cache); lobby_table::register_table(client_cache); player_table::register_table(client_cache); diff --git a/jong/src/stdb/player_table.rs b/jong/src/stdb/player_table.rs index f876249..93eb034 100644 --- a/jong/src/stdb/player_table.rs +++ b/jong/src/stdb/player_table.rs @@ -82,6 +82,7 @@ impl<'ctx> __sdk::Table for PlayerTableHandle<'ctx> { pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { let _table = client_cache.get_or_make_table::("player"); _table.add_unique_constraint::<__sdk::Identity>("identity", |row| &row.identity); + _table.add_unique_constraint::("id", |row| &row.id); } pub struct PlayerUpdateCallbackId(__sdk::CallbackId); @@ -142,3 +143,33 @@ impl<'ctx> PlayerIdentityUnique<'ctx> { self.imp.find(col_val) } } + +/// Access to the `id` unique index on the table `player`, +/// which allows point queries on the field of the same name +/// via the [`PlayerIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.player().id().find(...)`. +pub struct PlayerIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> PlayerTableHandle<'ctx> { + /// Get a handle on the `id` unique index on the table `player`. + pub fn id(&self) -> PlayerIdUnique<'ctx> { + PlayerIdUnique { + imp: self.imp.get_unique_constraint::("id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> PlayerIdUnique<'ctx> { + /// Find the subscribed row whose `id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u32) -> Option { + self.imp.find(col_val) + } +} diff --git a/jong/src/stdb/player_type.rs b/jong/src/stdb/player_type.rs index 266df28..278ed9e 100644 --- a/jong/src/stdb/player_type.rs +++ b/jong/src/stdb/player_type.rs @@ -8,6 +8,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; #[sats(crate = __lib)] pub struct Player { pub identity: __sdk::Identity, + pub id: u32, pub name: Option, pub lobby_id: u32, } diff --git a/jong/src/stdb/wall_table.rs b/jong/src/stdb/wall_table.rs index 79cd408..4a06950 100644 --- a/jong/src/stdb/wall_table.rs +++ b/jong/src/stdb/wall_table.rs @@ -83,6 +83,7 @@ impl<'ctx> __sdk::Table for WallTableHandle<'ctx> { pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { let _table = client_cache.get_or_make_table::("wall"); _table.add_unique_constraint::("id", |row| &row.id); + _table.add_unique_constraint::("lobby_id", |row| &row.lobby_id); } pub struct WallUpdateCallbackId(__sdk::CallbackId); @@ -141,3 +142,33 @@ impl<'ctx> WallIdUnique<'ctx> { self.imp.find(col_val) } } + +/// Access to the `lobby_id` unique index on the table `wall`, +/// which allows point queries on the field of the same name +/// via the [`WallLobbyIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.wall().lobby_id().find(...)`. +pub struct WallLobbyIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> WallTableHandle<'ctx> { + /// Get a handle on the `lobby_id` unique index on the table `wall`. + pub fn lobby_id(&self) -> WallLobbyIdUnique<'ctx> { + WallLobbyIdUnique { + imp: self.imp.get_unique_constraint::("lobby_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> WallLobbyIdUnique<'ctx> { + /// Find the subscribed row whose `lobby_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u32) -> Option { + self.imp.find(col_val) + } +} diff --git a/jong/src/stdb/wall_type.rs b/jong/src/stdb/wall_type.rs index 88a1e5f..732dbea 100644 --- a/jong/src/stdb/wall_type.rs +++ b/jong/src/stdb/wall_type.rs @@ -10,6 +10,7 @@ use super::tile_type::Tile; #[sats(crate = __lib)] pub struct Wall { pub id: u32, + pub lobby_id: u32, pub tiles: Vec, } diff --git a/jong/src/tile.rs b/jong/src/tile.rs index 34b73c5..3c4267c 100644 --- a/jong/src/tile.rs +++ b/jong/src/tile.rs @@ -1,55 +1,7 @@ use bevy::prelude::*; -// use spacetimedb::SpacetimeType; -// use strum::FromRepr; use jong_types::*; -// #[derive(Component, Debug, Clone, Copy, SpacetimeType)] -// pub struct Tile { -// pub suit: Suit, -// } - -// #[derive(/* MapEntities, */ Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, SpacetimeType)] -// pub enum Suit { -// Man(Rank), -// Pin(Rank), -// Sou(Rank), -// Wind(Wind), -// Dragon(Dragon), -// } - -// impl Suit { -// pub fn rank(&self) -> Option { -// match self { -// Suit::Man(rank) => Some(*rank), -// Suit::Pin(rank) => Some(*rank), -// Suit::Sou(rank) => Some(*rank), -// // Suit::Wind(wind) | Suit::Dragon(dragon) => None, -// _ => None, -// } -// } -// } - -// #[derive(Deref, DerefMut, Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, SpacetimeType)] -// pub struct Rank { -// pub number: u8, -// } - -// #[derive(FromRepr, Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, SpacetimeType)] -// pub enum Wind { -// Ton, -// Nan, -// Shaa, -// Pei, -// } - -// #[derive(Debug, FromRepr, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, SpacetimeType)] -// pub enum Dragon { -// Haku, -// Hatsu, -// Chun, -// } - #[derive(Component)] pub struct Dora; diff --git a/spacetimedb/Cargo.toml b/spacetimedb/Cargo.toml index 3a7227d..60070ef 100644 --- a/spacetimedb/Cargo.toml +++ b/spacetimedb/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jongline" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/spacetimedb/src/lib.rs b/spacetimedb/src/lib.rs index 8b21eb3..f8c1f9b 100644 --- a/spacetimedb/src/lib.rs +++ b/spacetimedb/src/lib.rs @@ -1,23 +1,45 @@ -use spacetimedb::{rand::seq::SliceRandom, reducer, table, Identity, ReducerContext, Table}; +use log::info; +use spacetimedb::{Identity, ReducerContext, Table, rand::seq::SliceRandom, reducer, table}; use jong_types::*; -#[table(name = player)] +#[derive(Debug)] +#[table(name = player, public)] pub struct Player { #[primary_key] identity: Identity, + #[auto_inc] + #[index(direct)] + #[unique] + id: u32, + name: Option, + + #[index(btree)] lobby_id: u32, } +#[table(name = bot)] +pub struct Bot { + #[primary_key] + #[auto_inc] + id: u32, + + #[index(btree)] + lobby_id: u32, +} + +#[derive(Debug)] #[table(name = lobby, public)] pub struct Lobby { #[primary_key] #[auto_inc] id: u32, - host: Identity, + #[index(direct)] + #[unique] + host_id: u32, } #[table(name = wall)] @@ -26,6 +48,10 @@ pub struct Wall { #[auto_inc] id: u32, + #[index(direct)] + #[unique] + lobby_id: u32, + tiles: Vec, } @@ -42,47 +68,75 @@ pub struct Pond { tiles: Vec, } -#[reducer] -pub fn add_player(ctx: &ReducerContext, name: Option) { - let identity = ctx.identity(); - ctx.db.player().insert(Player { - identity, - name, - lobby_id: 0, - }); -} +#[reducer(client_connected)] +pub fn login_or_add_player(ctx: &ReducerContext) { + let identity = ctx.sender; -#[reducer] -pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> { - if name.is_empty() { - return Err("names must not be empty".into()); - } - if let Some(player) = ctx.db.player().identity().find(ctx.sender) { - ctx.db.player().identity().update(Player { - name: Some(name), - ..player - }); - Ok(()) + // TODO remove player on disconnect + if let Ok(player) = ctx.db.player().try_insert(Player { + identity, + id: 0, + name: None, + lobby_id: 0, + }) { + info!("added player: {:?}", player); } else { - Err("Cannot set name for unknown user".into()) + let player = ctx.db.player().identity().find(identity).unwrap(); + info!("player {:?} has reconnected", player) } } #[reducer] pub fn join_or_create_lobby(ctx: &ReducerContext, lobby: Option) -> Result<(), String> { - let player = ctx.db.player().identity().find(ctx.sender).unwrap(); + let mut player = ctx + .db + .player() + .identity() + .find(ctx.sender) + .ok_or(format!("cannot find player {}", ctx.sender))?; - todo!() + let lobby_id = lobby.unwrap_or_else(|| { + let lobby = ctx.db.lobby().insert(Lobby { + id: 0, + host_id: player.id, + }); + info!("created lobby: {:?}", lobby); + + lobby.id + }); + + player.lobby_id = lobby_id; + + let player = ctx.db.player().identity().update(player); + + info!("player {} joined lobby {}", player.id, lobby_id); + Ok(()) } #[reducer] -pub fn insert_wall(ctx: &ReducerContext, player_ids: Vec) { - let tiles: Vec = tiles(); - ctx.db.wall().insert(Wall { id: 0, tiles }); +pub fn add_bot(ctx: &ReducerContext, lobby_id: u32) -> Result<(), String> { + if lobby_id == 0 { + Err("cannot add a bot without a lobby".into()) + } else if let Some(lobby) = ctx.db.lobby().id().find(lobby_id) + && (ctx.db.player().lobby_id().filter(lobby_id).count() + + ctx.db.bot().lobby_id().filter(lobby_id).count() + <= 4) + { + let bot = ctx.db.bot().insert(Bot { id: 0, lobby_id }); + info!("added bot {} to lobby {}", bot.id, lobby_id); + Ok(()) + } else { + Err("lobby doesn't exist".into()) + } } #[reducer] pub fn shuffle_wall(ctx: &ReducerContext) { + // if let Some(wall) = + + // let mut rng = ctx.rng(); + // let mut wall = tiles(); + // wall.shuffle(&mut rng); todo!() } @@ -123,3 +177,19 @@ pub fn sort_hand(ctx: &ReducerContext) { // } // log::info!("Hello, World!"); // } + +// #[reducer] +// pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> { +// if name.is_empty() { +// return Err("names must not be empty".into()); +// } +// if let Some(player) = ctx.db.player().identity().find(ctx.sender) { +// ctx.db.player().identity().update(Player { +// name: Some(name), +// ..player +// }); +// Ok(()) +// } else { +// Err("cannot set name for unknown user".into()) +// } +// }