Refactor Falukant certificate management: Consolidated certificate logic into the FalukantFamilyWorker's daily processing, removing the separate FalukantCertificateWorker. Updated SQL queries to include app_user_id and enhanced documentation for clarity on certificate scoring and daily recalculation logic.

This commit is contained in:
Torsten Schulz (local)
2026-03-25 11:12:55 +01:00
parent 3b25f8c3a0
commit 083fa26297
7 changed files with 163 additions and 221 deletions

View File

@@ -16,7 +16,7 @@ use worker::{
CharacterCreationWorker, ConnectionPool, DirectorWorker, EventsWorker, HouseWorker,
PoliticsWorker, ProduceWorker, StockageManager, TransportWorker, UndergroundWorker,
UserCharacterWorker, ValueRecalculationWorker, WeatherWorker, Worker,
FalukantFamilyWorker, FalukantCertificateWorker,
FalukantFamilyWorker,
};
static KEEP_RUNNING: AtomicBool = AtomicBool::new(true);
@@ -142,10 +142,6 @@ fn create_workers(pool: ConnectionPool, broker: MessageBroker) -> Vec<Box<dyn Wo
pool.clone(),
broker.clone(),
)),
Box::new(FalukantCertificateWorker::new(
pool.clone(),
broker.clone(),
)),
Box::new(HouseWorker::new(pool.clone(), broker.clone())),
Box::new(PoliticsWorker::new(pool.clone(), broker.clone())),
Box::new(TransportWorker::new(pool.clone(), broker.clone())),

View File

