From ac059f688d105187ce7526f113321a8f9b45c261 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 30 Mar 2026 10:11:55 +0200 Subject: [PATCH] Integrate death log functionality into character and event processing: Added death context handling in CharacterCreationWorker and EventsWorker to enhance user notifications related to character deaths. Updated SQL queries for retrieving deceased context and modified notification logic to support detailed death notifications. Enhanced user notification methods to accommodate both short event names and JSON payloads for improved clarity in messaging. --- .../009_falukant_region_display_name.sql | 5 + src/worker/character_creation.rs | 64 +++++++++-- src/worker/death_log.rs | 107 ++++++++++++++++++ src/worker/events.rs | 66 +++++++++-- src/worker/mod.rs | 1 + src/worker/sql.rs | 61 ++++++++++ src/worker/user_character.rs | 62 ++++++++-- 7 files changed, 336 insertions(+), 30 deletions(-) create mode 100644 migrations/009_falukant_region_display_name.sql create mode 100644 src/worker/death_log.rs diff --git a/migrations/009_falukant_region_display_name.sql b/migrations/009_falukant_region_display_name.sql new file mode 100644 index 0000000..522f0c5 --- /dev/null +++ b/migrations/009_falukant_region_display_name.sql @@ -0,0 +1,5 @@ +-- Optionaler Anzeigename für Regionen (Todes-/UI-Logs, Karten); ohne Spalte liefert der Daemon nur region_id. +ALTER TABLE falukant_data.region + ADD COLUMN IF NOT EXISTS name text; + +COMMENT ON COLUMN falukant_data.region.name IS 'Optional: Orts-/Regionsname für Logs und UI'; diff --git a/src/worker/character_creation.rs b/src/worker/character_creation.rs index ba636bc..4ec99e2 100644 --- a/src/worker/character_creation.rs +++ b/src/worker/character_creation.rs @@ -10,6 +10,7 @@ use std::thread; use std::time::Duration; use super::base::{BaseWorker, Worker, WorkerState}; +use super::death_log; use crate::worker::sql::{ QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED, QUERY_GET_TOWN_REGION_IDS, @@ -404,6 +405,14 @@ impl CharacterCreationWorker { .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + let death_ctx = match death_log::build_deceased_context(&mut conn, character_id) { + Ok(c) => Some(c), + Err(e) => { + eprintln!("[CharacterCreationWorker] Todes-Log Kontext: {e}"); + None + } + }; + // 1) Director löschen und User benachrichtigen conn.prepare("delete_director", QUERY_DELETE_DIRECTOR)?; let dir_result = conn.execute("delete_director", &[&character_id])?; @@ -412,7 +421,15 @@ impl CharacterCreationWorker { .get("employer_user_id") .and_then(|v| v.parse::().ok()) { - Self::notify_user(pool, broker, user_id, "director_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "director_death", user_id) + }); + Self::notify_user( + pool, + broker, + user_id, + tr.as_deref().unwrap_or("director_death"), + )?; } // 2) Relationships löschen und betroffene User benachrichtigen @@ -423,7 +440,15 @@ impl CharacterCreationWorker { .get("related_user_id") .and_then(|v| v.parse::().ok()) { - Self::notify_user(pool, broker, related_user_id, "relationship_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "relationship_death", related_user_id) + }); + Self::notify_user( + pool, + broker, + related_user_id, + tr.as_deref().unwrap_or("relationship_death"), + )?; } } @@ -437,13 +462,29 @@ impl CharacterCreationWorker { .get("father_user_id") .and_then(|v| v.parse::().ok()) { - Self::notify_user(pool, broker, father_user_id, "child_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "child_death", father_user_id) + }); + Self::notify_user( + pool, + broker, + father_user_id, + tr.as_deref().unwrap_or("child_death"), + )?; } if let Some(mother_user_id) = row .get("mother_user_id") .and_then(|v| v.parse::().ok()) { - Self::notify_user(pool, broker, mother_user_id, "child_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "child_death", mother_user_id) + }); + Self::notify_user( + pool, + broker, + mother_user_id, + tr.as_deref().unwrap_or("child_death"), + )?; } } @@ -457,24 +498,27 @@ impl CharacterCreationWorker { pool: &ConnectionPool, broker: &MessageBroker, user_id: i32, - event_type: &str, + tr_payload: &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, &event_type])?; + conn.execute("insert_notification", &[&user_id, &tr_payload])?; // 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); + if tr_payload.trim_start().starts_with('{') { + broker.publish(tr_payload.to_string()); + } else { + let message = + format!(r#"{{"event":"{tr_payload}","user_id":{user_id}}}"#); + broker.publish(message); + } Ok(()) } diff --git a/src/worker/death_log.rs b/src/worker/death_log.rs new file mode 100644 index 0000000..ce0ea94 --- /dev/null +++ b/src/worker/death_log.rs @@ -0,0 +1,107 @@ +//! Kontext für `falukant_log.notification` bei Charaktertod (Direktor, Partner, Eltern). +//! `tr` enthält JSON mit Verstorbenem, Ansässigkeit, Alter, Ehe/Kind/Liebschaft. +//! +//! **Aufruf vor** `DELETE` auf `relationship` / `child_relation`, sonst sind Partner/Kinder/Geliebte leer. +//! +//! **Geliebte** sind keine eigene Tabelle: `falukant_data.relationship` + Typ `falukant_type.relationship.tr = 'lover'`, +//! Zusatzdaten in `relationship_state`. Zählt nur, wenn `relationship_state.active` nicht `false` ist (wie Backend-UI). +//! `lover_role` (`secret_affair` / `lover` / `mistress_or_favorite`) wird für die Liste **nicht** gefiltert. +//! +//! Kinder aus Liebschaft sind in `child_relation.birth_context = 'lover'` markiert; zusätzlich unter `linked.lover_birth_children`. + +use crate::db::{DbConnection, DbError, Row}; +use serde_json::{json, Value}; + +use crate::worker::sql::{ + QUERY_DEATH_LOG_CHARACTER_BASE, QUERY_DEATH_LOG_CHILD_DISPLAY_NAMES, + QUERY_DEATH_LOG_CHILD_LOVER_BIRTH_DISPLAY_NAMES, QUERY_DEATH_LOG_LOVER_DISPLAY_NAMES, + QUERY_DEATH_LOG_SPOUSE_DISPLAY_NAMES, +}; + +fn collect_display_names(rows: Vec, key: &str) -> Vec { + let mut out = Vec::new(); + for r in rows { + if let Some(s) = r.get(key) { + let t = s.trim(); + if !t.is_empty() { + out.push(t.to_string()); + } + } + } + out +} + +/// Lädt Verstorbenen + Ehepartner, Kinder, **aktive Geliebte** (`tr = 'lover'`, `active IS NOT FALSE`). +/// Muss ausgeführt werden, bevor Beziehungen aus der DB gelöscht werden. +pub fn build_deceased_context(conn: &mut DbConnection, deceased_character_id: i32) -> Result { + conn.prepare("death_base", QUERY_DEATH_LOG_CHARACTER_BASE)?; + let base_rows = conn.execute("death_base", &[&deceased_character_id])?; + let base = base_rows.first().ok_or_else(|| { + DbError::new(format!( + "Todes-Log: Charakter {deceased_character_id} nicht gefunden" + )) + })?; + + let character_id = base + .get("character_id") + .and_then(|v| v.parse::().ok()) + .unwrap_or(deceased_character_id); + let display_name = base + .get("display_name") + .cloned() + .unwrap_or_else(|| character_id.to_string()); + let region_id = base + .get("region_id") + .and_then(|v| v.parse::().ok()); + let region_label = base + .get("region_label") + .cloned() + .unwrap_or_default(); + let age_years = base + .get("age_years") + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + + conn.prepare("death_spouse", QUERY_DEATH_LOG_SPOUSE_DISPLAY_NAMES)?; + let spouse_rows = conn.execute("death_spouse", &[&deceased_character_id])?; + let spouses = collect_display_names(spouse_rows, "display_name"); + + conn.prepare("death_child", QUERY_DEATH_LOG_CHILD_DISPLAY_NAMES)?; + let child_rows = conn.execute("death_child", &[&deceased_character_id])?; + let children = collect_display_names(child_rows, "display_name"); + + conn.prepare("death_child_lover_birth", QUERY_DEATH_LOG_CHILD_LOVER_BIRTH_DISPLAY_NAMES)?; + let child_lover_rows = conn.execute("death_child_lover_birth", &[&deceased_character_id])?; + let lover_birth_children = collect_display_names(child_lover_rows, "display_name"); + + conn.prepare("death_lover", QUERY_DEATH_LOG_LOVER_DISPLAY_NAMES)?; + let lover_rows = conn.execute("death_lover", &[&deceased_character_id])?; + let lovers = collect_display_names(lover_rows, "display_name"); + + Ok(json!({ + "deceased": { + "character_id": character_id, + "display_name": display_name, + "region_id": region_id, + "region_label": region_label, + "age_years": age_years, + }, + "linked": { + "spouses": spouses, + "children": children, + "lover_birth_children": lover_birth_children, + "lovers": lovers, + }, + })) +} + +/// Vollständiges `tr`-/WebSocket-JSON mit `event` und `user_id`. +pub fn wrap_death_notification(context: &Value, event: &str, user_id: i32) -> String { + json!({ + "event": event, + "user_id": user_id, + "deceased": context.get("deceased"), + "linked": context.get("linked"), + }) + .to_string() +} diff --git a/src/worker/events.rs b/src/worker/events.rs index c022f78..e49bd42 100644 --- a/src/worker/events.rs +++ b/src/worker/events.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use super::base::{BaseWorker, Worker, WorkerState}; +use super::death_log; use crate::worker::sql::{ QUERY_GET_RANDOM_USER, QUERY_GET_RANDOM_INFANT, @@ -1751,6 +1752,15 @@ impl EventsWorker { .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + // Todes-Kontext (Namen, Ort, Alter, Ehe/Kind/Liebschaft) vor Löschen der Beziehungen + let death_ctx = match death_log::build_deceased_context(&mut conn, character_id) { + Ok(c) => Some(c), + Err(e) => { + eprintln!("[EventsWorker] Todes-Log Kontext: {e}"); + None + } + }; + // 1) Director löschen und User benachrichtigen conn.prepare("delete_director", QUERY_DELETE_DIRECTOR)?; let dir_result = conn.execute("delete_director", &[&character_id])?; @@ -1759,7 +1769,15 @@ impl EventsWorker { .get("employer_user_id") .and_then(|v| v.parse::().ok()) { - Self::notify_user(pool, broker, user_id, "director_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "director_death", user_id) + }); + Self::notify_user( + pool, + broker, + user_id, + tr.as_deref().unwrap_or("director_death"), + )?; } } @@ -1771,7 +1789,15 @@ impl EventsWorker { .get("related_user_id") .and_then(|v| v.parse::().ok()) { - Self::notify_user(pool, broker, related_user_id, "relationship_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "relationship_death", related_user_id) + }); + Self::notify_user( + pool, + broker, + related_user_id, + tr.as_deref().unwrap_or("relationship_death"), + )?; } } @@ -1801,13 +1827,29 @@ impl EventsWorker { .get("father_user_id") .and_then(|v| v.parse::().ok()) { - Self::notify_user(pool, broker, father_user_id, "child_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "child_death", father_user_id) + }); + Self::notify_user( + pool, + broker, + father_user_id, + tr.as_deref().unwrap_or("child_death"), + )?; } if let Some(mother_user_id) = row .get("mother_user_id") .and_then(|v| v.parse::().ok()) { - Self::notify_user(pool, broker, mother_user_id, "child_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "child_death", mother_user_id) + }); + Self::notify_user( + pool, + broker, + mother_user_id, + tr.as_deref().unwrap_or("child_death"), + )?; } } @@ -1937,28 +1979,32 @@ impl EventsWorker { Ok(()) } + /// `tr_payload`: kurzer `event`-Name oder JSON-Objekt (Todes-Logs mit `deceased`/`linked`). fn notify_user( pool: &ConnectionPool, broker: &MessageBroker, user_id: i32, - event_type: &str, + tr_payload: &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, &event_type])?; + conn.execute("insert_notification", &[&user_id, &tr_payload])?; // 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); + if tr_payload.trim_start().starts_with('{') { + broker.publish(tr_payload.to_string()); + } else { + let message = + format!(r#"{{"event":"{tr_payload}","user_id":{user_id}}}"#); + broker.publish(message); + } Ok(()) } diff --git a/src/worker/mod.rs b/src/worker/mod.rs index b526c1d..3daed3c 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -11,6 +11,7 @@ mod user_character; mod transport; mod weather; mod events; +mod death_log; mod falukant_family; mod falukant_certificate; mod falukant_servants; diff --git a/src/worker/sql.rs b/src/worker/sql.rs index c957cb7..aa8116c 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -641,6 +641,67 @@ pub const QUERY_GET_REGION_CHARACTERS: &str = r#" SELECT id, health, user_id FROM falukant_data."character" WHERE region_id = $1 AND health > 0; "#; +/// Anzeige für Todes-Logs (`falukant_log.notification.tr` als JSON): Name, Wohnort, Alter, Verknüpfungen. +/// `region_id` = Ansässigkeit; `region_label` aus `falukant_data.region.name`, falls vorhanden, sonst `region_id` als Text. +pub const QUERY_DEATH_LOG_CHARACTER_BASE: &str = r#" + SELECT + c.id AS character_id, + COALESCE(TRIM(BOTH FROM COALESCE(fn.label::text, '') || ' ' || COALESCE(ln.label::text, '')), c.id::text) AS display_name, + c.region_id, + COALESCE(NULLIF(TRIM(dr.name::text), ''), c.region_id::text) AS region_label, + GREATEST(0, FLOOR((CURRENT_DATE - c.birthdate::date) / 365.25))::int AS age_years + FROM falukant_data.character c + LEFT JOIN falukant_predefine.firstname fn ON fn.id = c.first_name + LEFT JOIN falukant_predefine.lastname ln ON ln.id = c.last_name + LEFT JOIN falukant_data.region dr ON dr.id = c.region_id + WHERE c.id = $1::int; +"#; + +pub const QUERY_DEATH_LOG_SPOUSE_DISPLAY_NAMES: &str = r#" + SELECT COALESCE(TRIM(BOTH FROM COALESCE(fn.label::text, '') || ' ' || COALESCE(ln.label::text, '')), o.id::text) AS display_name + FROM falukant_data.relationship r + JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id + AND rt.tr IN ('married', 'engaged', 'wooing') + JOIN falukant_data.character o ON o.id = CASE WHEN r.character1_id = $1::int THEN r.character2_id ELSE r.character1_id END + LEFT JOIN falukant_predefine.firstname fn ON fn.id = o.first_name + LEFT JOIN falukant_predefine.lastname ln ON ln.id = o.last_name + WHERE r.character1_id = $1::int OR r.character2_id = $1::int; +"#; + +pub const QUERY_DEATH_LOG_CHILD_DISPLAY_NAMES: &str = r#" + SELECT COALESCE(TRIM(BOTH FROM COALESCE(fn.label::text, '') || ' ' || COALESCE(ln.label::text, '')), ch.id::text) AS display_name + FROM falukant_data.child_relation cr + JOIN falukant_data.character ch ON ch.id = cr.child_character_id + LEFT JOIN falukant_predefine.firstname fn ON fn.id = ch.first_name + LEFT JOIN falukant_predefine.lastname ln ON ln.id = ch.last_name + WHERE cr.father_character_id = $1::int OR cr.mother_character_id = $1::int; +"#; + +/// Kinder mit `birth_context = 'lover'` (unehelich / aus Liebschaft); Teilmenge von `QUERY_DEATH_LOG_CHILD_DISPLAY_NAMES`. +pub const QUERY_DEATH_LOG_CHILD_LOVER_BIRTH_DISPLAY_NAMES: &str = r#" + SELECT COALESCE(TRIM(BOTH FROM COALESCE(fn.label::text, '') || ' ' || COALESCE(ln.label::text, '')), ch.id::text) AS display_name + FROM falukant_data.child_relation cr + JOIN falukant_data.character ch ON ch.id = cr.child_character_id + LEFT JOIN falukant_predefine.firstname fn ON fn.id = ch.first_name + LEFT JOIN falukant_predefine.lastname ln ON ln.id = ch.last_name + WHERE (cr.father_character_id = $1::int OR cr.mother_character_id = $1::int) + AND cr.birth_context = 'lover'; +"#; + +/// Geliebte: Zeilen in `falukant_data.relationship`, Typ über `falukant_type.relationship` mit `tr = 'lover'` +/// (kein Filter auf `relationship_state.lover_role` — `secret_affair` / `lover` / `mistress_or_favorite` sind nur Ausprägungen). +/// „Aktiv“ wie Backend (`falukantService`): `relationship_state.active` darf nicht `false` sein (`IS NOT FALSE`). +pub const QUERY_DEATH_LOG_LOVER_DISPLAY_NAMES: &str = r#" + SELECT COALESCE(TRIM(BOTH FROM COALESCE(fn.label::text, '') || ' ' || COALESCE(ln.label::text, '')), o.id::text) AS display_name + FROM falukant_data.relationship r + JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' + JOIN falukant_data.relationship_state rs ON rs.relationship_id = r.id AND (rs.active IS NOT FALSE) + JOIN falukant_data.character o ON o.id = CASE WHEN r.character1_id = $1::int THEN r.character2_id ELSE r.character1_id END + LEFT JOIN falukant_predefine.firstname fn ON fn.id = o.first_name + LEFT JOIN falukant_predefine.lastname ln ON ln.id = o.last_name + WHERE r.character1_id = $1::int OR r.character2_id = $1::int; +"#; + pub const QUERY_DELETE_DIRECTOR: &str = r#" DELETE FROM falukant_data.director WHERE director_character_id = $1 RETURNING employer_user_id; "#; diff --git a/src/worker/user_character.rs b/src/worker/user_character.rs index 8d3f760..8d09c0b 100644 --- a/src/worker/user_character.rs +++ b/src/worker/user_character.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use super::base::{BaseWorker, Worker, WorkerState}; +use super::death_log; use crate::worker::sql::{ QUERY_GET_CHARACTERS_ZERO_HEALTH, QUERY_GET_USERS_TO_UPDATE, @@ -646,23 +647,28 @@ impl UserCharacterWorker { self.base.broker.publish(update_status); } - /// Wie `EventsWorker::notify_user`: Eintrag in `falukant_log.notification` (`tr` = event_type) + WS. + /// Wie `EventsWorker::notify_user`: Eintrag in `falukant_log.notification` (`tr` = Payload) + WS. + /// `tr_payload` kann ein Kurz-String (`director_death`) oder vollständiges JSON sein. fn notify_user_death( &self, conn: &mut crate::db::DbConnection, user_id: i32, - event_type: &str, + tr_payload: &str, ) -> Result<(), DbError> { if user_id <= 0 { return Ok(()); } - conn.execute("insert_notification", &[&user_id, &event_type])?; + conn.execute("insert_notification", &[&user_id, &tr_payload])?; let update_message = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id); self.base.broker.publish(update_message); - let message = - format!(r#"{{"event":"{event_type}","user_id":{}}}"#, user_id); - self.base.broker.publish(message); + if tr_payload.trim_start().starts_with('{') { + self.base.broker.publish(tr_payload.to_string()); + } else { + let message = + format!(r#"{{"event":"{tr_payload}","user_id":{user_id}}}"#); + self.base.broker.publish(message); + } Ok(()) } @@ -692,13 +698,28 @@ impl UserCharacterWorker { conn.prepare("delete_election_candidate", QUERY_DELETE_ELECTION_CANDIDATE)?; conn.prepare("insert_notification", QUERY_INSERT_NOTIFICATION)?; + let death_ctx = match death_log::build_deceased_context(&mut conn, character_id) { + Ok(c) => Some(c), + Err(e) => { + eprintln!("[UserCharacterWorker] Todes-Log Kontext: {e}"); + None + } + }; + let dir_result = conn.execute("delete_director", &[&character_id])?; for row in dir_result { if let Some(user_id) = row .get("employer_user_id") .and_then(|v| v.parse::().ok()) { - self.notify_user_death(&mut conn, user_id, "director_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "director_death", user_id) + }); + self.notify_user_death( + &mut conn, + user_id, + tr.as_deref().unwrap_or("director_death"), + )?; } } @@ -708,7 +729,14 @@ impl UserCharacterWorker { .get("related_user_id") .and_then(|v| v.parse::().ok()) { - self.notify_user_death(&mut conn, related_user_id, "relationship_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "relationship_death", related_user_id) + }); + self.notify_user_death( + &mut conn, + related_user_id, + tr.as_deref().unwrap_or("relationship_death"), + )?; } } @@ -719,13 +747,27 @@ impl UserCharacterWorker { .get("father_user_id") .and_then(|v| v.parse::().ok()) { - self.notify_user_death(&mut conn, father_user_id, "child_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "child_death", father_user_id) + }); + self.notify_user_death( + &mut conn, + father_user_id, + tr.as_deref().unwrap_or("child_death"), + )?; } if let Some(mother_user_id) = row .get("mother_user_id") .and_then(|v| v.parse::().ok()) { - self.notify_user_death(&mut conn, mother_user_id, "child_death")?; + let tr = death_ctx.as_ref().map(|c| { + death_log::wrap_death_notification(c, "child_death", mother_user_id) + }); + self.notify_user_death( + &mut conn, + mother_user_id, + tr.as_deref().unwrap_or("child_death"), + )?; } } conn.execute("delete_knowledge", &[&character_id])?;