Enhance church office management in Falukant daemon: Introduced falukantUpdateChurch event for church applications and appointments, updated SQL queries for church office processing, and refactored the PoliticsWorker to streamline daily tasks related to church offices. Improved handling of church application scoring and interim appointments, enhancing overall church dynamics and character interactions.

This commit is contained in:
Torsten Schulz (local)
2026-03-23 11:02:19 +01:00
parent 708ffc3eda
commit 9d7f61a329
5 changed files with 796 additions and 222 deletions

View File

@@ -1,11 +1,9 @@
use crate::db::{ConnectionPool, DbError, Row};
use crate::message_broker::MessageBroker;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
use chrono::{Local, Timelike};
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_COUNT_OFFICES_PER_REGION,
@@ -26,7 +24,6 @@ use crate::worker::sql::{
QUERY_FIND_AVAILABLE_CHURCH_OFFICES,
QUERY_FIND_CHURCH_SUPERVISOR,
QUERY_GET_CHURCH_OFFICE_REQUIREMENTS,
QUERY_GET_PENDING_CHURCH_APPLICATIONS,
QUERY_CHECK_CHARACTER_ELIGIBILITY,
QUERY_APPROVE_CHURCH_APPLICATION,
QUERY_REJECT_CHURCH_APPLICATION,
@@ -34,6 +31,14 @@ use crate::worker::sql::{
QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE,
QUERY_GET_OLD_PENDING_CHURCH_APPLICATIONS,
QUERY_AUTO_APPROVE_CHURCH_APPLICATION,
QUERY_COUNT_PENDING_CHURCH_APPS_BY_OFFICE_REGION,
QUERY_GET_CHURCH_OFFICE_OCCUPIED_COUNT,
QUERY_IS_CHARACTER_NPC,
QUERY_GET_PENDING_CHURCH_APPLICATIONS_FOR_SCORING,
QUERY_INTERIM_APPOINT_CHURCH_OFFICE,
QUERY_UPDATE_CHARACTER_HIGHEST_CHURCH_FROM_OFFICE_TYPE,
QUERY_FIND_INTERIM_CHURCH_NPC_CANDIDATE,
QUERY_REMOVE_LOWER_CHURCH_OFFICES_FOR_CHARACTER,
};
pub struct PoliticsWorker {
@@ -72,6 +77,7 @@ struct Office {
#[derive(Debug, Clone)]
struct AvailableChurchOffice {
office_type_id: i32,
hierarchy_level: i32,
seats_per_region: i32,
region_id: i32,
occupied_seats: i32,
@@ -82,17 +88,16 @@ struct ChurchSupervisor {
supervisor_character_id: i32,
}
#[derive(Debug, Clone)]
struct ChurchOfficeRequirement {
prerequisite_office_type_id: Option<i32>,
min_title_level: Option<i32>,
}
/// Bis einschließlich dieser `hierarchy_level` (church_office_type): Interimsbesetzung ohne Vorgesetzten.
const INTERIM_MAX_CHURCH_HIERARCHY: i32 = 6;
#[derive(Debug, Clone)]
struct ChurchApplication {
struct ChurchAppScoreRow {
application_id: i32,
office_type_id: i32,
applicant_character_id: i32,
region_id: i32,
seats_per_region: i32,
score: f64,
}
// --- SQL-Konstanten (1:1 aus politics_worker.h übernommen) ------------------
@@ -107,7 +112,6 @@ impl PoliticsWorker {
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
let mut last_execution: Option<Instant> = None;
let mut last_church_office_run: Option<Instant> = None;
let mut last_auto_approve_run: Option<Instant> = None;
while state.running_worker.load(Ordering::Relaxed) {
@@ -124,25 +128,6 @@ impl PoliticsWorker {
last_execution = Some(now);
}
// Church Office Job um 13 Uhr
if Self::is_time_13_00() {
let should_run_church = match last_church_office_run {
None => true,
Some(prev) => {
// Prüfe ob seit letztem Lauf mindestens 23 Stunden vergangen sind
// (um sicherzustellen, dass es nur einmal pro Tag läuft)
now.saturating_duration_since(prev) >= Duration::from_secs(23 * 3600)
}
};
if should_run_church {
if let Err(err) = Self::perform_church_office_task(&pool, &broker) {
eprintln!("[PoliticsWorker] Fehler bei performChurchOfficeTask: {err}");
}
last_church_office_run = Some(now);
}
}
// Automatische Annahme alter Applications (stündlich)
let should_run_auto_approve = match last_auto_approve_run {
None => true,
@@ -168,18 +153,15 @@ impl PoliticsWorker {
}
}
/// Prüft ob die aktuelle Uhrzeit 13:00 ist (mit Toleranz von ±1 Minute)
fn is_time_13_00() -> bool {
let now = Local::now();
let hour = now.hour();
let minute = now.minute();
hour == 13 && minute <= 1
}
fn perform_daily_politics_task(
pool: &ConnectionPool,
broker: &MessageBroker,
) -> Result<(), DbError> {
// 0) Täglich: Kirchenämter (NPC-Bewerbungen, NPC-Vorgesetzte, Interimsbesetzung)
if let Err(err) = Self::perform_church_office_task(pool, broker) {
eprintln!("[PoliticsWorker] Fehler bei perform_church_office_task: {err}");
}
// 1) Optional: Positionen evaluieren (aktuell nur Logging/Struktur)
Self::evaluate_political_positions(pool)?;
@@ -636,45 +618,47 @@ impl PoliticsWorker {
.collect())
}
/// Verarbeitet Church Office Jobs um 13 Uhr:
/// - Findet verfügbare Positionen
/// - Verarbeitet bestehende Bewerbungen
/// - Erstellt neue Bewerbungen falls nötig
/// Täglich: freie Sitze, Bewerbungen (Spieler bleiben offen), NPC-Vorgesetzte entscheiden per Score,
/// NPC-Bewerbungen erzeugen, Interimsbesetzung ohne Vorgesetzten (niedrige Hierarchie).
fn perform_church_office_task(
pool: &ConnectionPool,
broker: &MessageBroker,
) -> Result<(), DbError> {
eprintln!("[PoliticsWorker] Starte Church Office Task um 13 Uhr");
eprintln!("[PoliticsWorker] Starte Church Office Task (täglich)");
// 1) Verfügbare Church Office Positionen finden
let available_offices = Self::find_available_church_offices(pool)?;
eprintln!(
"[PoliticsWorker] Gefunden: {} verfügbare Church Office Positionen",
available_offices.len()
);
// 2) Für jede verfügbare Position Bewerbungen verarbeiten
for office in &available_offices {
// Supervisor finden
if let Some(supervisor) = Self::find_church_supervisor(pool, office.region_id, office.office_type_id)? {
// Bestehende Bewerbungen für diesen Supervisor verarbeiten
Self::process_church_applications(pool, broker, supervisor.supervisor_character_id)?;
if let Some(supervisor) =
Self::find_church_supervisor(pool, office.region_id, office.office_type_id)?
{
Self::process_church_supervisor_queue(pool, broker, supervisor.supervisor_character_id)?;
// Falls noch Plätze frei sind, neue Bewerbungen erstellen
let remaining_seats = office.seats_per_region - office.occupied_seats;
if remaining_seats > 0 {
Self::create_church_application_jobs(
pool,
office.office_type_id,
office.region_id,
supervisor.supervisor_character_id,
remaining_seats,
)?;
let pending_count =
Self::count_pending_church_apps(pool, office.office_type_id, office.region_id)?;
if pending_count < remaining_seats {
let need = remaining_seats - pending_count;
Self::create_church_application_jobs(
pool,
office.office_type_id,
office.region_id,
supervisor.supervisor_character_id,
need,
)?;
}
}
} else if office.hierarchy_level <= INTERIM_MAX_CHURCH_HIERARCHY {
Self::try_interim_church_appointment(pool, broker, office)?;
} else {
eprintln!(
"[PoliticsWorker] Kein Supervisor gefunden für office_type_id={}, region_id={}",
office.office_type_id, office.region_id
"[PoliticsWorker] Kein Supervisor, Interim deaktiviert (hierarchy_level={}): office_type_id={}, region_id={}",
office.hierarchy_level, office.office_type_id, office.region_id
);
}
}
@@ -683,6 +667,261 @@ impl PoliticsWorker {
Ok(())
}
fn publish_falukant_church_update(broker: &MessageBroker, user_id: i32, reason: &str) {
let church = format!(
r#"{{"event":"falukantUpdateChurch","user_id":{},"reason":"{}"}}"#,
user_id, reason
);
broker.publish(church);
let status = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
broker.publish(status);
}
fn count_pending_church_apps(
pool: &ConnectionPool,
office_type_id: i32,
region_id: i32,
) -> Result<i32, DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"cnt_pending_church",
QUERY_COUNT_PENDING_CHURCH_APPS_BY_OFFICE_REGION,
)
.map_err(|e| DbError::new(format!("[PoliticsWorker] prepare cnt_pending_church: {e}")))?;
let rows = conn
.execute("cnt_pending_church", &[&office_type_id, &region_id])
.map_err(|e| DbError::new(format!("[PoliticsWorker] exec cnt_pending_church: {e}")))?;
Ok(rows
.first()
.and_then(|r| r.get("cnt"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(0))
}
fn get_church_occupied_count(
pool: &ConnectionPool,
office_type_id: i32,
region_id: i32,
) -> Result<i32, DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"cnt_occ_church",
QUERY_GET_CHURCH_OFFICE_OCCUPIED_COUNT,
)
.map_err(|e| DbError::new(format!("[PoliticsWorker] prepare cnt_occ_church: {e}")))?;
let rows = conn
.execute("cnt_occ_church", &[&office_type_id, &region_id])
.map_err(|e| DbError::new(format!("[PoliticsWorker] exec cnt_occ_church: {e}")))?;
Ok(rows
.first()
.and_then(|r| r.get("cnt"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(0))
}
fn church_candidate_score(
supervisor_rep: f64,
applicant_rep: f64,
highest_ever: i32,
current_max: i32,
title_level: i32,
age_days: i32,
wait_days: f64,
) -> f64 {
let age_years = age_days / 365;
let age_bonus = if (25..=70).contains(&age_years) {
12.0
} else {
0.0
};
let base = (highest_ever as f64) * 4.0
+ (current_max as f64) * 3.0
+ applicant_rep * 0.45
+ (title_level as f64) * 1.1
+ age_bonus
+ wait_days * 0.15;
let inf = (supervisor_rep / 100.0).clamp(0.0, 1.0);
let noise = (1.0 - inf) * 28.0 * rand::random::<f64>();
base + noise
}
fn parse_pg_bool(v: Option<&str>) -> bool {
match v {
Some("t") | Some("true") => true,
Some("f") | Some("false") => false,
Some(s) => s.parse::<bool>().unwrap_or(false),
None => false,
}
}
fn character_eligible_for_church_office(
pool: &ConnectionPool,
character_id: i32,
office_type_id: i32,
) -> Result<bool, DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"get_church_office_requirements",
QUERY_GET_CHURCH_OFFICE_REQUIREMENTS,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare get_church_office_requirements: {e}"
))
})?;
conn.prepare(
"check_character_eligibility",
QUERY_CHECK_CHARACTER_ELIGIBILITY,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare check_character_eligibility: {e}"
))
})?;
let req_rows = conn
.execute("get_church_office_requirements", &[&office_type_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec get_church_office_requirements: {e}"
))
})?;
for req_row in req_rows {
let prerequisite_office_type_id = req_row
.get("prerequisite_office_type_id")
.and_then(|v| v.parse::<i32>().ok());
let min_title_level = req_row
.get("min_title_level")
.and_then(|v| v.parse::<i32>().ok());
let elig_rows = conn
.execute(
"check_character_eligibility",
&[
&character_id,
&prerequisite_office_type_id,
&min_title_level,
],
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec check_character_eligibility: {e}"
))
})?;
for elig_row in elig_rows {
let has_prerequisite = Self::parse_pg_bool(elig_row.get("has_prerequisite").map(|s| s.as_str()));
let meets_title_requirement =
Self::parse_pg_bool(elig_row.get("meets_title_requirement").map(|s| s.as_str()));
if !has_prerequisite || !meets_title_requirement {
return Ok(false);
}
}
}
Ok(true)
}
fn remove_lower_ranked_church_offices_for_character(
pool: &ConnectionPool,
character_id: i32,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"rm_lower_church",
QUERY_REMOVE_LOWER_CHURCH_OFFICES_FOR_CHARACTER,
)
.map_err(|e| DbError::new(format!("[PoliticsWorker] prepare rm_lower_church: {e}")))?;
conn.execute("rm_lower_church", &[&character_id])
.map_err(|e| DbError::new(format!("[PoliticsWorker] exec rm_lower_church: {e}")))?;
Ok(())
}
fn try_interim_church_appointment(
pool: &ConnectionPool,
broker: &MessageBroker,
office: &AvailableChurchOffice,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"find_interim_npc",
QUERY_FIND_INTERIM_CHURCH_NPC_CANDIDATE,
)
.map_err(|e| DbError::new(format!("[PoliticsWorker] prepare find_interim_npc: {e}")))?;
let rows = conn
.execute("find_interim_npc", &[&office.region_id, &office.office_type_id])
.map_err(|e| DbError::new(format!("[PoliticsWorker] exec find_interim_npc: {e}")))?;
let candidate_id = rows
.first()
.and_then(|r| r.get("character_id"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1);
if candidate_id < 0 {
return Ok(());
}
if !Self::character_eligible_for_church_office(pool, candidate_id, office.office_type_id)? {
return Ok(());
}
drop(conn);
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"interim_ins",
QUERY_INTERIM_APPOINT_CHURCH_OFFICE,
)
.map_err(|e| DbError::new(format!("[PoliticsWorker] prepare interim_ins: {e}")))?;
let ins = conn
.execute(
"interim_ins",
&[
&office.office_type_id,
&candidate_id,
&office.region_id,
&office.seats_per_region,
],
)
.map_err(|e| DbError::new(format!("[PoliticsWorker] exec interim_ins: {e}")))?;
if ins.is_empty() {
return Ok(());
}
conn.prepare(
"upd_hi_interim",
QUERY_UPDATE_CHARACTER_HIGHEST_CHURCH_FROM_OFFICE_TYPE,
)
.map_err(|e| DbError::new(format!("[PoliticsWorker] prepare upd_hi_interim: {e}")))?;
conn.execute("upd_hi_interim", &[&candidate_id, &office.office_type_id])
.map_err(|e| DbError::new(format!("[PoliticsWorker] exec upd_hi_interim: {e}")))?;
Self::remove_lower_ranked_church_offices_for_character(pool, candidate_id)?;
eprintln!(
"[PoliticsWorker] Interims-Kirchenamt: character_id={}, office_type_id={}, region_id={}",
candidate_id, office.office_type_id, office.region_id
);
if let Some(uid) = Self::get_user_id_for_character(pool, candidate_id)? {
Self::publish_falukant_church_update(broker, uid, "vacancy_fill");
}
Ok(())
}
fn find_available_church_offices(
pool: &ConnectionPool,
) -> Result<Vec<AvailableChurchOffice>, DbError> {
@@ -710,6 +949,7 @@ impl PoliticsWorker {
let mut offices = Vec::new();
for row in rows {
let office_type_id = parse_i32(&row, "office_type_id", -1);
let hierarchy_level = parse_i32(&row, "hierarchy_level", 99);
let region_id = parse_i32(&row, "region_id", -1);
let seats_per_region = parse_i32(&row, "seats_per_region", 0);
let occupied_seats = parse_i32(&row, "occupied_seats", 0);
@@ -717,6 +957,7 @@ impl PoliticsWorker {
if office_type_id >= 0 && region_id >= 0 {
offices.push(AvailableChurchOffice {
office_type_id,
hierarchy_level,
seats_per_region,
region_id,
occupied_seats,
@@ -763,7 +1004,37 @@ impl PoliticsWorker {
Ok(None)
}
fn process_church_applications(
/// Spieler-Vorgesetzte: nur Event (keine automatische Entscheidung). NPC: Score-basierte Auswahl.
fn process_church_supervisor_queue(
pool: &ConnectionPool,
broker: &MessageBroker,
supervisor_id: i32,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("is_npc_sup", QUERY_IS_CHARACTER_NPC)
.map_err(|e| DbError::new(format!("[PoliticsWorker] prepare is_npc_sup: {e}")))?;
let rows = conn
.execute("is_npc_sup", &[&supervisor_id])
.map_err(|e| DbError::new(format!("[PoliticsWorker] exec is_npc_sup: {e}")))?;
let is_npc = rows
.first()
.and_then(|r| r.get("is_npc"))
.map(|v| v == "t" || v == "true")
.unwrap_or(false);
if !is_npc {
if let Some(uid) = Self::get_user_id_for_character(pool, supervisor_id)? {
Self::publish_falukant_church_update(broker, uid, "applications");
}
return Ok(());
}
Self::npc_resolve_church_applications_for_supervisor(pool, broker, supervisor_id)
}
fn npc_resolve_church_applications_for_supervisor(
pool: &ConnectionPool,
broker: &MessageBroker,
supervisor_id: i32,
@@ -772,64 +1043,20 @@ impl PoliticsWorker {
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
// Bewerbungen für diesen Supervisor abrufen
conn.prepare(
"get_pending_church_applications",
QUERY_GET_PENDING_CHURCH_APPLICATIONS,
"score_church_apps",
QUERY_GET_PENDING_CHURCH_APPLICATIONS_FOR_SCORING,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare get_pending_church_applications: {e}"
"[PoliticsWorker] prepare score_church_apps: {e}"
))
})?;
let rows = conn
.execute("get_pending_church_applications", &[&supervisor_id])
.execute("score_church_apps", &[&supervisor_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec get_pending_church_applications: {e}"
))
})?;
let mut applications = Vec::new();
for row in rows {
let application_id = parse_i32(&row, "application_id", -1);
let office_type_id = parse_i32(&row, "office_type_id", -1);
let applicant_character_id = parse_i32(&row, "applicant_character_id", -1);
if application_id >= 0 {
applications.push(ChurchApplication {
application_id,
office_type_id,
applicant_character_id,
});
}
}
// Voraussetzungen prüfen und Bewerbungen verarbeiten
conn.prepare(
"get_church_office_requirements",
QUERY_GET_CHURCH_OFFICE_REQUIREMENTS,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare get_church_office_requirements: {e}"
))
})?;
conn.prepare(
"check_character_eligibility",
QUERY_CHECK_CHARACTER_ELIGIBILITY,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare check_character_eligibility: {e}"
))
})?;
conn.prepare("approve_church_application", QUERY_APPROVE_CHURCH_APPLICATION)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare approve_church_application: {e}"
"[PoliticsWorker] exec score_church_apps: {e}"
))
})?;
@@ -840,101 +1067,146 @@ impl PoliticsWorker {
))
})?;
for app in &applications {
// Voraussetzungen für dieses Amt abrufen
let req_rows = conn
.execute("get_church_office_requirements", &[&app.office_type_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec get_church_office_requirements: {e}"
))
})?;
conn.prepare("approve_church_application", QUERY_APPROVE_CHURCH_APPLICATION)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare approve_church_application: {e}"
))
})?;
let mut requirements = Vec::new();
for req_row in req_rows {
let prerequisite_office_type_id = req_row
.get("prerequisite_office_type_id")
.and_then(|v| v.parse::<i32>().ok());
let min_title_level = req_row
.get("min_title_level")
.and_then(|v| v.parse::<i32>().ok());
let mut scored: Vec<ChurchAppScoreRow> = Vec::new();
requirements.push(ChurchOfficeRequirement {
prerequisite_office_type_id,
min_title_level,
});
for row in rows {
let application_id = parse_i32(&row, "application_id", -1);
let office_type_id = parse_i32(&row, "office_type_id", -1);
let applicant_character_id = parse_i32(&row, "applicant_character_id", -1);
let region_id = parse_i32(&row, "region_id", -1);
let seats_per_region = parse_i32(&row, "seats_per_region", 1);
let supervisor_reputation = row
.get("supervisor_reputation")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(50.0);
let applicant_reputation = row
.get("applicant_reputation")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(50.0);
let applicant_highest_ever = parse_i32(&row, "applicant_highest_ever", 0);
let applicant_title_level = parse_i32(&row, "applicant_title_level", 0);
let applicant_current_max = parse_i32(&row, "applicant_current_max_hierarchy", 0);
let applicant_age_days = parse_i32(&row, "applicant_age_days", 0);
if application_id < 0 || applicant_character_id < 0 {
continue;
}
// Prüfe ob Character die Voraussetzungen erfüllt
let mut eligible = true;
for req in &requirements {
let elig_rows = conn
.execute(
"check_character_eligibility",
&[
&app.applicant_character_id,
&req.prerequisite_office_type_id,
&req.min_title_level,
],
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec check_character_eligibility: {e}"
))
})?;
for elig_row in elig_rows {
let has_prerequisite: bool = elig_row
.get("has_prerequisite")
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or(false);
let meets_title_requirement: bool = elig_row
.get("meets_title_requirement")
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or(false);
if !has_prerequisite || !meets_title_requirement {
eligible = false;
break;
}
}
}
// Bewerbung genehmigen oder ablehnen
if eligible {
let approve_rows = conn
.execute("approve_church_application", &[&app.application_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec approve_church_application: {e}"
))
})?;
if !approve_rows.is_empty() {
eprintln!(
"[PoliticsWorker] Church Application {} genehmigt (office_type_id={}, character_id={})",
app.application_id, app.office_type_id, app.applicant_character_id
);
// Benachrichtigung senden
if let Some(user_id) = Self::get_user_id_for_character(pool, app.applicant_character_id)? {
let msg = format!(
r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#,
user_id
);
broker.publish(msg);
}
}
} else {
conn.execute("reject_church_application", &[&app.application_id])
if !Self::character_eligible_for_church_office(pool, applicant_character_id, office_type_id)? {
conn.execute("reject_church_application", &[&application_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec reject_church_application: {e}"
))
})?;
eprintln!(
"[PoliticsWorker] Church Application {} abgelehnt (Voraussetzungen nicht erfüllt)",
app.application_id
"[PoliticsWorker] Church Application {} abgelehnt (NPC: nicht qualifiziert)",
application_id
);
continue;
}
let wait_days = 0.0_f64;
let score = Self::church_candidate_score(
supervisor_reputation,
applicant_reputation,
applicant_highest_ever,
applicant_current_max,
applicant_title_level,
applicant_age_days,
wait_days,
);
scored.push(ChurchAppScoreRow {
application_id,
office_type_id,
applicant_character_id,
region_id,
seats_per_region,
score,
});
}
drop(conn);
let mut groups: HashMap<(i32, i32), Vec<ChurchAppScoreRow>> = HashMap::new();
for s in scored {
groups
.entry((s.office_type_id, s.region_id))
.or_default()
.push(s);
}
for ((_ot, _reg), mut group) in groups {
group.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
let seats = group.first().map(|g| g.seats_per_region).unwrap_or(1);
let office_type_id = group.first().map(|g| g.office_type_id).unwrap_or(-1);
let region_id = group.first().map(|g| g.region_id).unwrap_or(-1);
if office_type_id < 0 || region_id < 0 {
continue;
}
let mut occupied = Self::get_church_occupied_count(pool, office_type_id, region_id)?;
let mut approved_here = 0usize;
for app in group {
if occupied >= seats {
break;
}
let approve_rows = {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"approve_church_application",
QUERY_APPROVE_CHURCH_APPLICATION,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare approve_church_application: {e}"
))
})?;
conn.execute("approve_church_application", &[&app.application_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec approve_church_application: {e}"
))
})?
};
if !approve_rows.is_empty() {
approved_here += 1;
occupied += 1;
eprintln!(
"[PoliticsWorker] Church Application {} genehmigt (NPC-Score, office_type_id={}, character_id={})",
app.application_id, app.office_type_id, app.applicant_character_id
);
if let Some(uid) =
Self::get_user_id_for_character(pool, app.applicant_character_id)?
{
Self::publish_falukant_church_update(broker, uid, "npc_decision");
}
}
}
if approved_here > 0 {
eprintln!(
"[PoliticsWorker] NPC-Kirche: {} Zusagen für office_type_id={}, region_id={}",
approved_here, office_type_id, region_id
);
}
}
@@ -1091,13 +1363,8 @@ impl PoliticsWorker {
application_id, character_id
);
// Benachrichtigung senden
if let Some(user_id) = Self::get_user_id_for_character(pool, *character_id)? {
let msg = format!(
r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#,
user_id
);
broker.publish(msg);
Self::publish_falukant_church_update(broker, user_id, "appointment");
}
}
}