use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::abi::{EntityId, TypeCode};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub(crate) struct ResourceLedger {
entities: BTreeSet<EntityId>,
component_sizes: BTreeMap<(EntityId, TypeCode), u64>,
total_bytes: u64,
}
impl ResourceLedger {
pub(crate) fn new() -> Self {
Self::default()
}
#[inline]
pub(crate) fn total_bytes(&self) -> u64 {
self.total_bytes
}
#[cfg_attr(not(test), allow(dead_code))]
#[inline]
pub(crate) fn total_entities(&self) -> u32 {
u32::try_from(self.entities.len()).unwrap_or(u32::MAX)
}
pub(crate) fn component_size(&self, entity: EntityId, tc: TypeCode) -> Option<u64> {
self.component_sizes.get(&(entity, tc)).copied()
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn entity_bytes(&self, id: EntityId) -> u64 {
self.component_sizes
.range((id, TypeCode(0))..=(id, TypeCode(u32::MAX)))
.fold(0u64, |acc, (_, size)| acc.saturating_add(*size))
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn type_count(&self, tc: TypeCode) -> u32 {
let n = self
.component_sizes
.keys()
.filter(|(_, t)| *t == tc)
.count();
u32::try_from(n).unwrap_or(u32::MAX)
}
pub(crate) fn add_entity(&mut self, id: EntityId) -> bool {
self.entities.insert(id)
}
pub(crate) fn remove_entity(&mut self, id: EntityId) -> u64 {
if !self.entities.remove(&id) {
return 0;
}
let keys: Vec<(EntityId, TypeCode)> = self
.component_sizes
.range((id, TypeCode(0))..=(id, TypeCode(u32::MAX)))
.map(|(k, _)| *k)
.collect();
let mut freed = 0u64;
for k in keys {
if let Some(size) = self.component_sizes.remove(&k) {
freed = freed.saturating_add(size);
self.total_bytes = self.total_bytes.saturating_sub(size);
}
}
freed
}
pub(crate) fn add_component(&mut self, entity: EntityId, tc: TypeCode, size: u64) -> bool {
if !self.entities.contains(&entity) {
return false;
}
match self.component_sizes.insert((entity, tc), size) {
Some(old) if size >= old => {
self.total_bytes = self.total_bytes.saturating_add(size - old);
}
Some(old) => {
self.total_bytes = self.total_bytes.saturating_sub(old - size);
}
None => {
self.total_bytes = self.total_bytes.saturating_add(size);
}
}
true
}
pub(crate) fn remove_component(&mut self, entity: EntityId, tc: TypeCode, _size: u64) -> bool {
if !self.entities.contains(&entity) {
return false;
}
if let Some(old) = self.component_sizes.remove(&(entity, tc)) {
self.total_bytes = self.total_bytes.saturating_sub(old);
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
fn e(n: u64) -> EntityId {
EntityId::new(n).unwrap()
}
fn t(n: u32) -> TypeCode {
TypeCode(n)
}
#[test]
fn empty_ledger_zeroes() {
let l = ResourceLedger::new();
assert_eq!(l.total_entities(), 0);
assert_eq!(l.total_bytes(), 0);
assert_eq!(l.entity_bytes(e(1)), 0);
assert_eq!(l.type_count(t(1)), 0);
}
#[test]
fn add_entity_increments_total() {
let mut l = ResourceLedger::new();
assert!(l.add_entity(e(1)));
assert!(l.add_entity(e(2)));
assert_eq!(l.total_entities(), 2);
}
#[test]
fn add_entity_idempotent() {
let mut l = ResourceLedger::new();
assert!(l.add_entity(e(1)));
assert!(!l.add_entity(e(1)));
assert_eq!(l.total_entities(), 1);
}
#[test]
fn add_component_updates_bytes_and_count() {
let mut l = ResourceLedger::new();
l.add_entity(e(1));
assert!(l.add_component(e(1), t(10), 100));
assert_eq!(l.entity_bytes(e(1)), 100);
assert_eq!(l.total_bytes(), 100);
assert_eq!(l.type_count(t(10)), 1);
}
#[test]
fn add_component_to_unknown_entity_is_noop() {
let mut l = ResourceLedger::new();
assert!(!l.add_component(e(1), t(10), 100));
assert_eq!(l.total_bytes(), 0);
assert_eq!(l.type_count(t(10)), 0);
}
#[test]
fn remove_component_balances_add() {
let mut l = ResourceLedger::new();
l.add_entity(e(1));
l.add_component(e(1), t(10), 100);
l.add_component(e(1), t(20), 50);
assert_eq!(l.total_bytes(), 150);
l.remove_component(e(1), t(10), 100);
assert_eq!(l.total_bytes(), 50);
assert_eq!(l.entity_bytes(e(1)), 50);
assert_eq!(l.type_count(t(10)), 0);
assert_eq!(l.type_count(t(20)), 1);
}
#[test]
fn remove_entity_returns_bytes_and_drops_total() {
let mut l = ResourceLedger::new();
l.add_entity(e(1));
l.add_component(e(1), t(10), 100);
l.add_component(e(1), t(20), 50);
let bytes = l.remove_entity(e(1));
assert_eq!(bytes, 150);
assert_eq!(l.total_entities(), 0);
assert_eq!(l.total_bytes(), 0);
assert_eq!(l.entity_bytes(e(1)), 0);
}
#[test]
fn remove_unknown_entity_is_noop() {
let mut l = ResourceLedger::new();
assert_eq!(l.remove_entity(e(999)), 0);
}
#[test]
fn add_remove_balanced_yields_empty() {
let mut l = ResourceLedger::new();
l.add_entity(e(1));
l.add_component(e(1), t(10), 100);
l.remove_component(e(1), t(10), 100);
l.remove_entity(e(1));
assert_eq!(l.total_bytes(), 0);
assert_eq!(l.total_entities(), 0);
}
#[test]
fn type_count_aggregates_across_entities() {
let mut l = ResourceLedger::new();
l.add_entity(e(1));
l.add_entity(e(2));
l.add_component(e(1), t(7), 10);
l.add_component(e(2), t(7), 20);
assert_eq!(l.type_count(t(7)), 2);
assert_eq!(l.total_bytes(), 30);
}
#[test]
fn type_count_underflow_is_saturating() {
let mut l = ResourceLedger::new();
l.add_entity(e(1));
l.remove_component(e(1), t(99), 50); assert_eq!(l.type_count(t(99)), 0);
}
#[test]
fn set_component_replace_adjusts_by_delta_no_double_count() {
let mut l = ResourceLedger::new();
l.add_entity(e(1));
assert!(l.add_component(e(1), t(7), 100));
assert!(l.add_component(e(1), t(7), 400)); assert_eq!(l.total_bytes(), 400);
assert_eq!(l.entity_bytes(e(1)), 400);
assert_eq!(l.type_count(t(7)), 1);
assert!(l.add_component(e(1), t(7), 50)); assert_eq!(l.total_bytes(), 50);
assert_eq!(l.type_count(t(7)), 1);
}
#[test]
fn remove_entity_cascades_component_accounting() {
let mut l = ResourceLedger::new();
l.add_entity(e(1));
l.add_component(e(1), t(7), 100);
l.add_component(e(1), t(9), 50);
assert_eq!(l.total_bytes(), 150);
assert_eq!(l.type_count(t(7)), 1);
assert_eq!(l.type_count(t(9)), 1);
assert_eq!(l.remove_entity(e(1)), 150);
assert_eq!(l.total_bytes(), 0);
assert_eq!(l.total_entities(), 0);
assert_eq!(l.type_count(t(7)), 0);
assert_eq!(l.type_count(t(9)), 0);
}
#[test]
fn remove_component_uses_stored_size_not_caller_size() {
let mut l = ResourceLedger::new();
l.add_entity(e(1));
l.add_component(e(1), t(7), 100);
assert!(l.remove_component(e(1), t(7), 999_999)); assert_eq!(l.total_bytes(), 0);
assert_eq!(l.entity_bytes(e(1)), 0);
}
}