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
20 changes: 15 additions & 5 deletions agent_docs/combat.md
Original file line number Diff line number Diff line change
Expand Up @@ -846,9 +846,11 @@ take 4/5 of the table amount plus up to 2/5 more (OrionAlpha's
what scales them). A mob worth no EXP still drops and
counts for quests. Quest drops are the entries
the overrides tag with a quest, and quest items (`info/quest`) that only one
quest's completion check (`Check.img/<quest>/1/item`) asks for. They roll
only while that quest is started and the player holds fewer than the check
asks for (any number if it doesn't name the item). Without a content pack,
quest's completion check (`Check.img/<quest>/1/item`) asks for. For a
player out of a party they roll only while that quest is started and the
player holds fewer than the check asks for (any number if it doesn't name the
item); in a party they roll anyway, as Cosmic's do, for the members who need
them. Without a content pack,
mobs drop only the overrides. The client actor rolls (it has
the player's quests) and asks the field to place the loot.

Expand All @@ -859,10 +861,18 @@ entering see lying drops (mode 2). Each drop lands on the map's footholds
ground below its column, searched from 50 px above the mob as v83 servers do.
A wall between the mob and the drop's column, or no ground under it, keeps
the drop on the ground below the mob, so drops don't end up inside a cliff or
in the air. Quest drops are shown only to their owner.
in the air. A quest drop is shown only to the players on the map who still
need it (`net::loot::wants`): the field hands its packet to each occupant's
client actor as `ServerMessage::QuestDrop`, which checks the player's quests
and inventory, as Cosmic's `spawnDrop` does. Removals go to everyone; clients
ignore drops they never saw.

For 15 s only the owner may pick a drop up, or with a party, the owner's
party as it was at the drop (quest drops: always only the owner). Drops of
party as it was at the drop; a player outside it who needs a quest drop
sees it but waits like anyone else. A quest drop is also refused to anyone
who doesn't need it, the owner included: the pickup carries a `QuestNeeds`
snapshot of the taker's started quests and the quest items they already hold
enough of. Drops of
mobs with `publicReward` are anyone's at once, as are those of mobs with
`explosiveReward`, which fly out 40 px apart instead of 25. The packet's
ownership type tells the client which (v83 FUN_0050483a, whose pets follow
Expand Down
5 changes: 3 additions & 2 deletions agent_docs/server-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ tests.
pack's drops rather than replace them: `like = <id>` borrows another
reactor's drops (Amherst's box 2001 has none of its own, so it borrows
2000's), and `quest_items` makes items quest drops that always drop during
that quest, adding them if the pack lacks them. So Pio's materials for
quest 1008 drop from the boxes even without a pack.
that quest (for a player who still needs them, or anyone in a party),
adding them if the pack lacks them. So Pio's materials for quest 1008 drop
from the boxes even without a pack.
- **Shops** (`ShopTable`): without `shops.json` no NPC keeps a shop, and
shopkeepers fall back to their normal speech.

Expand Down
96 changes: 95 additions & 1 deletion integration-harness/tests/drops_and_pickup.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
//! Mob drops: rolling, ownership, pickup into the inventory or meso total,
//! persistence, and quest-only drops.

use integration_harness::hunting::{attack, pickup, MapDrop};
use integration_harness::hunting::{attack, pickup, walk_to, MapDrop};
use integration_harness::preconditions::load_harness_config_or_fail;
use integration_harness::quests::{act, START};
use integration_harness::trace::{read_for, send, stat, until, warp, Trace};
use integration_harness::{login_to_world_session, relogin};
use net::packet::op::RecvOpcode;
use packet::io::write::PktWrite;
use packet::Packet;

const FOREST: i32 = 50_000;
const SNAIL: i32 = 100100;
const EAST_ENTRANCE: i32 = 30_000;
const SENTINEL: i32 = 9300018;
const TODD: i32 = 2004;
const SHELLPIECE: i32 = 4031802;

#[tokio::test]
async fn snail_drops_go_to_the_killer_first_and_persist() {
Expand Down Expand Up @@ -133,3 +139,91 @@ async fn the_shellpiece_only_drops_during_todds_quest() {
// override) does not happen.
assert!(seen.field.drops.is_empty(), "{:?}", seen.field.drops);
}

fn party_operation(op: u8) -> Packet {
let mut p = Packet::new_empty();
p.write_short(RecvOpcode::PartyOperation as i16).unwrap();
p.write_byte(op).unwrap();
p
}

#[tokio::test]
async fn a_party_members_quest_drop_shows_only_to_the_one_who_needs_it() {
let hunter_config = load_harness_config_or_fail().await;
let helper_config = load_harness_config_or_fail().await;
let mut hunter = login_to_world_session(&hunter_config).await.unwrap();
let mut helper = login_to_world_session(&helper_config).await.unwrap();
let (mut h, mut p) = (Trace::default(), Trace::default());

// The helper invites the hunter into a party.
let mut invite = party_operation(4);
invite
.write_str_with_length(&hunter.character_name)
.unwrap();
send(&mut helper.connection, invite).await;
until(&mut hunter.connection, &mut h, |t| {
!t.party_invites.is_empty()
})
.await;
let mut accept = party_operation(3);
accept.write_int(h.party_invites[0].0).unwrap();
send(&mut hunter.connection, accept).await;
until(&mut helper.connection, &mut p, |t| {
t.party_ops.contains(&0x0F)
})
.await;

// Only the helper is on Todd's hunt.
assert_eq!(
warp(&mut helper.connection, EAST_ENTRANCE, "", &mut p).await,
EAST_ENTRANCE
);
let marker = helper_config.character_name.clone();
act(&mut helper.connection, &mut p, &marker, START, 1035, TODD).await;
assert_eq!(p.states.get(&1035), Some(&1));
assert_eq!(
warp(&mut hunter.connection, EAST_ENTRANCE, "", &mut h).await,
EAST_ENTRANCE
);
until(&mut hunter.connection, &mut h, |t| {
!t.field.alive(SENTINEL).is_empty()
})
.await;

// The hunter's kill drops the shellpiece for the party, but only the
// helper, who needs it, sees it.
let sentinel = h.field.alive(SENTINEL)[0];
send(&mut hunter.connection, attack(sentinel, 8)).await;
until(&mut helper.connection, &mut p, |t| {
t.field.item_drop(SHELLPIECE).is_some()
})
.await;
read_for(&mut hunter.connection, &mut h, 500).await;
assert!(h.field.killed.contains(&sentinel));
assert!(
h.field.item_drop(SHELLPIECE).is_none(),
"{:?}",
h.field.drops
);

// The hunter can't take it; the helper can.
let shellpiece = p.field.item_drop(SHELLPIECE).unwrap();
send(&mut hunter.connection, walk_to(shellpiece.x, shellpiece.y)).await;
send(
&mut hunter.connection,
pickup(shellpiece.object_id, shellpiece.x, shellpiece.y),
)
.await;
read_for(&mut helper.connection, &mut p, 500).await;
assert!(p.field.removals(shellpiece.object_id).is_empty());
send(&mut helper.connection, walk_to(shellpiece.x, shellpiece.y)).await;
send(
&mut helper.connection,
pickup(shellpiece.object_id, shellpiece.x, shellpiece.y),
)
.await;
until(&mut helper.connection, &mut p, |t| {
t.added.iter().any(|added| added.2 == SHELLPIECE)
})
.await;
}
10 changes: 9 additions & 1 deletion net/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,13 @@ pub enum FieldRequest {
Kite { item_id: i32, message: String },
/// Pick up the drop `object_id`, answered with `ServerMessage::PickedUp`;
/// the player stands at (x, y).
Pickup { object_id: i32, x: i16, y: i16 },
Pickup {
object_id: i32,
x: i16,
y: i16,
/// The quest drops the player may take.
needs: crate::loot::QuestNeeds,
},
/// Throw `loot` down where the player stands; anyone may pick it up at
/// once. With `vanish` it falls and fades instead (quest items).
Throw {
Expand Down Expand Up @@ -745,6 +751,8 @@ pub enum FieldRequest {
mesos: bool,
items: bool,
ignored: Vec<i32>,
/// The quest drops the player may take.
needs: crate::loot::QuestNeeds,
},
/// The player's client moved their pet at `index` in their pet list.
PetMove {
Expand Down
Loading
Loading