Implement buddy warping#12
Conversation
yungcomputerchair
left a comment
There was a problem hiding this comment.
Solid work; I'm pretty sure this won't work if they're in different channels right now but that should be an easy fix. I'm also not sure if P_FE2CL_REP_PC_BUDDY_WARP_SAME_SHARD_SUCC needs to be in the mix, I'll get back to you on that
| } | ||
|
|
||
| let buddy_id = res.unwrap(); | ||
| let buddy: &rusty_fusion::entity::Player = state.get_player(buddy_id)?; |
There was a problem hiding this comment.
Can omit the type
| let buddy: &rusty_fusion::entity::Player = state.get_player(buddy_id)?; | |
| let buddy = state.get_player(buddy_id)?; |
| if buddy.instance_id.map_num != 0 { | ||
| let response = sP_FE2CL_REP_PC_BUDDY_WARP_FAIL { | ||
| iBuddyPCUID: buddy_uid, | ||
| iErrorCode: 1, // TODO: define error codes |
There was a problem hiding this comment.
I'll reach out to you offline about how to define error codes based on the client code
| log_if_failed( | ||
| clients | ||
| .get_self() | ||
| .send_packet(P_FE2CL_REP_PC_GOTO_SUCC, &resp), |
There was a problem hiding this comment.
this probably works fine but I'm wondering if P_FE2CL_REP_PC_BUDDY_WARP_SAME_SHARD_SUCC is relevant to us at all
| log_if_failed(from_shard.send_packet(P_LS2FE_REP_BUDDY_WARP_FAIL, &fail_resp)); | ||
|
|
||
| return Ok(()); |
There was a problem hiding this comment.
Since communicating back to the shard is pretty integral to the handling of this packet, I would treat the failure to send the packet as an error:
| log_if_failed(from_shard.send_packet(P_LS2FE_REP_BUDDY_WARP_FAIL, &fail_resp)); | |
| return Ok(()); | |
| return from_shard.send_packet(P_LS2FE_REP_BUDDY_WARP_FAIL, &fail_resp); |
| log_if_failed(from_shard.send_packet(P_LS2FE_REP_BUDDY_WARP_SUCC, &resp)); | ||
|
|
||
| Ok(()) |
There was a problem hiding this comment.
see above
| log_if_failed(from_shard.send_packet(P_LS2FE_REP_BUDDY_WARP_SUCC, &resp)); | |
| Ok(()) | |
| from_shard.send_packet(P_LS2FE_REP_BUDDY_WARP_SUCC, &resp) |
| z: buddy_z, | ||
| }); | ||
|
|
||
| player.instance_id = rusty_fusion::chunk::InstanceID::default(); |
There was a problem hiding this comment.
see above, make sure to factor in channel ID here
| player.instance_id = rusty_fusion::chunk::InstanceID::default(); | ||
|
|
||
| state.entity_map.update( | ||
| rusty_fusion::entity::EntityID::Player(pc_id), |
There was a problem hiding this comment.
code style nit: import the struct and then do
| rusty_fusion::entity::EntityID::Player(pc_id), | |
| EntityID::Player(pc_id), |
| Some(clients), | ||
| ); | ||
|
|
||
| let warp_pkt: sP_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC = |
There was a problem hiding this comment.
code style nit: omit the type
| let warp_pkt: sP_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC = | |
| let warp_pkt = |
|
|
||
| if buddy_metadata.map_num != 0 { | ||
| log( | ||
| Severity::Warning, |
There was a problem hiding this comment.
this can probably be less important
| Severity::Warning, | |
| Severity::Info, |
| iChannelNum: if buddy_channel > 1 { | ||
| buddy_channel as i32 | ||
| } else { | ||
| 0 | ||
| }, |
There was a problem hiding this comment.
am I missing something or can you just do:
| iChannelNum: if buddy_channel > 1 { | |
| buddy_channel as i32 | |
| } else { | |
| 0 | |
| }, | |
| iChannelNum: buddy_channel as i32, |
8eb76c9 to
0c4caa7
Compare
yungcomputerchair
left a comment
There was a problem hiding this comment.
On top of the comments I left, I'm pretty confused on the packet flow here.
What's the purpose of the P_FE2LS_REQ_BUDDY_WARP packet? I don't see it being sent anywhere. I see the shard sending P_FE2LS_REQ_BUDDY_WARP_VALIDATION when the buddy is not in the same shard, but that's it.
I think the intuitive flow & naming here here looks more like:
- client sends
CL2FE_REQ_BUDDY_WARPto shard 1 - shard 1 sends
FE2LS_REQ_BUDDY_WARPto login - login sends
LS2FE_REQ_BUDDY_WARPto shard 2 - shard 2 sends
FE2LS_REP_BUDDY_WARP_SUCC/FAILto login - login sends
LS2FE_REP_BUDDY_WARP_SUCC/FAILto shard 1 - shard 1 sends
FE2CL_REP_BUDDY_WARP_OTHER_SHARD_SUCC/FAILto client
| let mut invalid_warp = |error_code: i32, msg: String| -> FFResult<()> { | ||
| let response = sP_FE2CL_REP_PC_BUDDY_WARP_FAIL { | ||
| iBuddyPCUID: buddy_uid, | ||
| iErrorCode: error_code, // "Warp failure!\nUh oh! %s is in a location that you can't warp to! Try again later." | ||
| }; | ||
| log_if_failed( | ||
| clients | ||
| .get_self() | ||
| .send_packet(P_FE2CL_REP_PC_BUDDY_WARP_FAIL, &response), | ||
| ); | ||
| Err(FFError::build(Severity::Info, msg)) | ||
| }; |
There was a problem hiding this comment.
Love this. Need more usage of closures like this in the codebase. Awesome
| let pc_id = client.get_player_id()?; | ||
| let player = state.get_player(pc_id)?; | ||
| let player_payzone_flag = player.flags.payzone_flag; | ||
| let player_skyway_state = player.skyway_ride.clone(); |
There was a problem hiding this comment.
I would strongly prefer if we didn't clone the skyway ride state here; I don't think it's necessary. You can probably do something like this instead:
| let player_skyway_state = player.skyway_ride.clone(); | |
| let is_player_on_skyway = player.skyway_ride.is_some(); |
| let buddy_id = res.unwrap(); | ||
| let buddy = state.get_player(buddy_id)?; | ||
| let buddy_payzone_flag = buddy.flags.payzone_flag; | ||
| let buddy_skyway_state = buddy.skyway_ride.clone(); |
There was a problem hiding this comment.
see above comment on cloning
|
|
||
| if player_skyway_state.is_some() { | ||
| return invalid_warp( | ||
| 3, |
There was a problem hiding this comment.
it would be great if you defined these error codes in error.rs
| player.instance_id = InstanceID { | ||
| map_num: ID_OVERWORLD, | ||
| channel_num: if player.instance_id.channel_num != buddy_instance.channel_num { | ||
| buddy_instance.channel_num | ||
| } else { | ||
| 0 | ||
| }, | ||
| instance_num: buddy_instance.instance_num, | ||
| }; |
There was a problem hiding this comment.
I don't think we want to set the channel number to 0. Should just copy the instance ID directly from the buddy.
We only want this to be 0 in the response packet, so the client interprets it as "same channel". But as for the actual instance ID, channel 0 is invalid.
| player.instance_id = InstanceID { | |
| map_num: ID_OVERWORLD, | |
| channel_num: if player.instance_id.channel_num != buddy_instance.channel_num { | |
| buddy_instance.channel_num | |
| } else { | |
| 0 | |
| }, | |
| instance_num: buddy_instance.instance_num, | |
| }; | |
| player.instance_id = buddy_instance; |
| iWarpReceiverX: receiver_pos.x, | ||
| iWarpReceiverY: receiver_pos.y, | ||
| iWarpReceiverZ: receiver_pos.z, | ||
| iWarpReceiverShardID: config_get().shard.shard_id.get(), |
| log_if_failed( | ||
| clients | ||
| .get_login_server() | ||
| .unwrap() | ||
| .send_packet(P_FE2LS_REP_BUDDY_WARP_VALIDATION_SUCC, &response), | ||
| ); | ||
|
|
||
| Ok(()) |
There was a problem hiding this comment.
I would treat the failure to send this packet back as an error:
| log_if_failed( | |
| clients | |
| .get_login_server() | |
| .unwrap() | |
| .send_packet(P_FE2LS_REP_BUDDY_WARP_VALIDATION_SUCC, &response), | |
| ); | |
| Ok(()) | |
| clients | |
| .get_login_server() | |
| .unwrap() | |
| .send_packet(P_FE2LS_REP_BUDDY_WARP_VALIDATION_SUCC, &response) |
| player.instance_id = InstanceID { | ||
| map_num: ID_OVERWORLD, | ||
| channel_num: if receiver_channel_num as u8 != player_channel_num { | ||
| receiver_channel_num as u8 | ||
| } else { | ||
| 0 | ||
| }, | ||
| instance_num: if receiver_instance_num > 0 { | ||
| Some(receiver_instance_num as u32) | ||
| } else { | ||
| None | ||
| }, | ||
| }; |
There was a problem hiding this comment.
No need to set the instance ID since it's not preserved in the database anyway
| let receiver_z = pkt.iWarpReceiverZ; | ||
| let receiver_shard_id = pkt.iWarpReceiverShardID; | ||
| let receiver_channel_num = pkt.iWarpReceiverChannelNum; | ||
| let _receiver_map_num = pkt.iWarpReceiverMapNum; |
There was a problem hiding this comment.
Is this field unused? If so should remove it from the packet
| // has to be sent to prevent teleporting to "unknown" map | ||
| let resp = sP_FE2CL_REP_PC_GOTO_SUCC { | ||
| iX: receiver_x, | ||
| iY: receiver_y, | ||
| iZ: receiver_z, | ||
| }; |
There was a problem hiding this comment.
I really don't understand why we need to do this...
I would think that since the client is connecting to the other shard through the normal login flow, it should pick up the position from the DB. Maybe flush the player to the DB with db_run_sync() and see if this is still needed?
yungcomputerchair
left a comment
There was a problem hiding this comment.
This code is extremely clean and most of my (very few) comments are nits.
The only reason I'm flagging this for changes is that the buddy warp timer does not seem to be set serverside upon a successful warp, and the check for it is implemented incorrectly.
If you could do that (bonus points if you address the rest of the comment) then I will approve :)
|
|
||
| if buddy_is_on_skyway { | ||
| return invalid_warp(format!( | ||
| "Player {} is currently on a skyway ride", |
There was a problem hiding this comment.
| "Player {} is currently on a skyway ride", | |
| "Buddy {} is currently on a skyway ride", |
|
|
||
| if buddy_is_on_skyway { | ||
| return invalid_warp(format!( | ||
| "Player {} is currently on a skyway ride", |
There was a problem hiding this comment.
| "Player {} is currently on a skyway ride", | |
| "Buddy {} is currently on a skyway ride", |
|
|
||
| let other_shard_succ_pkt = sP_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC { | ||
| iBuddyPCUID: pkt.iBuddyPCUID, | ||
| iChannelNum: 0, |
There was a problem hiding this comment.
Should leave a comment here explaining why we always send back 0 in this case
| } | ||
|
|
||
| pub fn is_warp_on_cooldown(&self) -> bool { | ||
| self.buddy_warp_time > 0 |
There was a problem hiding this comment.
This is a timestamp of the last buddy warp, so really this will be something like self.buddy_warp_time > 0 && self.buddy_warp_time - util::get_timestamp_sec() < BUDDYWARP_INTERVAL.
Or even better, if you could make self.buddy_warp_time an Option<usize> which is initially None but then gets set to Some(timestamp) on warp.
Then this function can look like:
| self.buddy_warp_time > 0 | |
| self.buddy_warp_time.is_some_and(|ts| util::get_timestamp() - ts < BUDDYWARP_INTERVAL) |
(BUDDYWARP_INTERVAL can be imported from defines.rs)
There was a problem hiding this comment.
Even better yet if you could rename the field to something less confusing like last_buddy_warp_timestamp
| }); | ||
| let player_saved = player.clone(); | ||
|
|
||
| log_if_failed(db_run_sync(move |db| db.save_player(&player_saved))); |
There was a problem hiding this comment.
removed db save, the position bug wasn’t caused by not saving to the database. the real issue was that instancenum was being copied from the buddy making it fall into the condition of some, causing the save_player_internal condition to read the prewarp position upon saving instead of the actual player position.
There was a problem hiding this comment.
It's fine to remove it from the same-shard warp case, but we still need it for the cross-shard.
The reason is because there could potentially be a race condition: the processing of the client's disconnect might not finish before the client connects to the other shard.
Flushing the player to DB before we send the success packet guarantees that the position will be correct before the client even thinks about connecting to the other shard.
yungcomputerchair
left a comment
There was a problem hiding this comment.
Basically there but I'm too worried about the cross-shard race condition. Bring back the DB flush for the cross-shard case and we're good
| iBuddyWarpTime: self.last_buddy_warp_timestamp.map_or(0, |ts| { | ||
| let elapsed = util::get_timestamp_sec(SystemTime::now()).saturating_sub(ts); | ||
| if elapsed < BUDDYWARP_INTERVAL { | ||
| // client expects the absolute timestamp when the cooldown ends | ||
| (ts + BUDDYWARP_INTERVAL) as i32 | ||
| } else { | ||
| 0 | ||
| } | ||
| }), |
There was a problem hiding this comment.
no need to do this calculation since the client does its own math
| iBuddyWarpTime: self.last_buddy_warp_timestamp.map_or(0, |ts| { | |
| let elapsed = util::get_timestamp_sec(SystemTime::now()).saturating_sub(ts); | |
| if elapsed < BUDDYWARP_INTERVAL { | |
| // client expects the absolute timestamp when the cooldown ends | |
| (ts + BUDDYWARP_INTERVAL) as i32 | |
| } else { | |
| 0 | |
| } | |
| }), | |
| iBuddyWarpTime: self.last_buddy_warp_timestamp.unwrap_or(0), |
| }); | ||
| let player_saved = player.clone(); | ||
|
|
||
| log_if_failed(db_run_sync(move |db| db.save_player(&player_saved))); |
There was a problem hiding this comment.
It's fine to remove it from the same-shard warp case, but we still need it for the cross-shard.
The reason is because there could potentially be a race condition: the processing of the client's disconnect might not finish before the client connects to the other shard.
Flushing the player to DB before we send the success packet guarantees that the position will be correct before the client even thinks about connecting to the other shard.
when teleporting across shards, it obtains the position of the target buddy, sets the player position to the buddy position and then lastly sends the P_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC packet for the client to swap shards