diff --git a/docs/FALUKANT_PRODUCTION_CERTIFICATE.md b/docs/FALUKANT_PRODUCTION_CERTIFICATE.md index d61bd62..065ddfb 100644 --- a/docs/FALUKANT_PRODUCTION_CERTIFICATE.md +++ b/docs/FALUKANT_PRODUCTION_CERTIFICATE.md @@ -1,6 +1,6 @@ # Falukant: Produktionszertifikate (Daemon) -Die Zertifikatslogik läuft **ausschließlich im Daily-Tick** von `FalukantFamilyWorker` (`process_daily`, 24h), nicht in einem eigenen Worker-Thread. Sie schreibt `falukant_user.certificate` fort (max. **+1** pro Tag, keine normale Herabstufung). +Die Zertifikatslogik läuft im **24h-Daily-Tick** von `FalukantFamilyWorker` (`run_iteration`), **nicht** in einem eigenen Worker-Thread. Sie ist von der **Familien-Migration** entkoppelt: Schuldturm + Zertifikat laufen **immer** im Daily-Intervall; Liebhaber/Ehe/Monatslogik nur, wenn `QUERY_FAMILY_SCHEMA_READY` (Spalte `relationship_state.last_daily_processed_at`) erfüllt ist. Sie schreibt `falukant_user.certificate` fort (max. **+1** pro Tag, keine normale Herabstufung außer Bankrott / Erbfolge). Implementierung: `src/worker/falukant_certificate.rs` (`run_daily`). @@ -27,19 +27,24 @@ Rang aus **`political_office_type.name`** (Substring-Heuristik im Daemon, ohne D ## Abgeschlossene Produktionen -**`COUNT(*)`** aus `falukant_log.production` mit `producer_id = falukant_user.id`. +**`COUNT(*)`** aus `falukant_log.production` mit `producer_id = falukant_user.id` **oder** `character.id` (Backend kann je nach Kontext die eine oder andere ID schreiben). + +## Gewählter Charakter pro User + +Bei mehreren lebenden Charakteren: **`DISTINCT ON (fu.id) … ORDER BY fu.id, c.id DESC`** — der Charakter mit der **höchsten** `character.id` (typisch zuletzt genutzter Slot), damit Wissen/Ruf/Ämter näher an der UI liegen. ## Events (WebSocket) Bei Änderung der Stufe: -1. `falukantUpdateProductionCertificate` mit `reason: "daily_recalculation"`, `old_certificate`, `new_certificate` +1. `falukantUpdateProductionCertificate` mit `reason`, `old_certificate`, `new_certificate` 2. `falukantUpdateStatus` +**`reason`:** `daily_recalculation` (normaler Aufstieg), `bankruptcy` (Geld ≤ −5000), `succession_no_heir` (Tod ohne Erben → Stufe 1). + `user_id` in den Events: **`app_user_id`** aus der Query (`COALESCE(fu.user_id, fu.id)`), sonst Fallback `falukant_user_id`. ## Nicht umgesetzt (optional / später) -- **Tod ohne Erben** / Zertifikats-Reset - Feinere **Bankrott**-Definition - **`political_office_history`** (nicht im Repo) diff --git a/src/worker/falukant_certificate.rs b/src/worker/falukant_certificate.rs index 6414583..332798c 100644 --- a/src/worker/falukant_certificate.rs +++ b/src/worker/falukant_certificate.rs @@ -1,5 +1,5 @@ -//! 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 +//! Produktionszertifikat: tägliche Neuberechnung von `falukant_user.certificate` im **FalukantFamilyWorker**-24h-Tick +//! (`run_iteration`, unabhängig von der Familien-Schema-Migration; nicht in einem eigenen Worker-Thread). Spec: `docs/FALUKANT_PRODUCTION_CERTIFICATE.md` und //! „Falukant: Produktionszertifikate – Fach- und Integrationsspezifikation“. use crate::db::{DbError, Row}; @@ -7,7 +7,8 @@ use crate::message_broker::MessageBroker; use super::base::BaseWorker; use crate::worker::sql::{ - QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS, QUERY_UPDATE_FALUKANT_USER_CERTIFICATE, + QUERY_GET_FALUKANT_USER_CERT_AND_EVENT, QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS, + QUERY_UPDATE_FALUKANT_USER_CERTIFICATE, }; /// Wenn `money` darunter liegt, gilt der Spieler als bankrott → Zertifikat auf Stufe 1 (Spec §4.7). @@ -48,7 +49,7 @@ pub fn run_daily(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbErro // 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); + publish_certificate_event(broker, event_uid, current, 1, "bankruptcy"); continue; } @@ -88,17 +89,59 @@ pub fn run_daily(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbErro if new_certificate != current { conn.execute("cert_upd", &[&new_certificate, &fu_id])?; - publish_certificate_event(broker, event_uid, current, new_certificate); + publish_certificate_event( + broker, + event_uid, + current, + new_certificate, + "daily_recalculation", + ); } } Ok(()) } -fn publish_certificate_event(broker: &MessageBroker, user_id: i32, old_c: i32, new_c: i32) { +/// Nach Tod ohne gültigen Erben: Zertifikat auf Stufe 1 (Spec: Erbfolge bricht ab). +pub fn reset_certificate_on_succession_no_heir( + base: &BaseWorker, + broker: &MessageBroker, + falukant_user_id: i32, +) -> Result<(), DbError> { + if falukant_user_id < 1 { + return Ok(()); + } + let pool = &base.pool; + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("cert_get_ev", QUERY_GET_FALUKANT_USER_CERT_AND_EVENT)?; + let rows = conn.execute("cert_get_ev", &[&falukant_user_id])?; + let row = match rows.first() { + Some(r) => r, + None => return Ok(()), + }; + let current = parse_i32(row, "certificate", 1).clamp(1, 5); + let event_uid = parse_i32(row, "event_user_id", falukant_user_id); + if current <= 1 { + return Ok(()); + } + conn.prepare("cert_upd", QUERY_UPDATE_FALUKANT_USER_CERTIFICATE)?; + conn.execute("cert_upd", &[&1_i32, &falukant_user_id])?; + publish_certificate_event(broker, event_uid, current, 1, "succession_no_heir"); + Ok(()) +} + +fn publish_certificate_event( + broker: &MessageBroker, + user_id: i32, + old_c: i32, + new_c: i32, + reason: &str, +) { let msg = format!( - r#"{{"event":"falukantUpdateProductionCertificate","user_id":{},"reason":"daily_recalculation","old_certificate":{},"new_certificate":{}}}"#, - user_id, old_c, new_c + r#"{{"event":"falukantUpdateProductionCertificate","user_id":{},"reason":"{}","old_certificate":{},"new_certificate":{}}}"#, + user_id, reason, old_c, new_c ); broker.publish(msg); let status = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id); diff --git a/src/worker/falukant_family.rs b/src/worker/falukant_family.rs index a118ff1..1c4c655 100644 --- a/src/worker/falukant_family.rs +++ b/src/worker/falukant_family.rs @@ -75,15 +75,20 @@ impl FalukantFamilyWorker { if !self.schema_ready { if let Ok(true) = self.check_schema() { self.schema_ready = true; - } else { - std::thread::sleep(Duration::from_secs(1)); - return; } } if Self::should_run(self.last_daily, now, DAILY_INTERVAL) { - if let Err(e) = self.process_daily() { - eprintln!("[FalukantFamilyWorker] process_daily: {e}"); + 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}"); + } + if self.schema_ready { + if let Err(e) = self.process_daily() { + eprintln!("[FalukantFamilyWorker] process_daily: {e}"); + } } self.last_daily = Some(now); } @@ -155,12 +160,6 @@ impl FalukantFamilyWorker { if self.servants_schema_ready { super::falukant_servants::run_daily(&self.base, &self.base.broker)?; } - 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 diff --git a/src/worker/sql.rs b/src/worker/sql.rs index b14eea6..6482591 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -3471,7 +3471,8 @@ pub const QUERY_INSERT_CHILD_RELATION_LOVER: &str = r#" // --- Produktionszertifikat (Daemon Daily, Spec: Produktionszertifikate) --- -/// Ein Spielercharakter pro Falukant-User (niedrigste character.id bei mehreren lebenden). +/// Ein Spielercharakter pro Falukant-User (bei mehreren lebenden: **höchste** `character.id`, +/// typischerweise zuletzt aktiver Slot — konsistent mit UI, das oft den Hauptcharakter nutzt). pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#" SELECT DISTINCT ON (fu.id) fu.id AS falukant_user_id, @@ -3490,6 +3491,7 @@ pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#" SELECT COUNT(*)::bigint FROM falukant_log.production pl WHERE pl.producer_id = fu.id + OR pl.producer_id = c.id ), 0) AS completed_production_count, COALESCE(( SELECT MAX(cot.hierarchy_level)::int @@ -3515,7 +3517,7 @@ pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#" FROM falukant_data.falukant_user fu JOIN falukant_data.character c ON c.user_id = fu.id AND c.health > 0 LEFT JOIN falukant_type.title t ON t.id = c.title_of_nobility - ORDER BY fu.id, c.id; + ORDER BY fu.id, c.id DESC; "#; pub const QUERY_UPDATE_FALUKANT_USER_CERTIFICATE: &str = r#" @@ -3525,3 +3527,11 @@ pub const QUERY_UPDATE_FALUKANT_USER_CERTIFICATE: &str = r#" WHERE id = $2::int; "#; +/// Zertifikat + `event_user_id` für WebSocket (`COALESCE(user_id, id)` wie bei `QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS`). +pub const QUERY_GET_FALUKANT_USER_CERT_AND_EVENT: &str = r#" + SELECT COALESCE(certificate, 1)::int AS certificate, + COALESCE(user_id, id)::int AS event_user_id + FROM falukant_data.falukant_user + WHERE id = $1::int; +"#; + diff --git a/src/worker/user_character.rs b/src/worker/user_character.rs index b1056a4..6360876 100644 --- a/src/worker/user_character.rs +++ b/src/worker/user_character.rs @@ -933,6 +933,12 @@ impl UserCharacterWorker { if heir_id > 0 { self.set_new_character(falukant_user_id, heir_id)?; + } else if let Err(e) = super::falukant_certificate::reset_certificate_on_succession_no_heir( + &self.base, + &self.base.broker, + falukant_user_id, + ) { + eprintln!("[UserCharacterWorker] reset_certificate_on_succession_no_heir: {e}"); } self.set_new_money(falukant_user_id, new_money)?;