Skip to content
Merged
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
19 changes: 16 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ Work issue #<N> (alseif0x/rustycore).
3. Smallest faithful change + focused tests (positive/negative); validate with PROTOC=... cargo check/test.
4. Git: create the branch LINKED to the issue with `gh issue develop <N> --base 3.4.3`
(not a bare `git checkout -b`), 1 issue = 1 PR into `3.4.3` (put `Closes #<N>` in the PR body), commit per gap, NO push unless asked.
5. Do not mark "done" until capture-clean vs C++ (capture-diff harness = issue [01]/#66).
Once push is approved, open the PR immediately so CI and the configured Codex reviewer can run; creating the PR is not the same as closing/merging it.
5. Do not mark "done" until capture-clean vs C++ (capture-diff harness = issue [01]/#66)
and the required `Codex reviewer verdict` check is green for the PR's current HEAD.
```

**Linking the branch/PR to the issue:** the repo's **default branch is `3.4.3`** (the version/
Expand Down Expand Up @@ -113,7 +115,7 @@ Every implementation slice must follow this sequence:
7. Update migration docs/checklists with the new `#NEXT.R8.ENTITIES.xxx` item when closing a represented implementation gap.
8. Recalculate progress honestly.
9. Run validation.
10. Commit on the issue's feature branch, push, and open a PR into `3.4.3` (`Closes #<N>`); merge after review/CI. Tag releases on `3.4.3`.
10. Commit on the issue's feature branch, push, and open a PR into `3.4.3` (`Closes #<N>`); merge only after CI and the required `Codex reviewer verdict` check are both green on the PR's current HEAD, and every actionable reviewer comment is either fixed or explicitly documented as intentionally deferred. Tag releases on `3.4.3`.

Do not do "bulk close" inventory edits. A closed `#NEXT` item must correspond to real code and tests, with exact C++ refs, Rust targets, checks run, and remaining boundaries stated. Discovering or documenting a gap is useful, but it is not an implementation closeout.

Expand Down Expand Up @@ -284,11 +286,22 @@ git diff --check
git add <changed files>
git commit -m "<short faithful summary>"
git push origin <feature-branch> # NO push unless asked
# open the PR into 3.4.3 with `Closes #<N>` in the body; merge after review/CI.
# after push, open the PR into 3.4.3 with `Closes #<N>` in the body so CI and
# the configured Codex reviewer can run.
# Do not merge or close the issue until CI is green and the required
# `Codex reviewer verdict` check is green for the PR's current HEAD.
# If Codex leaves feedback, fix or explicitly defer every actionable comment,
# resolve the review threads, push the fix, comment `@codex review`, and wait
# for `Codex reviewer verdict` to pass again on the new HEAD.
```

Only do this after the slice is genuinely validated. If the tree contains changes from another agent, audit them before building on top of them.

Branch protection on `3.4.3` requires strict status checks, linear history, conversation
resolution, and these checks: `Format`, `Check core crates`, `Focused library tests`, and
`Codex reviewer verdict`. A PR can show normal CI green while still being blocked if Codex has
not reviewed the current HEAD or has left unresolved feedback.

## Local Context Files

The `.gitignore` excludes local agent/workflow files that may exist and contain useful context, such as `AGENTS.md`, `PLAN.md`, `MIGRATION_STATUS.md`, `INVENTORY.md`, `memory/`, `.claude/`, `.agents/`, `.openclaw/`, and similar directories. Read them if useful, but do not commit ignored local context unless the user explicitly asks for it.
8 changes: 8 additions & 0 deletions crates/wow-database/src/statements/character.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,9 @@ pub enum CharStatements {

/// UPDATE characters SET money = ? WHERE guid = ?
UPD_CHAR_MONEY,
/// C++ `CHAR_UPD_CHARACTER` persists this field immediately before powers.
/// UPDATE characters SET health = ? WHERE guid = ?
UPD_CHAR_HEALTH,
/// C++ `CHAR_UPD_CHARACTER` persists these fields in the full save.
/// UPDATE characters SET power1 = ?, ..., power10 = ? WHERE guid = ?
UPD_CHAR_POWERS,
Expand Down Expand Up @@ -2743,6 +2746,7 @@ impl StatementDef for CharStatements {
Self::UPD_CHAR_XP => "UPDATE characters SET xp = ? WHERE guid = ?",
Self::UPD_CHAR_LEVEL => "UPDATE characters SET level = ?, xp = ? WHERE guid = ?",
Self::UPD_CHAR_MONEY => "UPDATE characters SET money = ? WHERE guid = ?",
Self::UPD_CHAR_HEALTH => "UPDATE characters SET health = ? WHERE guid = ?",
Self::UPD_CHAR_POWERS => {
"UPDATE characters SET power1 = ?, power2 = ?, power3 = ?, power4 = ?, power5 = ?, power6 = ?, power7 = ?, power8 = ?, power9 = ?, power10 = ? WHERE guid = ?"
}
Expand Down Expand Up @@ -4585,6 +4589,10 @@ mod tests {
CharStatements::UPD_CHAR_MONEY.sql(),
"UPDATE characters SET money = ? WHERE guid = ?"
);
assert_eq!(
CharStatements::UPD_CHAR_HEALTH.sql(),
"UPDATE characters SET health = ? WHERE guid = ?"
);
assert_eq!(
CharStatements::UPD_CHAR_POWERS.sql(),
"UPDATE characters SET power1 = ?, power2 = ?, power3 = ?, power4 = ?, power5 = ?, power6 = ?, power7 = ?, power8 = ?, power9 = ?, power10 = ? WHERE guid = ?"
Expand Down
126 changes: 120 additions & 6 deletions crates/wow-world/src/handlers/character.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,17 @@ fn default_health_mana(class: u8) -> (u32, u32) {
}
}

fn max_health_u32_like_cpp(max_health: i64) -> u32 {
max_health.max(1).min(i64::from(u32::MAX)) as u32
}

fn restored_saved_health_like_cpp(saved_health: Option<u32>, max_health: i64) -> i64 {
let max_health = max_health_u32_like_cpp(max_health);
saved_health
.map(|health| i64::from(health.min(max_health)))
.unwrap_or(i64::from(max_health))
}

fn default_character_power1_like_cpp(class: u8, mana: u32) -> u32 {
match primary_power_type_for_class_like_cpp(class) {
PowerType::Energy => 100,
Expand Down Expand Up @@ -5502,6 +5513,9 @@ impl WorldSession {
([0i32; 5], 0, 0, 0, 0)
};

// C++ `Player::LoadFromDB` restores `fields.health` after `UpdateAllStats`,
// clamping it to the recalculated max and preserving zero as corpse state.
let saved_health = result.try_read::<u32>(51);
let loaded_powers = std::array::from_fn(|index| {
result
.try_read::<u32>(52 + index)
Expand Down Expand Up @@ -5571,7 +5585,7 @@ impl WorldSession {

(
PlayerCombatStats {
health: max_health,
health: restored_saved_health_like_cpp(saved_health, max_health),
max_health,
stats: [total_str, total_agi, total_sta, total_int, total_spi],
base_armor,
Expand All @@ -5597,7 +5611,7 @@ impl WorldSession {
let (h, m) = default_health_mana(class);
(
PlayerCombatStats {
health: h as i64,
health: restored_saved_health_like_cpp(saved_health, h as i64),
max_health: h as i64,
max_mana: m as i64,
..PlayerCombatStats::default()
Expand All @@ -5609,7 +5623,7 @@ impl WorldSession {
let (h, m) = default_health_mana(class);
(
PlayerCombatStats {
health: h as i64,
health: restored_saved_health_like_cpp(saved_health, h as i64),
max_health: h as i64,
max_mana: m as i64,
..PlayerCombatStats::default()
Expand Down Expand Up @@ -9186,7 +9200,7 @@ impl WorldSession {
// C++ first lets Battlefield own the leave flow when one exists. Rust
// has no battlefield manager attached to WorldSession yet, so this
// represented branch covers the AreaTable/homebind path only.
self.set_player_alive_like_cpp(true);
self.apply_represented_resurrection_percent_like_cpp(1.0);
if let Some(homebind) = self.represented_homebind_like_cpp() {
self.teleport_to(homebind.map_id, homebind.position).await;
}
Expand Down Expand Up @@ -12489,6 +12503,17 @@ impl WorldSession {
let hp_bonus = sta64.min(20) + (sta64 - 20).max(0) * 10 + gear_health as i64;
let max_health = base_hp + hp_bonus;

let computed_max_health_u32 = max_health_u32_like_cpp(max_health);
let (health, max_health_for_update) = self
.sync_canonical_player_max_health_like_cpp(computed_max_health_u32)
.unwrap_or_else(|| {
let current = self.player_health_like_cpp().min(computed_max_health_u32);
self.set_player_health_like_cpp(current, computed_max_health_u32);
(current, computed_max_health_u32)
});
let health = i64::from(health);
let max_health = i64::from(max_health_for_update);

// MaxMana from total INT
let int64 = total_int as i64;
let base_mp = ls.base_mana as i64;
Expand Down Expand Up @@ -12674,7 +12699,7 @@ impl WorldSession {
};

let changes = PlayerStatChanges {
health: max_health,
health,
max_health,
min_damage: min_d,
max_damage: max_d,
Expand Down Expand Up @@ -13728,7 +13753,7 @@ impl WorldSession {
if attached_controller {
let _ = self.ensure_canonical_world_map_for_current_player_like_cpp();
}
self.set_player_health_like_cpp(
self.sync_canonical_player_health_like_cpp(
combat.health.max(0).min(u32::MAX as i64) as u32,
combat.max_health.max(1).min(u32::MAX as i64) as u32,
);
Expand Down Expand Up @@ -14407,6 +14432,24 @@ mod tests {
player_guid: ObjectGuid,
current_mana: i32,
max_mana: i32,
) {
attach_stat_update_player_with_mana_and_health(
session,
player_guid,
current_mana,
max_mana,
100,
100,
);
}

fn attach_stat_update_player_with_mana_and_health(
session: &mut WorldSession,
player_guid: ObjectGuid,
current_mana: i32,
max_mana: i32,
current_health: u32,
max_health: u32,
) {
let mut player = wow_entities::Player::new(Some(1), false);
player
Expand All @@ -14419,6 +14462,8 @@ mod tests {
.unit_mut()
.world_mut()
.relocate(Position::new(10.0, 20.0, 30.0, 0.0));
player.unit_mut().set_max_health(u64::from(max_health));
player.unit_mut().set_health(u64::from(current_health));
player.unit_mut().set_power_index(PowerType::Mana, Some(0));
player.unit_mut().set_max_power(PowerType::Mana, max_mana);
player.unit_mut().set_power(PowerType::Mana, current_mana);
Expand All @@ -14443,6 +14488,17 @@ mod tests {
opcodes
}

#[test]
fn restored_saved_health_preserves_dead_zero_like_cpp() {
assert_eq!(restored_saved_health_like_cpp(Some(0), 110), 0);
}

#[test]
fn restored_saved_health_clamps_to_recomputed_max_like_cpp() {
assert_eq!(restored_saved_health_like_cpp(Some(500), 110), 110);
assert_eq!(restored_saved_health_like_cpp(Some(77), 110), 77);
}

#[test]
fn stat_update_preserves_current_mana_like_cpp() {
let (mut session, _send_rx) = make_session_with_send_capacity(1);
Expand Down Expand Up @@ -14487,6 +14543,64 @@ mod tests {
);
}

#[test]
fn stat_update_preserves_current_health_like_cpp() {
let (mut session, _send_rx) = make_session_with_send_capacity(1);
let player_guid = ObjectGuid::create_player(1, 79);
session.set_player_guid(Some(player_guid));
session.set_loaded_player_identity_like_cpp(571, 1, 5, 80, 0);
session.set_player_stats(Arc::new(stats_store_with_priest_level80(1000, 40)));
attach_stat_update_player_with_mana_and_health(
&mut session,
player_guid,
777,
1320,
77,
500,
);

let (_, changes) = session
.player_stat_changes_like_cpp()
.expect("stat changes");

assert_eq!(changes.health, 77);
assert_eq!(changes.max_health, 110);
assert_eq!(session.player_health_like_cpp(), 77);
assert_eq!(
session.canonical_player_health_snapshot_like_cpp(),
Some((77, 110))
);
}

#[test]
fn stat_update_clamps_current_health_to_new_max_like_cpp() {
let (mut session, _send_rx) = make_session_with_send_capacity(1);
let player_guid = ObjectGuid::create_player(1, 80);
session.set_player_guid(Some(player_guid));
session.set_loaded_player_identity_like_cpp(571, 1, 5, 80, 0);
session.set_player_stats(Arc::new(stats_store_with_priest_level80(1000, 40)));
attach_stat_update_player_with_mana_and_health(
&mut session,
player_guid,
777,
1320,
500,
500,
);

let (_, changes) = session
.player_stat_changes_like_cpp()
.expect("stat changes");

assert_eq!(changes.health, 110);
assert_eq!(changes.max_health, 110);
assert_eq!(session.player_health_like_cpp(), 110);
assert_eq!(
session.canonical_player_health_snapshot_like_cpp(),
Some((110, 110))
);
}

#[tokio::test]
async fn show_trade_skill_is_noop_null_like_cpp() {
let (mut session, send_rx) = make_session_with_send_capacity(1);
Expand Down
12 changes: 11 additions & 1 deletion crates/wow-world/src/handlers/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2656,7 +2656,12 @@ impl crate::session::WorldSession {
// full player-corpse runtime; this represented slice only clears the
// ghost/dead state when the already-known C++ gates pass.
self.set_player_ghost_flag_like_cpp(false);
self.set_player_alive_like_cpp(true);
let restore_percent = if self.player_in_represented_battleground_like_cpp() {
1.0
} else {
0.5
};
self.apply_represented_resurrection_percent_like_cpp(restore_percent);
}

/// CMSG_ACTIVATE_TAXI.
Expand Down Expand Up @@ -8727,13 +8732,18 @@ mod tests {
);
session.set_player_alive_like_cpp(false);
session.set_player_ghost_flag_like_cpp(true);
let _ = session.sync_canonical_player_health_like_cpp(0, 100);

session
.handle_reclaim_corpse(reclaim_corpse_packet(corpse_guid))
.await;

assert!(session.player_is_alive_like_cpp());
assert!(!session.player_has_ghost_flag_like_cpp());
assert_eq!(
session.canonical_player_health_snapshot_like_cpp(),
Some((50, 100))
);
assert!(send_rx.try_recv().is_err());
}

Expand Down
Loading
Loading