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: 7 additions & 12 deletions module/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use lyracore_shared::group::COMMAND_RESULT_WINDOW_MICROS;
use spacetimedb::{reducer, table, Identity, ReducerContext, Table, Timestamp};

#[cfg(feature = "debug_reducers")]
use crate::{game_group, game_group_member, game_world_entity};
use crate::{game_group, game_world_entity};

const COMMAND_LIFETIME_MICROS: i64 = 30_000_000;
const CLAIM_LEASE_MICROS: i64 = 2_000_000;
Expand Down Expand Up @@ -696,17 +696,12 @@ fn fixture_command_apply_after_authority(
.group_id()
.find(member.group_id)
.ok_or_else(|| CommandOutcome::NotMember.tag().to_string())?;
let members: Vec<_> = ctx
.db
.game_group_member()
.by_group()
.filter(&member.group_id)
.take(lyracore_shared::group::RAID_MAX_MEMBERS + 1)
.map(|row| row.character_guid)
.collect();
if members.len() > lyracore_shared::group::RAID_MAX_MEMBERS {
return Err(CommandOutcome::StalePartyMirror.tag().to_string());
}
let members: Vec<_> =
crate::group::bounded_roster(ctx, member.group_id, crate::group::group_kind_of(&group))
.ok_or_else(|| CommandOutcome::StalePartyMirror.tag().to_string())?
.into_iter()
.map(|row| row.character_guid)
.collect();
crate::group::admit_party_command_authority(
ctx,
member.group_id,
Expand Down
124 changes: 63 additions & 61 deletions module/src/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,8 @@ pub fn companion_target_facts(

/// Recheck the Gateway-certified authority projection and the bot Gate in the target transaction.
/// `None` means Package application may proceed; `Some` is a terminal typed Refusal. Companion
/// Orders cover the whole Raid: the bound is the Raid cap, and a longer list is a damaged mirror.
/// Orders cover the whole Raid: the bound is the member cap of the Group's kind, and a longer list
/// is a damaged mirror.
pub(crate) fn admit_party_command(
ctx: &ReducerContext,
admitted: &crate::bridge::AdmittedClientCommand,
Expand All @@ -765,19 +766,14 @@ pub(crate) fn admit_party_command(
let Some(group) = ctx.db.game_group().group_id().find(member.group_id) else {
return Some(CommandOutcome::StalePartyMirror);
};
let mut local: Vec<_> = ctx
.db
.game_group_member()
.by_group()
.filter(&member.group_id)
.take(RAID_MAX_MEMBERS + 1)
.map(|row| row.character_guid)
.collect();
let Some(roster) = bounded_roster(ctx, member.group_id, group_kind_of(&group)) else {
return Some(CommandOutcome::StalePartyMirror);
};
let mut local: Vec<_> = roster.into_iter().map(|row| row.character_guid).collect();
let mut certified = admitted.members.clone();
local.sort_unstable();
certified.sort_unstable();
if local.len() > RAID_MAX_MEMBERS
|| member.group_id != admitted.group_id
if member.group_id != admitted.group_id
|| group.leader_guid != admitted.leader_guid
|| admitted.leader_guid != admitted.issuer_guid
|| local != certified
Expand Down Expand Up @@ -828,16 +824,8 @@ pub(crate) fn has_known_party_member_in_partition(
{
return Err(PartyFactsUnavailableReason::MissingGroup);
}
let members: Vec<_> = ctx
.db
.game_group_member()
.by_group()
.filter(&member.group_id)
.take(GROUP_MAX_MEMBERS + 1)
.collect();
if members.len() > GROUP_MAX_MEMBERS {
return Err(PartyFactsUnavailableReason::FightLimit);
}
let members = bounded_roster(ctx, member.group_id, GroupKind::Party)
.ok_or(PartyFactsUnavailableReason::FightLimit)?;
Ok(members.into_iter().any(|candidate| {
candidate.character_guid != character_guid
&& ctx
Expand Down Expand Up @@ -885,8 +873,9 @@ fn known_party_partition(
/// entity on this Shard, so those facts are nullable.
///
/// Facts cover every member of the Group, Raid included, whatever the Subgroup. Reads stop with
/// `FightLimit` above 40 members (only a damaged mirror holds more), 24 incoming melee or
/// threat-source rows for one member, one pending cast for one member, or 24 aggregate enemy GUIDs.
/// `FightLimit` above the member cap of the Group's kind (only a damaged mirror holds more), 24
/// incoming melee or threat-source rows for one member, one pending cast for one member, or 24
/// aggregate enemy GUIDs.
/// Each retained enemy permits 80 threat sources (a full Raid and one pet each), 64 control auras,
/// and three effects on a pending spell. A missing parent Group stops with `MissingGroup`; neither
/// failure returns facts selected from an arbitrary prefix.
Expand All @@ -907,19 +896,12 @@ pub fn party_facts(
group_id: member.group_id,
reason: PartyFactsUnavailableReason::MissingGroup,
})?;
let members: Vec<_> = ctx
.db
.game_group_member()
.by_group()
.filter(&member.group_id)
.take(RAID_MAX_MEMBERS + 1)
.collect();
if members.len() > RAID_MAX_MEMBERS {
return Err(PartyFactsUnavailable {
group_id: member.group_id,
reason: PartyFactsUnavailableReason::FightLimit,
});
}
let unavailable = || PartyFactsUnavailable {
group_id: member.group_id,
reason: PartyFactsUnavailableReason::FightLimit,
};
let members =
bounded_roster(ctx, member.group_id, group_kind_of(&group)).ok_or_else(unavailable)?;
let members: Vec<_> = members
.into_iter()
.map(|member| {
Expand Down Expand Up @@ -957,10 +939,6 @@ pub fn party_facts(
let mut pending_control_targets = std::collections::BTreeMap::new();
const ENEMY_LIMIT: usize = 24;
const THREAT_SOURCE_LIMIT: usize = RAID_MAX_MEMBERS * 2;
let unavailable = || PartyFactsUnavailable {
group_id: member.group_id,
reason: PartyFactsUnavailableReason::FightLimit,
};
for guid in &party_guids {
if let Some(attack) = melee.attacker_guid().find(*guid) {
enemy_guids.insert(attack.target_guid);
Expand Down Expand Up @@ -1131,6 +1109,25 @@ pub(crate) fn members_of(ctx: &ReducerContext, group_id: u64) -> Vec<GroupMember
.collect()
}

/// The roster of `group_id`, read no further than the Group kind's member cap. `None` means the
/// mirror holds more rows than that kind allows, so the roster is damaged and no caller may act
/// on a prefix of it.
pub(crate) fn bounded_roster(
ctx: &ReducerContext,
group_id: u64,
kind: GroupKind,
) -> Option<Vec<GroupMember>> {
let cap = kind.member_cap();
let roster: Vec<_> = ctx
.db
.game_group_member()
.by_group()
.filter(&group_id)
.take(cap + 1)
.collect();
(roster.len() <= cap).then_some(roster)
}

/// Who hears a party line: every member of the speaker's Group, the speaker included, in a Party.
/// In a Raid it narrows to the speaker's own Subgroup (cm:ChatHandler.cpp:302-332,
/// cm:Group.cpp:777-788). Nobody's ignore list filters it. Read from this database's membership,
Expand Down Expand Up @@ -2144,7 +2141,8 @@ fn next_group_revision(current: Option<u64>, was_active: bool) -> u64 {
/// each Character whose membership the push changes: the mirror is how this Shard learns of it.
///
/// `group_kind` is the [`GroupKind`] byte and `raid_slots[n]` is `members[n]`'s [`RaidSlot`] byte.
/// A length mismatch, an unknown kind or an invalid slot refuses the whole push.
/// A length mismatch, an unknown kind, more members than the kind holds, or an invalid slot refuses
/// the whole push.
#[reducer]
#[allow(clippy::too_many_arguments)] // The roster and initiating Actor are the reducer wire contract.
pub fn sync_group_mirror(
Expand Down Expand Up @@ -2391,6 +2389,13 @@ fn mirror_kind_and_slots(
) -> Result<(GroupKind, std::collections::BTreeMap<u64, RaidSlot>), String> {
let kind = GroupKind::from_wire(group_kind)
.ok_or_else(|| format!("group mirror has unknown group kind {group_kind}"))?;
if members.len() > kind.member_cap() {
return Err(format!(
"group mirror has {} members for a group that holds at most {}",
members.len(),
kind.member_cap()
));
}
if raid_slots.len() != members.len() {
return Err(format!(
"group mirror has {} Raid Slots for {} members",
Expand Down Expand Up @@ -2445,8 +2450,8 @@ fn mirror_slots_differ(
}

/// Acknowledged Realm-core party authority read for one companion command attempt. Companion
/// Orders cover the whole Raid: both lists are bounded at the Raid cap, and a longer list is a
/// damaged mirror.
/// Orders cover the whole Raid: both lists are bounded at the member cap of the Group's kind, and a
/// longer list is a damaged mirror.
#[reducer]
pub fn admit_party_command_authority(
ctx: &ReducerContext,
Expand All @@ -2457,38 +2462,35 @@ pub fn admit_party_command_authority(
mut expected_members: Vec<u64>,
) -> Result<(), String> {
crate::helpers::require_operator(ctx)?;
let stale = || {
crate::bridge::CommandOutcome::StalePartyMirror
.tag()
.to_string()
};
let group = ctx
.db
.game_group()
.group_id()
.find(group_id)
.ok_or_else(|| {
crate::bridge::CommandOutcome::StalePartyMirror
.tag()
.to_string()
})?;
.ok_or_else(stale)?;
if group.leader_guid != leader_guid {
return Err(crate::bridge::CommandOutcome::NotLeader.tag().to_string());
}
let members = ctx.db.game_group_member();
let mut current_members: Vec<_> = members
.by_group()
.filter(&group_id)
.take(RAID_MAX_MEMBERS + 1)
let kind = group_kind_of(&group);
if expected_members.len() > kind.member_cap() {
return Err(stale());
}
let mut current_members: Vec<_> = bounded_roster(ctx, group_id, kind)
.ok_or_else(stale)?
.into_iter()
.map(|member| member.character_guid)
.collect();
if current_members.len() > RAID_MAX_MEMBERS || expected_members.len() > RAID_MAX_MEMBERS {
return Err(crate::bridge::CommandOutcome::StalePartyMirror
.tag()
.to_string());
}
current_members.sort_unstable();
expected_members.sort_unstable();
if current_members != expected_members {
return Err(crate::bridge::CommandOutcome::StalePartyMirror
.tag()
.to_string());
return Err(stale());
}
let members = ctx.db.game_group_member();
for guid in [leader_guid, bot_guid, authority_member_guid]
.into_iter()
.filter(|guid| *guid != 0)
Expand Down
Loading
Loading