Implement overproduction notification handling in ProduceWorker: Add logic to check for existing notifications and update them if necessary, or create a new notification if none exist. Introduce SQL queries for finding and updating overproduction notifications to enhance database interactions.
This commit is contained in:
250
YpDaemon/src/worker/base.rs
Normal file
250
YpDaemon/src/worker/base.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use crate::db::{ConnectionPool, DbError};
|
||||
use crate::worker::sql::{QUERY_UPDATE_MONEY, QUERY_GET_MONEY};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
pub trait Worker: Send {
|
||||
fn start_worker_thread(&mut self);
|
||||
fn stop_worker_thread(&mut self);
|
||||
fn enable_watchdog(&mut self);
|
||||
}
|
||||
|
||||
pub(crate) struct WorkerState {
|
||||
pub(crate) running_worker: AtomicBool,
|
||||
pub(crate) running_watchdog: AtomicBool,
|
||||
pub(crate) current_step: Mutex<String>,
|
||||
}
|
||||
|
||||
// Default tax percent and treasury user id used if no external config is available.
|
||||
// Percent, e.g. 10.0 => 10%.
|
||||
pub const DEFAULT_TAX_PERCENT: f64 = 10.0;
|
||||
pub const DEFAULT_TREASURY_USER_ID: i32 = 1;
|
||||
|
||||
impl WorkerState {
|
||||
pub(crate) fn new(name: &str) -> Self {
|
||||
Self {
|
||||
running_worker: AtomicBool::new(false),
|
||||
running_watchdog: AtomicBool::new(false),
|
||||
current_step: Mutex::new(format!("{name}: idle")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BaseWorker {
|
||||
pub name: String,
|
||||
pub pool: ConnectionPool,
|
||||
pub broker: MessageBroker,
|
||||
pub(crate) state: Arc<WorkerState>,
|
||||
worker_thread: Option<thread::JoinHandle<()>>,
|
||||
watchdog_thread: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl BaseWorker {
|
||||
pub fn new(name: &str, pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
pool,
|
||||
broker,
|
||||
state: Arc::new(WorkerState::new(name)),
|
||||
worker_thread: None,
|
||||
watchdog_thread: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_step<S: Into<String>>(&self, step: S) {
|
||||
if let Ok(mut guard) = self.state.current_step.lock() {
|
||||
*guard = step.into();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start_worker_with_loop<F>(&mut self, loop_fn: F)
|
||||
where
|
||||
F: Fn(Arc<WorkerState>) + Send + 'static,
|
||||
{
|
||||
if self.state.running_worker.swap(true, Ordering::SeqCst) {
|
||||
eprintln!("[{}] Worker thread already running, skipping start.", self.name);
|
||||
return;
|
||||
}
|
||||
|
||||
let state = Arc::clone(&self.state);
|
||||
|
||||
self.worker_thread = Some(thread::spawn(move || {
|
||||
loop_fn(state);
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn stop_worker(&mut self) {
|
||||
// Erst den Worker stoppen, dann auch den Watchdog beenden, damit keine
|
||||
// Hintergrund-Threads weiterlaufen.
|
||||
self.state.running_worker.store(false, Ordering::Relaxed);
|
||||
self.stop_watchdog();
|
||||
if let Some(handle) = self.worker_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start_watchdog(&mut self) {
|
||||
if self
|
||||
.state
|
||||
.running_watchdog
|
||||
.swap(true, Ordering::SeqCst)
|
||||
{
|
||||
eprintln!("[{}] Watchdog already enabled, skipping.", self.name);
|
||||
return;
|
||||
}
|
||||
|
||||
let state = Arc::clone(&self.state);
|
||||
|
||||
self.watchdog_thread = Some(thread::spawn(move || {
|
||||
while state.running_watchdog.load(Ordering::Relaxed) {
|
||||
// Nicht in einem großen 10s-Sleep blockieren, damit der
|
||||
// Shutdown (stop_watchdog) zügig reagieren kann. Stattdessen
|
||||
// in 1s-Scheiben schlafen und dazwischen das Flag prüfen.
|
||||
for _ in 0..10 {
|
||||
if !state.running_watchdog.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
|
||||
let step = state.current_step.lock().unwrap().clone();
|
||||
|
||||
// "idle"-Meldungen sind im Dauerbetrieb eher Spam und helfen
|
||||
// beim Debuggen selten. Deshalb nur loggen, wenn der Worker
|
||||
// sich nicht im Idle-Zustand befindet.
|
||||
if !step.ends_with(" idle") {
|
||||
// keine Info-Logs im Watchdog
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn stop_watchdog(&mut self) {
|
||||
self.state.running_watchdog.store(false, Ordering::Relaxed);
|
||||
if let Some(handle) = self.watchdog_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
|
||||
// Bei Bedarf kann hier später wieder ein expliziter Statuszugriff ergänzt werden.
|
||||
}
|
||||
|
||||
impl BaseWorker {
|
||||
/// Aktualisiert das Geld eines Falukant-Users über die DB-Funktion `falukant_data.update_money`.
|
||||
/// `action` entspricht dem Log-/Aktions-Tag (z.B. "credit pay rate", "debitor_prism").
|
||||
pub fn change_falukant_user_money(
|
||||
&self,
|
||||
falukant_user_id: i32,
|
||||
money_change: f64,
|
||||
action: &str,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = self
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
// Verwende parametrisierte Queries für Sicherheit gegen SQL-Injection
|
||||
conn.prepare("update_money", QUERY_UPDATE_MONEY)?;
|
||||
|
||||
// Validate float to avoid passing NaN/inf which the postgres client
|
||||
// may fail to serialize with an unclear error message.
|
||||
if !money_change.is_finite() {
|
||||
return Err(DbError::new(format!(
|
||||
"Ungültiger money_change: {} (not finite)",
|
||||
money_change
|
||||
)));
|
||||
}
|
||||
|
||||
// We must ensure the resulting money fits in numeric(10,2).
|
||||
// numeric(10,2) max absolute value is < 10^8 (100_000_000) before rounding.
|
||||
// Fetch current money for the user and clamp the delta if needed.
|
||||
conn.prepare("get_money_for_clamp", QUERY_GET_MONEY)?;
|
||||
let rows = conn.execute("get_money_for_clamp", &[&falukant_user_id])?;
|
||||
|
||||
let current_money: f64 = rows
|
||||
.first()
|
||||
.and_then(|r| r.get("money"))
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// compute tentative result
|
||||
let tentative = current_money + money_change;
|
||||
|
||||
// numeric(10,2) allows values with absolute < 10^8 (100_000_000)
|
||||
const MAX_ABS: f64 = 100_000_000.0 - 0.01; // leave room for scale
|
||||
|
||||
let adjusted_money_change = if tentative >= MAX_ABS {
|
||||
let clipped = MAX_ABS - current_money;
|
||||
eprintln!(
|
||||
"[BaseWorker] Clamping money_change: tentative {} exceeds numeric(10,2) max, clipping to {}",
|
||||
tentative, clipped
|
||||
);
|
||||
clipped
|
||||
} else if tentative <= -MAX_ABS {
|
||||
let clipped = -MAX_ABS - current_money;
|
||||
eprintln!(
|
||||
"[BaseWorker] Clamping money_change: tentative {} below min, clipping to {}",
|
||||
tentative, clipped
|
||||
);
|
||||
clipped
|
||||
} else {
|
||||
money_change
|
||||
};
|
||||
|
||||
// Send exact types matching the DB function signature:
|
||||
let uid_i32: i32 = falukant_user_id;
|
||||
let money_str = format!("{:.2}", adjusted_money_change);
|
||||
|
||||
// Note: we intentionally avoid parameterized call due to serialization
|
||||
// issues in this environment and instead execute a literal SQL below.
|
||||
fn escape_sql_literal(s: &str) -> String {
|
||||
s.replace('\'', "''")
|
||||
}
|
||||
let escaped_action = escape_sql_literal(action);
|
||||
let sql = format!(
|
||||
"SELECT falukant_data.update_money({uid}, {money}::numeric, '{act}');",
|
||||
uid = uid_i32,
|
||||
money = money_str,
|
||||
act = escaped_action
|
||||
);
|
||||
|
||||
let _ = conn.query(&sql)?;
|
||||
|
||||
// Best-effort: insert a money history entry so the UI/history views
|
||||
// can show the change even if the DB-function doesn't write it.
|
||||
// We don't want to fail the whole operation if this insert fails,
|
||||
// so log errors and continue.
|
||||
// Ensure money_history table exists (best-effort). If this fails,
|
||||
// we still don't want to abort the money update.
|
||||
let create_sql = r#"
|
||||
CREATE TABLE IF NOT EXISTS falukant_log.money_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
change NUMERIC(10,2) NOT NULL,
|
||||
action TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
"#;
|
||||
let _ = conn.query(create_sql);
|
||||
|
||||
let history_sql = format!(
|
||||
"INSERT INTO falukant_log.money_history (user_id, change, action, created_at) VALUES ({uid}, {money}::numeric, '{act}', NOW());",
|
||||
uid = uid_i32,
|
||||
money = money_str,
|
||||
act = escaped_action
|
||||
);
|
||||
if let Err(err) = conn.query(&history_sql) {
|
||||
eprintln!(
|
||||
"[BaseWorker] Warning: inserting money_history failed for user {}: {}",
|
||||
uid_i32, err
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
493
YpDaemon/src/worker/character_creation.rs
Normal file
493
YpDaemon/src/worker/character_creation.rs
Normal file
@@ -0,0 +1,493 @@
|
||||
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",
|
||||
&[
|
||||
®ion_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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1418
YpDaemon/src/worker/director.rs
Normal file
1418
YpDaemon/src/worker/director.rs
Normal file
File diff suppressed because it is too large
Load Diff
2046
YpDaemon/src/worker/events.rs
Normal file
2046
YpDaemon/src/worker/events.rs
Normal file
File diff suppressed because it is too large
Load Diff
881
YpDaemon/src/worker/user_character.rs
Normal file
881
YpDaemon/src/worker/user_character.rs
Normal file
@@ -0,0 +1,881 @@
|
||||
use crate::db::{ConnectionPool, DbError, Rows};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
use crate::worker::sql::{
|
||||
QUERY_GET_USERS_TO_UPDATE,
|
||||
QUERY_UPDATE_CHARACTERS_HEALTH,
|
||||
QUERY_UPDATE_MOOD,
|
||||
QUERY_UPDATE_GET_ITEMS_TO_UPDATE,
|
||||
QUERY_UPDATE_GET_CHARACTER_IDS,
|
||||
QUERY_UPDATE_KNOWLEDGE,
|
||||
QUERY_DELETE_LOG_ENTRY,
|
||||
QUERY_GET_OPEN_CREDITS,
|
||||
QUERY_UPDATE_CREDIT,
|
||||
QUERY_CLEANUP_CREDITS,
|
||||
QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM,
|
||||
QUERY_GET_CURRENT_MONEY,
|
||||
QUERY_GET_HOUSE_VALUE,
|
||||
QUERY_GET_SETTLEMENT_VALUE,
|
||||
QUERY_GET_INVENTORY_VALUE,
|
||||
QUERY_GET_CREDIT_DEBT,
|
||||
QUERY_COUNT_CHILDREN,
|
||||
QUERY_GET_HEIR,
|
||||
QUERY_RANDOM_HEIR,
|
||||
QUERY_SET_CHARACTER_USER,
|
||||
QUERY_UPDATE_USER_MONEY,
|
||||
QUERY_GET_FALUKANT_USER_ID,
|
||||
QUERY_AUTOBATISM,
|
||||
QUERY_GET_PREGNANCY_CANDIDATES,
|
||||
QUERY_INSERT_CHILD,
|
||||
QUERY_INSERT_CHILD_RELATION,
|
||||
QUERY_DELETE_DIRECTOR,
|
||||
QUERY_DELETE_RELATIONSHIP,
|
||||
QUERY_DELETE_CHILD_RELATION,
|
||||
QUERY_DELETE_KNOWLEDGE,
|
||||
QUERY_DELETE_DEBTORS_PRISM,
|
||||
QUERY_DELETE_POLITICAL_OFFICE,
|
||||
QUERY_DELETE_ELECTION_CANDIDATE,
|
||||
};
|
||||
|
||||
/// Vereinfachtes Abbild eines Characters aus `QUERY_GET_USERS_TO_UPDATE`.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Character {
|
||||
id: i32,
|
||||
age: i32,
|
||||
health: i32,
|
||||
}
|
||||
|
||||
pub struct UserCharacterWorker {
|
||||
base: BaseWorker,
|
||||
rng: StdRng,
|
||||
dist: Uniform<f64>,
|
||||
last_hourly_run: Option<Instant>,
|
||||
last_pregnancy_run: Option<Instant>,
|
||||
last_mood_run: Option<Instant>,
|
||||
}
|
||||
|
||||
// SQL moved to `src/worker/sql.rs`
|
||||
|
||||
impl UserCharacterWorker {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
let base = BaseWorker::new("UserCharacterWorker", pool, broker);
|
||||
let rng = StdRng::from_entropy();
|
||||
let dist = Uniform::from(0.0..1.0);
|
||||
|
||||
Self {
|
||||
base,
|
||||
rng,
|
||||
dist,
|
||||
last_hourly_run: None,
|
||||
last_pregnancy_run: None,
|
||||
last_mood_run: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_iteration(&mut self, state: &WorkerState) {
|
||||
self.base.set_current_step("UserCharacterWorker iteration");
|
||||
|
||||
self.maybe_run_hourly_tasks();
|
||||
self.maybe_run_mood_updates();
|
||||
self.maybe_run_daily_pregnancies();
|
||||
|
||||
// Entspricht in etwa der 1-Sekunden-Schleife im C++-Code
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
if let Err(err) = self.recalculate_knowledge() {
|
||||
eprintln!("[UserCharacterWorker] Fehler in recalculateKnowledge: {err}");
|
||||
}
|
||||
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
// worker stopping
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_run_hourly_tasks(&mut self) {
|
||||
let now = Instant::now();
|
||||
let should_run = match self.last_hourly_run {
|
||||
None => true,
|
||||
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(3600),
|
||||
};
|
||||
|
||||
if !should_run {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = self.run_hourly_tasks() {
|
||||
eprintln!("[UserCharacterWorker] Fehler in stündlichen Tasks: {err}");
|
||||
}
|
||||
|
||||
self.last_hourly_run = Some(now);
|
||||
}
|
||||
|
||||
fn run_hourly_tasks(&mut self) -> Result<(), DbError> {
|
||||
self.process_character_events()?;
|
||||
self.handle_credits()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn maybe_run_daily_pregnancies(&mut self) {
|
||||
let now = Instant::now();
|
||||
let should_run = match self.last_pregnancy_run {
|
||||
None => true,
|
||||
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(24 * 3600),
|
||||
};
|
||||
|
||||
if !should_run {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = self.process_pregnancies() {
|
||||
eprintln!("[UserCharacterWorker] Fehler in processPregnancies: {err}");
|
||||
}
|
||||
self.last_pregnancy_run = Some(now);
|
||||
}
|
||||
|
||||
fn process_character_events(&mut self) -> Result<(), DbError> {
|
||||
self.base.set_current_step("Get character data");
|
||||
|
||||
let rows = self.load_characters_to_update()?;
|
||||
let mut characters: Vec<Character> = rows
|
||||
.into_iter()
|
||||
.filter_map(Self::map_row_to_character)
|
||||
.collect();
|
||||
|
||||
for character in &mut characters {
|
||||
self.update_character_health(character)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_characters_to_update(&mut self) -> Result<Rows, DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_users_to_update", QUERY_GET_USERS_TO_UPDATE)?;
|
||||
conn.execute("get_users_to_update", &[])
|
||||
}
|
||||
|
||||
fn map_row_to_character(row: crate::db::Row) -> Option<Character> {
|
||||
Some(Character {
|
||||
id: row.get("id")?.parse().ok()?,
|
||||
age: row.get("age")?.parse().ok()?,
|
||||
health: row.get("health")?.parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn update_character_health(&mut self, character: &mut Character) -> Result<(), DbError> {
|
||||
let health_change = self.calculate_health_change(character.age);
|
||||
if health_change == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
character.health = std::cmp::max(0, character.health + health_change);
|
||||
|
||||
if character.health == 0 {
|
||||
self.handle_character_death(character.id)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(
|
||||
"update_characters_health",
|
||||
QUERY_UPDATE_CHARACTERS_HEALTH,
|
||||
)?;
|
||||
conn.execute(
|
||||
"update_characters_health",
|
||||
&[&character.health, &character.id],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_health_change(&mut self, age: i32) -> i32 {
|
||||
if age < 30 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if age >= 45 {
|
||||
let probability = (0.1 + (age - 45) as f64 * 0.02).min(1.0);
|
||||
if self.dist.sample(&mut self.rng) < probability {
|
||||
let damage_dist = Uniform::from(1..=10);
|
||||
return -damage_dist.sample(&mut self.rng);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
let probability = (age - 30) as f64 / 30.0;
|
||||
if self.dist.sample(&mut self.rng) < probability {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_run_mood_updates(&mut self) {
|
||||
let now = Instant::now();
|
||||
let should_run = match self.last_mood_run {
|
||||
None => true,
|
||||
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(60),
|
||||
};
|
||||
|
||||
if !should_run {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = self.update_characters_mood_randomized() {
|
||||
eprintln!("[UserCharacterWorker] Fehler in updateCharactersMood: {err}");
|
||||
}
|
||||
|
||||
self.last_mood_run = Some(now);
|
||||
}
|
||||
|
||||
/// Setzt die Stimmung einzelner lebender Charaktere zufällig neu.
|
||||
/// Jeder Charakter hat pro Minute eine kleine Chance auf einen Wechsel,
|
||||
/// so dass sich über die Zeit ein individueller, zufälliger Rhythmus entsteht.
|
||||
fn update_characters_mood_randomized(&mut self) -> Result<(), DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("update_mood", QUERY_UPDATE_MOOD)?;
|
||||
conn.execute("update_mood", &[])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recalculate_knowledge(&mut self) -> Result<(), DbError> {
|
||||
self.base.set_current_step("recalculate knowledge");
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(
|
||||
"get_items_to_update",
|
||||
QUERY_UPDATE_GET_ITEMS_TO_UPDATE,
|
||||
)?;
|
||||
let update_rows = conn.execute("get_items_to_update", &[])?;
|
||||
|
||||
for update_item in update_rows {
|
||||
let quantity: i32 = match update_item.get("quantity").and_then(|v| v.parse().ok()) {
|
||||
Some(q) => q,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if quantity < 10 {
|
||||
self.delete_production_log_entry(&mut conn, &update_item)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
self.update_knowledge_for_production(&mut conn, &update_item)?;
|
||||
self.delete_production_log_entry(&mut conn, &update_item)?;
|
||||
|
||||
if let Some(producer_id) = update_item
|
||||
.get("producer_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
self.send_knowledge_update(producer_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_knowledge_for_production(
|
||||
&mut self,
|
||||
conn: &mut crate::db::DbConnection,
|
||||
update_item: &crate::db::Row,
|
||||
) -> Result<(), DbError> {
|
||||
let producer_id = match update_item.get("producer_id").and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
Some(id) => id,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let product_id = match update_item.get("product_id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
Some(id) => id,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
conn.prepare(
|
||||
"get_character_ids",
|
||||
QUERY_UPDATE_GET_CHARACTER_IDS,
|
||||
)?;
|
||||
let characters_data =
|
||||
conn.execute("get_character_ids", &[&producer_id])?;
|
||||
|
||||
conn.prepare("update_knowledge", QUERY_UPDATE_KNOWLEDGE)?;
|
||||
|
||||
for character_row in characters_data {
|
||||
let character_id = match character_row
|
||||
.get("character_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
Some(id) => id,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let director_id = character_row
|
||||
.get("director_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
if director_id == 0 {
|
||||
conn.execute(
|
||||
"update_knowledge",
|
||||
&[&character_id, &product_id, &2_i32],
|
||||
)?;
|
||||
} else {
|
||||
conn.execute(
|
||||
"update_knowledge",
|
||||
&[&character_id, &product_id, &1_i32],
|
||||
)?;
|
||||
conn.execute(
|
||||
"update_knowledge",
|
||||
&[&director_id, &product_id, &1_i32],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_production_log_entry(
|
||||
&mut self,
|
||||
conn: &mut crate::db::DbConnection,
|
||||
update_item: &crate::db::Row,
|
||||
) -> Result<(), DbError> {
|
||||
let id = match update_item.get("id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
Some(id) => id,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
conn.prepare("delete_log_entry", QUERY_DELETE_LOG_ENTRY)?;
|
||||
conn.execute("delete_log_entry", &[&id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_knowledge_update(&self, producer_id: i32) {
|
||||
let message = format!(r#"{{"event":"knowledge_update","user_id":{}}}"#, producer_id);
|
||||
self.base.broker.publish(message);
|
||||
}
|
||||
|
||||
// Kredit-Logik (portiert aus handleCredits)
|
||||
fn handle_credits(&mut self) -> Result<(), DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_open_credits", QUERY_GET_OPEN_CREDITS)?;
|
||||
conn.prepare("update_credit", QUERY_UPDATE_CREDIT)?;
|
||||
conn.prepare("cleanup_credits", QUERY_CLEANUP_CREDITS)?;
|
||||
conn.prepare(
|
||||
"add_character_to_debtors_prism",
|
||||
QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM,
|
||||
)?;
|
||||
|
||||
let credits_rows = conn.execute("get_open_credits", &[])?;
|
||||
for row in credits_rows {
|
||||
if let Some(credit) = Self::map_row_to_credit(&row) {
|
||||
self.process_single_credit(&mut conn, &credit)?;
|
||||
}
|
||||
}
|
||||
|
||||
conn.execute("cleanup_credits", &[])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_row_to_credit(row: &crate::db::Row) -> Option<Credit> {
|
||||
Some(Credit {
|
||||
amount: row.get("amount")?.parse().ok()?,
|
||||
remaining_amount: row.get("remaining_amount")?.parse().ok()?,
|
||||
interest_rate: row.get("interest_rate")?.parse().ok()?,
|
||||
user_id: row.get("user_id")?.parse().ok()?,
|
||||
money: row.get("money")?.parse().ok()?,
|
||||
character_id: row.get("character_id")?.parse().ok()?,
|
||||
prism_started_previously: row
|
||||
.get("prism_started_previously")
|
||||
.map(|v| v == "t" || v == "true")
|
||||
.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
|
||||
fn process_single_credit(
|
||||
&mut self,
|
||||
conn: &mut crate::db::DbConnection,
|
||||
credit: &Credit,
|
||||
) -> Result<(), DbError> {
|
||||
let Credit {
|
||||
amount,
|
||||
mut remaining_amount,
|
||||
interest_rate,
|
||||
user_id,
|
||||
money,
|
||||
character_id,
|
||||
prism_started_previously,
|
||||
..
|
||||
} = *credit;
|
||||
|
||||
let pay_rate = amount / 10.0 + amount * interest_rate as f64 / 100.0;
|
||||
remaining_amount -= pay_rate;
|
||||
|
||||
// Kann der User zahlen?
|
||||
if pay_rate <= money - (pay_rate * 3.0) {
|
||||
if let Err(err) = self
|
||||
.base
|
||||
.change_falukant_user_money(user_id, -pay_rate, "credit pay rate")
|
||||
{
|
||||
eprintln!(
|
||||
"[UserCharacterWorker] Fehler bei change_falukant_user_money (credit pay rate): {err}"
|
||||
);
|
||||
}
|
||||
} else if prism_started_previously {
|
||||
if let Err(err) = self
|
||||
.base
|
||||
.change_falukant_user_money(user_id, pay_rate, "debitor_prism")
|
||||
{
|
||||
eprintln!(
|
||||
"[UserCharacterWorker] Fehler bei change_falukant_user_money (debitor_prism): {err}"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
conn.execute("add_character_to_debtors_prism", &[&character_id])?;
|
||||
}
|
||||
|
||||
conn.execute("update_credit", &[&remaining_amount, &user_id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Schwangerschafts-Logik (portiert aus processPregnancies)
|
||||
fn process_pregnancies(&mut self) -> Result<(), DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("autobatism", QUERY_AUTOBATISM)?;
|
||||
conn.execute("autobatism", &[])?;
|
||||
|
||||
conn.prepare("get_pregnancy_candidates", QUERY_GET_PREGNANCY_CANDIDATES)?;
|
||||
let rows = conn.execute("get_pregnancy_candidates", &[])?;
|
||||
|
||||
conn.prepare("insert_child", QUERY_INSERT_CHILD)?;
|
||||
conn.prepare("insert_child_relation", QUERY_INSERT_CHILD_RELATION)?;
|
||||
|
||||
for row in rows {
|
||||
self.process_single_pregnancy_candidate(&mut conn, &row)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_single_pregnancy_candidate(
|
||||
&mut self,
|
||||
conn: &mut crate::db::DbConnection,
|
||||
row: &crate::db::Row,
|
||||
) -> Result<(), DbError> {
|
||||
let father_cid = parse_i32(row, "father_cid", -1);
|
||||
let mother_cid = parse_i32(row, "mother_cid", -1);
|
||||
if father_cid < 0 || mother_cid < 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let title_of_nobility = parse_i32(row, "title_of_nobility", 0);
|
||||
let last_name = parse_i32(row, "last_name", 0);
|
||||
let region_id = parse_i32(row, "region_id", 0);
|
||||
|
||||
let father_uid = parse_opt_i32(row, "father_uid");
|
||||
let mother_uid = parse_opt_i32(row, "mother_uid");
|
||||
|
||||
let gender = if self.dist.sample(&mut self.rng) < 0.5 {
|
||||
"male"
|
||||
} else {
|
||||
"female"
|
||||
};
|
||||
|
||||
let inserted =
|
||||
conn.execute("insert_child", &[®ion_id, &gender, &last_name, &title_of_nobility])?;
|
||||
let child_cid = inserted
|
||||
.first()
|
||||
.and_then(|r| r.get("child_cid"))
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(-1);
|
||||
if child_cid < 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
"insert_child_relation",
|
||||
&[&father_cid, &mother_cid, &child_cid],
|
||||
)?;
|
||||
|
||||
if let Some(f_uid) = father_uid {
|
||||
self.send_children_update_and_status(f_uid);
|
||||
}
|
||||
if let Some(m_uid) = mother_uid {
|
||||
self.send_children_update_and_status(m_uid);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_children_update_and_status(&self, user_id: i32) {
|
||||
let children_update =
|
||||
format!(r#"{{"event":"children_update","user_id":{}}}"#, user_id);
|
||||
self.base.broker.publish(children_update);
|
||||
|
||||
let update_status =
|
||||
format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
self.base.broker.publish(update_status);
|
||||
}
|
||||
|
||||
// Todes- und Erb-Logik
|
||||
fn handle_character_death(&mut self, character_id: i32) -> Result<(), DbError> {
|
||||
self.set_heir(character_id)?;
|
||||
|
||||
let death_event = format!(
|
||||
r#"{{"event":"CharacterDeath","character_id":{}}}"#,
|
||||
character_id
|
||||
);
|
||||
self.base.broker.publish(death_event);
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("delete_director", QUERY_DELETE_DIRECTOR)?;
|
||||
conn.prepare("delete_relationship", QUERY_DELETE_RELATIONSHIP)?;
|
||||
conn.prepare("delete_child_relation", QUERY_DELETE_CHILD_RELATION)?;
|
||||
conn.prepare("delete_knowledge", QUERY_DELETE_KNOWLEDGE)?;
|
||||
conn.prepare("delete_debtors_prism", QUERY_DELETE_DEBTORS_PRISM)?;
|
||||
conn.prepare("delete_political_office", QUERY_DELETE_POLITICAL_OFFICE)?;
|
||||
conn.prepare("delete_election_candidate", QUERY_DELETE_ELECTION_CANDIDATE)?;
|
||||
|
||||
conn.execute("delete_director", &[&character_id])?;
|
||||
conn.execute("delete_relationship", &[&character_id])?;
|
||||
conn.execute("delete_child_relation", &[&character_id])?;
|
||||
conn.execute("delete_knowledge", &[&character_id])?;
|
||||
conn.execute("delete_debtors_prism", &[&character_id])?;
|
||||
conn.execute("delete_political_office", &[&character_id])?;
|
||||
conn.execute("delete_election_candidate", &[&character_id])?;
|
||||
|
||||
// Character selbst löschen
|
||||
conn.prepare(
|
||||
"delete_character",
|
||||
r#"DELETE FROM falukant_data.character WHERE id = $1"#,
|
||||
)?;
|
||||
conn.execute("delete_character", &[&character_id])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_heir(&mut self, character_id: i32) -> Result<(), DbError> {
|
||||
let falukant_user_id = self.get_falukant_user_id(character_id)?;
|
||||
if falukant_user_id < 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut heir_id = self.get_heir_from_children(character_id)?;
|
||||
let mut new_money = self.calculate_new_money(falukant_user_id, heir_id > 0)?;
|
||||
|
||||
if heir_id < 1 {
|
||||
heir_id = self.get_random_heir(character_id)?;
|
||||
new_money = self.calculate_new_money(falukant_user_id, heir_id > 0)?;
|
||||
}
|
||||
|
||||
if heir_id > 0 {
|
||||
self.set_new_character(falukant_user_id, heir_id)?;
|
||||
}
|
||||
self.set_new_money(falukant_user_id, new_money)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_falukant_user_id(&mut self, character_id: i32) -> Result<i32, DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_falukant_user_id", QUERY_GET_FALUKANT_USER_ID)?;
|
||||
let rows = conn.execute("get_falukant_user_id", &[&character_id])?;
|
||||
|
||||
Ok(rows
|
||||
.first()
|
||||
.and_then(|r| r.get("user_id"))
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(-1))
|
||||
}
|
||||
|
||||
fn get_heir_from_children(&mut self, deceased_character_id: i32) -> Result<i32, DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_heir", QUERY_GET_HEIR)?;
|
||||
let rows = conn.execute("get_heir", &[&deceased_character_id])?;
|
||||
|
||||
Ok(rows
|
||||
.first()
|
||||
.and_then(|r| r.get("child_character_id"))
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(-1))
|
||||
}
|
||||
|
||||
fn get_random_heir(&mut self, deceased_character_id: i32) -> Result<i32, DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("random_heir", QUERY_RANDOM_HEIR)?;
|
||||
let rows = conn.execute("random_heir", &[&deceased_character_id])?;
|
||||
|
||||
Ok(rows
|
||||
.first()
|
||||
.and_then(|r| r.get("child_character_id"))
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(-1))
|
||||
}
|
||||
|
||||
fn set_new_character(
|
||||
&mut self,
|
||||
falukant_user_id: i32,
|
||||
heir_character_id: i32,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("set_character_user", QUERY_SET_CHARACTER_USER)?;
|
||||
conn.execute(
|
||||
"set_character_user",
|
||||
&[&falukant_user_id, &heir_character_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_new_money(&mut self, falukant_user_id: i32, new_amount: f64) -> Result<(), DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("update_user_money", QUERY_UPDATE_USER_MONEY)?;
|
||||
conn.execute("update_user_money", &[&new_amount, &falukant_user_id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_current_money(&mut self, falukant_user_id: i32) -> Result<f64, DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_current_money", QUERY_GET_CURRENT_MONEY)?;
|
||||
let rows = conn.execute("get_current_money", &[&falukant_user_id])?;
|
||||
|
||||
Ok(rows
|
||||
.first()
|
||||
.and_then(|r| r.get("sum"))
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(0.0))
|
||||
}
|
||||
|
||||
fn get_house_value(&mut self, falukant_user_id: i32) -> Result<f64, DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("house_value", QUERY_GET_HOUSE_VALUE)?;
|
||||
let rows = conn.execute("house_value", &[&falukant_user_id])?;
|
||||
|
||||
Ok(rows
|
||||
.first()
|
||||
.and_then(|r| r.get("sum"))
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(0.0))
|
||||
}
|
||||
|
||||
fn get_settlement_value(&mut self, falukant_user_id: i32) -> Result<f64, DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("settlement_value", QUERY_GET_SETTLEMENT_VALUE)?;
|
||||
let rows = conn.execute("settlement_value", &[&falukant_user_id])?;
|
||||
|
||||
Ok(rows
|
||||
.first()
|
||||
.and_then(|r| r.get("sum"))
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(0.0))
|
||||
}
|
||||
|
||||
fn get_inventory_value(&mut self, falukant_user_id: i32) -> Result<f64, DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("inventory_value", QUERY_GET_INVENTORY_VALUE)?;
|
||||
let rows = conn.execute("inventory_value", &[&falukant_user_id])?;
|
||||
|
||||
Ok(rows
|
||||
.first()
|
||||
.and_then(|r| r.get("sum"))
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(0.0))
|
||||
}
|
||||
|
||||
fn get_credit_debt(&mut self, falukant_user_id: i32) -> Result<f64, DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("credit_debt", QUERY_GET_CREDIT_DEBT)?;
|
||||
let rows = conn.execute("credit_debt", &[&falukant_user_id])?;
|
||||
|
||||
Ok(rows
|
||||
.first()
|
||||
.and_then(|r| r.get("sum"))
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(0.0))
|
||||
}
|
||||
|
||||
fn get_child_count(&mut self, deceased_user_id: i32) -> Result<i32, DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("count_children", QUERY_COUNT_CHILDREN)?;
|
||||
let rows = conn.execute("count_children", &[&deceased_user_id])?;
|
||||
|
||||
Ok(rows
|
||||
.first()
|
||||
.and_then(|r| r.get("cnt"))
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
fn calculate_new_money(
|
||||
&mut self,
|
||||
falukant_user_id: i32,
|
||||
has_heir: bool,
|
||||
) -> Result<f64, DbError> {
|
||||
if !has_heir {
|
||||
return Ok(800.0);
|
||||
}
|
||||
|
||||
let cash = self.get_current_money(falukant_user_id)?;
|
||||
let houses = self.get_house_value(falukant_user_id)?;
|
||||
let settlements = self.get_settlement_value(falukant_user_id)?;
|
||||
let inventory = self.get_inventory_value(falukant_user_id)?;
|
||||
let debt = self.get_credit_debt(falukant_user_id)?;
|
||||
|
||||
let total_assets = cash + houses + settlements + inventory - debt;
|
||||
let child_count = self.get_child_count(falukant_user_id)?;
|
||||
let single = child_count <= 1;
|
||||
|
||||
let heir_share = if single {
|
||||
total_assets
|
||||
} else {
|
||||
total_assets * 0.8
|
||||
};
|
||||
|
||||
let net = heir_share - (houses + settlements + inventory + debt);
|
||||
if net <= 1000.0 {
|
||||
Ok(1000.0)
|
||||
} else {
|
||||
Ok(net)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kleine Hilfsfunktionen für robustes Parsen aus `Row`.
|
||||
fn parse_i32(row: &crate::db::Row, key: &str, default: i32) -> i32 {
|
||||
row.get(key)
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn parse_opt_i32(row: &crate::db::Row, key: &str) -> Option<i32> {
|
||||
row.get(key).and_then(|v| v.parse::<i32>().ok())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Credit {
|
||||
amount: f64,
|
||||
remaining_amount: f64,
|
||||
interest_rate: i32,
|
||||
user_id: i32,
|
||||
money: f64,
|
||||
character_id: i32,
|
||||
prism_started_previously: bool,
|
||||
}
|
||||
|
||||
impl Worker for UserCharacterWorker {
|
||||
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 = UserCharacterWorker::new(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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user