use crate::context::ResourceContext;
use crate::event::EventBus;
use super::config::ReputationConfig;
use super::events::*;
use super::hook::ReputationHook;
use super::state::ReputationState;
pub struct ReputationSystem<H: ReputationHook> {
hook: H,
}
impl<H: ReputationHook> ReputationSystem<H> {
pub fn new(hook: H) -> Self {
Self { hook }
}
pub async fn process_events(&mut self, resources: &mut ResourceContext) {
let change_requests = {
if let Some(mut bus) = resources.get_mut::<EventBus>().await {
let reader = bus.reader::<ReputationChangeRequested>();
reader.iter().cloned().collect::<Vec<_>>()
} else {
Vec::new()
}
};
let set_requests = {
if let Some(mut bus) = resources.get_mut::<EventBus>().await {
let reader = bus.reader::<ReputationSetRequested>();
reader.iter().cloned().collect::<Vec<_>>()
} else {
Vec::new()
}
};
for request in change_requests {
self.process_change_request(request, resources).await;
}
for request in set_requests {
self.process_set_request(request, resources).await;
}
}
async fn process_change_request(
&mut self,
request: ReputationChangeRequested,
resources: &mut ResourceContext,
) {
let subject_id = request.subject_id;
let category = request.category.as_deref();
if let Err(_reason) = self
.hook
.validate_change(&subject_id, request.delta, category, resources)
.await
{
return;
}
let effective_delta = self
.hook
.calculate_delta(&subject_id, request.delta, category, resources)
.await;
let (old_score, old_threshold_name) = {
let config = match resources.get::<ReputationConfig>().await {
Some(c) => c,
None => return,
};
let state = match resources.get::<ReputationState>().await {
Some(s) => s,
None => return,
};
let old_score = match category {
Some(cat) => state
.get_category(&subject_id, cat)
.unwrap_or(config.default_score),
None => state.get(&subject_id).unwrap_or(config.default_score),
};
let old_threshold = config.get_threshold(old_score).map(|t| t.name.clone());
(old_score, old_threshold)
};
let new_score = {
let config = resources.get::<ReputationConfig>().await.unwrap();
let mut state = resources.get_mut::<ReputationState>().await.unwrap();
let (_, new_score) = match category {
Some(cat) => state.adjust_category(
&subject_id,
cat.to_string(),
effective_delta,
config.default_score,
),
None => state.adjust(&subject_id, effective_delta, config.default_score),
};
if config.auto_clamp {
if let Some((min, max)) = config.score_range {
match category {
Some(cat) => {
state.clamp_score_category(&subject_id, cat, min, max);
}
None => {
state.clamp_score(&subject_id, min, max);
}
}
}
}
match category {
Some(cat) => state.get_category(&subject_id, cat).unwrap_or(new_score),
None => state.get(&subject_id).unwrap_or(new_score),
}
};
self.hook
.on_reputation_changed(
&subject_id,
old_score,
new_score,
effective_delta,
category,
resources,
)
.await;
let new_threshold_name = {
let config = resources.get::<ReputationConfig>().await.unwrap();
config.get_threshold(new_score).map(|t| t.name.clone())
};
if old_threshold_name != new_threshold_name {
if let Some(new_threshold_name) = &new_threshold_name {
let config = resources.get::<ReputationConfig>().await.unwrap();
let old_threshold = old_threshold_name
.as_ref()
.and_then(|name| config.thresholds.iter().find(|t| &t.name == name));
let new_threshold = config
.thresholds
.iter()
.find(|t| &t.name == new_threshold_name);
if let Some(new_threshold) = new_threshold {
self.hook
.on_threshold_crossed(&subject_id, old_threshold, new_threshold, resources)
.await;
}
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
bus.publish(ReputationThresholdCrossedEvent {
subject_id: subject_id.clone(),
old_threshold: old_threshold_name.clone(),
new_threshold: new_threshold_name.clone(),
score: new_score,
category: category.map(|s| s.to_string()),
});
}
}
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
bus.publish(ReputationChangedEvent {
subject_id,
old_score,
new_score,
delta: effective_delta,
category: request.category,
reason: request.reason,
});
}
async fn process_set_request(
&mut self,
request: ReputationSetRequested,
resources: &mut ResourceContext,
) {
let subject_id = request.subject_id;
let category = request.category.as_deref();
let old_score = {
let config = match resources.get::<ReputationConfig>().await {
Some(c) => c,
None => return,
};
let state = match resources.get::<ReputationState>().await {
Some(s) => s,
None => return,
};
match category {
Some(cat) => state
.get_category(&subject_id, cat)
.unwrap_or(config.default_score),
None => state.get(&subject_id).unwrap_or(config.default_score),
}
};
let delta = request.score - old_score;
if let Err(_reason) = self
.hook
.validate_change(&subject_id, delta, category, resources)
.await
{
return;
}
let old_threshold_name = {
let config = resources.get::<ReputationConfig>().await.unwrap();
config.get_threshold(old_score).map(|t| t.name.clone())
};
{
let mut state = resources.get_mut::<ReputationState>().await.unwrap();
match category {
Some(cat) => {
state.set_category(&subject_id, cat.to_string(), request.score);
}
None => {
state.set(&subject_id, request.score);
}
}
let config = resources.get::<ReputationConfig>().await.unwrap();
if config.auto_clamp {
if let Some((min, max)) = config.score_range {
match category {
Some(cat) => {
state.clamp_score_category(&subject_id, cat, min, max);
}
None => {
state.clamp_score(&subject_id, min, max);
}
}
}
}
};
let new_score = request.score;
self.hook
.on_reputation_changed(
&subject_id,
old_score,
new_score,
delta,
category,
resources,
)
.await;
let new_threshold_name = {
let config = resources.get::<ReputationConfig>().await.unwrap();
config.get_threshold(new_score).map(|t| t.name.clone())
};
if old_threshold_name != new_threshold_name {
if let Some(new_threshold_name) = &new_threshold_name {
let config = resources.get::<ReputationConfig>().await.unwrap();
let old_threshold = old_threshold_name
.as_ref()
.and_then(|name| config.thresholds.iter().find(|t| &t.name == name));
let new_threshold = config
.thresholds
.iter()
.find(|t| &t.name == new_threshold_name);
if let Some(new_threshold) = new_threshold {
self.hook
.on_threshold_crossed(&subject_id, old_threshold, new_threshold, resources)
.await;
}
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
bus.publish(ReputationThresholdCrossedEvent {
subject_id: subject_id.clone(),
old_threshold: old_threshold_name.clone(),
new_threshold: new_threshold_name.clone(),
score: new_score,
category: category.map(|s| s.to_string()),
});
}
}
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
bus.publish(ReputationChangedEvent {
subject_id,
old_score,
new_score,
delta,
category: request.category,
reason: None,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugin::reputation::hook::DefaultReputationHook;
use crate::plugin::reputation::types::*;
#[tokio::test]
async fn test_system_process_change_request() {
let mut resources = ResourceContext::new();
resources.insert(EventBus::new());
resources.insert(ReputationConfig::default());
resources.insert(ReputationState::new());
let mut system = ReputationSystem::new(DefaultReputationHook);
{
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
bus.publish(ReputationChangeRequested {
subject_id: SubjectId::new("player", "kingdom"),
delta: 15.0,
category: None,
reason: Some("Completed quest".into()),
});
bus.dispatch();
}
system.process_events(&mut resources).await;
let state = resources.get::<ReputationState>().await.unwrap();
let score = state.get(&SubjectId::new("player", "kingdom")).unwrap();
assert_eq!(score, 15.0);
{
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
bus.dispatch();
}
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
let reader = bus.reader::<ReputationChangedEvent>();
let events: Vec<_> = reader.iter().cloned().collect();
assert_eq!(events.len(), 1);
assert_eq!(events[0].new_score, 15.0);
assert_eq!(events[0].delta, 15.0);
}
#[tokio::test]
async fn test_system_process_set_request() {
let mut resources = ResourceContext::new();
resources.insert(EventBus::new());
resources.insert(ReputationConfig::default());
resources.insert(ReputationState::new());
let mut system = ReputationSystem::new(DefaultReputationHook);
{
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
bus.publish(ReputationSetRequested {
subject_id: SubjectId::new("player", "kingdom"),
score: 75.0,
category: None,
});
bus.dispatch();
}
system.process_events(&mut resources).await;
let state = resources.get::<ReputationState>().await.unwrap();
let score = state.get(&SubjectId::new("player", "kingdom")).unwrap();
assert_eq!(score, 75.0);
}
#[tokio::test]
async fn test_system_threshold_crossing() {
let mut resources = ResourceContext::new();
resources.insert(EventBus::new());
let mut config = ReputationConfig::default();
config.add_threshold(ReputationThreshold::new("Neutral", -10.0, 10.0));
config.add_threshold(ReputationThreshold::new("Friendly", 10.0, 50.0));
resources.insert(config);
resources.insert(ReputationState::new());
let mut system = ReputationSystem::new(DefaultReputationHook);
{
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
bus.publish(ReputationChangeRequested {
subject_id: SubjectId::new("player", "kingdom"),
delta: 15.0,
category: None,
reason: None,
});
bus.dispatch();
}
system.process_events(&mut resources).await;
{
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
bus.dispatch();
}
let mut bus = resources.get_mut::<EventBus>().await.unwrap();
let reader = bus.reader::<ReputationThresholdCrossedEvent>();
let events: Vec<_> = reader.iter().cloned().collect();
assert_eq!(events.len(), 1);
assert_eq!(events[0].old_threshold, Some("Neutral".into()));
assert_eq!(events[0].new_threshold, "Friendly");
}
}