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
12 changes: 12 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,18 @@ the effective run speed. Never a second state machine.
The one shared operation that removes the active Land Mount's aura rows and re-derives the Mount
Projection. Idempotent, and a no-op for a rider who is not mounted.

### Death and resurrection

**Self-Resurrection Option**:
The one spell a dead Character may cast on itself from the death dialog. Chosen at death from the
Character's Soulstone aura, kept through Release Spirit, and spent when the Character is resurrected
by any path or leaves the world. The client sees it as `PLAYER_SELF_RES_SPELL`.
_Avoid_: self-res offer, rez offer, resurrect offer (an offer is a resurrect request from another Character)

**Soulstone**:
The item a Warlock creates, and the Aura its use puts on a Character. A Character that dies while the
Aura is on it gets a Self-Resurrection Option. The Aura itself does not survive death.

### Procs

**Proc**:
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ explains why two rungs of the ladder are written down instead of automated.
| [`guild-client-check.md`](./guild-client-check.md) | Guilds between two real 5875 clients across a Shard Boundary: founding, invites, chat, ranks, Transfer, emblem, Charter and deletion. Status: outstanding, needs a human. |
| [`mail-client-check.md`](./mail-client-check.md) | Mail against a real 5875 client. Status: outstanding, needs a human. |
| [`meeting-stone-client-check.md`](./meeting-stone-client-check.md) | Meeting stones between two real 5875 clients across a Shard Boundary: the JOIN packet, tooltip, minimap button, every status line, Transfer, logout and a stone-formed Party. Status: outstanding, needs a human. |
| [`self-resurrection-client-check.md`](./self-resurrection-client-check.md) | The Soulstone Self-Resurrection Option against a real 5875 client: the death dialog button, the revived vitals, the buff duration and the Release Spirit edge. Status: outstanding, needs a human. |
| [`duel-client-check.md`](./duel-client-check.md) | Duel visuals against a real 5875 client, which the automated tests cannot see. Status: outstanding. |
| [`chat-client-check.md`](./chat-client-check.md) | Realm-wide chat between two real 5875 clients across a Shard Boundary: party, channels and moderation, AFK and DND, whisper, friends, `/who`, ignore, say range, proximity emotes, the language Gate and the flood mute. Status: outstanding, needs a human. |
| [`raid-client-check.md`](./raid-client-check.md) | Raids, Group Broadcasts, raid chat, member stats across Shards and the Instance Removal countdown against real 5875 clients. Status: outstanding, needs a human. |
Expand Down
47 changes: 47 additions & 0 deletions docs/self-resurrection-client-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Self-resurrection client check

Execution status: outstanding. This checklist needs a human with an unmodified 1.12.1 build-5875
client. The automated tests prove the Self-Resurrection Option at the reducer seam, the
`PLAYER_SELF_RES_SPELL` bytes, and the `CMSG_SELF_RES` dispatch. They cannot prove what the client
draws or when it sends the opcode. The field index and the 30 minute duration come from emulator
source and the ClassicDB dump, not from a client.

Use only an isolated non-production LyraCore stack. Never point these steps at a production database,
and never use `spacetime publish -c`. Point the client's realmlist at the isolated stack first. Import
the operator-owned 1.12.1 client data with spells and items, so the five Soulstone Resurrection
ranks, their self-resurrect spells and the Soulstone item templates exist. Run the Gateway at debug
log level.

Prepare one Warlock of level 18 or higher, and one Priest who knows Resurrection for the ally step.

Data, before any client:

- [ ] On the isolated stack, read `game_spell` and `game_spell_effect` for spells 20707 and 3026 with
`spacetime sql`. For 20707 confirm `duration_ms = 1800000`, `cast_time_ms = 3000`, and one
effect with kind `0xB5` (`A_SELF_RESURRECT`), `p0 = 3026`, `p0_kind = 15`. For 3026 confirm
one effect with kind `0x26` (`E_SELF_RESURRECT`), `base_points = -400`, `p0 = 700`. Record any
difference.

Soulstone:

- [ ] Grant the Warlock a Minor Soulstone (`debug_grant_item <guid> 5232 1`) and use it on itself.
Record whether the 3 second cast bar shows, and whether the buff shows a 30 minute timer.
- [ ] Kill the Warlock (`debug_set_health <guid> 0`). Confirm the death dialog shows a second button
beside Release Spirit. Record its exact text.
- [ ] Click it. Confirm the death screen closes, health shows 400, mana shows 700 (or the Warlock's
max mana if lower), no Resurrection Sickness, and no corpse. Record the numbers.
- [ ] Record whether the client plays any cast animation or sound on use. The server sends no cast
packet for it.
- [ ] Repeat, but click Release Spirit first. Record whether the ghost sees any self-resurrection
prompt or button. The server keeps the option for the ghost; if the client offers a way to use
it, confirm it works.
- [ ] Repeat, but die inside the Deadmines and release into Westfall. Record whether the option is
still offered. The server does not carry it across a Shard hop.
- [ ] Die with a Soulstone, let the Priest resurrect the Warlock, and accept. Then die without a
Soulstone. Confirm the dialog shows only Release Spirit.
- [ ] Die with a Soulstone, log out on the death screen, and log back in. Record the state.
- [ ] Use a Soulstone, then move during the 3 second cast. Record whether the item is lost. Every
timed consumable is consumed when its cast starts.

