Skip to content

Implement buddy warping#12

Merged
yungcomputerchair merged 8 commits into
yungcomputerchair:mainfrom
lwcasgc:buddies
Nov 12, 2025
Merged

Implement buddy warping#12
yungcomputerchair merged 8 commits into
yungcomputerchair:mainfrom
lwcasgc:buddies

Conversation

@lwcasgc

@lwcasgc lwcasgc commented Nov 8, 2025

Copy link
Copy Markdown
Contributor

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

@yungcomputerchair yungcomputerchair left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/bin/shard/buddy.rs Outdated
}

let buddy_id = res.unwrap();
let buddy: &rusty_fusion::entity::Player = state.get_player(buddy_id)?;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can omit the type

Suggested change
let buddy: &rusty_fusion::entity::Player = state.get_player(buddy_id)?;
let buddy = state.get_player(buddy_id)?;

Comment thread src/bin/shard/buddy.rs Outdated
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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll reach out to you offline about how to define error codes based on the client code

Comment thread src/bin/shard/buddy.rs Outdated
log_if_failed(
clients
.get_self()
.send_packet(P_FE2CL_REP_PC_GOTO_SUCC, &resp),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this probably works fine but I'm wondering if P_FE2CL_REP_PC_BUDDY_WARP_SAME_SHARD_SUCC is relevant to us at all

Comment thread src/bin/login/shard.rs Outdated
Comment on lines +610 to +612
log_if_failed(from_shard.send_packet(P_LS2FE_REP_BUDDY_WARP_FAIL, &fail_resp));

return Ok(());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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);

Comment thread src/bin/login/shard.rs Outdated
Comment on lines +626 to +628
log_if_failed(from_shard.send_packet(P_LS2FE_REP_BUDDY_WARP_SUCC, &resp));

Ok(())

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

Suggested change
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)

Comment thread src/bin/shard/login.rs Outdated
z: buddy_z,
});

player.instance_id = rusty_fusion::chunk::InstanceID::default();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above, make sure to factor in channel ID here

Comment thread src/bin/shard/login.rs Outdated
player.instance_id = rusty_fusion::chunk::InstanceID::default();

state.entity_map.update(
rusty_fusion::entity::EntityID::Player(pc_id),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code style nit: import the struct and then do

Suggested change
rusty_fusion::entity::EntityID::Player(pc_id),
EntityID::Player(pc_id),

Comment thread src/bin/shard/login.rs Outdated
Some(clients),
);

let warp_pkt: sP_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC =

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code style nit: omit the type

Suggested change
let warp_pkt: sP_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC =
let warp_pkt =

Comment thread src/bin/login/shard.rs Outdated

if buddy_metadata.map_num != 0 {
log(
Severity::Warning,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can probably be less important

Suggested change
Severity::Warning,
Severity::Info,

Comment thread src/bin/shard/login.rs Outdated
Comment on lines +486 to +490
iChannelNum: if buddy_channel > 1 {
buddy_channel as i32
} else {
0
},

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

am I missing something or can you just do:

Suggested change
iChannelNum: if buddy_channel > 1 {
buddy_channel as i32
} else {
0
},
iChannelNum: buddy_channel as i32,

@lwcasgc
lwcasgc force-pushed the buddies branch 2 times, most recently from 8eb76c9 to 0c4caa7 Compare November 9, 2025 15:46
@lwcasgc
lwcasgc marked this pull request as draft November 9, 2025 15:48
@lwcasgc
lwcasgc marked this pull request as ready for review November 9, 2025 18:27
@lwcasgc
lwcasgc marked this pull request as draft November 9, 2025 18:29
@lwcasgc
lwcasgc marked this pull request as ready for review November 9, 2025 18:45

@yungcomputerchair yungcomputerchair left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_WARP to shard 1
  • shard 1 sends FE2LS_REQ_BUDDY_WARP to login
  • login sends LS2FE_REQ_BUDDY_WARP to shard 2
  • shard 2 sends FE2LS_REP_BUDDY_WARP_SUCC/FAIL to login
  • login sends LS2FE_REP_BUDDY_WARP_SUCC/FAIL to shard 1
  • shard 1 sends FE2CL_REP_BUDDY_WARP_OTHER_SHARD_SUCC/FAIL to client

Comment thread src/bin/shard/buddy.rs Outdated
Comment on lines +362 to +373
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))
};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this. Need more usage of closures like this in the codebase. Awesome

Comment thread src/bin/shard/buddy.rs Outdated
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();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
let player_skyway_state = player.skyway_ride.clone();
let is_player_on_skyway = player.skyway_ride.is_some();

Comment thread src/bin/shard/buddy.rs Outdated
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();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above comment on cloning

Comment thread src/bin/shard/buddy.rs Outdated

if player_skyway_state.is_some() {
return invalid_warp(
3,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be great if you defined these error codes in error.rs

Comment thread src/bin/shard/buddy.rs Outdated
Comment on lines +447 to +455
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,
};

@yungcomputerchair yungcomputerchair Nov 9, 2025

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Comment thread src/bin/shard/login.rs Outdated
iWarpReceiverX: receiver_pos.x,
iWarpReceiverY: receiver_pos.y,
iWarpReceiverZ: receiver_pos.z,
iWarpReceiverShardID: config_get().shard.shard_id.get(),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant field

Comment thread src/bin/shard/login.rs Outdated
Comment on lines +570 to +577
log_if_failed(
clients
.get_login_server()
.unwrap()
.send_packet(P_FE2LS_REP_BUDDY_WARP_VALIDATION_SUCC, &response),
);

Ok(())

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would treat the failure to send this packet back as an error:

Suggested change
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)

Comment thread src/bin/shard/login.rs Outdated
Comment on lines +608 to +620
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
},
};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to set the instance ID since it's not preserved in the database anyway

Comment thread src/bin/shard/login.rs Outdated
let receiver_z = pkt.iWarpReceiverZ;
let receiver_shard_id = pkt.iWarpReceiverShardID;
let receiver_channel_num = pkt.iWarpReceiverChannelNum;
let _receiver_map_num = pkt.iWarpReceiverMapNum;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this field unused? If so should remove it from the packet

Comment thread src/bin/shard/login.rs Outdated
Comment on lines +637 to +642
// 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,
};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 yungcomputerchair left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Comment thread src/bin/shard/buddy.rs Outdated

if buddy_is_on_skyway {
return invalid_warp(format!(
"Player {} is currently on a skyway ride",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Player {} is currently on a skyway ride",
"Buddy {} is currently on a skyway ride",

Comment thread src/bin/shard/login.rs Outdated

if buddy_is_on_skyway {
return invalid_warp(format!(
"Player {} is currently on a skyway ride",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Player {} is currently on a skyway ride",
"Buddy {} is currently on a skyway ride",

Comment thread src/bin/shard/login.rs Outdated

let other_shard_succ_pkt = sP_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC {
iBuddyPCUID: pkt.iBuddyPCUID,
iChannelNum: 0,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should leave a comment here explaining why we always send back 0 in this case

Comment thread src/entity/player.rs Outdated
}

pub fn is_warp_on_cooldown(&self) -> bool {
self.buddy_warp_time > 0

@yungcomputerchair yungcomputerchair Nov 12, 2025

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even better yet if you could rename the field to something less confusing like last_buddy_warp_timestamp

Comment thread src/bin/shard/buddy.rs Outdated
});
let player_saved = player.clone();

log_if_failed(db_run_sync(move |db| db.save_player(&player_saved)));

@lwcasgc lwcasgc Nov 12, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 yungcomputerchair left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/entity/player.rs Outdated
Comment on lines +677 to +685
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
}
}),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to do this calculation since the client does its own math

Suggested change
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),

Comment thread src/bin/shard/buddy.rs Outdated
});
let player_saved = player.clone();

log_if_failed(db_run_sync(move |db| db.save_player(&player_saved)));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 yungcomputerchair left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work 👍

@yungcomputerchair
yungcomputerchair merged commit 65b5ee5 into yungcomputerchair:main Nov 12, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants