pub extern crate inventory;
pub struct EventPayloadRegistration {
pub kind_bits: u16,
pub payload_version: u16,
pub type_name: &'static str,
}
inventory::collect!(EventPayloadRegistration);
pub struct UpcastRegistration {
pub kind_bits: u16,
pub from_version: u16,
pub step: fn(rmpv::Value) -> Result<rmpv::Value, Box<dyn std::error::Error + Send + Sync>>,
}
inventory::collect!(UpcastRegistration);
pub fn upcast_steps_for(kind_bits: u16) -> Vec<&'static UpcastRegistration> {
inventory::iter::<UpcastRegistration>()
.filter(|reg| reg.kind_bits == kind_bits)
.collect()
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IncompleteUpcastChain {
pub kind_bits: u16,
pub current_version: u16,
pub missing_from_versions: Vec<u16>,
pub type_name: &'static str,
}
pub fn find_incomplete_upcast_chains() -> Vec<IncompleteUpcastChain> {
let mut declared: std::collections::HashMap<u16, (u16, &'static str)> =
std::collections::HashMap::new();
for reg in inventory::iter::<EventPayloadRegistration>() {
declared
.entry(reg.kind_bits)
.and_modify(|current| {
if reg.payload_version > current.0 {
*current = (reg.payload_version, reg.type_name);
}
})
.or_insert((reg.payload_version, reg.type_name));
}
let mut out = Vec::new();
for (kind_bits, (current_version, type_name)) in declared {
if current_version <= 1 {
continue;
}
let registered: std::collections::HashSet<u16> = inventory::iter::<UpcastRegistration>()
.filter(|step| step.kind_bits == kind_bits)
.map(|step| step.from_version)
.collect();
let missing_from_versions: Vec<u16> = (1..current_version)
.filter(|hop| !registered.contains(hop))
.collect();
if !missing_from_versions.is_empty() {
out.push(IncompleteUpcastChain {
kind_bits,
current_version,
missing_from_versions,
type_name,
});
}
}
out.sort_by(|a, b| a.kind_bits.cmp(&b.kind_bits));
out
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct EventKindCollision {
pub kind_bits: u16,
pub first_type_name: &'static str,
pub second_type_name: &'static str,
}
pub fn find_kind_collisions() -> Vec<EventKindCollision> {
let mut seen: std::collections::HashMap<u16, &'static str> = std::collections::HashMap::new();
let mut collisions = Vec::new();
for item in inventory::iter::<EventPayloadRegistration>() {
if let Some(prior) = seen.insert(item.kind_bits, item.type_name) {
collisions.push(EventKindCollision {
kind_bits: item.kind_bits,
first_type_name: prior,
second_type_name: item.type_name,
});
}
}
collisions
}
pub fn assert_no_kind_collisions() {
let first = find_kind_collisions().into_iter().next();
assert!(
first.is_none(),
"batpak EventKind collision detected.\n\
Types `{prior}` and `{ty}` share the same (category, type_id) \
pair (kind_bits = 0x{bits:04X}).\n\
Each EventPayload type must have a unique kind within a binary.",
prior = first.as_ref().map_or("", |c| c.first_type_name),
ty = first.as_ref().map_or("", |c| c.second_type_name),
bits = first.as_ref().map_or(0, |c| c.kind_bits),
);
}
pub fn scan_for_kind_collisions() {
assert_no_kind_collisions();
}
pub const TYPE_ID_MAX: u16 = 0x0FFF;
pub fn validate_category(cat: u8) -> Result<(), &'static str> {
match cat {
0x0 => Err("category 0x0 is reserved for system events"),
0xD => Err("category 0xD is reserved for effect events"),
1..=15 => Ok(()),
_ => Err("category must fit in 4 bits (0x1–0xF, excluding 0x0 and 0xD)"),
}
}
pub fn validate_type_id(tid: u16) -> Result<(), &'static str> {
if tid > TYPE_ID_MAX {
Err("type_id must fit in 12 bits (0x000–0xFFF)")
} else {
Ok(())
}
}