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

1254 lines
37 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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::SeedableRng;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::base::{BaseWorker, Worker, WorkerState};
/// 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-Queries (1:1 aus der C++-Implementierung übernommen, gruppiert nach Themen)
const QUERY_GET_USERS_TO_UPDATE: &str = r#"
SELECT id, CURRENT_DATE - birthdate::date AS age, health
FROM falukant_data."character"
WHERE user_id IS NOT NULL;
"#;
const QUERY_UPDATE_CHARACTERS_HEALTH: &str = r#"
UPDATE falukant_data."character"
SET health = $1
WHERE id = $2;
"#;
// Mood-Update mit zufälliger Auswahl pro Charakter:
// Jeder lebende Charakter hat pro Aufruf (ca. 1x pro Minute)
// eine kleine Chance auf Mood-Wechsel. Die Bedingung `random() < 1.0 / 50.0`
// ergibt im Erwartungswert ca. alle 50 Minuten einen Wechsel, verteilt
// individuell und zufällig.
const QUERY_UPDATE_MOOD: &str = r#"
UPDATE falukant_data."character" AS c
SET mood_id = falukant_data.get_random_mood_id()
WHERE c.health > 0
AND random() < (1.0 / 50.0);
"#;
const QUERY_UPDATE_GET_ITEMS_TO_UPDATE: &str = r#"
SELECT id, product_id, producer_id, quantity
FROM falukant_log.production p
WHERE p.production_timestamp::date < current_date;
"#;
const QUERY_UPDATE_GET_CHARACTER_IDS: &str = r#"
SELECT fu.id AS user_id,
c.id AS character_id,
c2.id AS director_id
FROM falukant_data.falukant_user fu
JOIN falukant_data.character c
ON c.user_id = fu.id
LEFT JOIN falukant_data.director d
ON d.employer_user_id = fu.id
LEFT JOIN falukant_data.character c2
ON c2.id = d.director_character_id
WHERE fu.id = $1;
"#;
const QUERY_UPDATE_KNOWLEDGE: &str = r#"
UPDATE falukant_data.knowledge
SET knowledge = LEAST(knowledge + $3, 100)
WHERE character_id = $1
AND product_id = $2;
"#;
const QUERY_DELETE_LOG_ENTRY: &str = r#"
DELETE FROM falukant_log.production
WHERE id = $1;
"#;
// Kredit- und Vermögens-Queries
const QUERY_GET_OPEN_CREDITS: &str = r#"
SELECT
c.id AS credit_id,
c.amount,
c.remaining_amount,
c.interest_rate,
fu.id AS user_id,
fu.money,
c2.id AS character_id,
dp.created_at AS debitor_prism_start,
dp.created_at::date < current_date AS prism_started_previously
FROM falukant_data.credit c
JOIN falukant_data.falukant_user fu
ON fu.id = c.id
JOIN falukant_data.character c2
ON c2.user_id = c.falukant_user_id
LEFT JOIN falukant_data.debtors_prism dp
ON dp.character_id = c2.id
WHERE c.remaining_amount > 0
AND c.updated_at::date < current_date;
"#;
const QUERY_UPDATE_CREDIT: &str = r#"
UPDATE falukant_data.credit c
SET remaining_amount = $1
WHERE falukant_user_id = $2;
"#;
const QUERY_CLEANUP_CREDITS: &str = r#"
DELETE FROM falukant_data.credit
WHERE remaining_amount >= 0.01;
"#;
const QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM: &str = r#"
INSERT INTO falukant_data.debtors_prism (character_id)
VALUES ($1);
"#;
const QUERY_GET_CURRENT_MONEY: &str = r#"
SELECT COALESCE(money, 0) AS sum
FROM falukant_data.falukant_user
WHERE user_id = $1;
"#;
const QUERY_HOUSE_VALUE: &str = r#"
SELECT COALESCE(SUM(h.cost), 0) AS sum
FROM falukant_data.user_house AS uh
JOIN falukant_type.house AS h
ON uh.house_type_id = h.id
WHERE uh.user_id = $1;
"#;
const QUERY_SETTLEMENT_VALUE: &str = r#"
SELECT COALESCE(SUM(b.base_cost), 0) AS sum
FROM falukant_data.branch AS br
JOIN falukant_type.branch AS b
ON br.branch_type_id = b.id
WHERE br.falukant_user_id = $1;
"#;
const QUERY_INVENTORY_VALUE: &str = r#"
SELECT COALESCE(SUM(i.quantity * p.sell_cost), 0) AS sum
FROM falukant_data.inventory AS i
JOIN falukant_type.product AS p
ON i.product_id = p.id
JOIN falukant_data.branch AS br
ON i.stock_id = br.id
WHERE br.falukant_user_id = $1;
"#;
const QUERY_CREDIT_DEBT: &str = r#"
SELECT COALESCE(SUM(remaining_amount), 0) AS sum
FROM falukant_data.credit
WHERE falukant_user_id = $1;
"#;
const QUERY_COUNT_CHILDREN: &str = r#"
SELECT COUNT(*) AS cnt
FROM falukant_data.child_relation
WHERE father_character_id = $1
OR mother_character_id = $1;
"#;
// Vererbungs-Queries
const QUERY_GET_HEIR: &str = r#"
SELECT child_character_id
FROM falukant_data.child_relation
WHERE father_character_id = $1
OR mother_character_id = $1
ORDER BY (is_heir IS TRUE) DESC,
updated_at DESC
LIMIT 1;
"#;
const QUERY_RANDOM_HEIR: &str = r#"
WITH chosen AS (
SELECT
cr.id AS relation_id,
cr.child_character_id
FROM
falukant_data.child_relation AS cr
JOIN
falukant_data.character AS ch
ON ch.id = cr.child_character_id
WHERE
(cr.father_character_id = $1 OR cr.mother_character_id = $1)
AND ch.region_id = (
SELECT region_id
FROM falukant_data.character
WHERE id = $1
)
AND ch.birthdate >= NOW() - INTERVAL '10 days'
AND ch.title_of_nobility = (
SELECT id
FROM falukant_type.title
WHERE label_tr = 'noncivil'
)
ORDER BY RANDOM()
LIMIT 1
)
UPDATE
falukant_data.child_relation AS cr2
SET
is_heir = TRUE,
updated_at = NOW()
FROM
chosen
WHERE
cr2.id = chosen.relation_id
RETURNING
chosen.child_character_id;
"#;
const QUERY_SET_CHARACTER_USER: &str = r#"
UPDATE falukant_data.character
SET user_id = $1,
updated_at = NOW()
WHERE id = $2;
"#;
const QUERY_UPDATE_USER_MONEY: &str = r#"
UPDATE falukant_data.falukant_user
SET money = $1,
updated_at = NOW()
WHERE user_id = $2;
"#;
const QUERY_GET_FALUKANT_USER_ID: &str = r#"
SELECT user_id
FROM falukant_data.character
WHERE id = $1
LIMIT 1;
"#;
// Schwangerschafts-Queries
const QUERY_AUTOBATISM: &str = r#"
UPDATE falukant_data.child_relation
SET name_set = TRUE
WHERE id IN (
SELECT cr.id
FROM falukant_data.child_relation cr
JOIN falukant_data.character c
ON c.id = cr.child_character_id
WHERE cr.name_set = FALSE
AND c.birthdate < current_date - INTERVAL '5 days'
);
"#;
const QUERY_GET_PREGNANCY_CANDIDATES: &str = r#"
SELECT
r.character1_id AS father_cid,
r.character2_id AS mother_cid,
c1.title_of_nobility,
c1.last_name,
c1.region_id,
fu1.id AS father_uid,
fu2.id AS mother_uid,
((CURRENT_DATE - c1.birthdate::date)
+ (CURRENT_DATE - c2.birthdate::date)) / 2 AS avg_age_days,
100.0 /
(1 + EXP(
0.0647 * (
((CURRENT_DATE - c1.birthdate::date)
+ (CURRENT_DATE - c2.birthdate::date)) / 2
) - 0.0591
)) AS prob_pct
FROM falukant_data.relationship r
JOIN falukant_type.relationship r2
ON r2.id = r.relationship_type_id
AND r2.tr = 'married'
JOIN falukant_data.character c1
ON c1.id = r.character1_id
JOIN falukant_data.character c2
ON c2.id = r.character2_id
LEFT JOIN falukant_data.falukant_user fu1
ON fu1.id = c1.user_id
LEFT JOIN falukant_data.falukant_user fu2
ON fu2.id = c2.user_id
WHERE random() * 100 < (
100.0 /
(1 + EXP(
0.11166347 * (
((CURRENT_DATE - c1.birthdate::date)
+ (CURRENT_DATE - c2.birthdate::date)) / 2
) - 2.638267
))
);
"#;
const QUERY_INSERT_CHILD: &str = r#"
INSERT INTO falukant_data.character (
user_id,
region_id,
first_name,
last_name,
birthdate,
gender,
title_of_nobility,
mood_id,
created_at,
updated_at
) VALUES (
NULL,
$1::int,
(
SELECT id
FROM falukant_predefine.firstname
WHERE gender = $2
ORDER BY RANDOM()
LIMIT 1
),
$3::int,
NOW(),
$2::varchar,
$4::int,
(
SELECT id
FROM falukant_type.mood
ORDER BY RANDOM()
LIMIT 1
),
NOW(),
NOW()
)
RETURNING id AS child_cid;
"#;
const QUERY_INSERT_CHILD_RELATION: &str = r#"
INSERT INTO falukant_data.child_relation (
father_character_id,
mother_character_id,
child_character_id,
name_set,
created_at,
updated_at
)
VALUES (
$1::int,
$2::int,
$3::int,
FALSE,
NOW(), NOW()
);
"#;
// Aufräum-Queries beim Tod eines Characters
const QUERY_DELETE_DIRECTOR: &str = r#"
DELETE FROM falukant_data.director
WHERE director_character_id = $1;
"#;
const QUERY_DELETE_RELATIONSHIP: &str = r#"
DELETE FROM falukant_data.relationship
WHERE character1_id = $1
OR character2_id = $1;
"#;
const QUERY_DELETE_CHILD_RELATION: &str = r#"
DELETE FROM falukant_data.child_relation
WHERE child_character_id = $1
OR father_character_id = $1
OR mother_character_id = $1;
"#;
const QUERY_DELETE_KNOWLEDGE: &str = r#"
DELETE FROM falukant_data.knowledge
WHERE character_id = $1;
"#;
const QUERY_DELETE_DEBTORS_PRISM: &str = r#"
DELETE FROM falukant_data.debtors_prism
WHERE character_id = $1;
"#;
/// Löscht alle Ämter eines Charakters und stellt anschließend sicher, dass
/// für die betroffenen Amtstyp/Region-Kombinationen nicht mehr Ämter
/// besetzt sind als durch `seats_per_region` vorgesehen.
///
/// Die überzähligen Ämter werden deterministisch nach `created_at DESC`
/// gekappt, d.h. neuere Amtsinhaber bleiben bevorzugt im Amt.
const QUERY_DELETE_POLITICAL_OFFICE: &str = r#"
WITH removed AS (
DELETE FROM falukant_data.political_office
WHERE character_id = $1
RETURNING office_type_id, region_id
),
affected AS (
SELECT DISTINCT office_type_id, region_id
FROM removed
),
seats AS (
SELECT
pot.id AS office_type_id,
rt.id AS region_id,
pot.seats_per_region AS seats_total
FROM falukant_type.political_office_type AS pot
JOIN falukant_type.region AS rt
ON pot.region_type = rt.label_tr
JOIN affected AS a
ON a.office_type_id = pot.id
AND a.region_id = rt.id
),
ranked AS (
SELECT
po.id,
po.office_type_id,
po.region_id,
s.seats_total,
ROW_NUMBER() OVER (
PARTITION BY po.office_type_id, po.region_id
ORDER BY po.created_at DESC
) AS rn
FROM falukant_data.political_office AS po
JOIN seats AS s
ON s.office_type_id = po.office_type_id
AND s.region_id = po.region_id
),
to_delete AS (
SELECT id
FROM ranked
WHERE rn > seats_total
)
DELETE FROM falukant_data.political_office
WHERE id IN (SELECT id FROM to_delete);
"#;
const QUERY_DELETE_ELECTION_CANDIDATE: &str = r#"
DELETE FROM falukant_data.election_candidate
WHERE character_id = $1;
"#;
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) {
return;
}
}
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", &[&region_id, &gender, &last_name, &title_of_nobility])?;
let child_cid = inserted
.get(0)
.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
.get(0)
.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
.get(0)
.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
.get(0)
.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
.get(0)
.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_HOUSE_VALUE)?;
let rows = conn.execute("house_value", &[&falukant_user_id])?;
Ok(rows
.get(0)
.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_SETTLEMENT_VALUE)?;
let rows = conn.execute("settlement_value", &[&falukant_user_id])?;
Ok(rows
.get(0)
.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_INVENTORY_VALUE)?;
let rows = conn.execute("inventory_value", &[&falukant_user_id])?;
Ok(rows
.get(0)
.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_CREDIT_DEBT)?;
let rows = conn.execute("credit_debt", &[&falukant_user_id])?;
Ok(rows
.get(0)
.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
.get(0)
.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();
}
}