Files
yourpart-daemon/src/worker/character_creation.rs

494 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::db::{ConnectionPool, DbError, Rows};
use crate::message_broker::MessageBroker;
use rand::distributions::{Distribution, Uniform};
use rand::rngs::StdRng;
use rand::{thread_rng, Rng, SeedableRng};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED,
QUERY_GET_TOWN_REGION_IDS,
QUERY_LOAD_FIRST_NAMES,
QUERY_LOAD_LAST_NAMES,
QUERY_INSERT_CHARACTER,
QUERY_GET_ELIGIBLE_NPC_FOR_DEATH,
QUERY_DELETE_DIRECTOR,
QUERY_DELETE_RELATIONSHIP,
QUERY_DELETE_CHILD_RELATION,
QUERY_INSERT_NOTIFICATION,
QUERY_MARK_CHARACTER_DECEASED,
};
pub struct CharacterCreationWorker {
pub(crate) base: BaseWorker,
rng: StdRng,
dist: Uniform<i32>,
first_name_cache: HashMap<String, HashSet<i32>>,
last_name_cache: HashSet<i32>,
death_check_running: Arc<AtomicBool>,
death_thread: Option<thread::JoinHandle<()>>,
}
impl CharacterCreationWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
Self::new_internal(pool, broker, true)
}
/// Interner Konstruktor, der optional den NPC-Todes-Monitor startet.
fn new_internal(pool: ConnectionPool, broker: MessageBroker, start_death_thread: bool) -> Self {
let base = BaseWorker::new("CharacterCreationWorker", pool.clone(), broker.clone());
let rng = StdRng::from_entropy();
let dist = Uniform::from(2..=3);
let death_check_running = Arc::new(AtomicBool::new(start_death_thread));
let death_thread = if start_death_thread {
let death_flag = Arc::clone(&death_check_running);
let pool_clone = pool;
let broker_clone = broker;
Some(thread::spawn(move || {
while death_flag.load(Ordering::Relaxed) {
if let Err(err) =
CharacterCreationWorker::monitor_character_deaths(&pool_clone, &broker_clone)
{
eprintln!(
"[CharacterCreationWorker] Fehler beim Überprüfen von NPC-Todesfällen: {err}"
);
}
// Warte 1 Stunde, aber mit frühem Abbruch, wenn death_flag false wird
for _ in 0..3600 {
if !death_flag.load(Ordering::Relaxed) {
break;
}
thread::sleep(Duration::from_secs(1));
}
}
}))
} else {
None
};
Self {
base,
rng,
dist,
first_name_cache: HashMap::new(),
last_name_cache: HashSet::new(),
death_check_running,
death_thread,
}
}
/// Variante ohne separaten Todes-Monitor-Thread wird nur in der Worker-Loop benutzt.
fn new_for_loop(pool: ConnectionPool, broker: MessageBroker) -> Self {
Self::new_internal(pool, broker, false)
}
fn is_today_character_created(&self) -> bool {
match self.fetch_today_characters() {
Ok(rows) => !rows.is_empty(),
Err(err) => {
eprintln!(
"[CharacterCreationWorker] Fehler in is_today_character_created: {err}"
);
false
}
}
}
fn fetch_today_characters(&self) -> Result<Rows, crate::db::DbError> {
const STMT_NAME: &str = "is_previous_day_character_created";
let mut conn = self
.base
.pool
.get()
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(STMT_NAME, QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED)?;
conn.execute(STMT_NAME, &[])
}
fn create_characters_for_today(&mut self) {
self.load_names();
if self.first_name_cache.is_empty() || self.last_name_cache.is_empty() {
eprintln!(
"[CharacterCreationWorker] Fehler: Namen konnten nicht geladen werden (Stub-Implementierung)."
);
return;
}
let town_ids = self.get_town_region_ids();
for region_id in town_ids {
self.create_characters_for_region(region_id);
}
}
fn create_characters_for_region(&mut self, region_id: i32) {
let nobility_stands = [1, 2, 3];
let genders = ["male", "female"];
for &nobility in &nobility_stands {
for &gender in &genders {
let num_chars = self.rng.sample(self.dist);
for _ in 0..num_chars {
self.create_character(region_id, gender, nobility);
}
}
}
}
fn create_character(&mut self, region_id: i32, gender: &str, title_of_nobility: i32) {
let first_set = self
.first_name_cache
.get(gender)
.cloned()
.unwrap_or_default();
let first_name_id = Self::get_random_from_set(&first_set);
if first_name_id == -1 {
eprintln!("[CharacterCreationWorker] Fehler: Kein passender Vorname gefunden.");
return;
}
let last_name_id = Self::get_random_from_set(&self.last_name_cache);
if last_name_id == -1 {
eprintln!("[CharacterCreationWorker] Fehler: Kein passender Nachname gefunden.");
return;
}
if let Err(err) = Self::insert_character(
&self.base.pool,
region_id,
first_name_id,
last_name_id,
gender,
title_of_nobility,
) {
eprintln!("[CharacterCreationWorker] Fehler in createCharacter: {err}");
}
}
fn get_town_region_ids(&self) -> Vec<i32> {
match self.load_town_region_ids() {
Ok(rows) => rows
.into_iter()
.filter_map(|row| row.get("id")?.parse::<i32>().ok())
.collect(),
Err(err) => {
eprintln!(
"[CharacterCreationWorker] Fehler in getTownRegionIds: {err}"
);
Vec::new()
}
}
}
fn load_town_region_ids(&self) -> Result<Rows, crate::db::DbError> {
const STMT_NAME: &str = "get_town_region_ids";
let mut conn = self
.base
.pool
.get()
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(STMT_NAME, QUERY_GET_TOWN_REGION_IDS)?;
conn.execute(STMT_NAME, &[])
}
fn load_names(&mut self) {
if (self.first_name_cache.is_empty() || self.last_name_cache.is_empty())
&& let Err(err) = self.load_first_and_last_names()
{
eprintln!("[CharacterCreationWorker] Fehler in loadNames: {err}");
}
}
fn load_first_and_last_names(&mut self) -> Result<(), crate::db::DbError> {
let mut conn = self
.base
.pool
.get()
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
// Vornamen
conn.prepare("load_first_names", QUERY_LOAD_FIRST_NAMES)?;
let first_rows = conn.execute("load_first_names", &[])?;
for row in first_rows {
let id = match row.get("id").and_then(|v| v.parse::<i32>().ok()) {
Some(id) => id,
None => continue,
};
let gender = row.get("gender").cloned().unwrap_or_default();
self.first_name_cache.entry(gender).or_default().insert(id);
}
// Nachnamen
conn.prepare("load_last_names", QUERY_LOAD_LAST_NAMES)?;
let last_rows = conn.execute("load_last_names", &[])?;
for row in last_rows {
if let Some(id) = row.get("id").and_then(|v| v.parse::<i32>().ok()) {
self.last_name_cache.insert(id);
}
}
Ok(())
}
fn get_random_from_set(set: &HashSet<i32>) -> i32 {
if set.is_empty() {
return -1;
}
let mut rng = thread_rng();
let idx = rng.gen_range(0..set.len());
*set.iter().nth(idx).unwrap_or(&-1)
}
fn run_iteration(&mut self, state: &WorkerState) {
self.base
.set_current_step("Check if previous day character was created");
if !self.is_today_character_created() {
self.base
.set_current_step("Create characters for today");
self.create_characters_for_today();
}
self.sleep_one_minute(state);
}
fn sleep_one_minute(&self, state: &WorkerState) {
self.base
.set_current_step("Sleep for 60 seconds");
for _ in 0..60 {
if !state.running_worker.load(Ordering::Relaxed) {
break;
}
thread::sleep(Duration::from_secs(1));
}
self.base.set_current_step("Loop done");
}
}
impl Worker for CharacterCreationWorker {
fn start_worker_thread(&mut self) {
let pool = self.base.pool.clone();
let broker = self.base.broker.clone();
self.base
.start_worker_with_loop(move |state: Arc<WorkerState>| {
let mut worker = CharacterCreationWorker::new_for_loop(pool.clone(), broker.clone());
while state.running_worker.load(Ordering::Relaxed) {
worker.run_iteration(&state);
}
});
}
fn stop_worker_thread(&mut self) {
self.base.stop_worker();
}
fn enable_watchdog(&mut self) {
self.base.start_watchdog();
}
}
impl Drop for CharacterCreationWorker {
fn drop(&mut self) {
self.death_check_running
.store(false, Ordering::Relaxed);
if let Some(handle) = self.death_thread.take() {
let _ = handle.join();
}
}
}
// Zusätzliche Logik: NPC-Todesfälle überwachen und verarbeiten
impl CharacterCreationWorker {
fn insert_character(
pool: &ConnectionPool,
region_id: i32,
first_name_id: i32,
last_name_id: i32,
gender: &str,
title_of_nobility: i32,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("insert_character", QUERY_INSERT_CHARACTER)?;
conn.execute(
"insert_character",
&[
&region_id,
&first_name_id,
&last_name_id,
&gender,
&title_of_nobility,
],
)?;
Ok(())
}
fn monitor_character_deaths(
pool: &ConnectionPool,
broker: &MessageBroker,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"get_eligible_npc_for_death",
QUERY_GET_ELIGIBLE_NPC_FOR_DEATH,
)?;
let rows = conn.execute("get_eligible_npc_for_death", &[])?;
for row in rows {
let character_id = row
.get("id")
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1);
let age = row
.get("age")
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(0);
if character_id > 0 && Self::calculate_death_probability(age)
&& let Err(err) = Self::handle_character_death(pool, broker, character_id)
{
eprintln!(
"[CharacterCreationWorker] Fehler beim Bearbeiten des NPC-Todes (id={character_id}): {err}"
);
}
}
Ok(())
}
fn calculate_death_probability(age: i32) -> bool {
if age < 60 {
return false;
}
let base_probability = 0.01_f64;
let increase_per_year = 0.01_f64;
let death_probability =
base_probability + increase_per_year * (age.saturating_sub(60) as f64);
let mut rng = thread_rng();
let dist = Uniform::from(0.0..1.0);
let roll: f64 = dist.sample(&mut rng);
roll < death_probability
}
fn handle_character_death(
pool: &ConnectionPool,
broker: &MessageBroker,
character_id: i32,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
// 1) Director löschen und User benachrichtigen
conn.prepare("delete_director", QUERY_DELETE_DIRECTOR)?;
let dir_result = conn.execute("delete_director", &[&character_id])?;
if let Some(row) = dir_result.first()
&& let Some(user_id) = row
.get("employer_user_id")
.and_then(|v| v.parse::<i32>().ok())
{
Self::notify_user(pool, broker, user_id, "director_death")?;
}
// 2) Relationships löschen und betroffene User benachrichtigen
conn.prepare("delete_relationship", QUERY_DELETE_RELATIONSHIP)?;
let rel_result = conn.execute("delete_relationship", &[&character_id])?;
for row in rel_result {
if let Some(related_user_id) = row
.get("related_user_id")
.and_then(|v| v.parse::<i32>().ok())
{
Self::notify_user(pool, broker, related_user_id, "relationship_death")?;
}
}
// 3) Child-Relations löschen und Eltern benachrichtigen
conn.prepare("delete_child_relation", QUERY_DELETE_CHILD_RELATION)?;
let child_result = conn.execute("delete_child_relation", &[&character_id])?;
for row in child_result {
if let Some(father_user_id) = row
.get("father_user_id")
.and_then(|v| v.parse::<i32>().ok())
{
Self::notify_user(pool, broker, father_user_id, "child_death")?;
}
if let Some(mother_user_id) = row
.get("mother_user_id")
.and_then(|v| v.parse::<i32>().ok())
{
Self::notify_user(pool, broker, mother_user_id, "child_death")?;
}
}
// 4) Charakter als verstorben markieren
Self::mark_character_as_deceased(pool, character_id)?;
Ok(())
}
fn notify_user(
pool: &ConnectionPool,
broker: &MessageBroker,
user_id: i32,
event_type: &str,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("insert_notification", QUERY_INSERT_NOTIFICATION)?;
conn.execute("insert_notification", &[&user_id])?;
// falukantUpdateStatus
let update_message =
format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
broker.publish(update_message);
// ursprüngliche Benachrichtigung
let message =
format!(r#"{{"event":"{event_type}","user_id":{}}}"#, user_id);
broker.publish(message);
Ok(())
}
fn mark_character_as_deceased(
pool: &ConnectionPool,
character_id: i32,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("mark_character_deceased", QUERY_MARK_CHARACTER_DECEASED)?;
conn.execute("mark_character_deceased", &[&character_id])?;
Ok(())
}
}