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
1 change: 1 addition & 0 deletions crates/lyracore-shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod faction;
pub mod group;
pub mod instance;
pub mod item;
pub mod loot;
pub mod loot_roll;
pub mod mail;
pub mod nav;
Expand Down
117 changes: 117 additions & 0 deletions crates/lyracore-shared/src/loot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! The loot tier contract shared by the Module and the Gateway.

/// Why the Module refused a loot Durable Request. The tag is the whole reducer error text, so
/// neither tier matches on human prose. The Module keeps the detail in its own log.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LootRefusal {
/// The Actor is not in the Loot Source's Loot Tag eligibility set.
LootTagIneligible,
/// The Loot Source is on another map, in another instance, or beyond loot range.
OutOfRange,
/// The named Loot Source does not exist, or is not a lootable creature corpse.
NoLootSource,
/// The looter is not in the world, or is dead.
LooterUnavailable,
/// The Loot Source has nothing left in the requested slot or purse.
NothingToLoot,
/// No open Loot Roll answers this vote, or the voter already voted.
RollUnavailable,
/// The Actor does not hold the master-looter right on that row.
NotMasterLooter,
/// The master-loot recipient is no longer in the world.
RecipientUnavailable,
/// The master-loot recipient has no room for the item.
RecipientInventoryFull,
}

impl LootRefusal {
pub const ALL: [Self; 9] = [
Self::LootTagIneligible,
Self::OutOfRange,
Self::NoLootSource,
Self::LooterUnavailable,
Self::NothingToLoot,
Self::RollUnavailable,
Self::NotMasterLooter,
Self::RecipientUnavailable,
Self::RecipientInventoryFull,
];

pub fn as_tag(self) -> &'static str {
match self {
Self::LootTagIneligible => "loot:loot_tag_ineligible",
Self::OutOfRange => "loot:out_of_range",
Self::NoLootSource => "loot:no_loot_source",
Self::LooterUnavailable => "loot:looter_unavailable",
Self::NothingToLoot => "loot:nothing_to_loot",
Self::RollUnavailable => "loot:roll_unavailable",
Self::NotMasterLooter => "loot:not_master_looter",
Self::RecipientUnavailable => "loot:recipient_unavailable",
Self::RecipientInventoryFull => "loot:recipient_inventory_full",
}
}

pub fn parse_tag(tag: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|refusal| refusal.as_tag() == tag)
}
}

/// A failure in the trusted Gateway entry before a loot core can answer the request. These tags
/// cross the reducer boundary so legacy untagged gameplay compatibility cannot hide a broken
/// Operator or Actor invariant.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LootBoundaryFailure {
OperatorRejected,
MissingActor,
}

impl LootBoundaryFailure {
pub const ALL: [Self; 2] = [Self::OperatorRejected, Self::MissingActor];

pub fn as_tag(self) -> &'static str {
match self {
Self::OperatorRejected => "loot:boundary_operator_rejected",
Self::MissingActor => "loot:boundary_missing_actor",
}
}

pub fn parse_tag(tag: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|failure| failure.as_tag() == tag)
}
}

#[cfg(test)]
mod tests {
use super::{LootBoundaryFailure, LootRefusal};

#[test]
fn every_refusal_tag_round_trips() {
for refusal in LootRefusal::ALL {
assert_eq!(LootRefusal::parse_tag(refusal.as_tag()), Some(refusal));
}
assert_eq!(LootRefusal::parse_tag("loot:"), None);
assert_eq!(
LootRefusal::parse_tag("loot_tag_ineligible: actor_guid=7 corpse_guid=11"),
None
);
assert_eq!(
LootRefusal::parse_tag("gw_take_loot reducer timed out after 10s"),
None
);
}

#[test]
fn every_boundary_failure_tag_round_trips() {
for failure in LootBoundaryFailure::ALL {
assert_eq!(
LootBoundaryFailure::parse_tag(failure.as_tag()),
Some(failure)
);
assert_eq!(LootRefusal::parse_tag(failure.as_tag()), None);
}
}
}
8 changes: 3 additions & 5 deletions gateway/src/codec/loot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,11 @@ pub fn build_loot_response_raw(guid: u64, money: u32, items: &[LootItemView]) ->
(SMSG_LOOT_RESPONSE_OPCODE, body)
}

/// Build the vanilla response for a character who cannot loot this creature corpse.
pub fn build_loot_didnt_kill_response(guid: u64) -> SMSG_LOOT_RESPONSE {
/// Build the vanilla response that answers a loot Refusal on the named Loot Source.
pub fn build_loot_error_response(guid: u64, loot_error: LootMethodError) -> SMSG_LOOT_RESPONSE {
SMSG_LOOT_RESPONSE {
guid: Guid::new(guid),
loot_method: SMSG_LOOT_RESPONSE_LootMethod::ErrorX {
loot_error: LootMethodError::DidntKill,
},
loot_method: SMSG_LOOT_RESPONSE_LootMethod::ErrorX { loot_error },
gold: Gold::new(0),
items: Vec::new(),
}
Expand Down
1 change: 0 additions & 1 deletion gateway/src/stdb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ pub(crate) mod world_index;
pub(crate) mod world_view; // shared per-shard spatial, broadcast, private, and owner dispatch

pub use connection::Coordinator;
pub(crate) use connection::{is_reducer_refusal, reducer_refusal_reason};
pub use subscriptions::PlayerSubscriptions;
// Re-exported so `crate::stdb::{RealmRow, AccountRow}` resolves (they are the return types of
// `Coordinator::realm` / `account_by_username`). `allow(unused_imports)` because in this *binary*
Expand Down
Loading
Loading