Entferne automatische Annahme alter Bewerbungen und implementiere Entscheidung durch NPCs für offene Stellen ohne Vorgesetzten
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 5m13s
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 5m13s
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Falukant: Kirchenämter (YpDaemon / PoliticsWorker)
|
# Falukant: Kirchenämter (YpDaemon / PoliticsWorker)
|
||||||
|
|
||||||
Umsetzung des Zielmodells: Laufbahn `highest_church_hierarchy_ever`, NPC-Bewerbungen, NPC-Vorgesetzte mit Score, Spieler-Vorgesetzte ohne Daemon-Entscheidung, Interimsbesetzung, Events `falukantUpdateChurch`.
|
Umsetzung des Zielmodells: Laufbahn `highest_church_hierarchy_ever`, NPC-Bewerbungen, NPC-Vorgesetzte mit Score, Spieler-Vorgesetzte ohne Daemon-Entscheidung und Events `falukantUpdateChurch`.
|
||||||
|
|
||||||
## Migration
|
## Migration
|
||||||
|
|
||||||
@@ -13,16 +13,16 @@ Umsetzung des Zielmodells: Laufbahn `highest_church_hierarchy_ever`, NPC-Bewerbu
|
|||||||
| Was | Wann |
|
| Was | Wann |
|
||||||
|-----|------|
|
|-----|------|
|
||||||
| Kirchen-Gesamtprozess | Täglich im **gleichen** Lauf wie `perform_daily_politics_task` (nicht mehr nur 13:00) |
|
| Kirchen-Gesamtprozess | Täglich im **gleichen** Lauf wie `perform_daily_politics_task` (nicht mehr nur 13:00) |
|
||||||
| Auto-Approve alter Bewerbungen | Stündlich, **nur** wenn `supervisor.user_id IS NULL` (NPC-Vorgesetzter), 36 h+ |
|
| NPC-Entscheidung | Nach mindestens **2 Tagen** offener Bewerbung, täglich |
|
||||||
|
|
||||||
## Logik (Kurz)
|
## Logik (Kurz)
|
||||||
|
|
||||||
1. **Freie Sitze** je `church_office_type` / Region (`QUERY_FIND_AVAILABLE_CHURCH_OFFICES`, inkl. `hierarchy_level`).
|
1. **Freie Sitze** je `church_office_type` / Region (`QUERY_FIND_AVAILABLE_CHURCH_OFFICES`, inkl. `hierarchy_level`).
|
||||||
2. **Vorgesetzter** = nächsthöhere besetzte Hierarchie in der Region (`QUERY_FIND_CHURCH_SUPERVISOR`).
|
2. **Vorgesetzter** = nächsthöhere besetzte Hierarchie in der Region (`QUERY_FIND_CHURCH_SUPERVISOR`).
|
||||||
3. **Spieler-Vorgesetzter** (`character.user_id IS NOT NULL`): `falukantUpdateChurch` mit `reason: applications` — **keine** automatische Annahme/Ablehnung durch den Daemon.
|
3. **Spieler-Vorgesetzter** (`character.user_id IS NOT NULL`): `falukantUpdateChurch` mit `reason: applications` — **keine** automatische Annahme/Ablehnung durch den Daemon.
|
||||||
4. **NPC-Vorgesetzter**: Bewerber mit `churchCandidateScore` (Reputation, höchste Hierarchie, aktuelle Ämter, Titel, Alter, Zufallsanteil abhängig von `supervisor.reputation`). Pro `(office_type_id, region)` werden nur so viele Zusagen erteilt, wie Plätze frei sind.
|
4. **NPC-Vorgesetzter**: Nach der zweitägigen Bewerbungsfrist bewertet er Bewerber mit `churchCandidateScore` und entscheidet über die freien Sitze.
|
||||||
5. **NPC-Bewerbungen**: Nur Charaktere mit `user_id IS NULL`; nur wenn `pending < freie Sitze` (nachzüglernde Jobs).
|
5. **Kein Vorgesetzter**: Der Kirchen-NPC führt denselben Auswahlvorgang nach zwei Tagen durch. Das deckt insbesondere Einstiegsämter ab und verhindert dauerhaft offene Bewerbungen.
|
||||||
6. **Interimsbesetzung**: Kein Vorgesetzter, `hierarchy_level <= 6` (bis einschließlich „Bishop“-Ebene im Typ-Stammbaum — anpassbar über Konstante `INTERIM_MAX_CHURCH_HIERARCHY` in `politics.rs`): bester NPC-Kandidat + direktes `INSERT` in `church_office`, Update `highest_church_hierarchy_ever`, Entfernen niedrigerer kirchlicher Ämter.
|
6. **NPC-Laufbahn**: Nur Charaktere mit `user_id IS NULL`; Einstieg ohne Kirchenamt, jede Beförderung ausschließlich aus einem aktuell gehaltenen Amt genau eine Hierarchiestufe darunter.
|
||||||
|
|
||||||
## Bewerbungsvoraussetzungen (Daemon-SQL)
|
## Bewerbungsvoraussetzungen (Daemon-SQL)
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ Titelbedingungen unverändert (`min_title_level`).
|
|||||||
|
|
||||||
## Genehmigung / Amtsverlust
|
## Genehmigung / Amtsverlust
|
||||||
|
|
||||||
- `QUERY_APPROVE_CHURCH_APPLICATION` und `QUERY_AUTO_APPROVE_CHURCH_APPLICATION`: nach Eintrag `church_office` → Update `highest_church_hierarchy_ever`, dann `DELETE` niedrigerer konkurrierender `church_office` desselben Charakters (`remove_lower_ranked`).
|
- `QUERY_APPROVE_CHURCH_APPLICATION`: nach Eintrag `church_office` → Update `highest_church_hierarchy_ever`, dann `DELETE` niedrigerer konkurrierender `church_office` desselben Charakters (`remove_lower_ranked`).
|
||||||
|
|
||||||
## WebSocket
|
## WebSocket
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ Titelbedingungen unverändert (`min_title_level`).
|
|||||||
| `falukantUpdateChurch` | `{"event":"falukantUpdateChurch","user_id":N,"reason":"…"}` |
|
| `falukantUpdateChurch` | `{"event":"falukantUpdateChurch","user_id":N,"reason":"…"}` |
|
||||||
| `falukantUpdateStatus` | wie üblich, direkt danach |
|
| `falukantUpdateStatus` | wie üblich, direkt danach |
|
||||||
|
|
||||||
**reason:** `applications` (Spieler-Vorgesetzter), `npc_decision` (NPC hat zugesagt), `appointment` (Auto-Approve 36 h, nur NPC-Supervisor), `vacancy_fill` (Interimsbesetzung, nur wenn Bewerber ein Spielercharakter ist).
|
**reason:** `applications` (Spieler-Vorgesetzter), `npc_decision` (Kirchen-NPC hat entschieden).
|
||||||
|
|
||||||
Details: [`FALUKANT_UI_WEBSOCKET.md`](./FALUKANT_UI_WEBSOCKET.md).
|
Details: [`FALUKANT_UI_WEBSOCKET.md`](./FALUKANT_UI_WEBSOCKET.md).
|
||||||
|
|
||||||
@@ -55,5 +55,5 @@ Details: [`FALUKANT_UI_WEBSOCKET.md`](./FALUKANT_UI_WEBSOCKET.md).
|
|||||||
|
|
||||||
## Code
|
## Code
|
||||||
|
|
||||||
- `src/worker/politics.rs` — `perform_church_office_task`, `process_church_supervisor_queue`, `npc_resolve_church_applications_for_supervisor`, `try_interim_church_appointment`, Hilfsfunktionen.
|
- `src/worker/politics.rs` — `perform_church_office_task`, `process_church_supervisor_queue`, NPC-Entscheidungen mit und ohne Vorgesetzten, Hilfsfunktionen.
|
||||||
- `src/worker/sql.rs` — Abschnitt „Church Office Queries“.
|
- `src/worker/sql.rs` — Abschnitt „Church Office Queries“.
|
||||||
|
|||||||
@@ -39,10 +39,7 @@ use crate::worker::sql::{
|
|||||||
QUERY_GET_CHURCH_OFFICE_OCCUPIED_COUNT,
|
QUERY_GET_CHURCH_OFFICE_OCCUPIED_COUNT,
|
||||||
QUERY_IS_CHARACTER_NPC,
|
QUERY_IS_CHARACTER_NPC,
|
||||||
QUERY_GET_PENDING_CHURCH_APPLICATIONS_FOR_SCORING,
|
QUERY_GET_PENDING_CHURCH_APPLICATIONS_FOR_SCORING,
|
||||||
QUERY_INTERIM_APPOINT_CHURCH_OFFICE,
|
QUERY_GET_PENDING_CHURCH_APPLICATIONS_WITHOUT_SUPERVISOR,
|
||||||
QUERY_UPDATE_CHARACTER_HIGHEST_CHURCH_FROM_OFFICE_TYPE,
|
|
||||||
QUERY_FIND_INTERIM_CHURCH_NPC_CANDIDATE,
|
|
||||||
QUERY_REMOVE_LOWER_CHURCH_OFFICES_FOR_CHARACTER,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct PoliticsWorker {
|
pub struct PoliticsWorker {
|
||||||
@@ -81,7 +78,6 @@ struct Office {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct AvailableChurchOffice {
|
struct AvailableChurchOffice {
|
||||||
office_type_id: i32,
|
office_type_id: i32,
|
||||||
hierarchy_level: i32,
|
|
||||||
seats_per_region: i32,
|
seats_per_region: i32,
|
||||||
region_id: i32,
|
region_id: i32,
|
||||||
occupied_seats: i32,
|
occupied_seats: i32,
|
||||||
@@ -92,9 +88,6 @@ struct ChurchSupervisor {
|
|||||||
supervisor_character_id: i32,
|
supervisor_character_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bis einschließlich dieser `hierarchy_level` (church_office_type): Interimsbesetzung ohne Vorgesetzten.
|
|
||||||
const INTERIM_MAX_CHURCH_HIERARCHY: i32 = 6;
|
|
||||||
|
|
||||||
struct ChurchAppScoreRow {
|
struct ChurchAppScoreRow {
|
||||||
application_id: i32,
|
application_id: i32,
|
||||||
office_type_id: i32,
|
office_type_id: i32,
|
||||||
@@ -116,7 +109,6 @@ impl PoliticsWorker {
|
|||||||
|
|
||||||
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
|
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
|
||||||
let mut last_execution: Option<Instant> = None;
|
let mut last_execution: Option<Instant> = None;
|
||||||
let mut last_auto_approve_run: Option<Instant> = None;
|
|
||||||
|
|
||||||
while state.running_worker.load(Ordering::Relaxed) {
|
while state.running_worker.load(Ordering::Relaxed) {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
@@ -132,21 +124,6 @@ impl PoliticsWorker {
|
|||||||
last_execution = Some(now);
|
last_execution = Some(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatische Annahme alter Applications (stündlich)
|
|
||||||
let should_run_auto_approve = match last_auto_approve_run {
|
|
||||||
None => true,
|
|
||||||
Some(prev) => {
|
|
||||||
now.saturating_duration_since(prev) >= Duration::from_secs(3600)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_run_auto_approve {
|
|
||||||
if let Err(err) = Self::auto_approve_old_church_applications(&pool, &broker) {
|
|
||||||
eprintln!("[PoliticsWorker] Fehler bei auto_approve_old_church_applications: {err}");
|
|
||||||
}
|
|
||||||
last_auto_approve_run = Some(now);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entspricht ungefähr der 5-Sekunden-Schleife im C++-Code
|
// Entspricht ungefähr der 5-Sekunden-Schleife im C++-Code
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
if !state.running_worker.load(Ordering::Relaxed) {
|
if !state.running_worker.load(Ordering::Relaxed) {
|
||||||
@@ -770,18 +747,37 @@ impl PoliticsWorker {
|
|||||||
pool,
|
pool,
|
||||||
office.office_type_id,
|
office.office_type_id,
|
||||||
office.region_id,
|
office.region_id,
|
||||||
supervisor.supervisor_character_id,
|
Some(supervisor.supervisor_character_id),
|
||||||
need,
|
need,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if office.hierarchy_level <= INTERIM_MAX_CHURCH_HIERARCHY {
|
|
||||||
Self::try_interim_church_appointment(pool, broker, office)?;
|
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
// Einstiegsämter haben naturgemäß keinen Vorgesetzten. Offene
|
||||||
"[PoliticsWorker] Kein Supervisor, Interim deaktiviert (hierarchy_level={}): office_type_id={}, region_id={}",
|
// Spielerbewerbungen dürfen deshalb nicht dauerhaft pending
|
||||||
office.hierarchy_level, office.office_type_id, office.region_id
|
// bleiben: Der Kirchen-NPC entscheidet sie für diese Stelle.
|
||||||
);
|
Self::npc_resolve_church_applications_without_supervisor(pool, broker, office)?;
|
||||||
|
|
||||||
|
let occupied = Self::get_church_occupied_count(
|
||||||
|
pool,
|
||||||
|
office.office_type_id,
|
||||||
|
office.region_id,
|
||||||
|
)?;
|
||||||
|
let pending = Self::count_pending_church_apps(
|
||||||
|
pool,
|
||||||
|
office.office_type_id,
|
||||||
|
office.region_id,
|
||||||
|
)?;
|
||||||
|
let missing_applications = (office.seats_per_region - occupied - pending).max(0);
|
||||||
|
if missing_applications > 0 {
|
||||||
|
Self::create_church_application_jobs(
|
||||||
|
pool,
|
||||||
|
office.office_type_id,
|
||||||
|
office.region_id,
|
||||||
|
None,
|
||||||
|
missing_applications,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,7 +850,9 @@ impl PoliticsWorker {
|
|||||||
age_days: i32,
|
age_days: i32,
|
||||||
wait_days: f64,
|
wait_days: f64,
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
let age_years = age_days / 365;
|
// Ein Kalendertag ist ein Spieljahr; die Datenbankdifferenz ist somit
|
||||||
|
// bereits das Alter in Spieljahren.
|
||||||
|
let age_years = age_days;
|
||||||
let age_bonus = if (25..=70).contains(&age_years) {
|
let age_bonus = if (25..=70).contains(&age_years) {
|
||||||
12.0
|
12.0
|
||||||
} else {
|
} else {
|
||||||
@@ -950,100 +948,6 @@ impl PoliticsWorker {
|
|||||||
Ok(true)
|
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(
|
fn find_available_church_offices(
|
||||||
pool: &ConnectionPool,
|
pool: &ConnectionPool,
|
||||||
) -> Result<Vec<AvailableChurchOffice>, DbError> {
|
) -> Result<Vec<AvailableChurchOffice>, DbError> {
|
||||||
@@ -1071,7 +975,6 @@ impl PoliticsWorker {
|
|||||||
let mut offices = Vec::new();
|
let mut offices = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let office_type_id = parse_i32(&row, "office_type_id", -1);
|
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 region_id = parse_i32(&row, "region_id", -1);
|
||||||
let seats_per_region = parse_i32(&row, "seats_per_region", 0);
|
let seats_per_region = parse_i32(&row, "seats_per_region", 0);
|
||||||
let occupied_seats = parse_i32(&row, "occupied_seats", 0);
|
let occupied_seats = parse_i32(&row, "occupied_seats", 0);
|
||||||
@@ -1079,7 +982,6 @@ impl PoliticsWorker {
|
|||||||
if office_type_id >= 0 && region_id >= 0 {
|
if office_type_id >= 0 && region_id >= 0 {
|
||||||
offices.push(AvailableChurchOffice {
|
offices.push(AvailableChurchOffice {
|
||||||
office_type_id,
|
office_type_id,
|
||||||
hierarchy_level,
|
|
||||||
seats_per_region,
|
seats_per_region,
|
||||||
region_id,
|
region_id,
|
||||||
occupied_seats,
|
occupied_seats,
|
||||||
@@ -1336,11 +1238,102 @@ impl PoliticsWorker {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Entscheidet Bewerbungen für Ämter, die noch keinen Vorgesetzten haben.
|
||||||
|
/// Das betrifft insbesondere `lay-preacher`: Ohne diesen Pfad bleiben die
|
||||||
|
/// vom Backend erzeugten Bewerbungen für immer auf `pending`.
|
||||||
|
fn npc_resolve_church_applications_without_supervisor(
|
||||||
|
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(
|
||||||
|
"score_church_apps_no_supervisor",
|
||||||
|
QUERY_GET_PENDING_CHURCH_APPLICATIONS_WITHOUT_SUPERVISOR,
|
||||||
|
)?;
|
||||||
|
conn.prepare("reject_church_application", QUERY_REJECT_CHURCH_APPLICATION)?;
|
||||||
|
conn.prepare("approve_church_application", QUERY_APPROVE_CHURCH_APPLICATION)?;
|
||||||
|
|
||||||
|
let rows = conn.execute(
|
||||||
|
"score_church_apps_no_supervisor",
|
||||||
|
&[&office.office_type_id, &office.region_id],
|
||||||
|
)?;
|
||||||
|
if rows.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
let application_id = parse_i32(&row, "application_id", -1);
|
||||||
|
let applicant_character_id = parse_i32(&row, "applicant_character_id", -1);
|
||||||
|
if application_id < 0 || applicant_character_id < 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Self::character_eligible_for_church_office(
|
||||||
|
pool,
|
||||||
|
applicant_character_id,
|
||||||
|
office.office_type_id,
|
||||||
|
)? {
|
||||||
|
conn.execute("reject_church_application", &[&application_id])?;
|
||||||
|
if let Some(uid) = Self::get_user_id_for_character(pool, applicant_character_id)? {
|
||||||
|
Self::publish_falukant_church_update(broker, uid, "npc_decision");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let score = Self::church_candidate_score(
|
||||||
|
row.get("supervisor_reputation")
|
||||||
|
.and_then(|v| v.parse::<f64>().ok())
|
||||||
|
.unwrap_or(50.0),
|
||||||
|
row.get("applicant_reputation")
|
||||||
|
.and_then(|v| v.parse::<f64>().ok())
|
||||||
|
.unwrap_or(50.0),
|
||||||
|
parse_i32(&row, "applicant_highest_ever", 0),
|
||||||
|
parse_i32(&row, "applicant_current_max_hierarchy", 0),
|
||||||
|
parse_i32(&row, "applicant_title_level", 0),
|
||||||
|
parse_i32(&row, "applicant_age_days", 0),
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
candidates.push((application_id, applicant_character_id, score));
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.sort_by(|a, b| {
|
||||||
|
b.2.partial_cmp(&a.2)
|
||||||
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut occupied = Self::get_church_occupied_count(
|
||||||
|
pool,
|
||||||
|
office.office_type_id,
|
||||||
|
office.region_id,
|
||||||
|
)?;
|
||||||
|
for (application_id, applicant_character_id, _) in candidates {
|
||||||
|
let approved = occupied < office.seats_per_region
|
||||||
|
&& !conn
|
||||||
|
.execute("approve_church_application", &[&application_id])?
|
||||||
|
.is_empty();
|
||||||
|
if approved {
|
||||||
|
occupied += 1;
|
||||||
|
} else {
|
||||||
|
conn.execute("reject_church_application", &[&application_id])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(uid) = Self::get_user_id_for_character(pool, applicant_character_id)? {
|
||||||
|
Self::publish_falukant_church_update(broker, uid, "npc_decision");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn create_church_application_jobs(
|
fn create_church_application_jobs(
|
||||||
pool: &ConnectionPool,
|
pool: &ConnectionPool,
|
||||||
office_type_id: i32,
|
office_type_id: i32,
|
||||||
region_id: i32,
|
region_id: i32,
|
||||||
supervisor_id: i32,
|
supervisor_id: Option<i32>,
|
||||||
count: i32,
|
count: i32,
|
||||||
) -> Result<(), DbError> {
|
) -> Result<(), DbError> {
|
||||||
let mut conn = pool
|
let mut conn = pool
|
||||||
@@ -1359,7 +1352,10 @@ impl PoliticsWorker {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let rows = conn
|
let rows = conn
|
||||||
.execute("get_characters_for_church_office", &[®ion_id, &count])
|
.execute(
|
||||||
|
"get_characters_for_church_office",
|
||||||
|
&[®ion_id, &office_type_id, &count],
|
||||||
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
DbError::new(format!(
|
DbError::new(format!(
|
||||||
"[PoliticsWorker] exec get_characters_for_church_office: {e}"
|
"[PoliticsWorker] exec get_characters_for_church_office: {e}"
|
||||||
@@ -1411,7 +1407,9 @@ impl PoliticsWorker {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Automatische Annahme von Church Applications, die älter als 36 Stunden sind
|
/// Der frühere stündliche Auto-Approve ist bewusst nicht mehr im Scheduler
|
||||||
|
/// registriert; Entscheidungen laufen nach der zweitägigen Bewerbungsfrist.
|
||||||
|
#[allow(dead_code)]
|
||||||
fn auto_approve_old_church_applications(
|
fn auto_approve_old_church_applications(
|
||||||
pool: &ConnectionPool,
|
pool: &ConnectionPool,
|
||||||
broker: &MessageBroker,
|
broker: &MessageBroker,
|
||||||
|
|||||||
@@ -3215,7 +3215,8 @@ pub const QUERY_CREATE_CHURCH_APPLICATION_JOB: &str = r#"
|
|||||||
RETURNING id;
|
RETURNING id;
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
/// Nur NPCs: Spielerbewerbungen laufen über die UI.
|
/// Nur NPCs: Für den Einstieg ohne Amt, für eine Beförderung ausschließlich aus
|
||||||
|
/// dem unmittelbar niedrigeren derzeitigen Kirchenamt.
|
||||||
pub const QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE: &str = r#"
|
pub const QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE: &str = r#"
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
c.id AS character_id,
|
c.id AS character_id,
|
||||||
@@ -3224,17 +3225,29 @@ pub const QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE: &str = r#"
|
|||||||
c.title_of_nobility,
|
c.title_of_nobility,
|
||||||
t.level AS title_level
|
t.level AS title_level
|
||||||
FROM falukant_data.character c
|
FROM falukant_data.character c
|
||||||
|
JOIN falukant_type.church_office_type target ON target.id = $2::int
|
||||||
LEFT JOIN falukant_type.title t ON c.title_of_nobility = t.id
|
LEFT JOIN falukant_type.title t ON c.title_of_nobility = t.id
|
||||||
WHERE c.region_id = $1
|
WHERE c.region_id = $1
|
||||||
AND c.health > 0
|
AND c.health > 0
|
||||||
AND c.user_id IS NULL
|
AND c.user_id IS NULL
|
||||||
AND NOT EXISTS(
|
AND (
|
||||||
SELECT 1
|
(target.hierarchy_level <= 1 AND NOT EXISTS(
|
||||||
FROM falukant_data.church_office co
|
SELECT 1
|
||||||
WHERE co.character_id = c.id
|
FROM falukant_data.church_office co
|
||||||
|
WHERE co.character_id = c.id
|
||||||
|
))
|
||||||
|
OR
|
||||||
|
(target.hierarchy_level > 1 AND EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM falukant_data.church_office co
|
||||||
|
JOIN falukant_type.church_office_type current
|
||||||
|
ON current.id = co.office_type_id
|
||||||
|
WHERE co.character_id = c.id
|
||||||
|
AND current.hierarchy_level = target.hierarchy_level - 1
|
||||||
|
))
|
||||||
)
|
)
|
||||||
ORDER BY RANDOM()
|
ORDER BY RANDOM()
|
||||||
LIMIT $2;
|
LIMIT $3;
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
pub const QUERY_COUNT_PENDING_CHURCH_APPS_BY_OFFICE_REGION: &str = r#"
|
pub const QUERY_COUNT_PENDING_CHURCH_APPS_BY_OFFICE_REGION: &str = r#"
|
||||||
@@ -3285,9 +3298,45 @@ pub const QUERY_GET_PENDING_CHURCH_APPLICATIONS_FOR_SCORING: &str = r#"
|
|||||||
LEFT JOIN falukant_type.title t ON t.id = ac.title_of_nobility
|
LEFT JOIN falukant_type.title t ON t.id = ac.title_of_nobility
|
||||||
WHERE ca.status = 'pending'
|
WHERE ca.status = 'pending'
|
||||||
AND ca.supervisor_id = $1::int
|
AND ca.supervisor_id = $1::int
|
||||||
|
AND ca.created_at <= NOW() - INTERVAL '2 days'
|
||||||
ORDER BY ca.created_at ASC;
|
ORDER BY ca.created_at ASC;
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
/// Einstiegsämter haben noch keinen Amtsinhaber als Vorgesetzten. Diese offenen
|
||||||
|
/// Bewerbungen werden deshalb vom Kirchen-NPC für die konkrete Stelle entschieden.
|
||||||
|
pub const QUERY_GET_PENDING_CHURCH_APPLICATIONS_WITHOUT_SUPERVISOR: &str = r#"
|
||||||
|
SELECT
|
||||||
|
ca.id AS application_id,
|
||||||
|
ca.office_type_id,
|
||||||
|
ca.character_id AS applicant_character_id,
|
||||||
|
ca.region_id,
|
||||||
|
ca.created_at,
|
||||||
|
cot.hierarchy_level AS office_hierarchy_level,
|
||||||
|
cot.seats_per_region,
|
||||||
|
50.0::float8 AS supervisor_reputation,
|
||||||
|
COALESCE(ac.reputation, 50)::float8 AS applicant_reputation,
|
||||||
|
COALESCE(ac.highest_church_hierarchy_ever, 0)::int AS applicant_highest_ever,
|
||||||
|
COALESCE(t.level, 0)::int AS applicant_title_level,
|
||||||
|
COALESCE((
|
||||||
|
SELECT MAX(cot2.hierarchy_level)
|
||||||
|
FROM falukant_data.church_office co2
|
||||||
|
JOIN falukant_type.church_office_type cot2 ON cot2.id = co2.office_type_id
|
||||||
|
WHERE co2.character_id = ac.id
|
||||||
|
), 0)::int AS applicant_current_max_hierarchy,
|
||||||
|
(CURRENT_DATE - ac.birthdate::date)::int AS applicant_age_days
|
||||||
|
FROM falukant_data.church_application ca
|
||||||
|
JOIN falukant_data.character ac ON ac.id = ca.character_id
|
||||||
|
JOIN falukant_type.church_office_type cot ON cot.id = ca.office_type_id
|
||||||
|
LEFT JOIN falukant_type.title t ON t.id = ac.title_of_nobility
|
||||||
|
WHERE ca.status = 'pending'
|
||||||
|
AND ca.supervisor_id IS NULL
|
||||||
|
AND ca.office_type_id = $1::int
|
||||||
|
AND ca.region_id = $2::int
|
||||||
|
AND ca.created_at <= NOW() - INTERVAL '2 days'
|
||||||
|
ORDER BY ca.created_at ASC;
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub const QUERY_INTERIM_APPOINT_CHURCH_OFFICE: &str = r#"
|
pub const QUERY_INTERIM_APPOINT_CHURCH_OFFICE: &str = r#"
|
||||||
INSERT INTO falukant_data.church_office
|
INSERT INTO falukant_data.church_office
|
||||||
(office_type_id, character_id, region_id, supervisor_id, created_at, updated_at)
|
(office_type_id, character_id, region_id, supervisor_id, created_at, updated_at)
|
||||||
@@ -3307,6 +3356,7 @@ pub const QUERY_INTERIM_APPOINT_CHURCH_OFFICE: &str = r#"
|
|||||||
RETURNING id, office_type_id, character_id, region_id;
|
RETURNING id, office_type_id, character_id, region_id;
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub const QUERY_UPDATE_CHARACTER_HIGHEST_CHURCH_FROM_OFFICE_TYPE: &str = r#"
|
pub const QUERY_UPDATE_CHARACTER_HIGHEST_CHURCH_FROM_OFFICE_TYPE: &str = r#"
|
||||||
UPDATE falukant_data.character c
|
UPDATE falukant_data.character c
|
||||||
SET highest_church_hierarchy_ever = GREATEST(
|
SET highest_church_hierarchy_ever = GREATEST(
|
||||||
@@ -3317,6 +3367,7 @@ pub const QUERY_UPDATE_CHARACTER_HIGHEST_CHURCH_FROM_OFFICE_TYPE: &str = r#"
|
|||||||
RETURNING c.id;
|
RETURNING c.id;
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub const QUERY_FIND_INTERIM_CHURCH_NPC_CANDIDATE: &str = r#"
|
pub const QUERY_FIND_INTERIM_CHURCH_NPC_CANDIDATE: &str = r#"
|
||||||
SELECT c.id AS character_id
|
SELECT c.id AS character_id
|
||||||
FROM falukant_data.character c
|
FROM falukant_data.character c
|
||||||
@@ -3337,6 +3388,7 @@ pub const QUERY_FIND_INTERIM_CHURCH_NPC_CANDIDATE: &str = r#"
|
|||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub const QUERY_REMOVE_LOWER_CHURCH_OFFICES_FOR_CHARACTER: &str = r#"
|
pub const QUERY_REMOVE_LOWER_CHURCH_OFFICES_FOR_CHARACTER: &str = r#"
|
||||||
DELETE FROM falukant_data.church_office co
|
DELETE FROM falukant_data.church_office co
|
||||||
WHERE co.character_id = $1::int
|
WHERE co.character_id = $1::int
|
||||||
@@ -4100,4 +4152,16 @@ mod tests {
|
|||||||
assert!(query.contains(">= 16"), "{query}");
|
assert!(query.contains(">= 16"), "{query}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn npc_church_promotions_wait_two_days_and_require_the_direct_lower_rank() {
|
||||||
|
assert!(QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE
|
||||||
|
.contains("current.hierarchy_level = target.hierarchy_level - 1"));
|
||||||
|
for query in [
|
||||||
|
QUERY_GET_PENDING_CHURCH_APPLICATIONS_FOR_SCORING,
|
||||||
|
QUERY_GET_PENDING_CHURCH_APPLICATIONS_WITHOUT_SUPERVISOR,
|
||||||
|
] {
|
||||||
|
assert!(query.contains("created_at <= NOW() - INTERVAL '2 days'"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user