cont... some query invariant isn't upheld once enter lobby, crashes

This commit is contained in:
Tao Tien 2026-02-24 08:42:23 -08:00
parent cc07cc89c6
commit 550ae73287
5 changed files with 243 additions and 136 deletions

View file

@ -140,36 +140,37 @@ pub struct HandView {
#[view(name = view_closed_hands, public)] #[view(name = view_closed_hands, public)]
fn view_closed_hands(ctx: &ViewContext) -> Vec<HandView> { fn view_closed_hands(ctx: &ViewContext) -> Vec<HandView> {
let this_player = ctx.db.player().identity().find(ctx.sender).unwrap(); let this_player = ctx.db.player().identity().find(ctx.sender).unwrap();
ctx.db if let Some(lobby) = ctx.db.lobby().id().find(this_player.lobby_id) {
.lobby() lobby
.id()
.find(this_player.lobby_id)
.unwrap()
.players .players
.iter() .iter()
.filter_map(|&player| { .filter_map(|&player| match player {
let (hand_length, drawn) = match player {
PlayerOrBot::Player { id } => { PlayerOrBot::Player { id } => {
let player_hand = ctx.db.player_hand().player_id().find(id).unwrap(); if let Some(player_hand) = ctx.db.player_hand().player_id().find(id) {
(
player_hand.hand.len() as u8,
player_hand.turn_state == TurnState::Tsumo
&& player_hand.working_tile.is_some(),
)
}
PlayerOrBot::Bot { id } => {
let bot = ctx.db.bot().id().find(id).unwrap();
(
bot.hand.len() as u8,
bot.turn_state == TurnState::Tsumo && bot.working_tile.is_some(),
)
}
};
Some(HandView { Some(HandView {
player, player,
hand_length, hand_length: player_hand.hand.len() as u8,
drawn, drawn: player_hand.turn_state == TurnState::Tsumo
&& player_hand.working_tile.is_some(),
}) })
} else {
None
}
}
PlayerOrBot::Bot { id } => {
if let Some(bot) = ctx.db.bot().id().find(id) {
Some(HandView {
player,
hand_length: bot.hand.len() as u8,
drawn: bot.turn_state == TurnState::Tsumo && bot.working_tile.is_some(),
})
} else {
None
}
}
}) })
.collect() .collect()
} else {
vec![]
}
} }

View file

@ -1,3 +1,4 @@
use bevy::platform::collections::HashMap;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_spacetimedb::{ use bevy_spacetimedb::{
ReadInsertUpdateMessage, ReadStdbConnectedMessage, ReadStdbDisconnectedMessage, ReadInsertUpdateMessage, ReadStdbConnectedMessage, ReadStdbDisconnectedMessage,
@ -5,8 +6,8 @@ use bevy_spacetimedb::{
}; };
use jong_db::{ use jong_db::{
self, DbConnection, LobbyTableAccess, PlayerHand, PlayerTableAccess, RemoteTables, self, BotTableAccess, DbConnection, LobbyTableAccess, PlayerHand, PlayerTableAccess,
ViewClosedHandsTableAccess, ViewHandTableAccess as _, RemoteTables, ViewClosedHandsTableAccess, ViewHandTableAccess as _,
}; };
use jong_db::{add_bot, set_ready}; use jong_db::{add_bot, set_ready};
use jong_types::*; use jong_types::*;
@ -88,10 +89,7 @@ fn subscriptions(stdb: SpacetimeDB, mut commands: Commands) {
.on_error(|_, err| error!("sub failed: {err}")) .on_error(|_, err| error!("sub failed: {err}"))
.subscribe([ .subscribe([
// TODO change these to sub/unsub based on being in lobby and some such // TODO change these to sub/unsub based on being in lobby and some such
format!( "SELECT p.* FROM player p JOIN lobby l ON p.lobby_id = l.id".to_string(),
"SELECT * FROM player p WHERE p.identity = '{}'",
stdb.identity()
),
"SELECT l.* FROM lobby l JOIN player p ON l.id = p.lobby_id".to_string(), "SELECT l.* FROM lobby l JOIN player p ON l.id = p.lobby_id".to_string(),
"SELECT c.* FROM player_clock c JOIN player p ON c.player_id = p.id".to_string(), "SELECT c.* FROM player_clock c JOIN player p ON c.player_id = p.id".to_string(),
"SELECT b.* FROM bot b JOIN lobby l ON l.id = b.lobby_id".to_string(), "SELECT b.* FROM bot b JOIN lobby l ON l.id = b.lobby_id".to_string(),
@ -104,24 +102,55 @@ fn subscriptions(stdb: SpacetimeDB, mut commands: Commands) {
} }
} }
/// restores (spawns) entities to be consistent with server state /// spawns entities to be consistent with server state
// TODO figure out a way to call this for later changes in the various on_ins_upd systems
fn on_subscribed( fn on_subscribed(
_event: On<Subscribed>, _event: On<Subscribed>,
stdb: SpacetimeDB, stdb: SpacetimeDB,
mut commands: Commands, mut commands: Commands,
mut next_gamestate: ResMut<NextState<jong_types::states::GameState>>, mut next_gamestate: ResMut<NextState<GameState>>,
mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>, mut next_turnstate: ResMut<NextState<TurnState>>,
) { ) {
if let Some(player) = stdb.db().player().iter().next() {} while let Some(player) = stdb.db().player().iter().next() {
if player.identity == stdb.identity() {
spawn_main_player(&stdb, &mut commands, &mut next_turnstate, &player);
} else {
spawn_other_player(&stdb, &mut commands, &player);
}
}
while let Some(bot) = stdb.db().bot().iter().next() {
let id = PlayerOrBot::Bot { id: bot.id };
let hand_view = stdb
.db()
.view_closed_hands()
.iter()
.find(|v| PlayerOrBot::from(&v.player) == id)
.unwrap();
let hand_ent = commands.spawn((Hand, Closed(hand_view.hand_length))).id();
commands.spawn(Player { id }).add_child(hand_ent);
}
if let Some(lobby) = stdb.db().lobby().iter().next() { if let Some(lobby) = stdb.db().lobby().iter().next() {
next_gamestate.set(lobby.game_state.into()); next_gamestate.set(lobby.game_state.into());
} }
}
let hand_ent = commands.spawn(Hand).id(); fn spawn_main_player(
let pond_ent = commands.spawn(Pond).id(); stdb: &SpacetimeDB,
commands: &mut Commands,
next_turnstate: &mut ResMut<NextState<TurnState>>,
player: &jong_db::Player,
) {
let main_player = commands
.spawn((
Player {
id: PlayerOrBot::Player { id: player.id },
},
MainPlayer,
))
.id();
if let Some(player_hand) = stdb.db().view_hand().iter().next() { if let Some(player_hand) = stdb.db().view_hand().iter().next() {
let hand_tiles: Vec<_> = player_hand let hand_tiles: Vec<_> = player_hand
.hand .hand
@ -133,83 +162,32 @@ fn on_subscribed(
.iter() .iter()
.map(|dbt| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) .map(|dbt| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id())
.collect(); .collect();
commands.entity(pond_ent).add_children(&pond_tiles); let pond = commands.spawn(Hand).add_children(&pond_tiles).id();
commands.entity(hand_ent).add_children(&hand_tiles); let hand = commands.spawn(Pond).add_children(&hand_tiles).id();
commands.entity(main_player).add_children(&[pond, hand]);
if player_hand.turn_state == jong_db::TurnState::Tsumo if player_hand.turn_state == jong_db::TurnState::Tsumo
&& let Some(drawn_dbt) = player_hand.working_tile && let Some(drawn_dbt) = player_hand.working_tile
{ {
commands.spawn((Drawn, Tile::from(&drawn_dbt.tile), TileId(drawn_dbt.id))); let drawn = commands
.spawn((Drawn, Tile::from(&drawn_dbt.tile), TileId(drawn_dbt.id)))
.id();
commands.entity(main_player).add_child(drawn);
} }
next_turnstate.set(player_hand.turn_state.into()); next_turnstate.set(player_hand.turn_state.into());
} }
} }
fn on_view_hand_update( fn spawn_other_player(stdb: &SpacetimeDB, commands: &mut Commands, player: &jong_db::Player) {
stdb: SpacetimeDB, let id = PlayerOrBot::Player { id: player.id };
mut messages: ReadUpdateMessage<jong_db::PlayerHand>, let hand_view = stdb
.db()
mut commands: Commands, .view_closed_hands()
hand: Single<Entity, With<Hand>>,
pond: Single<Entity, With<Pond>>,
// drawn: Option<Single<Entity, With<Drawn>>>,
tiles: Query<(Entity, &TileId)>,
mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>,
) {
// trace!("on_view_hand_update");
// TODO can this and similar run at startup or on play/reconnect?
for msg in messages.read() {
trace!("new hand: {:?}", msg.new);
let hand_tiles: Vec<_> = msg
.new
.hand
.iter() .iter()
.map(|dbt| { .find(|v| PlayerOrBot::from(&v.player) == id)
tiles .unwrap();
.iter() let hand_ent = commands.spawn((Hand, Closed(hand_view.hand_length))).id();
.find_map(|(e, t)| if *t == TileId(dbt.id) { Some(e) } else { None }) commands.spawn(Player { id }).add_child(hand_ent);
.unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id())
})
.collect();
commands.entity(*hand).replace_children(&hand_tiles);
let pond_tiles: Vec<_> = msg
.new
.pond
.iter()
.map(|dbt| {
tiles
.iter()
.find_map(|(e, t)| if *t == TileId(dbt.id) { Some(e) } else { None })
.unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id())
})
.collect();
commands.entity(*pond).replace_children(&pond_tiles);
match msg.new.turn_state {
jong_db::TurnState::None => {
trace!("turnstate none");
// TODO do we reconcile hand state here or in ::End?
}
jong_db::TurnState::Tsumo => {
trace!("turnstate tsumo");
let dbt = msg
.new
.working_tile
.as_ref()
.expect("entered tsumo without a drawn tile");
commands.spawn((Drawn, Tile::from(&dbt.tile), TileId(dbt.id)));
}
jong_db::TurnState::Menzen => todo!(),
jong_db::TurnState::RiichiKan => todo!(),
jong_db::TurnState::RonChiiPonKan => todo!(),
jong_db::TurnState::End => todo!(),
}
next_turnstate.set(msg.new.turn_state.into());
}
} }
fn on_player_insert_update( fn on_player_insert_update(
@ -218,18 +196,26 @@ fn on_player_insert_update(
mut commands: Commands, mut commands: Commands,
hand: Option<Single<Entity, With<Hand>>>, main_player: Option<Single<&MainPlayer>>,
pond: Option<Single<Entity, With<Pond>>>, other_players: Query<&Player>,
) {
// TODO this should be startup
if hand.is_none() {
commands.spawn(Hand);
}
if pond.is_none() {
commands.spawn(Pond);
}
for msg in messages.read() {} mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>,
) {
for msg in messages.read() {
if main_player.is_none() && msg.new.identity == stdb.identity() {
spawn_main_player(&stdb, &mut commands, &mut next_turnstate, &msg.new);
} else if other_players.iter().any(|p| {
if let PlayerOrBot::Player { id } = &p.id {
*id == msg.new.id
} else {
false
}
}) {
spawn_other_player(&stdb, &mut commands, &msg.new);
} else {
// TODO update case
}
}
} }
fn on_lobby_insert_update( fn on_lobby_insert_update(
@ -280,3 +266,77 @@ fn on_lobby_insert_update(
next_gamestate.set(msg.new.game_state.into()); next_gamestate.set(msg.new.game_state.into());
} }
} }
fn on_view_hand_update(
stdb: SpacetimeDB,
mut messages: ReadUpdateMessage<jong_db::PlayerHand>,
mut commands: Commands,
tiles: Query<(Entity, &TileId)>,
main_player: Single<&Children, With<MainPlayer>>,
hand: Query<Entity, With<Hand>>,
pond: Query<Entity, With<Pond>>,
// drawn: Option<Single<Entity, With<Drawn>>>,
mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>,
) {
trace!("on_view_hand_update");
// TODO can this and similar run at startup or on play/reconnect?
for msg in messages.read() {
trace!("new hand: {:?}", msg.new);
let hand_tiles: Vec<_> = msg
.new
.hand
.iter()
.map(|dbt| {
tiles
.iter()
.find_map(|(e, t)| if *t == TileId(dbt.id) { Some(e) } else { None })
.unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id())
})
.collect();
commands
.entity(hand.iter().find(|e| main_player.contains(e)).unwrap())
.replace_children(&hand_tiles);
let pond_tiles: Vec<_> = msg
.new
.pond
.iter()
.map(|dbt| {
tiles
.iter()
.find_map(|(e, t)| if *t == TileId(dbt.id) { Some(e) } else { None })
.unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id())
})
.collect();
commands
.entity(pond.iter().find(|e| main_player.contains(e)).unwrap())
.replace_children(&pond_tiles);
match msg.new.turn_state {
jong_db::TurnState::None => {
trace!("turnstate none");
// TODO do we reconcile hand state here or in ::End?
}
jong_db::TurnState::Tsumo => {
trace!("turnstate tsumo");
let dbt = msg
.new
.working_tile
.as_ref()
.expect("entered tsumo without a drawn tile");
commands.spawn((Drawn, Tile::from(&dbt.tile), TileId(dbt.id)));
}
jong_db::TurnState::Menzen => todo!(),
jong_db::TurnState::RiichiKan => todo!(),
jong_db::TurnState::RonChiiPonKan => todo!(),
jong_db::TurnState::End => todo!(),
}
next_turnstate.set(msg.new.turn_state.into());
}
}

View file

@ -1,7 +1,11 @@
use bevy::prelude::*; use bevy::prelude::*;
use jong_types::PlayerOrBot;
#[derive(Component)] #[derive(Component)]
pub struct Player; pub struct Player {
pub(crate) id: PlayerOrBot,
}
#[derive(Component)] #[derive(Component)]
pub struct MainPlayer; pub struct MainPlayer;
@ -15,6 +19,9 @@ pub struct TileId(pub u32);
#[derive(Component)] #[derive(Component)]
pub struct Hand; pub struct Hand;
#[derive(Component)]
pub struct Closed(pub(crate) u8);
#[derive(Component)] #[derive(Component)]
pub struct Pond; pub struct Pond;

View file

@ -85,12 +85,10 @@ impl Plugin for TuiPlugin {
.add_systems( .add_systems(
Update, Update,
( (
( (render::render_main_hand, render::render_main_pond)
render::render_hand,
render::render_pond,
)
.run_if(in_state(GameState::Play)), .run_if(in_state(GameState::Play)),
render::render, render::render,
query_tester,
) )
.chain() .chain()
.in_set(TuiSet::Render), .in_set(TuiSet::Render),
@ -98,11 +96,31 @@ impl Plugin for TuiPlugin {
} }
} }
fn query_tester(
main_player: Single<&Children, With<MainPlayer>>,
// hand: Query<(&Children, Entity), With<Hand>>,
) {
trace!("owo");
// let hand = hand
// .iter()
// .find_map(|(c, e)| {
// if main_player.contains(&e) {
// Some(c)
// } else {
// None
// }
// })
// .unwrap();
// debug!("{hand:?}");
}
fn discard_tile( fn discard_tile(
stdb: SpacetimeDB, stdb: SpacetimeDB,
mut commands: Commands, mut commands: Commands,
mut selected: MessageReader<ConfirmSelect>, mut selected: MessageReader<ConfirmSelect>,
// main_player: Single<&Children, With<MainPlayer>>,
// only main player will have a Drawn tile?
drawn: Single<(Entity, &TileId), With<Drawn>>, drawn: Single<(Entity, &TileId), With<Drawn>>,
tiles: Query<&TileId>, tiles: Query<&TileId>,
) { ) {

View file

@ -95,23 +95,33 @@ pub(crate) fn render(
// FIXME we don't care about other players atm // FIXME we don't care about other players atm
#[allow(clippy::too_many_arguments, clippy::type_complexity)] #[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(crate) fn render_hand( pub(crate) fn render_main_hand(
mut commands: Commands,
mut tui: ResMut<RatatuiContext>, mut tui: ResMut<RatatuiContext>,
hovered: Query<Entity, With<Hovered>>,
layouts: Res<HandLayouts>, layouts: Res<HandLayouts>,
mut commands: Commands,
tiles: Query<&jong_types::Tile>, tiles: Query<&jong_types::Tile>,
// main_player: Single<(&Player, Entity /* , &Wind */), With<MainPlayer>>, hovered: Query<Entity, With<Hovered>>,
hand: Single<(&Children, Entity), With<Hand>>,
main_player: Single<&Children, With<MainPlayer>>,
hand: Query<(&Children, Entity), With<Hand>>,
drawn_tile: Option<Single<Entity, With<Drawn>>>, drawn_tile: Option<Single<Entity, With<Drawn>>>,
) -> Result { ) -> Result {
let mut frame = tui.get_frame(); let mut frame = tui.get_frame();
debug_blocks(layouts.clone(), &mut frame); debug_blocks(layouts.clone(), &mut frame);
let hand: Vec<_> = hand let hand: Vec<_> = hand
.0 .iter()
.find_map(|(c, e)| {
if main_player.contains(&e) {
Some(c)
} else {
None
}
})
.unwrap()
.iter() .iter()
.map(|entity| -> Result<_> { .map(|entity| -> Result<_> {
let tile = tiles.get(entity).unwrap_or_else(|_| panic!("{entity:?}")); let tile = tiles.get(entity).unwrap_or_else(|_| panic!("{entity:?}"));
@ -196,20 +206,31 @@ pub(crate) fn render_hand(
Ok(()) Ok(())
} }
pub(crate) fn render_pond( pub(crate) fn render_main_pond(
mut commands: Commands,
mut tui: ResMut<RatatuiContext>, mut tui: ResMut<RatatuiContext>,
hovered: Query<Entity, With<Hovered>>,
layouts: Res<HandLayouts>, layouts: Res<HandLayouts>,
pond: Single<(&Children, Entity), With<Pond>>, mut commands: Commands,
tiles: Query<&Tile>, tiles: Query<&Tile>,
hovered: Query<Entity, With<Hovered>>,
main_player: Single<&Children, With<MainPlayer>>,
pond: Query<(&Children, Entity), With<Pond>>,
) -> Result { ) -> Result {
let mut frame = tui.get_frame(); let mut frame = tui.get_frame();
let pond: Vec<_> = pond let pond: Vec<_> = pond
.0 .iter()
.find_map(|(c, e)| {
if main_player.contains(&e) {
Some(c)
} else {
None
}
})
.unwrap()
.iter() .iter()
.map(|entity| -> Result<_> { .map(|entity| -> Result<_> {
let tile = tiles.get(entity).unwrap(); let tile = tiles.get(entity).unwrap();