From b3578c65b8c71efa52c2d78c6b46af91752c7d33 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 31 Mar 2026 10:29:05 +0200 Subject: [PATCH] Enhance production certificate logic and event handling: Updated the daily recalculation process in `FalukantFamilyWorker` to decouple it from family migration, ensuring consistent execution. Introduced a new mechanism to reset certificates upon succession without heirs, improving user experience. Refined SQL queries for better data retrieval and event publishing, including distinct character selection logic. Enhanced documentation for clarity on the updated processes and their implications. --- docs/FALUKANT_PRODUCTION_CERTIFICATE.md | 13 ++++-- src/worker/falukant_certificate.rs | 59 +++++++++++++++++++++---- src/worker/falukant_family.rs | 21 +++++---- src/worker/sql.rs | 14 +++++- src/worker/user_character.rs | 6 +++ 5 files changed, 88 insertions(+), 25 deletions(-) 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)?;