@@ -1,132 +1,98 @@
//! Produktionszertifikat: tägliche Neuberechnung von `falukant_user.certificate`.
//! Spec: docs/FALUKANT_PRODUCTION_CERTIFICATE.md
//! Produktionszertifikat: tägliche Neuberechnung von `falukant_user.certificate` im **FalukantFamilyWorker-Daily-Tick**
//! (nicht in einem eigenen Worker-Thread). Spec: `docs/FALUKANT_PRODUCTION_CERTIFICATE.md` und
//! „Falukant: Produktionszertifikate Fach- und Integrationsspezifikation“.
use crate::db::{ConnectionPool, DbError, Row};
use crate::db::{DbError, Row};
use crate::message_broker::MessageBroker;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use super::base::{BaseWorker, Worker, WorkerState};
use super::base::BaseWorker;
use crate::worker::sql::{
QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS, QUERY_UPDATE_FALUKANT_USER_CERTIFICATE,
};
const DAILY_INTERVAL: Duration = Duration::from_secs(24 * 3600);
/// Wenn `money` darunter liegt, gilt der Spieler als bankrott → Zertifikat auf Stufe 1.
/// Wenn `money` darunter liegt, gilt der Spieler als bankrott → Zertifikat auf Stufe 1 (Spec §4.7).
const BANKRUPTCY_MONEY_THRESHOLD: f64 = -5000.0;
pub struct FalukantCertificateWorker {
base: BaseWorker,
}
/// Einmal pro Daily-Tick (`FalukantFamilyWorker::process_daily`).
pub fn run_daily(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbError> {
let pool = &base.pool;
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
impl FalukantCertificateWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
Self {
base: BaseWorker::new("FalukantCertificateWorker", pool, broker),
conn.prepare("cert_rows", QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS)?;
conn.prepare("cert_upd", QUERY_UPDATE_FALUKANT_USER_CERTIFICATE)?;
let rows = conn.execute("cert_rows", &[])?;
for row in rows {
let fu_id = parse_i32(&row, "falukant_user_id", -1);
if fu_id < 0 {
continue;
}
let app_uid = parse_i32(&row, "app_user_id", -1);
let event_uid = if app_uid > 0 { app_uid } else { fu_id };
let current = parse_i32(&row, "certificate", 1).clamp(1, 5);
let money = parse_f64(&row, "money", 0.0);
let avg_knowledge = parse_f64(&row, "avg_knowledge", 0.0);
let completed = parse_i64(&row, "completed_production_count", 0);
let max_church_hierarchy = parse_i32(&row, "max_church_hierarchy", 0);
let pol_names = row
.get("political_office_names")
.cloned()
.unwrap_or_default();
let reputation = parse_f64(&row, "reputation", 50.0);
let title_level = parse_i32(&row, "title_level", 0);
let house_position = parse_i32(&row, "house_position", 0);
// Bankrott: Herabsetzung (Spec §4.7)
if money <= BANKRUPTCY_MONEY_THRESHOLD && current > 1 {
conn.execute("cert_upd", &[&1_i32, &fu_id])?;
publish_certificate_event(broker, event_uid, current, 1);
continue;
}
let knowledge_points = knowledge_points_from_avg(avg_knowledge);
let production_points = production_points_from_count(completed);
let political_rank = max_political_rank_from_names(&pol_names);
let church_rank = church_rank_from_hierarchy(max_church_hierarchy);
let highest_office_rank = political_rank.max(church_rank).min(5);
let office_points = highest_office_rank.min(5);
let nobility_points = (title_level - 1).clamp(0, 5);
let reputation_points = reputation_points_from_rep(reputation);
let house_points = house_points_from_position(house_position);
let certificate_score = knowledge_points as f64 * 0.45
+ production_points as f64 * 0.30
+ office_points as f64 * 0.08
+ nobility_points as f64 * 0.05
+ reputation_points as f64 * 0.07
+ house_points as f64 * 0.05;
let raw_target = raw_target_from_score(certificate_score);
let effective_target = effective_certificate_target(
raw_target,
avg_knowledge,
completed,
office_points,
nobility_points,
reputation_points,
house_points,
);
let new_certificate = if effective_target > current {
(current + 1).min(effective_target).min(5)
} else {
current
};
if new_certificate != current {
conn.execute("cert_upd", &[&new_certificate, &fu_id])?;
publish_certificate_event(broker, event_uid, current, new_certificate);
}
}
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
let mut last: Option<std::time::Instant> = None;
while state.running_worker.load(Ordering::Relaxed) {
let now = std::time::Instant::now();
let run = match last {
None => true,
Some(t) => now.saturating_duration_since(t) >= DAILY_INTERVAL,
};
if run {
if let Err(e) = Self::process_daily(&pool, &broker) {
eprintln!("[FalukantCertificateWorker] process_daily: {e}");
}
last = Some(now);
}
for _ in 0..60 {
if !state.running_worker.load(Ordering::Relaxed) {
break;
}
std::thread::sleep(Duration::from_secs(1));
}
}
}
fn process_daily(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("cert_rows", QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS)?;
conn.prepare("cert_upd", QUERY_UPDATE_FALUKANT_USER_CERTIFICATE)?;
let rows = conn.execute("cert_rows", &[])?;
for row in rows {
let fu_id = parse_i32(&row, "falukant_user_id", -1);
if fu_id < 0 {
continue;
}
let current = parse_i32(&row, "certificate", 1).clamp(1, 127);
let money = parse_f64(&row, "money", 0.0);
let avg_knowledge = parse_f64(&row, "avg_knowledge", 0.0);
let completed = parse_i64(&row, "completed_production_count", 0);
let max_church_hierarchy = parse_i32(&row, "max_church_hierarchy", 0);
let pol_names = row
.get("political_office_names")
.cloned()
.unwrap_or_default();
let reputation = parse_f64(&row, "reputation", 50.0);
let title_level = parse_i32(&row, "title_level", 0);
let house_position = parse_i32(&row, "house_position", 0);
// Bankrott: Herabsetzung erlaubt (Spec)
if money <= BANKRUPTCY_MONEY_THRESHOLD && current > 1 {
conn.execute("cert_upd", &[&1_i32, &fu_id])?;
publish_certificate_event(broker, fu_id, current, 1);
continue;
}
let knowledge_points = knowledge_points_from_avg(avg_knowledge);
let production_points = production_points_from_count(completed);
let political_rank = max_political_rank_from_names(&pol_names);
let church_rank = church_rank_from_hierarchy(max_church_hierarchy);
let highest_office_rank = political_rank.max(church_rank).min(5);
let office_points = highest_office_rank.min(5);
let nobility_points = (title_level - 1).clamp(0, 5);
let reputation_points = reputation_points_from_rep(reputation);
let house_points = house_points_from_position(house_position);
let certificate_score = knowledge_points as f64 * 0.35
+ production_points as f64 * 0.20
+ office_points as f64 * 0.15
+ nobility_points as f64 * 0.10
+ reputation_points as f64 * 0.10
+ house_points as f64 * 0.10;
let raw_target = raw_target_from_score(certificate_score);
let effective_target = effective_certificate_target(
raw_target,
avg_knowledge,
completed,
office_points,
nobility_points,
reputation_points,
house_points,
);
let new_certificate = if effective_target > current {
(current + 1).min(effective_target)
} else {
current
};
if new_certificate != current {
conn.execute("cert_upd", &[&new_certificate, &fu_id])?;
publish_certificate_event(broker, fu_id, current, new_certificate);
}
}
Ok(())
}
Ok(())
}
fn publish_certificate_event(broker: &MessageBroker, user_id: i32, old_c: i32, new_c: i32) {
@@ -189,7 +155,6 @@ fn production_points_from_count(n: i64) -> i32 {
}
}
/// Kirchliche Ämter: `hierarchy_level` auf 05 begrenzen (Daemon).
fn church_rank_from_hierarchy(h: i32) -> i32 {
if h <= 0 {
0
@@ -198,7 +163,7 @@ fn church_rank_from_hierarchy(h: i32) -> i32 {
}
}
/// Politische Amtsnamen → Rang 15 (konfigurierbar im Daemon).
/// Politische Amtsnamen → Rang 15 (Heuristik im Daemon, ohne DB-Änderung).
fn political_name_to_rank(name: &str) -> i32 {
let n = name.to_lowercase();
if n.contains("reich")
@@ -282,72 +247,36 @@ fn house_points_from_position(position: i32) -> i32 {
}
}
/// Spec §4.6 Schwellen für die Zielstufe (vor Mindestanforderungen).
fn raw_target_from_score(score: f64) -> i32 {
if score >= 4.0 {
if score >= 3.8 {
5
} else if score >= 3.0 {
} else if score >= 2.8 {
4
} else if score >= 2.1 {
} else if score >= 1.8 {
3
} else if score >= 1.2 {
} else if score >= 0.9 {
2
} else {
1
}
}
fn status_one(
office_points: i32,
nobility_points: i32,
reputation_points: i32,
house_points: i32,
) -> bool {
office_points >= 1
|| nobility_points >= 1
|| reputation_points >= 2
|| house_points >= 1
}
fn status_count_cert4(
office_points: i32,
nobility_points: i32,
reputation_points: i32,
house_points: i32,
) -> i32 {
fn cert5_two_of_social(office_points: i32, nobility_points: i32, house_points: i32) -> bool {
let mut c = 0;
if office_points >= 1 {
if office_points >= 2 {
c += 1;
}
if nobility_points >= 1 {
c += 1;
}
if reputation_points >= 2 {
c += 1;
}
if house_points >= 1 {
c += 1;
}
c
}
fn cert5_extra_two(
office_points: i32,
nobility_points: i32,
house_points: i32,
) -> i32 {
let mut c = 0;
if office_points >= 2 {
c += 1;
}
if nobility_points >= 2 {
c += 1;
}
if house_points >= 2 {
c += 1;
}
c
c >= 2
}
/// Mindestanforderungen je Stufe (Spec §4.5).
fn meets_min_for_level(
level: i32,
avg_knowledge: f64,
@@ -359,38 +288,27 @@ fn meets_min_for_level(
) -> bool {
match level {
1 => true,
2 => avg_knowledge >= 25.0 && completed >= 5,
3 => {
avg_knowledge >= 40.0
&& completed >= 20
&& status_one(
office_points,
nobility_points,
reputation_points,
house_points,
)
}
2 => avg_knowledge >= 15.0 && completed >= 4,
3 => avg_knowledge >= 28.0 && completed >= 15,
4 => {
avg_knowledge >= 55.0
&& completed >= 60
&& status_count_cert4(
office_points,
nobility_points,
reputation_points,
house_points,
) >= 2
avg_knowledge >= 45.0
&& completed >= 45
&& (office_points >= 1
|| nobility_points >= 1
|| reputation_points >= 2
|| house_points >= 2)
}
5 => {
avg_knowledge >= 70.0
&& completed >= 150
&& reputation_points >= 3
&& cert5_extra_two(office_points, nobility_points, house_points) >= 2
avg_knowledge >= 60.0
&& completed >= 110
&& reputation_points >= 2
&& cert5_two_of_social(office_points, nobility_points, house_points)
}
_ => false,
}
}
/// Höchste Stufe ≤ `raw_target`, die alle Mindestanforderungen erfüllt.
/// Höchste Stufe ≤ `raw_target`, die alle Mindestanforderungen erfüllt (Spec §4.6).
fn effective_certificate_target(
raw_target: i32,
avg_knowledge: f64,
@@ -416,23 +334,3 @@ fn effective_certificate_target(
}
1
}
impl Worker for FalukantCertificateWorker {
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>| {
FalukantCertificateWorker::run_loop(pool.clone(), broker.clone(), state);
});
}
fn stop_worker_thread(&mut self) {
self.base.stop_worker();
}
fn enable_watchdog(&mut self) {
self.base.start_watchdog();
}
}

View File

@@ -158,6 +158,9 @@ impl FalukantFamilyWorker {
if let Err(e) = super::falukant_debtors::run_daily(&self.base, &self.base.broker) {
eprintln!("[FalukantFamilyWorker] falukant_debtors::run_daily: {e}");
}
if let Err(e) = super::falukant_certificate::run_daily(&self.base, &self.base.broker) {
eprintln!("[FalukantFamilyWorker] falukant_certificate::run_daily: {e}");
}
let mut conn = self
.base

View File

@@ -33,5 +33,4 @@ pub use transport::TransportWorker;
pub use weather::WeatherWorker;
pub use events::EventsWorker;
pub use falukant_family::FalukantFamilyWorker;
pub use falukant_certificate::FalukantCertificateWorker;

View File

@@ -3135,6 +3135,7 @@ pub const QUERY_INSERT_CHILD_RELATION_LOVER: &str = r#"
pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#"
SELECT DISTINCT ON (fu.id)
fu.id AS falukant_user_id,
COALESCE(fu.user_id, fu.id)::int AS app_user_id,
COALESCE(fu.certificate, 1)::int AS certificate,
COALESCE(fu.money, 0)::float8 AS money,
c.id AS character_id,
@@ -3179,7 +3180,8 @@ pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#"
pub const QUERY_UPDATE_FALUKANT_USER_CERTIFICATE: &str = r#"
UPDATE falukant_data.falukant_user
SET certificate = $1::int
SET certificate = $1::int,
updated_at = NOW()
WHERE id = $2::int;
"#;