Record the server commit, the client build on the login screen, Character names and levels, each
dialog text, each number read from the client, and any disconnect or error dialog.
27 changes: 27 additions & 0 deletions gateway/src/codec/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2615,6 +2615,33 @@ fn player_ammo_id_values_is_player_only_no_object_type() {
);
}

#[test]
fn self_res_spell_values_sets_only_field_1224() {
// PLAYER_SELF_RES_SPELL is vanilla update field 1224 (cmangos-classic UpdateFields.h:
// PLAYER_AMMO_ID + 1). 1224 = 38 * 32 + 8, so the mask spans 39 blocks: 38 empty ones (block 0
// carries no OBJECT_FIELD_TYPE bit 2), then bit 8 of block 38, then the one value word. Minor
// Soulstone's self-resurrect spell is 3026 = 0x0BD2.
let msg = build_self_res_spell_values(1, 3026);
let mut buf = Vec::new();
msg.write_unencrypted_server(&mut buf).unwrap();
let mut tail = vec![39u8];
tail.extend(std::iter::repeat_n(0u8, 38 * 4));
tail.extend([0x00, 0x01, 0x00, 0x00]);
tail.extend([0xD2, 0x0B, 0x00, 0x00]);
assert!(
buf.ends_with(&tail),
"the VALUES mask must carry field 1224 alone; tail was {:02x?}",
&buf[buf.len().saturating_sub(tail.len())..]
);

let cleared = build_self_res_spell_values(1, 0);
let mut buf = Vec::new();
cleared.write_unencrypted_server(&mut buf).unwrap();
tail.truncate(tail.len() - 4);
tail.extend([0x00; 4]);
assert!(buf.ends_with(&tail), "spell 0 clears the field");
}

#[test]
fn player_xp_values_carries_xp_and_next_level_no_object_type() {
let msg = build_player_xp_values(1, 450, 1000);
Expand Down
9 changes: 9 additions & 0 deletions gateway/src/codec/values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,15 @@ pub fn build_player_ammo_id_values(guid: u64, ammo_entry: u32) -> SMSG_UPDATE_OB
})
}

/// Build a VALUES partial-update carrying `PLAYER_SELF_RES_SPELL`, the Self-Resurrection Option the
/// 5875 death dialog offers as its second button. `spell_id` 0 clears it. Owner-only field; same
/// `dirty_reset` discipline as `build_coinage_values`, so `OBJECT_FIELD_TYPE` is never re-sent.
pub fn build_self_res_spell_values(guid: u64, spell_id: u32) -> SMSG_UPDATE_OBJECT {
player_values(guid, |player| {
player.set_player_self_res_spell(spell_id as i32);
})
}

/// `PLAYER_CHARACTER_POINTS1` partial VALUES — the talent pane's unspent-points counter. Pushed
/// LIVE after a talent pick (fires the client's CHARACTER_POINTS_CHANGED → TalentFrame refresh)
/// and once after the login CREATE for a character with spent points (the CREATE's formula counts
Expand Down
68 changes: 68 additions & 0 deletions gateway/src/stdb/bindings/debug_self_resurrect_reducer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.

#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};

#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub(super) struct DebugSelfResurrectArgs {
pub character_guid: u64,
}

impl From<DebugSelfResurrectArgs> for super::Reducer {
fn from(args: DebugSelfResurrectArgs) -> Self {
Self::DebugSelfResurrect {
character_guid: args.character_guid,
}
}
}

impl __sdk::InModule for DebugSelfResurrectArgs {
type Module = super::RemoteModule;
}

#[allow(non_camel_case_types)]
/// Extension trait for access to the reducer `debug_self_resurrect`.
///
/// Implemented for [`super::RemoteReducers`].
pub trait debug_self_resurrect {
/// Request that the remote module invoke the reducer `debug_self_resurrect` to run as soon as possible.
///
/// This method returns immediately, and errors only if we are unable to send the request.
/// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status.
/// /// Use [`debug_self_resurrect:debug_self_resurrect_then`] to run a callback after the reducer completes.
fn debug_self_resurrect(&self, character_guid: u64) -> __sdk::Result<()> {
self.debug_self_resurrect_then(character_guid, |_, _| {})
}

/// Request that the remote module invoke the reducer `debug_self_resurrect` to run as soon as possible,
/// registering `callback` to run when we are notified that the reducer completed.
///
/// This method returns immediately, and errors only if we are unable to send the request.
/// The reducer will run asynchronously in the future,
/// and its status can be observed with the `callback`.
fn debug_self_resurrect_then(
&self,
character_guid: u64,

callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}

impl debug_self_resurrect for super::RemoteReducers {
fn debug_self_resurrect_then(
&self,
character_guid: u64,

callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(DebugSelfResurrectArgs { character_guid }, callback)
}
}
Loading
Loading