-
Notifications
You must be signed in to change notification settings - Fork 47
Feature: Add Max Players Game Setting and Fix Join Logic #478
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Feature: Add Max Players Game Setting and Fix Join Logic #478
Conversation
|
@roberthsheng wow, this is a cool set of changes. Thank you for working on it! Out of curiosity, are public rooms actually being used / hitting max capacity? I'll take a moment to properly review later, but if you get the linters to pass I can throw up the build on shengji-beta so you can playtest for a bit? |
backend/src/shengji_handler.rs
Outdated
| let logger_ = logger_.new(o!("player_id" => player_id.0)); | ||
| debug!(logger_, "Received player ID"); | ||
| if let Ok(player_id_option) = subscribe_player_id_rx.await { | ||
| let logger_ = logger_.new(o!("player_id" => format!("{:?}", player_id_option.map(|p|p.0)))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can just use dbg! here?
| .map(|state| GameMessage::State { state }) | ||
| } else { | ||
| Some(v) | ||
| // Only filter state if we have a valid player ID |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the player stay connected if they didn't get a player ID? that means that the room is at capacity, right? can we just close the connection and ask the player to reconnect in the frontend?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way I understand it we close the connection fairly quickly now when registration fails. The flow goes
1. register_user returns an Err when the room is full
2. handle_user_connected catches this Err
3. Instead of returning Ok(()), we have handle_user_connected propagate the Err
4. The top-level entrypoint function receives this Err, logs it, and the task for that specific connection terminates
I think the player_subscribe_task might stay alive for a very short time after the error is sent by execute_operation until the oneshot channel signals failure (with None), but the main handler that would process further user messages (run_game_for_player) is never started, and the overall connection context is torn down due to the error in entrypoint.
and so in doing so the "game full" error message is reliably sent before the connection closes completely, which helped me avoid an issue where the connection closure raced the error message delivery. This way, I think explicit frontend reconnect prompts should no longer be necessary - lmk your thoughts on this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, got it. I understand what you're trying to do now
backend/src/shengji_handler.rs
Outdated
| } | ||
| UserMessage::Action(action) => { | ||
| execute_operation( | ||
| let action_clone_for_log = action.clone(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I usually just call these action_ and logger_
backend/src/shengji_handler.rs
Outdated
| ) { | ||
| execute_operation( | ||
| let room_name = room.as_bytes().to_vec(); | ||
| let room_name_str = String::from_utf8_lossy(&room_name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm, what's going on here? room is a String so it should be UTF-8 already right?
It looks like on 458 we just need an &str, so perhaps revert this bit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes my bad! Reverting
backend/src/utils.rs
Outdated
| Ok(new_version) => { | ||
| match backend_storage.clone().get(room_name_.clone()).await { | ||
| Ok(updated_versioned_game) => { | ||
| if updated_versioned_game.monotonic_id == new_version { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my brain isn't awake enough to think about whether this part is correct but curious why this needed to change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Calling execute_operation_with_messages runs the game logic and updates the state atomically in storage-> returns new version number if successful
- After return Ok new version, we do a separate backend_storage.get(...) to get the newly updated state
- The
if updated_versioned_game.monotonic_id == new_versioncheck verifies that the state we fetched from the get is actually the state corresponding to new_version that was just successfully written.
I just added this as a defense against possible (though unlikely I think with current storage implementations) race conditions or eventual consistency issues where the get might momentarily return a slightly older state immediately after the atomic update completes, just to make sure we reflect the immediate result of initiating client action
Erring to the side of safety if this is hand wave-y I can change it back around
| .arg("states_created") | ||
| .ignore() | ||
| .query_async(connection_manager) | ||
| .query_async::<_, ()>(connection_manager) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks! though tbh prod is still using a hashmap, it works fine hahaha
|
re: are public rooms actually being used / hitting max capacity? definitely were being used a lot more a few years back. the main way my friends and I use the feature now is actually so we don't have to send the link to each other and just join the game by clicking from the home page. im hoping shengji spikes back up in popularity and a max player cap would actually be put to good use one day :') |
rbtying
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One larger comment on the eager read after write - I don't think it's necessary as a result of your other changes. But let me see if I can get this to a point where it's on -beta!
| .map(|state| GameMessage::State { state }) | ||
| } else { | ||
| Some(v) | ||
| // Only filter state if we have a valid player ID |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, got it. I understand what you're trying to do now
| .await; | ||
| .await | ||
| { | ||
| error!(logger, "Beep operation failed"; "error" => format!("{:?}", e)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cargo clippy will probably ask you to dbg this one too
| .await; | ||
| Ok(()) | ||
| } else { | ||
| let err_msg = format!("Operation succeeded but version mismatch after fetching state (expected {}, got {})", new_version, updated_versioned_game.monotonic_id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I think it's a correct/valid state as part of the protocol for a new version to come back with a higher monotonic ID than the one we tried to write - that means that we raced with another change, but the current state is still overall correct. I'm not sure that we should error in such a case.
It's also I think ~fine for the state in this process to fall out of date because any writes require the latest monotonic ID - do we need the optimistic read?
|
@roberthsheng it looks like you made the "mistake" of upgrading my ancient frontend deps - I'm trying to split that out here #479 |
Thanks for making shengji on web - I've spent countless hours on this site :-)
Closes #424
Summary:
This pull request implements a new game setting that allows the host to set a maximum player limit for a game room. It also addresses several bugs related to the player joining process, particularly when a game lobby is full.
Changes:
Max Players Feature:
max_playersoption within the game's configuration (core/src/settings.rs).frontend/src/Initialize.tsx) for hosts to adjust the limit.frontend/src/PublicRoomsPane.tsx) to display both the current number of players and the maximum limit, if set (e.g., "5/8"). The backend API (backend/src/state_dump.rs) was updated accordingly.Limit Enforcement:
add_player,make_playerincore/src/settings.rs) to prevent players or observers from joining if themax_playerslimit has been reached.Bug Fixes:
backend/src/shengji_handler.rs,backend/src/utils.rs).backend/src/utils.rs,frontend/src/websocketHandler.ts).frontend/src/PublicRoomsPane.tsx,frontend/src/JoinRoom.tsx).Code Maintenance:
storage/src/redis_storage.rsby specifying explicit types, improving future compatibility.Testing:
Manual testing was performed throughout development to verify: