Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions crates/wow-world/src/handlers/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9380,6 +9380,125 @@ mod tests {
);
}

#[tokio::test]
async fn duel_accept_live_bridge_records_and_applies_once_like_cpp() {
let (mut source_session, source_send_rx) = make_session();
let (mut partner_session, partner_send_rx) = make_session();
let canonical = shared_canonical_map_manager_for_misc_test();
let source_guid = ObjectGuid::create_player(1, 77);
let partner_guid = ObjectGuid::create_player(1, 88);
let arbiter_guid = ObjectGuid::create_world_object(
wow_core::guid::HighGuid::GameObject,
0,
1,
571,
0,
7,
100,
);
add_canonical_test_player_on_map_for_misc_test(
&canonical,
source_guid,
Position::new(1.0, 2.0, 3.0, 0.0),
571,
0,
);
add_canonical_test_player_on_map_for_misc_test(
&canonical,
partner_guid,
Position::new(2.0, 2.0, 3.0, 0.0),
571,
0,
);
{
let mut manager = canonical.lock().unwrap();
let map = manager.find_map_mut(571, 0).unwrap().map_mut();
map.get_typed_player_mut(source_guid)
.unwrap()
.set_duel_info_like_cpp(Some(wow_entities::PlayerDuelInfoLikeCpp {
opponent: partner_guid,
state: wow_entities::PlayerDuelStateLikeCpp::Challenged,
}));
map.get_typed_player_mut(partner_guid)
.unwrap()
.set_duel_info_like_cpp(Some(wow_entities::PlayerDuelInfoLikeCpp {
opponent: source_guid,
state: wow_entities::PlayerDuelStateLikeCpp::Challenged,
}));
}
source_session.set_player_guid(Some(source_guid));
source_session.set_player_map_position_like_cpp(571, Position::new(1.0, 2.0, 3.0, 0.0));
source_session.set_canonical_map_manager(Arc::clone(&canonical));
partner_session.set_player_guid(Some(partner_guid));

let registry = Arc::new(PlayerRegistry::default());
registry.insert(
partner_guid,
broadcast_info_with_command_tx(partner_session.session_command_tx()),
);
source_session.set_player_registry(registry);

assert!(
source_session.record_and_apply_represented_live_intent_like_cpp(
crate::session::RepresentedLiveIntentLikeCpp::DuelAccepted(
crate::session::RepresentedDuelAcceptedLikeCpp {
opponent_guid: partner_guid,
arbiter_guid,
countdown_ms: crate::session::DUEL_COUNTDOWN_MS_LIKE_CPP,
},
),
)
);
partner_session
.process_represented_session_commands_like_cpp()
.await;

assert_eq!(
source_session.represented_duel_accepts_like_cpp(),
&[crate::session::RepresentedDuelAcceptedLikeCpp {
opponent_guid: partner_guid,
arbiter_guid,
countdown_ms: crate::session::DUEL_COUNTDOWN_MS_LIKE_CPP,
}],
"the bridge records the accepted intent exactly once"
);
{
let manager = canonical.lock().unwrap();
let map = manager.find_map(571, 0).unwrap().map();
assert_eq!(
map.get_typed_player(source_guid)
.unwrap()
.duel_info_like_cpp()
.unwrap()
.state,
wow_entities::PlayerDuelStateLikeCpp::Countdown
);
assert_eq!(
map.get_typed_player(partner_guid)
.unwrap()
.duel_info_like_cpp()
.unwrap()
.state,
wow_entities::PlayerDuelStateLikeCpp::Countdown
);
}
let source_bytes = source_send_rx
.try_recv()
.expect("source bridge duel countdown");
let partner_bytes = partner_send_rx
.try_recv()
.expect("partner bridge duel countdown");
assert_eq!(source_bytes, partner_bytes);
assert!(
source_send_rx.try_recv().is_err(),
"the bridge applies the source packet once"
);
assert!(
partner_send_rx.try_recv().is_err(),
"the bridge applies the partner packet once"
);
}

#[tokio::test]
async fn duel_response_accepts_challenged_duel_and_sends_countdown_like_cpp() {
let (mut source_session, source_send_rx) = make_session();
Expand Down
98 changes: 76 additions & 22 deletions crates/wow-world/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2290,6 +2290,13 @@ pub(crate) struct RepresentedDuelAcceptedLikeCpp {
pub countdown_ms: u32,
}

/// Validated represented intent whose live side effect is applied through the
/// represented->live bridge instead of directly inside the packet handler.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RepresentedLiveIntentLikeCpp {
DuelAccepted(RepresentedDuelAcceptedLikeCpp),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RepresentedDuelCancelOutcomeLikeCpp {
Interrupted,
Expand Down Expand Up @@ -37793,6 +37800,71 @@ impl WorldSession {
}
}

/// Single boundary for validated represented intents that now have a live
/// application. The handler constructs one intent after C++ validation;
/// this bridge records the represented evidence and applies the live state
/// mutation exactly once.
pub(crate) fn record_and_apply_represented_live_intent_like_cpp(
&mut self,
intent: RepresentedLiveIntentLikeCpp,
) -> bool {
self.record_represented_live_intent_like_cpp(intent);
self.apply_represented_live_intent_like_cpp(intent)
}

fn record_represented_live_intent_like_cpp(&mut self, intent: RepresentedLiveIntentLikeCpp) {
match intent {
RepresentedLiveIntentLikeCpp::DuelAccepted(accepted) => {
self.represented_duel_accepts_like_cpp.push(accepted);
}
}
}

fn apply_represented_live_intent_like_cpp(
&mut self,
intent: RepresentedLiveIntentLikeCpp,
) -> bool {
match intent {
RepresentedLiveIntentLikeCpp::DuelAccepted(accepted) => {
self.apply_represented_duel_accepted_live_like_cpp(accepted)
}
}
}

/// C++ `WorldSession::HandleDuelAccepted` switches both players to
/// `DUEL_STATE_COUNTDOWN` and sends one `DuelCountdown` packet to each.
fn apply_represented_duel_accepted_live_like_cpp(
&mut self,
accepted: RepresentedDuelAcceptedLikeCpp,
) -> bool {
let Some(player_guid) = self.player_guid() else {
return false;
};

self.set_represented_duel_state_like_cpp(
player_guid,
accepted.opponent_guid,
wow_entities::PlayerDuelStateLikeCpp::Countdown,
);
self.set_represented_duel_state_like_cpp(
accepted.opponent_guid,
player_guid,
wow_entities::PlayerDuelStateLikeCpp::Countdown,
);

use wow_packet::ServerPacket;
let packet = wow_packet::packets::misc::DuelCountdown {
countdown_ms: accepted.countdown_ms,
};
let packet_bytes = packet.to_bytes();
self.send_raw_packet(&packet_bytes);
self.send_represented_duel_countdown_to_opponent_like_cpp(
accepted.opponent_guid,
packet_bytes,
);
true
}

/// C++ `Spell::EffectDuel`.
///
/// Represented boundary: canonical connected players only. This creates the
Expand Down Expand Up @@ -37908,31 +37980,13 @@ impl WorldSession {
return false;
}

self.set_represented_duel_state_like_cpp(
player_guid,
opponent_guid,
wow_entities::PlayerDuelStateLikeCpp::Countdown,
);
self.set_represented_duel_state_like_cpp(
opponent_guid,
player_guid,
wow_entities::PlayerDuelStateLikeCpp::Countdown,
);

use wow_packet::ServerPacket;
let packet = wow_packet::packets::misc::DuelCountdown {
countdown_ms: DUEL_COUNTDOWN_MS_LIKE_CPP,
};
let packet_bytes = packet.to_bytes();
self.send_raw_packet(&packet_bytes);
self.send_represented_duel_countdown_to_opponent_like_cpp(opponent_guid, packet_bytes);
self.represented_duel_accepts_like_cpp
.push(RepresentedDuelAcceptedLikeCpp {
self.record_and_apply_represented_live_intent_like_cpp(
RepresentedLiveIntentLikeCpp::DuelAccepted(RepresentedDuelAcceptedLikeCpp {
opponent_guid,
arbiter_guid,
countdown_ms: DUEL_COUNTDOWN_MS_LIKE_CPP,
});
true
}),
)
}

fn handle_duel_cancelled_like_cpp(&mut self) -> bool {
Expand Down
4 changes: 4 additions & 0 deletions docs/migration/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ This is why the old "98% represented" metric and "bags don't open" coexist witho
contradiction: ~98% of logic is *represented*, a much smaller fraction is *live*. The plan's
job is to convert represented→live for the playable path, then for everything.

Initial bridge convention: [represented-live-bridge.md](represented-live-bridge.md) documents
the handler → represented intent → live application boundary. The first converted example is
accepted-duel countdown state.

---

## 1. The honest progress picture (three axes, not one number)
Expand Down
32 changes: 32 additions & 0 deletions docs/migration/represented-live-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Represented -> Live Bridge Convention

Issue #19 defines the minimal bridge convention for converting represented
handler work into live runtime mutations without ad-hoc per-handler wiring.

## Convention

1. Packet handlers still own decoding and C++ validation.
2. After validation, the handler builds one `RepresentedLiveIntentLikeCpp`.
3. The handler calls `WorldSession::record_and_apply_represented_live_intent_like_cpp(intent)`
exactly once.
4. The bridge records the represented evidence and applies the live mutation.
5. The handler must not also mutate the same live state or send the same packets directly.

The bridge is intentionally narrow. Add one intent variant at a time, with the
C++ anchor, live owner, packet side effects, and a focused test proving one
record + one application. Do not move DB writes, map locks, or broad runtime
ownership through this bridge unless that slice explicitly owns those concerns.

## First Converted Example

`RepresentedLiveIntentLikeCpp::DuelAccepted` converts the accepted-duel path.

- C++ source: `src/server/game/Handlers/DuelHandler.cpp`, `WorldSession::HandleDuelAccepted`.
- Rust handler validation: `WorldSession::handle_duel_accepted_like_cpp`.
- Rust bridge application: `WorldSession::apply_represented_duel_accepted_live_like_cpp`.

The handler verifies the challenged duel state and arbiter, then emits one
`DuelAccepted` intent. The bridge records the intent, switches both canonical
players to `PlayerDuelStateLikeCpp::Countdown`, and sends one `DuelCountdown`
packet to each participant. Tests cover the direct bridge path and the packet
handler path.
Loading