Initial commit: Rust YpDaemon
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
1364
Cargo.lock
generated
Normal file
1364
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "YpDaemon"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8"
|
||||
postgres = "0.19"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.48", features = ["rt-multi-thread", "macros", "net", "sync", "time"] }
|
||||
tokio-tungstenite = "0.23"
|
||||
futures-util = "0.3"
|
||||
ctrlc = "3"
|
||||
tokio-rustls = "0.25"
|
||||
rustls-pemfile = "2"
|
||||
libsystemd = "0.7"
|
||||
40
src/config.rs
Normal file
40
src/config.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
values: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut values = HashMap::new();
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
if line.trim().is_empty() || line.trim_start().starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
values.insert(key.trim().to_string(), value.trim().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { values })
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Result<String, String> {
|
||||
self.values
|
||||
.get(key)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("Konfigurationsschlüssel nicht gefunden: {key}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
207
src/db/connection.rs
Normal file
207
src/db/connection.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
use postgres::{Client, NoTls};
|
||||
use postgres::Error as PgError;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
pub type Row = HashMap<String, String>;
|
||||
pub type Rows = Vec<Row>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DbError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl DbError {
|
||||
pub fn new(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DbError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DbError {}
|
||||
|
||||
impl From<PgError> for DbError {
|
||||
fn from(err: PgError) -> Self {
|
||||
DbError::new(format!("Postgres-Fehler: {err}"))
|
||||
}
|
||||
}
|
||||
|
||||
struct Database {
|
||||
client: Client,
|
||||
// Name -> SQL
|
||||
prepared: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
fn connect(conn_str: &str) -> Result<Self, DbError> {
|
||||
let client = Client::connect(conn_str, NoTls)?;
|
||||
Ok(Self {
|
||||
client,
|
||||
prepared: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn is_valid(&mut self) -> bool {
|
||||
self.client
|
||||
.simple_query("SELECT 1")
|
||||
.map(|_| true)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn query(&mut self, sql: &str) -> Result<Rows, DbError> {
|
||||
let rows = self.client.query(sql, &[])?;
|
||||
Ok(rows.into_iter().map(Self::row_to_map).collect())
|
||||
}
|
||||
|
||||
fn prepare(&mut self, name: &str, sql: &str) -> Result<(), DbError> {
|
||||
self.prepared.insert(name.to_string(), sql.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&mut self,
|
||||
name: &str,
|
||||
params: &[&(dyn postgres::types::ToSql + Sync)],
|
||||
) -> Result<Rows, DbError> {
|
||||
let sql = self
|
||||
.prepared
|
||||
.get(name)
|
||||
.ok_or_else(|| DbError::new(format!("Unbekanntes Statement: {name}")))?;
|
||||
|
||||
let rows = self.client.query(sql.as_str(), params)?;
|
||||
Ok(rows.into_iter().map(Self::row_to_map).collect())
|
||||
}
|
||||
|
||||
fn row_to_map(row: postgres::Row) -> Row {
|
||||
let mut map = HashMap::with_capacity(row.len());
|
||||
for (idx, col) in row.columns().iter().enumerate() {
|
||||
let name = col.name().to_string();
|
||||
let value: Option<String> = row.get(idx);
|
||||
map.insert(name, value.unwrap_or_default());
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
struct InnerPool {
|
||||
connections: Mutex<Vec<Database>>,
|
||||
available: Condvar,
|
||||
conn_str: String,
|
||||
}
|
||||
|
||||
impl InnerPool {
|
||||
fn new(conn_str: String, size: usize) -> Result<Self, DbError> {
|
||||
let mut connections = Vec::with_capacity(size);
|
||||
for _ in 0..size {
|
||||
connections.push(Database::connect(&conn_str)?);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
connections: Mutex::new(connections),
|
||||
available: Condvar::new(),
|
||||
conn_str,
|
||||
})
|
||||
}
|
||||
|
||||
fn get(&self) -> Result<Database, DbError> {
|
||||
let mut guard = self.connections.lock().unwrap();
|
||||
|
||||
loop {
|
||||
if let Some(mut db) = guard.pop() {
|
||||
if db.is_valid() {
|
||||
return Ok(db);
|
||||
}
|
||||
|
||||
// Versuche, eine neue Verbindung aufzubauen
|
||||
match Database::connect(&self.conn_str) {
|
||||
Ok(new_db) => return Ok(new_db),
|
||||
Err(err) => {
|
||||
eprintln!("[ConnectionPool] Fehler beim Neuaufbau der Verbindung: {err}");
|
||||
// kurze Pause und erneut versuchen
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
guard = self.available.wait(guard).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn put_back(&self, db: Database) {
|
||||
let mut guard = self.connections.lock().unwrap();
|
||||
guard.push(db);
|
||||
self.available.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConnectionPool {
|
||||
inner: Arc<InnerPool>,
|
||||
}
|
||||
|
||||
impl ConnectionPool {
|
||||
pub fn new(conn_str: String, size: usize) -> Result<Self, DbError> {
|
||||
let inner = InnerPool::new(conn_str, size)?;
|
||||
Ok(Self {
|
||||
inner: Arc::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get(&self) -> Result<DbConnection, DbError> {
|
||||
let db = self.inner.get()?;
|
||||
Ok(DbConnection {
|
||||
inner: self.inner.clone(),
|
||||
db: Some(db),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DbConnection {
|
||||
inner: Arc<InnerPool>,
|
||||
db: Option<Database>,
|
||||
}
|
||||
|
||||
impl DbConnection {
|
||||
#[allow(dead_code)]
|
||||
pub fn query(&mut self, sql: &str) -> Result<Rows, DbError> {
|
||||
self.database_mut().query(sql)
|
||||
}
|
||||
|
||||
pub fn prepare(&mut self, name: &str, sql: &str) -> Result<(), DbError> {
|
||||
self.database_mut().prepare(name, sql)
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
&mut self,
|
||||
name: &str,
|
||||
params: &[&(dyn postgres::types::ToSql + Sync)],
|
||||
) -> Result<Rows, DbError> {
|
||||
self.database_mut().execute(name, params)
|
||||
}
|
||||
|
||||
fn database_mut(&mut self) -> &mut Database {
|
||||
self.db
|
||||
.as_mut()
|
||||
.expect("DbConnection ohne aktive Database verwendet")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DbConnection {
|
||||
fn drop(&mut self) {
|
||||
if let Some(db) = self.db.take() {
|
||||
self.inner.put_back(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
src/db/mod.rs
Normal file
5
src/db/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod connection;
|
||||
|
||||
pub use connection::{ConnectionPool, DbConnection, DbError, Row, Rows};
|
||||
|
||||
|
||||
186
src/main.rs
Normal file
186
src/main.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
mod config;
|
||||
mod db;
|
||||
mod message_broker;
|
||||
mod worker;
|
||||
mod websocket_server;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use config::Config;
|
||||
use libsystemd::daemon::{self, NotifyState};
|
||||
use message_broker::MessageBroker;
|
||||
use websocket_server::WebSocketServer;
|
||||
use worker::{
|
||||
CharacterCreationWorker, ConnectionPool, DirectorWorker, HouseWorker, PoliticsWorker,
|
||||
ProduceWorker, StockageManager, UndergroundWorker, UserCharacterWorker,
|
||||
ValueRecalculationWorker, Worker,
|
||||
};
|
||||
|
||||
static KEEP_RUNNING: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run_daemon() {
|
||||
eprintln!("Fehler im Daemon: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_daemon() -> Result<(), Box<dyn std::error::Error>> {
|
||||
install_signal_handler()?;
|
||||
|
||||
let config = load_config()?;
|
||||
let pool = create_connection_pool(&config)?;
|
||||
let websocket_config = load_websocket_config(&config)?;
|
||||
|
||||
let broker = MessageBroker::new();
|
||||
let mut websocket_server = WebSocketServer::new(
|
||||
websocket_config.port,
|
||||
pool.clone(),
|
||||
broker.clone(),
|
||||
websocket_config.ssl_enabled,
|
||||
websocket_config.cert_path,
|
||||
websocket_config.key_path,
|
||||
);
|
||||
|
||||
let mut workers = create_workers(pool, broker.clone());
|
||||
|
||||
websocket_server.set_workers(&workers);
|
||||
start_system(&mut websocket_server, &mut workers, &broker);
|
||||
|
||||
// systemd: melden, dass der Dienst jetzt "bereit" ist
|
||||
let _ = daemon::notify(false, &[NotifyState::Ready]);
|
||||
|
||||
run_main_loop();
|
||||
|
||||
shutdown_system(&mut websocket_server, &mut workers, &broker);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_signal_handler() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Behandle SIGINT/SIGTERM (z.B. Strg+C) und leite auf das globale Flag um.
|
||||
ctrlc::set_handler(|| {
|
||||
KEEP_RUNNING.store(false, Ordering::SeqCst);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct WebSocketConfig {
|
||||
port: u16,
|
||||
ssl_enabled: bool,
|
||||
cert_path: Option<String>,
|
||||
key_path: Option<String>,
|
||||
}
|
||||
|
||||
fn load_config() -> Result<Config, Box<dyn std::error::Error>> {
|
||||
// Pfad später ggf. konfigurierbar machen
|
||||
let config = Config::from_file("/etc/yourpart/daemon.conf")?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn create_connection_pool(config: &Config) -> Result<ConnectionPool, Box<dyn std::error::Error>> {
|
||||
let host = config.get("DB_HOST")?;
|
||||
let port = config.get("DB_PORT")?;
|
||||
let name = config.get("DB_NAME")?;
|
||||
let user = config.get("DB_USER")?;
|
||||
let password = config.get("DB_PASSWORD")?;
|
||||
|
||||
let conn_str = format!(
|
||||
"host={} port={} dbname={} user={} password={}",
|
||||
host, port, name, user, password
|
||||
);
|
||||
|
||||
// Pool-Größe analog zur C++-Implementierung
|
||||
let pool = db::ConnectionPool::new(conn_str, 10)?;
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
fn load_websocket_config(config: &Config) -> Result<WebSocketConfig, Box<dyn std::error::Error>> {
|
||||
let port: u16 = config.get("WEBSOCKET_PORT")?.parse()?;
|
||||
let ssl_enabled =
|
||||
config.get("WEBSOCKET_SSL_ENABLED").unwrap_or_else(|_| "false".into()) == "true";
|
||||
|
||||
let cert_path = if ssl_enabled {
|
||||
Some(config.get("WEBSOCKET_SSL_CERT_PATH")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let key_path = if ssl_enabled {
|
||||
Some(config.get("WEBSOCKET_SSL_KEY_PATH")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(WebSocketConfig {
|
||||
port,
|
||||
ssl_enabled,
|
||||
cert_path,
|
||||
key_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_workers(pool: ConnectionPool, broker: MessageBroker) -> Vec<Box<dyn Worker>> {
|
||||
vec![
|
||||
Box::new(CharacterCreationWorker::new(pool.clone(), broker.clone())),
|
||||
Box::new(ProduceWorker::new(pool.clone(), broker.clone())),
|
||||
Box::new(StockageManager::new(pool.clone(), broker.clone())),
|
||||
Box::new(DirectorWorker::new(pool.clone(), broker.clone())),
|
||||
Box::new(ValueRecalculationWorker::new(
|
||||
pool.clone(),
|
||||
broker.clone(),
|
||||
)),
|
||||
Box::new(UserCharacterWorker::new(
|
||||
pool.clone(),
|
||||
broker.clone(),
|
||||
)),
|
||||
Box::new(HouseWorker::new(pool.clone(), broker.clone())),
|
||||
Box::new(PoliticsWorker::new(pool.clone(), broker.clone())),
|
||||
Box::new(UndergroundWorker::new(pool, broker)),
|
||||
]
|
||||
}
|
||||
|
||||
fn start_system(
|
||||
websocket_server: &mut WebSocketServer,
|
||||
workers: &mut [Box<dyn Worker>],
|
||||
broker: &MessageBroker,
|
||||
) {
|
||||
broker.start();
|
||||
websocket_server.run();
|
||||
|
||||
for worker in workers {
|
||||
worker.start_worker_thread();
|
||||
worker.enable_watchdog();
|
||||
}
|
||||
}
|
||||
|
||||
fn shutdown_system(
|
||||
websocket_server: &mut WebSocketServer,
|
||||
workers: &mut [Box<dyn Worker>],
|
||||
broker: &MessageBroker,
|
||||
) {
|
||||
// systemd: wir fahren nun kontrolliert herunter
|
||||
let _ = daemon::notify(false, &[NotifyState::Stopping]);
|
||||
|
||||
// 1) Worker stoppen – sie prüfen regelmäßig ihr `running_worker`-Flag und
|
||||
// brechen daher auch bei längeren Work-Intervallen zügig ab.
|
||||
for worker in workers {
|
||||
worker.stop_worker_thread();
|
||||
}
|
||||
|
||||
// 2) WebSocket-Server stoppen (Tokio-Runtime herunterfahren)
|
||||
websocket_server.stop();
|
||||
|
||||
// 3) MessageBroker-Hook – aktuell noch Stub, aber hier zentral ergänzt
|
||||
// für eine spätere interne Queue/Thread-Implementierung.
|
||||
broker.stop();
|
||||
}
|
||||
|
||||
fn run_main_loop() {
|
||||
while KEEP_RUNNING.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
115
src/message_broker.rs
Normal file
115
src/message_broker.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
// Platzhalter-Implementierung, angelehnt an die C++-Version.
|
||||
// Später können wir hier auf Kanäle und ggf. async (Tokio) umstellen.
|
||||
|
||||
type Callback = Arc<dyn Fn(String) + Send + Sync + 'static>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MessageBroker {
|
||||
inner: Arc<Inner>,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
subscribers: Mutex<Vec<Callback>>,
|
||||
sender: mpsc::Sender<String>,
|
||||
receiver: Mutex<Option<mpsc::Receiver<String>>>,
|
||||
running: AtomicBool,
|
||||
started: AtomicBool,
|
||||
}
|
||||
|
||||
impl MessageBroker {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = mpsc::channel::<String>();
|
||||
Self {
|
||||
inner: Arc::new(Inner {
|
||||
subscribers: Mutex::new(Vec::new()),
|
||||
sender: tx,
|
||||
receiver: Mutex::new(Some(rx)),
|
||||
running: AtomicBool::new(true),
|
||||
started: AtomicBool::new(false),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn publish(&self, message: String) {
|
||||
// Nachrichten werden in eine interne Queue gestellt und von einem
|
||||
// Hintergrund-Thread an alle Subscriber verteilt.
|
||||
//
|
||||
// Falls der Empfänger bereits beendet wurde, ignorieren wir den Fehler
|
||||
// still (Broker fährt gerade herunter).
|
||||
let _ = self.inner.sender.send(message);
|
||||
}
|
||||
|
||||
pub fn subscribe<F>(&self, f: F)
|
||||
where
|
||||
F: Fn(String) + Send + Sync + 'static,
|
||||
{
|
||||
let mut guard = self.inner.subscribers.lock().unwrap();
|
||||
guard.push(Arc::new(f));
|
||||
}
|
||||
|
||||
pub fn start(&self) {
|
||||
// Idempotent: nur einmal einen Hintergrund-Thread starten, der
|
||||
// Nachrichten aus der Queue liest und an Subscriber verteilt.
|
||||
if self
|
||||
.inner
|
||||
.started
|
||||
.swap(true, Ordering::SeqCst)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let inner = Arc::clone(&self.inner);
|
||||
let rx_opt = {
|
||||
let mut guard = inner.receiver.lock().unwrap();
|
||||
guard.take()
|
||||
};
|
||||
|
||||
if let Some(rx) = rx_opt {
|
||||
thread::spawn(move || {
|
||||
// Arbeite Nachrichten ab, solange `running` true ist oder noch
|
||||
// Nachrichten im Kanal vorhanden sind.
|
||||
loop {
|
||||
if !inner.running.load(Ordering::Relaxed) {
|
||||
// Wir beenden trotzdem erst, wenn der Kanal leer oder
|
||||
// getrennt ist – recv_timeout mit kurzer Wartezeit.
|
||||
match rx.recv_timeout(Duration::from_millis(50)) {
|
||||
Ok(msg) => dispatch_to_subscribers(&inner, &msg),
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => break,
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
} else {
|
||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(msg) => dispatch_to_subscribers(&inner, &msg),
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => continue,
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
// Signalisiert dem Hintergrund-Thread, dass er nach Abarbeiten der
|
||||
// aktuellen Nachrichten-Schlange beenden soll.
|
||||
self.inner.running.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_to_subscribers(inner: &Inner, message: &str) {
|
||||
let subs = {
|
||||
let guard = inner.subscribers.lock().unwrap();
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
for cb in subs {
|
||||
cb(message.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
463
src/websocket_server.rs
Normal file
463
src/websocket_server.rs
Normal file
@@ -0,0 +1,463 @@
|
||||
use crate::db::ConnectionPool;
|
||||
use crate::message_broker::MessageBroker;
|
||||
use crate::worker::Worker;
|
||||
use futures_util::{FutureExt, SinkExt, StreamExt};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value as Json;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
use tokio::sync::{broadcast, mpsc, Mutex};
|
||||
use tokio_rustls::rustls::{self, ServerConfig};
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::accept_async;
|
||||
use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer};
|
||||
|
||||
/// Einfacher WebSocket-Server auf Basis von Tokio + tokio-tungstenite.
|
||||
///
|
||||
/// Unterstützt:
|
||||
/// - `setUserId`-Event vom Client (`{"event":"setUserId","data":{"userId":"..."}}`)
|
||||
/// - Versenden von Broker-Nachrichten mit `user_id`-Feld an passende Verbindungen
|
||||
/// - Broadcasting von Nachrichten ohne `user_id` an alle
|
||||
pub struct WebSocketServer {
|
||||
port: u16,
|
||||
pool: ConnectionPool,
|
||||
broker: MessageBroker,
|
||||
use_ssl: bool,
|
||||
cert_path: Option<String>,
|
||||
key_path: Option<String>,
|
||||
workers: Vec<*const dyn Worker>,
|
||||
running: Arc<AtomicBool>,
|
||||
runtime: Option<Runtime>,
|
||||
}
|
||||
|
||||
/// Einfache Registry, um Verbindungsstatistiken für `getConnections` zu liefern.
|
||||
#[derive(Default)]
|
||||
struct ConnectionRegistry {
|
||||
total: usize,
|
||||
unauthenticated: usize,
|
||||
by_user: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
fn create_tls_acceptor(
|
||||
cert_path: Option<&str>,
|
||||
key_path: Option<&str>,
|
||||
) -> Result<TlsAcceptor, Box<dyn std::error::Error>> {
|
||||
let cert_path = cert_path.ok_or("SSL aktiviert, aber kein Zertifikatspfad gesetzt")?;
|
||||
let key_path = key_path.ok_or("SSL aktiviert, aber kein Key-Pfad gesetzt")?;
|
||||
|
||||
let cert_file = File::open(cert_path)?;
|
||||
let mut cert_reader = BufReader::new(cert_file);
|
||||
|
||||
let mut cert_chain: Vec<CertificateDer<'static>> = Vec::new();
|
||||
for cert_result in certs(&mut cert_reader) {
|
||||
let cert: CertificateDer<'static> = cert_result?;
|
||||
cert_chain.push(cert);
|
||||
}
|
||||
|
||||
if cert_chain.is_empty() {
|
||||
return Err("Zertifikatsdatei enthält keine Zertifikate".into());
|
||||
}
|
||||
|
||||
let key_file = File::open(key_path)?;
|
||||
let mut key_reader = BufReader::new(key_file);
|
||||
|
||||
// Versuche zuerst PKCS8, dann ggf. RSA-Key
|
||||
let mut keys: Vec<PrivateKeyDer<'static>> = pkcs8_private_keys(&mut key_reader)
|
||||
.map(|res: Result<PrivatePkcs8KeyDer<'static>, _>| res.map(PrivateKeyDer::Pkcs8))
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
if keys.is_empty() {
|
||||
// Leser zurücksetzen und RSA-Keys versuchen
|
||||
let key_file = File::open(key_path)?;
|
||||
let mut key_reader = BufReader::new(key_file);
|
||||
keys = rsa_private_keys(&mut key_reader)
|
||||
.map(|res: Result<PrivatePkcs1KeyDer<'static>, _>| res.map(PrivateKeyDer::Pkcs1))
|
||||
.collect::<Result<_, _>>()?;
|
||||
}
|
||||
|
||||
if keys.is_empty() {
|
||||
return Err("Key-Datei enthält keinen privaten Schlüssel (PKCS8 oder RSA)".into());
|
||||
}
|
||||
|
||||
let private_key = keys.remove(0);
|
||||
|
||||
let config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(cert_chain, private_key)?;
|
||||
|
||||
Ok(TlsAcceptor::from(Arc::new(config)))
|
||||
}
|
||||
|
||||
impl WebSocketServer {
|
||||
pub fn new(
|
||||
port: u16,
|
||||
pool: ConnectionPool,
|
||||
broker: MessageBroker,
|
||||
use_ssl: bool,
|
||||
cert_path: Option<String>,
|
||||
key_path: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
port,
|
||||
pool,
|
||||
broker,
|
||||
use_ssl,
|
||||
cert_path,
|
||||
key_path,
|
||||
workers: Vec::new(),
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
runtime: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_workers(&mut self, workers: &[Box<dyn Worker>]) {
|
||||
self.workers.clear();
|
||||
for w in workers {
|
||||
self.workers.push(&**w as *const dyn Worker);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
if self.running.swap(true, Ordering::SeqCst) {
|
||||
eprintln!("[WebSocketServer] Läuft bereits.");
|
||||
return;
|
||||
}
|
||||
|
||||
if self.use_ssl {
|
||||
println!(
|
||||
"Starte WebSocket-Server auf Port {} mit SSL (cert: {:?}, key: {:?})",
|
||||
self.port, self.cert_path, self.key_path
|
||||
);
|
||||
// Hinweis: SSL-Unterstützung ist noch nicht implementiert.
|
||||
} else {
|
||||
println!("Starte WebSocket-Server auf Port {} (ohne SSL)", self.port);
|
||||
}
|
||||
|
||||
let addr = format!("0.0.0.0:{}", self.port);
|
||||
let running_flag = self.running.clone();
|
||||
let broker = self.broker.clone();
|
||||
|
||||
// Gemeinsame Registry für alle Verbindungen
|
||||
let registry = Arc::new(Mutex::new(ConnectionRegistry::default()));
|
||||
|
||||
// Broadcast-Kanal für Broker-Nachrichten
|
||||
let (tx, _) = broadcast::channel::<String>(1024);
|
||||
let tx_clone = tx.clone();
|
||||
|
||||
// Broker-Subscription: jede gepublishte Nachricht geht in den Broadcast-Kanal
|
||||
broker.subscribe(move |msg: String| {
|
||||
let _ = tx_clone.send(msg);
|
||||
});
|
||||
|
||||
// Optionalen TLS-Akzeptor laden, falls SSL aktiviert ist
|
||||
let tls_acceptor = if self.use_ssl {
|
||||
match create_tls_acceptor(
|
||||
self.cert_path.as_deref(),
|
||||
self.key_path.as_deref(),
|
||||
) {
|
||||
Ok(acc) => Some(acc),
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"[WebSocketServer] TLS-Initialisierung fehlgeschlagen, starte ohne SSL: {err}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rt = Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Tokio Runtime konnte nicht erstellt werden");
|
||||
|
||||
rt.spawn(run_accept_loop(
|
||||
addr,
|
||||
running_flag,
|
||||
tx,
|
||||
self.pool.clone(),
|
||||
registry,
|
||||
tls_acceptor,
|
||||
));
|
||||
|
||||
self.runtime = Some(rt);
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
if !self.running.swap(false, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
println!("WebSocket-Server wird gestoppt.");
|
||||
if let Some(rt) = self.runtime.take() {
|
||||
rt.shutdown_background();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct IncomingMessage {
|
||||
#[serde(default)]
|
||||
event: String,
|
||||
#[serde(default)]
|
||||
data: Json,
|
||||
}
|
||||
|
||||
async fn run_accept_loop(
|
||||
addr: String,
|
||||
running: Arc<AtomicBool>,
|
||||
tx: broadcast::Sender<String>,
|
||||
_pool: ConnectionPool,
|
||||
registry: Arc<Mutex<ConnectionRegistry>>,
|
||||
tls_acceptor: Option<TlsAcceptor>,
|
||||
) {
|
||||
let listener = match TcpListener::bind(&addr).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("[WebSocketServer] Fehler beim Binden an {}: {}", addr, e);
|
||||
running.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("[WebSocketServer] Lauscht auf {}", addr);
|
||||
|
||||
while running.load(Ordering::SeqCst) {
|
||||
let (stream, peer) = match listener.accept().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("[WebSocketServer] accept() fehlgeschlagen: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let peer_addr = peer;
|
||||
let rx = tx.subscribe();
|
||||
let registry_clone = registry.clone();
|
||||
let tls_acceptor_clone = tls_acceptor.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Some(acc) = tls_acceptor_clone {
|
||||
match acc.accept(stream).await {
|
||||
Ok(tls_stream) => {
|
||||
handle_connection(tls_stream, peer_addr, rx, registry_clone).await
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"[WebSocketServer] TLS-Handshake fehlgeschlagen ({peer_addr}): {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handle_connection(stream, peer_addr, rx, registry_clone).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection<S>(
|
||||
stream: S,
|
||||
peer_addr: SocketAddr,
|
||||
mut broker_rx: broadcast::Receiver<String>,
|
||||
registry: Arc<Mutex<ConnectionRegistry>>,
|
||||
) where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
let ws_stream = match accept_async(stream).await {
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
eprintln!("[WebSocketServer] WebSocket-Handshake fehlgeschlagen ({peer_addr}): {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("[WebSocketServer] Neue Verbindung von {}", peer_addr);
|
||||
|
||||
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
|
||||
|
||||
// Kanal für Antworten direkt an diesen Client (z.B. getConnections)
|
||||
let (client_tx, mut client_rx) = mpsc::channel::<String>(32);
|
||||
|
||||
// Neue Verbindung in der Registry zählen (zunächst als unauthentifiziert)
|
||||
{
|
||||
let mut reg = registry.lock().await;
|
||||
reg.total += 1;
|
||||
reg.unauthenticated += 1;
|
||||
}
|
||||
|
||||
// user_id der Verbindung (nach setUserId)
|
||||
let user_id = Arc::new(tokio::sync::Mutex::new(Option::<String>::None));
|
||||
let user_id_for_incoming = user_id.clone();
|
||||
let user_id_for_broker = user_id.clone();
|
||||
let registry_for_incoming = registry.clone();
|
||||
let client_tx_incoming = client_tx.clone();
|
||||
|
||||
// Eingehende Nachrichten vom Client
|
||||
let incoming = async move {
|
||||
while let Some(msg) = ws_receiver.next().await {
|
||||
match msg {
|
||||
Ok(Message::Text(txt)) => {
|
||||
if let Ok(parsed) = serde_json::from_str::<IncomingMessage>(&txt) {
|
||||
match parsed.event.as_str() {
|
||||
"setUserId" => {
|
||||
if let Some(uid) =
|
||||
parsed.data.get("userId").and_then(|v| v.as_str())
|
||||
{
|
||||
{
|
||||
// Registry aktualisieren: von unauthentifiziert -> Nutzer
|
||||
let mut reg = registry_for_incoming.lock().await;
|
||||
if reg.unauthenticated > 0 {
|
||||
reg.unauthenticated -= 1;
|
||||
}
|
||||
*reg.by_user.entry(uid.to_string()).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
let mut guard = user_id_for_incoming.lock().await;
|
||||
*guard = Some(uid.to_string());
|
||||
println!(
|
||||
"[WebSocketServer] User-ID gesetzt für {}: {}",
|
||||
peer_addr, uid
|
||||
);
|
||||
}
|
||||
}
|
||||
"getConnections" => {
|
||||
// Einfache Übersicht über aktuelle Verbindungen zurückgeben.
|
||||
let snapshot = {
|
||||
let reg = registry_for_incoming.lock().await;
|
||||
serde_json::json!({
|
||||
"event": "getConnectionsResponse",
|
||||
"total": reg.total,
|
||||
"unauthenticated": reg.unauthenticated,
|
||||
"users": reg.by_user,
|
||||
})
|
||||
.to_string()
|
||||
};
|
||||
let _ = client_tx_incoming.send(snapshot).await;
|
||||
}
|
||||
_ => {
|
||||
// Unbekannte Events ignorieren
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Message::Ping(_)) => {
|
||||
// Ping wird aktuell nur geloggt/ignoriert; optional könnte man hier ein eigenes
|
||||
// Ping/Pong-Handling ergänzen.
|
||||
}
|
||||
Ok(Message::Close(_)) => break,
|
||||
Err(e) => {
|
||||
eprintln!("[WebSocketServer] Fehler bei Nachricht von {peer_addr}: {e}");
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Broker-Nachrichten an den Client
|
||||
let outgoing = async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Nachrichten aus dem MessageBroker
|
||||
broker_msg = broker_rx.recv() => {
|
||||
let msg = match broker_msg {
|
||||
Ok(m) => m,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
// Filter nach user_id, falls gesetzt
|
||||
let target_user = {
|
||||
let guard = user_id_for_broker.lock().await;
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
if let Some(uid) = target_user.clone() {
|
||||
if let Ok(json) = serde_json::from_str::<Json>(&msg) {
|
||||
let matches_user = json
|
||||
.get("user_id")
|
||||
.and_then(|v| {
|
||||
if let Some(s) = v.as_str() {
|
||||
Some(s.to_string())
|
||||
} else if let Some(n) = v.as_i64() {
|
||||
Some(n.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|v| v == uid)
|
||||
.unwrap_or(false);
|
||||
|
||||
if !matches_user {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = ws_sender.send(Message::Text(msg)).await {
|
||||
eprintln!(
|
||||
"[WebSocketServer] Fehler beim Senden an {}: {}",
|
||||
peer_addr, e
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Antworten aus der Verbindung selbst (z.B. getConnections)
|
||||
client_msg = client_rx.recv() => {
|
||||
match client_msg {
|
||||
Some(msg) => {
|
||||
if let Err(e) = ws_sender.send(Message::Text(msg)).await {
|
||||
eprintln!(
|
||||
"[WebSocketServer] Fehler beim Senden an {}: {}",
|
||||
peer_addr, e
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Kanal wurde geschlossen
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
futures_util::future::select(incoming.boxed(), outgoing.boxed()).await;
|
||||
|
||||
// Verbindung aus der Registry entfernen
|
||||
let final_uid = {
|
||||
let guard = user_id.lock().await;
|
||||
guard.clone()
|
||||
};
|
||||
{
|
||||
let mut reg = registry.lock().await;
|
||||
if reg.total > 0 {
|
||||
reg.total -= 1;
|
||||
}
|
||||
if let Some(uid) = final_uid {
|
||||
if let Some(count) = reg.by_user.get_mut(&uid) {
|
||||
if *count > 0 {
|
||||
*count -= 1;
|
||||
}
|
||||
if *count == 0 {
|
||||
reg.by_user.remove(&uid);
|
||||
}
|
||||
}
|
||||
} else if reg.unauthenticated > 0 {
|
||||
reg.unauthenticated -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
println!("[WebSocketServer] Verbindung geschlossen: {}", peer_addr);
|
||||
}
|
||||
|
||||
151
src/worker/base.rs
Normal file
151
src/worker/base.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use crate::db::{ConnectionPool, DbError};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
pub trait Worker: Send {
|
||||
fn start_worker_thread(&mut self);
|
||||
fn stop_worker_thread(&mut self);
|
||||
fn enable_watchdog(&mut self);
|
||||
}
|
||||
|
||||
pub(crate) struct WorkerState {
|
||||
pub(crate) running_worker: AtomicBool,
|
||||
pub(crate) running_watchdog: AtomicBool,
|
||||
pub(crate) current_step: Mutex<String>,
|
||||
}
|
||||
|
||||
impl WorkerState {
|
||||
pub(crate) fn new(name: &str) -> Self {
|
||||
Self {
|
||||
running_worker: AtomicBool::new(false),
|
||||
running_watchdog: AtomicBool::new(false),
|
||||
current_step: Mutex::new(format!("{name}: idle")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BaseWorker {
|
||||
pub name: String,
|
||||
pub pool: ConnectionPool,
|
||||
pub broker: MessageBroker,
|
||||
pub(crate) state: Arc<WorkerState>,
|
||||
worker_thread: Option<thread::JoinHandle<()>>,
|
||||
watchdog_thread: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl BaseWorker {
|
||||
pub fn new(name: &str, pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
pool,
|
||||
broker,
|
||||
state: Arc::new(WorkerState::new(name)),
|
||||
worker_thread: None,
|
||||
watchdog_thread: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_step<S: Into<String>>(&self, step: S) {
|
||||
if let Ok(mut guard) = self.state.current_step.lock() {
|
||||
*guard = step.into();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start_worker_with_loop<F>(&mut self, loop_fn: F)
|
||||
where
|
||||
F: Fn(Arc<WorkerState>) + Send + 'static,
|
||||
{
|
||||
if self.state.running_worker.swap(true, Ordering::SeqCst) {
|
||||
eprintln!("[{}] Worker thread already running, skipping start.", self.name);
|
||||
return;
|
||||
}
|
||||
|
||||
let state = Arc::clone(&self.state);
|
||||
|
||||
self.worker_thread = Some(thread::spawn(move || {
|
||||
loop_fn(state);
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn stop_worker(&mut self) {
|
||||
self.state.running_worker.store(false, Ordering::Relaxed);
|
||||
if let Some(handle) = self.worker_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start_watchdog(&mut self) {
|
||||
if self
|
||||
.state
|
||||
.running_watchdog
|
||||
.swap(true, Ordering::SeqCst)
|
||||
{
|
||||
eprintln!("[{}] Watchdog already enabled, skipping.", self.name);
|
||||
return;
|
||||
}
|
||||
|
||||
let state = Arc::clone(&self.state);
|
||||
let name = self.name.clone();
|
||||
|
||||
self.watchdog_thread = Some(thread::spawn(move || {
|
||||
while state.running_watchdog.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
|
||||
if !state.running_watchdog.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let step = state.current_step.lock().unwrap().clone();
|
||||
eprintln!("[{name}] Watchdog: current step = {step}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn stop_watchdog(&mut self) {
|
||||
self.state.running_watchdog.store(false, Ordering::Relaxed);
|
||||
if let Some(handle) = self.watchdog_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_running(&self) -> bool {
|
||||
self.state.running_worker.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
const QUERY_UPDATE_MONEY: &str = r#"
|
||||
SELECT falukant_data.update_money($1, $2, $3);
|
||||
"#;
|
||||
|
||||
impl BaseWorker {
|
||||
/// Aktualisiert das Geld eines Falukant-Users über die DB-Funktion `falukant_data.update_money`.
|
||||
/// `action` entspricht dem Log-/Aktions-Tag (z.B. "credit pay rate", "debitor_prism").
|
||||
pub fn change_falukant_user_money(
|
||||
&self,
|
||||
falukant_user_id: i32,
|
||||
money_change: f64,
|
||||
action: &str,
|
||||
) -> Result<(), DbError> {
|
||||
use postgres::types::ToSql;
|
||||
|
||||
let mut conn = self
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("update_money", QUERY_UPDATE_MONEY)?;
|
||||
|
||||
let p1: &(dyn ToSql + Sync) = &falukant_user_id;
|
||||
let p2: &(dyn ToSql + Sync) = &money_change;
|
||||
let p3: &(dyn ToSql + Sync) = &action;
|
||||
|
||||
conn.execute("update_money", &[p1, p2, p3])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
619
src/worker/character_creation.rs
Normal file
619
src/worker/character_creation.rs
Normal file
@@ -0,0 +1,619 @@
|
||||
use crate::db::{ConnectionPool, DbError, Rows};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{thread_rng, Rng, SeedableRng};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
|
||||
pub struct CharacterCreationWorker {
|
||||
pub(crate) base: BaseWorker,
|
||||
rng: StdRng,
|
||||
dist: Uniform<i32>,
|
||||
first_name_cache: HashMap<String, HashSet<i32>>,
|
||||
last_name_cache: HashSet<i32>,
|
||||
death_check_running: Arc<AtomicBool>,
|
||||
death_thread: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
// SQL-Queries analog zur C++-Implementierung
|
||||
const QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED: &str = r#"
|
||||
SELECT created_at
|
||||
FROM falukant_data."character"
|
||||
WHERE user_id IS NULL
|
||||
AND created_at::date = CURRENT_DATE
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1;
|
||||
"#;
|
||||
|
||||
const QUERY_GET_TOWN_REGION_IDS: &str = r#"
|
||||
SELECT fdr.id
|
||||
FROM falukant_data.region fdr
|
||||
JOIN falukant_type.region ftr ON fdr.region_type_id = ftr.id
|
||||
WHERE ftr.label_tr = 'city';
|
||||
"#;
|
||||
|
||||
const QUERY_LOAD_FIRST_NAMES: &str = r#"
|
||||
SELECT id, gender
|
||||
FROM falukant_predefine.firstname;
|
||||
"#;
|
||||
|
||||
const QUERY_LOAD_LAST_NAMES: &str = r#"
|
||||
SELECT id
|
||||
FROM falukant_predefine.lastname;
|
||||
"#;
|
||||
|
||||
const QUERY_INSERT_CHARACTER: &str = r#"
|
||||
INSERT INTO falukant_data.character(
|
||||
user_id,
|
||||
region_id,
|
||||
first_name,
|
||||
last_name,
|
||||
birthdate,
|
||||
gender,
|
||||
created_at,
|
||||
updated_at,
|
||||
title_of_nobility
|
||||
) VALUES (
|
||||
NULL,
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
NOW(),
|
||||
$4,
|
||||
NOW(),
|
||||
NOW(),
|
||||
$5
|
||||
);
|
||||
"#;
|
||||
|
||||
const QUERY_GET_ELIGIBLE_NPC_FOR_DEATH: &str = r#"
|
||||
WITH aged AS (
|
||||
SELECT
|
||||
c.id,
|
||||
(current_date - c.birthdate::date) AS age,
|
||||
c.user_id
|
||||
FROM
|
||||
falukant_data.character c
|
||||
WHERE
|
||||
c.user_id IS NULL
|
||||
AND (current_date - c.birthdate::date) > 60
|
||||
),
|
||||
always_sel AS (
|
||||
SELECT *
|
||||
FROM aged
|
||||
WHERE age > 85
|
||||
),
|
||||
random_sel AS (
|
||||
SELECT *
|
||||
FROM aged
|
||||
WHERE age <= 85
|
||||
ORDER BY random()
|
||||
LIMIT 10
|
||||
)
|
||||
SELECT *
|
||||
FROM always_sel
|
||||
UNION ALL
|
||||
SELECT *
|
||||
FROM random_sel;
|
||||
"#;
|
||||
|
||||
const QUERY_DELETE_DIRECTOR: &str = r#"
|
||||
DELETE FROM falukant_data.director
|
||||
WHERE director_character_id = $1
|
||||
RETURNING employer_user_id;
|
||||
"#;
|
||||
|
||||
const QUERY_DELETE_RELATIONSHIP: &str = r#"
|
||||
WITH deleted AS (
|
||||
DELETE FROM falukant_data.relationship
|
||||
WHERE character1_id = $1
|
||||
OR character2_id = $1
|
||||
RETURNING
|
||||
CASE
|
||||
WHEN character1_id = $1 THEN character2_id
|
||||
ELSE character1_id
|
||||
END AS related_character_id,
|
||||
relationship_type_id
|
||||
)
|
||||
SELECT
|
||||
c.user_id AS related_user_id
|
||||
FROM deleted d
|
||||
JOIN falukant_data.character c
|
||||
ON c.id = d.related_character_id;
|
||||
"#;
|
||||
|
||||
const QUERY_DELETE_CHILD_RELATION: &str = r#"
|
||||
WITH deleted AS (
|
||||
DELETE FROM falukant_data.child_relation
|
||||
WHERE child_character_id = $1
|
||||
RETURNING
|
||||
father_character_id,
|
||||
mother_character_id
|
||||
)
|
||||
SELECT
|
||||
cf.user_id AS father_user_id,
|
||||
cm.user_id AS mother_user_id
|
||||
FROM deleted d
|
||||
JOIN falukant_data.character cf
|
||||
ON cf.id = d.father_character_id
|
||||
JOIN falukant_data.character cm
|
||||
ON cm.id = d.mother_character_id;
|
||||
"#;
|
||||
|
||||
const QUERY_INSERT_NOTIFICATION: &str = r#"
|
||||
INSERT INTO falukant_log.notification (
|
||||
user_id,
|
||||
tr,
|
||||
shown,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES ($1, 'director_death', FALSE, NOW(), NOW());
|
||||
"#;
|
||||
|
||||
const QUERY_MARK_CHARACTER_DECEASED: &str = r#"
|
||||
DELETE FROM falukant_data.character
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
impl CharacterCreationWorker {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self::new_internal(pool, broker, true)
|
||||
}
|
||||
|
||||
/// Interner Konstruktor, der optional den NPC-Todes-Monitor startet.
|
||||
fn new_internal(pool: ConnectionPool, broker: MessageBroker, start_death_thread: bool) -> Self {
|
||||
let base = BaseWorker::new("CharacterCreationWorker", pool.clone(), broker.clone());
|
||||
let rng = StdRng::from_entropy();
|
||||
let dist = Uniform::from(2..=3);
|
||||
let death_check_running = Arc::new(AtomicBool::new(start_death_thread));
|
||||
|
||||
let death_thread = if start_death_thread {
|
||||
let death_flag = Arc::clone(&death_check_running);
|
||||
let pool_clone = pool;
|
||||
let broker_clone = broker;
|
||||
Some(thread::spawn(move || {
|
||||
while death_flag.load(Ordering::Relaxed) {
|
||||
if let Err(err) =
|
||||
CharacterCreationWorker::monitor_character_deaths(&pool_clone, &broker_clone)
|
||||
{
|
||||
eprintln!(
|
||||
"[CharacterCreationWorker] Fehler beim Überprüfen von NPC-Todesfällen: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
// Warte 1 Stunde, aber mit frühem Abbruch, wenn death_flag false wird
|
||||
for _ in 0..3600 {
|
||||
if !death_flag.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
base,
|
||||
rng,
|
||||
dist,
|
||||
first_name_cache: HashMap::new(),
|
||||
last_name_cache: HashSet::new(),
|
||||
death_check_running,
|
||||
death_thread,
|
||||
}
|
||||
}
|
||||
|
||||
/// Variante ohne separaten Todes-Monitor-Thread – wird nur in der Worker-Loop benutzt.
|
||||
fn new_for_loop(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self::new_internal(pool, broker, false)
|
||||
}
|
||||
|
||||
fn is_today_character_created(&self) -> bool {
|
||||
match self.fetch_today_characters() {
|
||||
Ok(rows) => !rows.is_empty(),
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"[CharacterCreationWorker] Fehler in is_today_character_created: {err}"
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_today_characters(&self) -> Result<Rows, crate::db::DbError> {
|
||||
const STMT_NAME: &str = "is_previous_day_character_created";
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(STMT_NAME, QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED)?;
|
||||
conn.execute(STMT_NAME, &[])
|
||||
}
|
||||
|
||||
fn create_characters_for_today(&mut self) {
|
||||
self.load_names();
|
||||
if self.first_name_cache.is_empty() || self.last_name_cache.is_empty() {
|
||||
eprintln!(
|
||||
"[CharacterCreationWorker] Fehler: Namen konnten nicht geladen werden (Stub-Implementierung)."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let town_ids = self.get_town_region_ids();
|
||||
for region_id in town_ids {
|
||||
self.create_characters_for_region(region_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_characters_for_region(&mut self, region_id: i32) {
|
||||
let nobility_stands = [1, 2, 3];
|
||||
let genders = ["male", "female"];
|
||||
|
||||
for &nobility in &nobility_stands {
|
||||
for &gender in &genders {
|
||||
let num_chars = self.rng.sample(self.dist);
|
||||
for _ in 0..num_chars {
|
||||
self.create_character(region_id, gender, nobility);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_character(&mut self, region_id: i32, gender: &str, title_of_nobility: i32) {
|
||||
let first_set = self
|
||||
.first_name_cache
|
||||
.get(gender)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let first_name_id = Self::get_random_from_set(&first_set);
|
||||
if first_name_id == -1 {
|
||||
eprintln!("[CharacterCreationWorker] Fehler: Kein passender Vorname gefunden.");
|
||||
return;
|
||||
}
|
||||
|
||||
let last_name_id = Self::get_random_from_set(&self.last_name_cache);
|
||||
if last_name_id == -1 {
|
||||
eprintln!("[CharacterCreationWorker] Fehler: Kein passender Nachname gefunden.");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = Self::insert_character(
|
||||
&self.base.pool,
|
||||
region_id,
|
||||
first_name_id,
|
||||
last_name_id,
|
||||
gender,
|
||||
title_of_nobility,
|
||||
) {
|
||||
eprintln!("[CharacterCreationWorker] Fehler in createCharacter: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn get_town_region_ids(&self) -> Vec<i32> {
|
||||
match self.load_town_region_ids() {
|
||||
Ok(rows) => rows
|
||||
.into_iter()
|
||||
.filter_map(|row| row.get("id")?.parse::<i32>().ok())
|
||||
.collect(),
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"[CharacterCreationWorker] Fehler in getTownRegionIds: {err}"
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_town_region_ids(&self) -> Result<Rows, crate::db::DbError> {
|
||||
const STMT_NAME: &str = "get_town_region_ids";
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(STMT_NAME, QUERY_GET_TOWN_REGION_IDS)?;
|
||||
conn.execute(STMT_NAME, &[])
|
||||
}
|
||||
|
||||
fn load_names(&mut self) {
|
||||
if self.first_name_cache.is_empty() || self.last_name_cache.is_empty() {
|
||||
if let Err(err) = self.load_first_and_last_names() {
|
||||
eprintln!("[CharacterCreationWorker] Fehler in loadNames: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_first_and_last_names(&mut self) -> Result<(), crate::db::DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
// Vornamen
|
||||
conn.prepare("load_first_names", QUERY_LOAD_FIRST_NAMES)?;
|
||||
let first_rows = conn.execute("load_first_names", &[])?;
|
||||
for row in first_rows {
|
||||
let id = match row.get("id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
Some(id) => id,
|
||||
None => continue,
|
||||
};
|
||||
let gender = row.get("gender").cloned().unwrap_or_default();
|
||||
self.first_name_cache.entry(gender).or_default().insert(id);
|
||||
}
|
||||
|
||||
// Nachnamen
|
||||
conn.prepare("load_last_names", QUERY_LOAD_LAST_NAMES)?;
|
||||
let last_rows = conn.execute("load_last_names", &[])?;
|
||||
for row in last_rows {
|
||||
if let Some(id) = row.get("id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
self.last_name_cache.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_random_from_set(set: &HashSet<i32>) -> i32 {
|
||||
if set.is_empty() {
|
||||
return -1;
|
||||
}
|
||||
let mut rng = thread_rng();
|
||||
let idx = rng.gen_range(0..set.len());
|
||||
*set.iter().nth(idx).unwrap_or(&-1)
|
||||
}
|
||||
|
||||
fn run_iteration(&mut self, state: &WorkerState) {
|
||||
self.base
|
||||
.set_current_step("Check if previous day character was created");
|
||||
|
||||
if !self.is_today_character_created() {
|
||||
self.base
|
||||
.set_current_step("Create characters for today");
|
||||
self.create_characters_for_today();
|
||||
}
|
||||
|
||||
self.sleep_one_minute(state);
|
||||
}
|
||||
|
||||
fn sleep_one_minute(&self, state: &WorkerState) {
|
||||
self.base
|
||||
.set_current_step("Sleep for 60 seconds");
|
||||
|
||||
for _ in 0..60 {
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
|
||||
self.base.set_current_step("Loop done");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Worker for CharacterCreationWorker {
|
||||
fn start_worker_thread(&mut self) {
|
||||
let pool = self.base.pool.clone();
|
||||
let broker = self.base.broker.clone();
|
||||
|
||||
self.base
|
||||
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
||||
let mut worker = CharacterCreationWorker::new_for_loop(pool.clone(), broker.clone());
|
||||
while state.running_worker.load(Ordering::Relaxed) {
|
||||
worker.run_iteration(&state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CharacterCreationWorker {
|
||||
fn drop(&mut self) {
|
||||
self.death_check_running
|
||||
.store(false, Ordering::Relaxed);
|
||||
if let Some(handle) = self.death_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zusätzliche Logik: NPC-Todesfälle überwachen und verarbeiten
|
||||
|
||||
impl CharacterCreationWorker {
|
||||
fn insert_character(
|
||||
pool: &ConnectionPool,
|
||||
region_id: i32,
|
||||
first_name_id: i32,
|
||||
last_name_id: i32,
|
||||
gender: &str,
|
||||
title_of_nobility: i32,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("insert_character", QUERY_INSERT_CHARACTER)?;
|
||||
conn.execute(
|
||||
"insert_character",
|
||||
&[
|
||||
®ion_id,
|
||||
&first_name_id,
|
||||
&last_name_id,
|
||||
&gender,
|
||||
&title_of_nobility,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn monitor_character_deaths(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(
|
||||
"get_eligible_npc_for_death",
|
||||
QUERY_GET_ELIGIBLE_NPC_FOR_DEATH,
|
||||
)?;
|
||||
let rows = conn.execute("get_eligible_npc_for_death", &[])?;
|
||||
|
||||
for row in rows {
|
||||
let character_id = row
|
||||
.get("id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(-1);
|
||||
let age = row
|
||||
.get("age")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
if character_id > 0 && Self::calculate_death_probability(age) {
|
||||
if let Err(err) = Self::handle_character_death(pool, broker, character_id) {
|
||||
eprintln!(
|
||||
"[CharacterCreationWorker] Fehler beim Bearbeiten des NPC-Todes (id={character_id}): {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_death_probability(age: i32) -> bool {
|
||||
if age < 60 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let base_probability = 0.01_f64;
|
||||
let increase_per_year = 0.01_f64;
|
||||
let death_probability =
|
||||
base_probability + increase_per_year * (age.saturating_sub(60) as f64);
|
||||
|
||||
let mut rng = thread_rng();
|
||||
let dist = Uniform::from(0.0..1.0);
|
||||
let roll: f64 = dist.sample(&mut rng);
|
||||
roll < death_probability
|
||||
}
|
||||
|
||||
fn handle_character_death(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
character_id: i32,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
// 1) Director löschen und User benachrichtigen
|
||||
conn.prepare("delete_director", QUERY_DELETE_DIRECTOR)?;
|
||||
let dir_result = conn.execute("delete_director", &[&character_id])?;
|
||||
if let Some(row) = dir_result.get(0) {
|
||||
if let Some(user_id) = row
|
||||
.get("employer_user_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
Self::notify_user(pool, broker, user_id, "director_death")?;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Relationships löschen und betroffene User benachrichtigen
|
||||
conn.prepare("delete_relationship", QUERY_DELETE_RELATIONSHIP)?;
|
||||
let rel_result = conn.execute("delete_relationship", &[&character_id])?;
|
||||
for row in rel_result {
|
||||
if let Some(related_user_id) = row
|
||||
.get("related_user_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
Self::notify_user(pool, broker, related_user_id, "relationship_death")?;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Child-Relations löschen und Eltern benachrichtigen
|
||||
conn.prepare("delete_child_relation", QUERY_DELETE_CHILD_RELATION)?;
|
||||
let child_result = conn.execute("delete_child_relation", &[&character_id])?;
|
||||
for row in child_result {
|
||||
if let Some(father_user_id) = row
|
||||
.get("father_user_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
Self::notify_user(pool, broker, father_user_id, "child_death")?;
|
||||
}
|
||||
if let Some(mother_user_id) = row
|
||||
.get("mother_user_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
Self::notify_user(pool, broker, mother_user_id, "child_death")?;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Charakter als verstorben markieren
|
||||
Self::mark_character_as_deceased(pool, character_id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_user(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
user_id: i32,
|
||||
event_type: &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])?;
|
||||
|
||||
// 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);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mark_character_as_deceased(
|
||||
pool: &ConnectionPool,
|
||||
character_id: i32,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("mark_character_deceased", QUERY_MARK_CHARACTER_DECEASED)?;
|
||||
conn.execute("mark_character_deceased", &[&character_id])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
584
src/worker/director.rs
Normal file
584
src/worker/director.rs
Normal file
@@ -0,0 +1,584 @@
|
||||
use crate::db::{DbConnection, DbError, Row};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::db::ConnectionPool;
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Director {
|
||||
id: i32,
|
||||
may_produce: bool,
|
||||
may_sell: bool,
|
||||
may_start_transport: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ProductionPlan {
|
||||
falukant_user_id: i32,
|
||||
money: i32,
|
||||
certificate: i32,
|
||||
branch_id: i32,
|
||||
product_id: i32,
|
||||
stock_size: i32,
|
||||
used_in_stock: i32,
|
||||
running_productions: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct InventoryItem {
|
||||
id: i32,
|
||||
product_id: i32,
|
||||
quantity: i32,
|
||||
quality: i32,
|
||||
sell_cost: f64,
|
||||
user_id: i32,
|
||||
region_id: i32,
|
||||
branch_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SalaryItem {
|
||||
id: i32,
|
||||
employer_user_id: i32,
|
||||
income: i32,
|
||||
}
|
||||
|
||||
pub struct DirectorWorker {
|
||||
base: BaseWorker,
|
||||
last_run: Option<Instant>,
|
||||
}
|
||||
|
||||
// SQL-Queries (1:1 aus director_worker.h)
|
||||
const QUERY_GET_DIRECTORS: &str = r#"
|
||||
SELECT
|
||||
d.may_produce,
|
||||
d.may_sell,
|
||||
d.may_start_transport,
|
||||
b.id AS branch_id,
|
||||
fu.id AS falukantUserId,
|
||||
d.id
|
||||
FROM falukant_data.director d
|
||||
JOIN falukant_data.falukant_user fu
|
||||
ON fu.id = d.employer_user_id
|
||||
JOIN falukant_data.character c
|
||||
ON c.id = d.director_character_id
|
||||
JOIN falukant_data.branch b
|
||||
ON b.region_id = c.region_id
|
||||
AND b.falukant_user_id = fu.id
|
||||
WHERE current_time BETWEEN '08:00:00' AND '17:00:00';
|
||||
"#;
|
||||
|
||||
const QUERY_GET_BEST_PRODUCTION: &str = r#"
|
||||
SELECT
|
||||
fdu.id falukant_user_id,
|
||||
fdu.money,
|
||||
fdu.certificate,
|
||||
ftp.id product_id,
|
||||
ftp.label_tr,
|
||||
(
|
||||
SELECT SUM(quantity)
|
||||
FROM falukant_data.stock fds
|
||||
WHERE fds.branch_id = fdb.id
|
||||
) AS stock_size,
|
||||
COALESCE((
|
||||
SELECT SUM(COALESCE(fdi.quantity, 0))
|
||||
FROM falukant_data.stock fds
|
||||
JOIN falukant_data.inventory fdi
|
||||
ON fdi.stock_id = fds.id
|
||||
WHERE fds.branch_id = fdb.id
|
||||
), 0) AS used_in_stock,
|
||||
(ftp.sell_cost * (fdtpw.worth_percent + (fdk_character.knowledge * 2 + fdk_director.knowledge) / 3) / 100 - 6 * ftp.category)
|
||||
/ (300.0 * ftp.production_time) AS worth,
|
||||
fdb.id AS branch_id,
|
||||
(
|
||||
SELECT COUNT(id)
|
||||
FROM falukant_data.production
|
||||
WHERE branch_id = fdb.id
|
||||
) AS running_productions,
|
||||
COALESCE((
|
||||
SELECT SUM(COALESCE(fdp.quantity, 0)) quantity
|
||||
FROM falukant_data.production fdp
|
||||
WHERE fdp.branch_id = fdb.id
|
||||
), 0) AS running_productions_quantity
|
||||
FROM falukant_data.director fdd
|
||||
JOIN falukant_data.character fdc
|
||||
ON fdc.id = fdd.director_character_id
|
||||
JOIN falukant_data.falukant_user fdu
|
||||
ON fdd.employer_user_id = fdu.id
|
||||
JOIN falukant_data.character user_character
|
||||
ON user_character.user_id = fdu.id
|
||||
JOIN falukant_data.branch fdb
|
||||
ON fdb.falukant_user_id = fdu.id
|
||||
AND fdb.region_id = fdc.region_id
|
||||
JOIN falukant_data.town_product_worth fdtpw
|
||||
ON fdtpw.region_id = fdb.region_id
|
||||
JOIN falukant_data.knowledge fdk_character
|
||||
ON fdk_character.product_id = fdtpw.product_id
|
||||
AND fdk_character.character_id = user_character.id
|
||||
JOIN falukant_data.knowledge fdk_director
|
||||
ON fdk_director.product_id = fdtpw.product_id
|
||||
AND fdk_director.character_id = fdd.director_character_id
|
||||
JOIN falukant_type.product ftp
|
||||
ON ftp.id = fdtpw.product_id
|
||||
AND ftp.category <= fdu.certificate
|
||||
WHERE fdd.id = $1
|
||||
ORDER BY worth DESC
|
||||
LIMIT 1;
|
||||
"#;
|
||||
|
||||
const QUERY_INSERT_PRODUCTION: &str = r#"
|
||||
INSERT INTO falukant_data.production (branch_id, product_id, quantity)
|
||||
VALUES ($1, $2, $3);
|
||||
"#;
|
||||
|
||||
const QUERY_GET_INVENTORY: &str = r#"
|
||||
SELECT
|
||||
i.id,
|
||||
i.product_id,
|
||||
i.quantity,
|
||||
i.quality,
|
||||
p.sell_cost,
|
||||
fu.id AS user_id,
|
||||
b.region_id,
|
||||
b.id AS branch_id
|
||||
FROM falukant_data.inventory i
|
||||
JOIN falukant_data.stock s
|
||||
ON s.id = i.stock_id
|
||||
JOIN falukant_data.branch b
|
||||
ON b.id = s.branch_id
|
||||
JOIN falukant_data.falukant_user fu
|
||||
ON fu.id = b.falukant_user_id
|
||||
JOIN falukant_data.director d
|
||||
ON d.employer_user_id = fu.id
|
||||
JOIN falukant_type.product p
|
||||
ON p.id = i.product_id
|
||||
WHERE d.id = $1;
|
||||
"#;
|
||||
|
||||
const QUERY_REMOVE_INVENTORY: &str = r#"
|
||||
DELETE FROM falukant_data.inventory
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
const QUERY_ADD_SELL_LOG: &str = r#"
|
||||
INSERT INTO falukant_log.sell (region_id, product_id, quantity, seller_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (region_id, product_id, seller_id)
|
||||
DO UPDATE
|
||||
SET quantity = falukant_log.sell.quantity + EXCLUDED.quantity;
|
||||
"#;
|
||||
|
||||
const QUERY_GET_SALARY_TO_PAY: &str = r#"
|
||||
SELECT d.id, d.employer_user_id, d.income
|
||||
FROM falukant_data.director d
|
||||
WHERE DATE(d.last_salary_payout) < DATE(NOW());
|
||||
"#;
|
||||
|
||||
const QUERY_SET_SALARY_PAYED: &str = r#"
|
||||
UPDATE falukant_data.director
|
||||
SET last_salary_payout = NOW()
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
const QUERY_UPDATE_SATISFACTION: &str = r#"
|
||||
WITH new_sats AS (
|
||||
SELECT
|
||||
d.id,
|
||||
ROUND(
|
||||
d.income::numeric
|
||||
/
|
||||
(
|
||||
c.title_of_nobility
|
||||
* POWER(1.231, AVG(k.knowledge) / 1.5)
|
||||
)
|
||||
* 100
|
||||
) AS new_satisfaction
|
||||
FROM falukant_data.director d
|
||||
JOIN falukant_data.knowledge k
|
||||
ON d.director_character_id = k.character_id
|
||||
JOIN falukant_data.character c
|
||||
ON c.id = d.director_character_id
|
||||
GROUP BY d.id, c.title_of_nobility, d.income
|
||||
)
|
||||
UPDATE falukant_data.director dir
|
||||
SET satisfaction = ns.new_satisfaction
|
||||
FROM new_sats ns
|
||||
WHERE dir.id = ns.id
|
||||
AND dir.satisfaction IS DISTINCT FROM ns.new_satisfaction
|
||||
RETURNING dir.employer_user_id;
|
||||
"#;
|
||||
|
||||
impl DirectorWorker {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
base: BaseWorker::new("DirectorWorker", pool, broker),
|
||||
last_run: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_iteration(&mut self, state: &WorkerState) {
|
||||
self.base.set_current_step("DirectorWorker iteration");
|
||||
|
||||
let now = Instant::now();
|
||||
let should_run = match self.last_run {
|
||||
None => true,
|
||||
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(60),
|
||||
};
|
||||
|
||||
if should_run {
|
||||
if let Err(err) = self.perform_all_tasks() {
|
||||
eprintln!("[DirectorWorker] Fehler beim Ausführen der Aufgabe: {err}");
|
||||
}
|
||||
self.last_run = Some(now);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_all_tasks(&mut self) -> Result<(), DbError> {
|
||||
self.perform_task()?;
|
||||
self.pay_salary()?;
|
||||
self.calculate_satisfaction()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn perform_task(&mut self) -> Result<(), DbError> {
|
||||
self.base
|
||||
.set_current_step("Get director actions from DB");
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_directors", QUERY_GET_DIRECTORS)?;
|
||||
let directors_rows = conn.execute("get_directors", &[])?;
|
||||
|
||||
let directors: Vec<Director> = directors_rows
|
||||
.into_iter()
|
||||
.filter_map(Self::map_row_to_director)
|
||||
.collect();
|
||||
|
||||
for director in directors {
|
||||
if director.may_produce {
|
||||
self.start_productions(&director)?;
|
||||
}
|
||||
if director.may_start_transport {
|
||||
self.start_transports_stub(&director);
|
||||
}
|
||||
if director.may_sell {
|
||||
self.start_sellings(&director)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_row_to_director(row: Row) -> Option<Director> {
|
||||
Some(Director {
|
||||
id: row.get("id")?.parse().ok()?,
|
||||
may_produce: row.get("may_produce").map(|v| v == "t" || v == "true").unwrap_or(false),
|
||||
may_sell: row.get("may_sell").map(|v| v == "t" || v == "true").unwrap_or(false),
|
||||
may_start_transport: row
|
||||
.get("may_start_transport")
|
||||
.map(|v| v == "t" || v == "true")
|
||||
.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
|
||||
fn start_productions(&mut self, director: &Director) -> Result<(), DbError> {
|
||||
self.base
|
||||
.set_current_step("DirectorWorker: start_productions");
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_to_produce", QUERY_GET_BEST_PRODUCTION)?;
|
||||
let rows = conn.execute("get_to_produce", &[&director.id])?;
|
||||
if rows.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let plan = match Self::map_row_to_production_plan(&rows[0]) {
|
||||
Some(p) => p,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
self.create_production_batches(&mut conn, &plan)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_row_to_production_plan(row: &Row) -> Option<ProductionPlan> {
|
||||
Some(ProductionPlan {
|
||||
falukant_user_id: row.get("falukant_user_id")?.parse().ok()?,
|
||||
money: row.get("money")?.parse().ok()?,
|
||||
certificate: row.get("certificate")?.parse().ok()?,
|
||||
branch_id: row.get("branch_id")?.parse().ok()?,
|
||||
product_id: row.get("product_id")?.parse().ok()?,
|
||||
stock_size: row.get("stock_size")?.parse().ok()?,
|
||||
used_in_stock: row.get("used_in_stock")?.parse().ok()?,
|
||||
running_productions: row.get("running_productions")?.parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_production_batches(
|
||||
&mut self,
|
||||
conn: &mut DbConnection,
|
||||
plan: &ProductionPlan,
|
||||
) -> Result<(), DbError> {
|
||||
let running = plan.running_productions;
|
||||
if running >= 2 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let free_capacity =
|
||||
plan.stock_size - plan.used_in_stock - plan.running_productions;
|
||||
|
||||
let one_piece_cost = plan.certificate * 6;
|
||||
let max_money_production = if one_piece_cost > 0 {
|
||||
plan.money / one_piece_cost
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let to_produce = free_capacity
|
||||
.min(max_money_production)
|
||||
.min(300)
|
||||
.max(0);
|
||||
|
||||
if to_produce < 1 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let production_cost = to_produce * one_piece_cost;
|
||||
|
||||
if let Err(err) = self.base.change_falukant_user_money(
|
||||
plan.falukant_user_id,
|
||||
-(production_cost as f64),
|
||||
"director starts production",
|
||||
) {
|
||||
eprintln!(
|
||||
"[DirectorWorker] Fehler bei change_falukant_user_money: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
conn.prepare("insert_production", QUERY_INSERT_PRODUCTION)?;
|
||||
|
||||
let mut remaining = to_produce;
|
||||
while remaining > 0 {
|
||||
let batch = remaining.min(100);
|
||||
conn.execute(
|
||||
"insert_production",
|
||||
&[&plan.branch_id, &plan.product_id, &batch],
|
||||
)?;
|
||||
remaining -= batch;
|
||||
}
|
||||
|
||||
let message = format!(
|
||||
r#"{{"event":"production_started","branch_id":{}}}"#,
|
||||
plan.branch_id
|
||||
);
|
||||
self.base.broker.publish(message);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_transports_stub(&self, _director: &Director) {
|
||||
// TODO: Transportlogik bei Bedarf aus dem C++-Code nachziehen.
|
||||
}
|
||||
|
||||
fn start_sellings(&mut self, director: &Director) -> Result<(), DbError> {
|
||||
self.base
|
||||
.set_current_step("DirectorWorker: start_sellings");
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_to_sell", QUERY_GET_INVENTORY)?;
|
||||
let rows = conn.execute("get_to_sell", &[&director.id])?;
|
||||
|
||||
let mut items: Vec<InventoryItem> =
|
||||
rows.into_iter().filter_map(Self::map_row_to_inventory_item).collect();
|
||||
|
||||
conn.prepare("remove_inventory", QUERY_REMOVE_INVENTORY)?;
|
||||
conn.prepare("add_sell_log", QUERY_ADD_SELL_LOG)?;
|
||||
|
||||
for item in items.drain(..) {
|
||||
self.sell_single_inventory_item(&mut conn, &item)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_row_to_inventory_item(row: Row) -> Option<InventoryItem> {
|
||||
Some(InventoryItem {
|
||||
id: row.get("id")?.parse().ok()?,
|
||||
product_id: row.get("product_id")?.parse().ok()?,
|
||||
quantity: row.get("quantity")?.parse().ok()?,
|
||||
quality: row.get("quality")?.parse().ok()?,
|
||||
sell_cost: row.get("sell_cost")?.parse().ok()?,
|
||||
user_id: row.get("user_id")?.parse().ok()?,
|
||||
region_id: row.get("region_id")?.parse().ok()?,
|
||||
branch_id: row.get("branch_id")?.parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn sell_single_inventory_item(
|
||||
&mut self,
|
||||
conn: &mut DbConnection,
|
||||
item: &InventoryItem,
|
||||
) -> Result<(), DbError> {
|
||||
if item.quantity <= 0 {
|
||||
conn.execute("remove_inventory", &[&item.id])?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let min_price = item.sell_cost * 0.6;
|
||||
let piece_sell_price =
|
||||
min_price + (item.sell_cost - min_price) * (item.quality as f64 / 100.0);
|
||||
let sell_price = piece_sell_price * item.quantity as f64;
|
||||
|
||||
if let Err(err) = self.base.change_falukant_user_money(
|
||||
item.user_id,
|
||||
sell_price,
|
||||
"sell products",
|
||||
) {
|
||||
eprintln!(
|
||||
"[DirectorWorker] Fehler bei change_falukant_user_money (sell products): {err}"
|
||||
);
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
"add_sell_log",
|
||||
&[
|
||||
&item.region_id,
|
||||
&item.product_id,
|
||||
&item.quantity,
|
||||
&item.user_id,
|
||||
],
|
||||
)?;
|
||||
|
||||
conn.execute("remove_inventory", &[&item.id])?;
|
||||
|
||||
let message = format!(
|
||||
r#"{{"event":"selled_items","branch_id":{}}}"#,
|
||||
item.branch_id
|
||||
);
|
||||
self.base.broker.publish(message);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pay_salary(&mut self) -> Result<(), DbError> {
|
||||
self.base.set_current_step("DirectorWorker: pay_salary");
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_salary_to_pay", QUERY_GET_SALARY_TO_PAY)?;
|
||||
conn.prepare("set_salary_payed", QUERY_SET_SALARY_PAYED)?;
|
||||
|
||||
let rows = conn.execute("get_salary_to_pay", &[])?;
|
||||
let salaries: Vec<SalaryItem> =
|
||||
rows.into_iter().filter_map(Self::map_row_to_salary_item).collect();
|
||||
|
||||
for item in salaries {
|
||||
if let Err(err) = self.base.change_falukant_user_money(
|
||||
item.employer_user_id,
|
||||
-(item.income as f64),
|
||||
"director payed out",
|
||||
) {
|
||||
eprintln!(
|
||||
"[DirectorWorker] Fehler bei change_falukant_user_money (director payed out): {err}"
|
||||
);
|
||||
}
|
||||
|
||||
conn.execute("set_salary_payed", &[&item.id])?;
|
||||
|
||||
let message =
|
||||
format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, item.employer_user_id);
|
||||
self.base.broker.publish(message);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_row_to_salary_item(row: Row) -> Option<SalaryItem> {
|
||||
Some(SalaryItem {
|
||||
id: row.get("id")?.parse().ok()?,
|
||||
employer_user_id: row.get("employer_user_id")?.parse().ok()?,
|
||||
income: row.get("income")?.parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn calculate_satisfaction(&mut self) -> Result<(), DbError> {
|
||||
self.base
|
||||
.set_current_step("DirectorWorker: calculate_satisfaction");
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("update_satisfaction", QUERY_UPDATE_SATISFACTION)?;
|
||||
let rows = conn.execute("update_satisfaction", &[])?;
|
||||
|
||||
for row in rows {
|
||||
if let Some(employer_id) = row
|
||||
.get("employer_user_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
let message = format!(
|
||||
r#"{{"event":"directorchanged","user_id":{}}}"#,
|
||||
employer_id
|
||||
);
|
||||
self.base.broker.publish(message);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Worker for DirectorWorker {
|
||||
fn start_worker_thread(&mut self) {
|
||||
let pool = self.base.pool.clone();
|
||||
let broker = self.base.broker.clone();
|
||||
|
||||
self.base
|
||||
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
||||
let mut worker = DirectorWorker::new(pool.clone(), broker.clone());
|
||||
while state.running_worker.load(Ordering::Relaxed) {
|
||||
worker.run_iteration(&state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
157
src/worker/house.rs
Normal file
157
src/worker/house.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use crate::db::{ConnectionPool, DbError};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
|
||||
pub struct HouseWorker {
|
||||
base: BaseWorker,
|
||||
}
|
||||
|
||||
// SQL-Queries analog zu `houseworker.h`
|
||||
const QUERY_GET_NEW_HOUSE_DATA: &str = r#"
|
||||
SELECT
|
||||
h.id AS house_id
|
||||
FROM
|
||||
falukant_type.house AS h
|
||||
WHERE
|
||||
random() < 0.0001
|
||||
AND label_tr <> 'under_bridge';
|
||||
"#;
|
||||
|
||||
const QUERY_ADD_NEW_BUYABLE_HOUSE: &str = r#"
|
||||
INSERT INTO falukant_data.buyable_house (house_type_id)
|
||||
VALUES ($1);
|
||||
"#;
|
||||
|
||||
const QUERY_UPDATE_BUYABLE_HOUSE_STATE: &str = r#"
|
||||
UPDATE falukant_data.buyable_house
|
||||
SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)),
|
||||
floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)),
|
||||
wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)),
|
||||
window_condition = ROUND(window_condition - random() * (3 + 0 * id));
|
||||
"#;
|
||||
|
||||
const QUERY_UPDATE_USER_HOUSE_STATE: &str = r#"
|
||||
UPDATE falukant_data.user_house
|
||||
SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)),
|
||||
floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)),
|
||||
wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)),
|
||||
window_condition = ROUND(window_condition - random() * (3 + 0 * id))
|
||||
WHERE house_type_id NOT IN (
|
||||
SELECT id
|
||||
FROM falukant_type.house h
|
||||
WHERE h.label_tr = 'under_bridge'
|
||||
);
|
||||
"#;
|
||||
|
||||
impl HouseWorker {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
base: BaseWorker::new("HouseWorker", pool, broker),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_loop(pool: ConnectionPool, _broker: MessageBroker, state: Arc<WorkerState>) {
|
||||
let mut last_hourly_run: Option<Instant> = None;
|
||||
let mut last_daily_run: Option<Instant> = None;
|
||||
|
||||
while state.running_worker.load(Ordering::Relaxed) {
|
||||
let now = Instant::now();
|
||||
|
||||
// Stündliche Aufgaben: neue Häuser erzeugen
|
||||
let should_run_hourly = match last_hourly_run {
|
||||
None => true,
|
||||
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(3600),
|
||||
};
|
||||
|
||||
if should_run_hourly {
|
||||
if let Err(err) = Self::perform_task_inner(&pool) {
|
||||
eprintln!("[HouseWorker] Fehler in performTask: {err}");
|
||||
}
|
||||
last_hourly_run = Some(now);
|
||||
}
|
||||
|
||||
// Tägliche Aufgaben: Hauszustände verschlechtern
|
||||
let should_run_daily = match last_daily_run {
|
||||
None => true,
|
||||
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(24 * 3600),
|
||||
};
|
||||
|
||||
if should_run_daily {
|
||||
if let Err(err) = Self::perform_house_state_change_inner(&pool) {
|
||||
eprintln!("[HouseWorker] Fehler in performHouseStateChange: {err}");
|
||||
}
|
||||
last_daily_run = Some(now);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_task_inner(pool: &ConnectionPool) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("[HouseWorker] DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_new_house_data", QUERY_GET_NEW_HOUSE_DATA)?;
|
||||
let rows = conn.execute("get_new_house_data", &[])?;
|
||||
|
||||
conn.prepare("add_new_buyable_house", QUERY_ADD_NEW_BUYABLE_HOUSE)?;
|
||||
|
||||
for row in rows {
|
||||
if let Some(house_id) = row
|
||||
.get("house_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
conn.execute("add_new_buyable_house", &[&house_id])?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn perform_house_state_change_inner(pool: &ConnectionPool) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("[HouseWorker] DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(
|
||||
"update_buyable_house_state",
|
||||
QUERY_UPDATE_BUYABLE_HOUSE_STATE,
|
||||
)?;
|
||||
conn.prepare(
|
||||
"update_user_house_state",
|
||||
QUERY_UPDATE_USER_HOUSE_STATE,
|
||||
)?;
|
||||
|
||||
conn.execute("update_buyable_house_state", &[])?;
|
||||
conn.execute("update_user_house_state", &[])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Worker for HouseWorker {
|
||||
fn start_worker_thread(&mut self) {
|
||||
let pool = self.base.pool.clone();
|
||||
let broker = self.base.broker.clone();
|
||||
|
||||
self.base
|
||||
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
||||
Self::run_loop(pool.clone(), broker.clone(), state);
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
23
src/worker/mod.rs
Normal file
23
src/worker/mod.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
mod base;
|
||||
mod character_creation;
|
||||
mod director;
|
||||
mod stockage_manager;
|
||||
mod house;
|
||||
mod produce;
|
||||
mod politics;
|
||||
mod underground;
|
||||
mod value_recalculation;
|
||||
mod user_character;
|
||||
|
||||
pub use base::Worker;
|
||||
pub use crate::db::ConnectionPool;
|
||||
pub use character_creation::CharacterCreationWorker;
|
||||
pub use director::DirectorWorker;
|
||||
pub use stockage_manager::StockageManager;
|
||||
pub use house::HouseWorker;
|
||||
pub use produce::ProduceWorker;
|
||||
pub use politics::PoliticsWorker;
|
||||
pub use underground::UndergroundWorker;
|
||||
pub use value_recalculation::ValueRecalculationWorker;
|
||||
pub use user_character::UserCharacterWorker;
|
||||
|
||||
732
src/worker/politics.rs
Normal file
732
src/worker/politics.rs
Normal file
@@ -0,0 +1,732 @@
|
||||
use crate::db::{ConnectionPool, DbError, Row};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
|
||||
pub struct PoliticsWorker {
|
||||
base: BaseWorker,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct OfficeCounts {
|
||||
region_id: i32,
|
||||
required: i32,
|
||||
occupied: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Election {
|
||||
election_id: i32,
|
||||
region_id: i32,
|
||||
posts_to_fill: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Office {
|
||||
office_id: i32,
|
||||
office_type_id: i32,
|
||||
character_id: i32,
|
||||
region_id: i32,
|
||||
}
|
||||
|
||||
// --- SQL-Konstanten (1:1 aus politics_worker.h übernommen) ------------------
|
||||
|
||||
const QUERY_COUNT_OFFICES_PER_REGION: &str = r#"
|
||||
WITH
|
||||
seats_per_region AS (
|
||||
SELECT
|
||||
pot.id AS office_type_id,
|
||||
rt.id AS region_id,
|
||||
pot.seats_per_region AS seats_total
|
||||
FROM falukant_type.political_office_type AS pot
|
||||
JOIN falukant_type.region AS rt
|
||||
ON pot.region_type = rt.label_tr
|
||||
),
|
||||
occupied AS (
|
||||
SELECT
|
||||
po.office_type_id,
|
||||
po.region_id,
|
||||
COUNT(*) AS occupied_count
|
||||
FROM falukant_data.political_office AS po
|
||||
GROUP BY po.office_type_id, po.region_id
|
||||
),
|
||||
combined AS (
|
||||
SELECT
|
||||
spr.region_id,
|
||||
spr.seats_total AS required_count,
|
||||
COALESCE(o.occupied_count, 0) AS occupied_count
|
||||
FROM seats_per_region AS spr
|
||||
LEFT JOIN occupied AS o
|
||||
ON spr.office_type_id = o.office_type_id
|
||||
AND spr.region_id = o.region_id
|
||||
)
|
||||
SELECT
|
||||
region_id,
|
||||
SUM(required_count) AS required_count,
|
||||
SUM(occupied_count) AS occupied_count
|
||||
FROM combined
|
||||
GROUP BY region_id;
|
||||
"#;
|
||||
|
||||
const QUERY_SELECT_NEEDED_ELECTIONS: &str = r#"
|
||||
WITH
|
||||
target_date AS (
|
||||
SELECT NOW()::date AS election_date
|
||||
),
|
||||
expired_today AS (
|
||||
DELETE FROM falukant_data.political_office AS po
|
||||
USING falukant_type.political_office_type AS pot
|
||||
WHERE po.office_type_id = pot.id
|
||||
AND (po.created_at + (pot.term_length * INTERVAL '1 day'))::date
|
||||
= (SELECT election_date FROM target_date)
|
||||
RETURNING
|
||||
pot.id AS office_type_id,
|
||||
po.region_id AS region_id
|
||||
),
|
||||
gaps_per_region AS (
|
||||
SELECT
|
||||
office_type_id,
|
||||
region_id,
|
||||
COUNT(*) AS gaps
|
||||
FROM expired_today
|
||||
GROUP BY office_type_id, region_id
|
||||
),
|
||||
to_schedule AS (
|
||||
SELECT
|
||||
g.office_type_id,
|
||||
g.region_id,
|
||||
g.gaps,
|
||||
td.election_date
|
||||
FROM gaps_per_region AS g
|
||||
CROSS JOIN target_date AS td
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM falukant_data.election AS e
|
||||
WHERE e.office_type_id = g.office_type_id
|
||||
AND e.region_id = g.region_id
|
||||
AND e.date::date = td.election_date
|
||||
)
|
||||
),
|
||||
new_elections AS (
|
||||
INSERT INTO falukant_data.election
|
||||
(office_type_id, date, posts_to_fill, created_at, updated_at, region_id)
|
||||
SELECT
|
||||
ts.office_type_id,
|
||||
ts.election_date,
|
||||
ts.gaps,
|
||||
NOW(),
|
||||
NOW(),
|
||||
ts.region_id
|
||||
FROM to_schedule AS ts
|
||||
RETURNING
|
||||
id AS election_id,
|
||||
region_id,
|
||||
posts_to_fill
|
||||
)
|
||||
SELECT
|
||||
ne.election_id,
|
||||
ne.region_id,
|
||||
ne.posts_to_fill
|
||||
FROM new_elections AS ne
|
||||
ORDER BY ne.region_id, ne.election_id;
|
||||
"#;
|
||||
|
||||
const QUERY_INSERT_CANDIDATES: &str = r#"
|
||||
INSERT INTO falukant_data.candidate
|
||||
(election_id, character_id, created_at, updated_at)
|
||||
SELECT
|
||||
$1 AS election_id,
|
||||
sub.id AS character_id,
|
||||
NOW() AS created_at,
|
||||
NOW() AS updated_at
|
||||
FROM (
|
||||
WITH RECURSIVE region_tree AS (
|
||||
SELECT r.id
|
||||
FROM falukant_data.region AS r
|
||||
WHERE r.id = $2
|
||||
UNION ALL
|
||||
SELECT r2.id
|
||||
FROM falukant_data.region AS r2
|
||||
JOIN region_tree AS rt
|
||||
ON r2.parent_id = rt.id
|
||||
)
|
||||
SELECT ch.id
|
||||
FROM falukant_data.character AS ch
|
||||
JOIN region_tree AS rt2
|
||||
ON ch.region_id = rt2.id
|
||||
WHERE ch.user_id IS NULL
|
||||
AND ch.birthdate <= NOW() - INTERVAL '21 days'
|
||||
AND ch.title_of_nobility IN (
|
||||
SELECT id
|
||||
FROM falukant_type.title
|
||||
WHERE label_tr != 'noncivil'
|
||||
)
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ($3 * 2)
|
||||
) AS sub(id);
|
||||
"#;
|
||||
|
||||
const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#"
|
||||
WITH
|
||||
expired_offices AS (
|
||||
DELETE FROM falukant_data.political_office AS po
|
||||
USING falukant_type.political_office_type AS pot
|
||||
WHERE po.office_type_id = pot.id
|
||||
AND (po.created_at + (pot.term_length * INTERVAL '1 day')) <= NOW()
|
||||
RETURNING
|
||||
pot.id AS office_type_id,
|
||||
po.region_id AS region_id
|
||||
),
|
||||
distinct_types AS (
|
||||
SELECT DISTINCT office_type_id, region_id FROM expired_offices
|
||||
),
|
||||
votes_per_candidate AS (
|
||||
SELECT
|
||||
dt.office_type_id,
|
||||
dt.region_id,
|
||||
c.character_id,
|
||||
COUNT(v.id) AS vote_count
|
||||
FROM distinct_types AS dt
|
||||
JOIN falukant_data.election AS e
|
||||
ON e.office_type_id = dt.office_type_id
|
||||
JOIN falukant_data.vote AS v
|
||||
ON v.election_id = e.id
|
||||
JOIN falukant_data.candidate AS c
|
||||
ON c.election_id = e.id
|
||||
AND c.id = v.candidate_id
|
||||
WHERE e.date >= (NOW() - INTERVAL '30 days')
|
||||
GROUP BY dt.office_type_id, dt.region_id, c.character_id
|
||||
),
|
||||
ranked_winners AS (
|
||||
SELECT
|
||||
vpc.office_type_id,
|
||||
vpc.region_id,
|
||||
vpc.character_id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY vpc.office_type_id, vpc.region_id
|
||||
ORDER BY vpc.vote_count DESC
|
||||
) AS rn
|
||||
FROM votes_per_candidate AS vpc
|
||||
),
|
||||
selected_winners AS (
|
||||
SELECT
|
||||
rw.office_type_id,
|
||||
rw.region_id,
|
||||
rw.character_id
|
||||
FROM ranked_winners AS rw
|
||||
JOIN falukant_type.political_office_type AS pot
|
||||
ON pot.id = rw.office_type_id
|
||||
WHERE rw.rn <= pot.seats_per_region
|
||||
),
|
||||
insert_winners AS (
|
||||
INSERT INTO falukant_data.political_office
|
||||
(office_type_id, character_id, created_at, updated_at, region_id)
|
||||
SELECT
|
||||
sw.office_type_id,
|
||||
sw.character_id,
|
||||
NOW(),
|
||||
NOW(),
|
||||
sw.region_id
|
||||
FROM selected_winners AS sw
|
||||
RETURNING id AS new_office_id, office_type_id, character_id, region_id
|
||||
),
|
||||
count_inserted AS (
|
||||
SELECT
|
||||
office_type_id,
|
||||
region_id,
|
||||
COUNT(*) AS inserted_count
|
||||
FROM insert_winners
|
||||
GROUP BY office_type_id, region_id
|
||||
),
|
||||
needed_to_fill AS (
|
||||
SELECT
|
||||
dt.office_type_id,
|
||||
dt.region_id,
|
||||
(pot.seats_per_region - COALESCE(ci.inserted_count, 0)) AS gaps
|
||||
FROM distinct_types AS dt
|
||||
JOIN falukant_type.political_office_type AS pot
|
||||
ON pot.id = dt.office_type_id
|
||||
LEFT JOIN count_inserted AS ci
|
||||
ON ci.office_type_id = dt.office_type_id
|
||||
AND ci.region_id = dt.region_id
|
||||
WHERE (pot.seats_per_region - COALESCE(ci.inserted_count, 0)) > 0
|
||||
),
|
||||
random_candidates AS (
|
||||
SELECT
|
||||
rtf.office_type_id,
|
||||
rtf.region_id,
|
||||
ch.id AS character_id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY rtf.office_type_id, rtf.region_id
|
||||
ORDER BY RANDOM()
|
||||
) AS rn
|
||||
FROM needed_to_fill AS rtf
|
||||
JOIN falukant_data.character AS ch
|
||||
ON ch.region_id = rtf.region_id
|
||||
AND ch.user_id IS NULL
|
||||
AND ch.birthdate <= NOW() - INTERVAL '21 days'
|
||||
AND ch.title_of_nobility IN (
|
||||
SELECT id FROM falukant_type.title WHERE label_tr != 'noncivil'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM falukant_data.political_office AS po2
|
||||
JOIN falukant_type.political_office_type AS pot2
|
||||
ON pot2.id = po2.office_type_id
|
||||
WHERE po2.character_id = ch.id
|
||||
AND (po2.created_at + (pot2.term_length * INTERVAL '1 day')) >
|
||||
NOW() + INTERVAL '2 days'
|
||||
)
|
||||
),
|
||||
insert_random AS (
|
||||
INSERT INTO falukant_data.political_office
|
||||
(office_type_id, character_id, created_at, updated_at, region_id)
|
||||
SELECT
|
||||
rc.office_type_id,
|
||||
rc.character_id,
|
||||
NOW(),
|
||||
NOW(),
|
||||
rc.region_id
|
||||
FROM random_candidates AS rc
|
||||
JOIN needed_to_fill AS rtf
|
||||
ON rtf.office_type_id = rc.office_type_id
|
||||
AND rtf.region_id = rc.region_id
|
||||
WHERE rc.rn <= rtf.gaps
|
||||
RETURNING id AS new_office_id, office_type_id, character_id, region_id
|
||||
)
|
||||
SELECT
|
||||
new_office_id AS office_id,
|
||||
office_type_id,
|
||||
character_id,
|
||||
region_id
|
||||
FROM insert_winners
|
||||
UNION ALL
|
||||
SELECT
|
||||
new_office_id AS office_id,
|
||||
office_type_id,
|
||||
character_id,
|
||||
region_id
|
||||
FROM insert_random;
|
||||
"#;
|
||||
|
||||
const QUERY_USERS_IN_CITIES_OF_REGIONS: &str = r#"
|
||||
WITH RECURSIVE region_tree AS (
|
||||
SELECT id
|
||||
FROM falukant_data.region
|
||||
WHERE id = $1
|
||||
UNION ALL
|
||||
SELECT r2.id
|
||||
FROM falukant_data.region AS r2
|
||||
JOIN region_tree AS rt
|
||||
ON r2.parent_id = rt.id
|
||||
)
|
||||
SELECT DISTINCT ch.user_id
|
||||
FROM falukant_data.character AS ch
|
||||
JOIN region_tree AS rt2
|
||||
ON ch.region_id = rt2.id
|
||||
WHERE ch.user_id IS NOT NULL;
|
||||
"#;
|
||||
|
||||
const QUERY_NOTIFY_OFFICE_EXPIRATION: &str = r#"
|
||||
INSERT INTO falukant_log.notification
|
||||
(user_id, tr, created_at, updated_at)
|
||||
SELECT
|
||||
po.character_id,
|
||||
'notify_office_expiring',
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM falukant_data.political_office AS po
|
||||
JOIN falukant_type.political_office_type AS pot
|
||||
ON po.office_type_id = pot.id
|
||||
WHERE (po.created_at + (pot.term_length * INTERVAL '1 day'))
|
||||
BETWEEN (NOW() + INTERVAL '2 days')
|
||||
AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second');
|
||||
"#;
|
||||
|
||||
const QUERY_NOTIFY_ELECTION_CREATED: &str = r#"
|
||||
INSERT INTO falukant_log.notification
|
||||
(user_id, tr, created_at, updated_at)
|
||||
VALUES
|
||||
($1, 'notify_election_created', NOW(), NOW());
|
||||
"#;
|
||||
|
||||
const QUERY_NOTIFY_OFFICE_FILLED: &str = r#"
|
||||
INSERT INTO falukant_log.notification
|
||||
(user_id, tr, created_at, updated_at)
|
||||
VALUES
|
||||
($1, 'notify_office_filled', NOW(), NOW());
|
||||
"#;
|
||||
|
||||
const QUERY_GET_USERS_WITH_EXPIRING_OFFICES: &str = r#"
|
||||
SELECT DISTINCT ch.user_id
|
||||
FROM falukant_data.political_office AS po
|
||||
JOIN falukant_type.political_office_type AS pot
|
||||
ON po.office_type_id = pot.id
|
||||
JOIN falukant_data.character AS ch
|
||||
ON po.character_id = ch.id
|
||||
WHERE ch.user_id IS NOT NULL
|
||||
AND (po.created_at + (pot.term_length * INTERVAL '1 day'))
|
||||
BETWEEN (NOW() + INTERVAL '2 days')
|
||||
AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second');
|
||||
"#;
|
||||
|
||||
const QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS: &str = r#"
|
||||
SELECT DISTINCT ch.user_id
|
||||
FROM falukant_data.election AS e
|
||||
JOIN falukant_data.character AS ch
|
||||
ON ch.region_id = e.region_id
|
||||
WHERE ch.user_id IS NOT NULL
|
||||
AND e.date >= NOW() - INTERVAL '1 day';
|
||||
"#;
|
||||
|
||||
const QUERY_GET_USERS_WITH_FILLED_OFFICES: &str = r#"
|
||||
SELECT DISTINCT ch.user_id
|
||||
FROM falukant_data.political_office AS po
|
||||
JOIN falukant_data.character AS ch
|
||||
ON po.character_id = ch.id
|
||||
WHERE ch.user_id IS NOT NULL
|
||||
AND po.created_at >= NOW() - INTERVAL '1 minute';
|
||||
"#;
|
||||
|
||||
const QUERY_PROCESS_ELECTIONS: &str = r#"
|
||||
SELECT office_id, office_type_id, character_id, region_id
|
||||
FROM falukant_data.process_elections();
|
||||
"#;
|
||||
|
||||
impl PoliticsWorker {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
base: BaseWorker::new("PoliticsWorker", pool, broker),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
|
||||
let mut last_execution: Option<Instant> = None;
|
||||
|
||||
while state.running_worker.load(Ordering::Relaxed) {
|
||||
let now = Instant::now();
|
||||
let should_run = match last_execution {
|
||||
None => true,
|
||||
Some(prev) => now.saturating_duration_since(prev) >= Duration::from_secs(24 * 3600),
|
||||
};
|
||||
|
||||
if should_run {
|
||||
if let Err(err) = Self::perform_daily_politics_task(&pool, &broker) {
|
||||
eprintln!("[PoliticsWorker] Fehler bei performDailyPoliticsTask: {err}");
|
||||
}
|
||||
last_execution = Some(now);
|
||||
}
|
||||
|
||||
// Entspricht ungefähr der 5-Sekunden-Schleife im C++-Code
|
||||
for _ in 0..5 {
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_daily_politics_task(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
) -> Result<(), DbError> {
|
||||
// 1) Optional: Positionen evaluieren (aktuell nur Logging/Struktur)
|
||||
let _ = Self::evaluate_political_positions(pool)?;
|
||||
|
||||
// 2) Ämter, die bald auslaufen, benachrichtigen
|
||||
Self::notify_office_expirations(pool, broker)?;
|
||||
|
||||
// 3) Abgelaufene Ämter verarbeiten und neue besetzen
|
||||
let new_offices_direct = Self::process_expired_offices_and_fill(pool)?;
|
||||
if !new_offices_direct.is_empty() {
|
||||
Self::notify_office_filled(pool, broker, &new_offices_direct)?;
|
||||
}
|
||||
|
||||
// 4) Neue Wahlen planen und Kandidaten eintragen
|
||||
let elections = Self::schedule_elections(pool)?;
|
||||
if !elections.is_empty() {
|
||||
Self::insert_candidates_for_elections(pool, &elections)?;
|
||||
|
||||
// Benachrichtige User in betroffenen Regionen
|
||||
let region_ids: HashSet<i32> =
|
||||
elections.iter().map(|e| e.region_id).collect();
|
||||
let user_ids =
|
||||
Self::get_user_ids_in_cities_of_regions(pool, ®ion_ids)?;
|
||||
Self::notify_election_created(pool, broker, &user_ids)?;
|
||||
}
|
||||
|
||||
// 5) Wahlen auswerten und neu besetzte Ämter melden
|
||||
let new_offices_from_elections = Self::process_elections(pool)?;
|
||||
if !new_offices_from_elections.is_empty() {
|
||||
Self::notify_office_filled(pool, broker, &new_offices_from_elections)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn evaluate_political_positions(
|
||||
pool: &ConnectionPool,
|
||||
) -> Result<Vec<OfficeCounts>, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(
|
||||
"count_offices_per_region",
|
||||
QUERY_COUNT_OFFICES_PER_REGION,
|
||||
)?;
|
||||
let rows = conn.execute("count_offices_per_region", &[])?;
|
||||
|
||||
let mut result = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let region_id = parse_i32(&row, "region_id", -1);
|
||||
let required = parse_i32(&row, "required_count", 0);
|
||||
let occupied = parse_i32(&row, "occupied_count", 0);
|
||||
if region_id >= 0 {
|
||||
result.push(OfficeCounts {
|
||||
region_id,
|
||||
required,
|
||||
occupied,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn schedule_elections(pool: &ConnectionPool) -> Result<Vec<Election>, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("select_needed_elections", QUERY_SELECT_NEEDED_ELECTIONS)?;
|
||||
let rows = conn.execute("select_needed_elections", &[])?;
|
||||
|
||||
let mut elections = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let election_id = parse_i32(&row, "election_id", -1);
|
||||
let region_id = parse_i32(&row, "region_id", -1);
|
||||
let posts_to_fill = parse_i32(&row, "posts_to_fill", 0);
|
||||
if election_id >= 0 && region_id >= 0 {
|
||||
elections.push(Election {
|
||||
election_id,
|
||||
region_id,
|
||||
posts_to_fill,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(elections)
|
||||
}
|
||||
|
||||
fn insert_candidates_for_elections(
|
||||
pool: &ConnectionPool,
|
||||
elections: &[Election],
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("insert_candidates", QUERY_INSERT_CANDIDATES)?;
|
||||
|
||||
for e in elections {
|
||||
conn.execute(
|
||||
"insert_candidates",
|
||||
&[&e.election_id, &e.region_id, &e.posts_to_fill],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_expired_offices_and_fill(
|
||||
pool: &ConnectionPool,
|
||||
) -> Result<Vec<Office>, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("process_expired_and_fill", QUERY_PROCESS_EXPIRED_AND_FILL)?;
|
||||
let rows = conn.execute("process_expired_and_fill", &[])?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.filter_map(map_row_to_office)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn get_user_ids_in_cities_of_regions(
|
||||
pool: &ConnectionPool,
|
||||
region_ids: &HashSet<i32>,
|
||||
) -> Result<Vec<i32>, DbError> {
|
||||
if region_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_users_in_cities", QUERY_USERS_IN_CITIES_OF_REGIONS)?;
|
||||
|
||||
let mut user_ids = Vec::new();
|
||||
for rid in region_ids {
|
||||
let rows = conn.execute("get_users_in_cities", &[rid])?;
|
||||
for row in rows {
|
||||
if let Some(uid) = row.get("user_id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
user_ids.push(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(user_ids)
|
||||
}
|
||||
|
||||
fn notify_office_expirations(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("notify_office_expiration", QUERY_NOTIFY_OFFICE_EXPIRATION)?;
|
||||
conn.execute("notify_office_expiration", &[])?;
|
||||
|
||||
conn.prepare(
|
||||
"get_users_with_expiring_offices",
|
||||
QUERY_GET_USERS_WITH_EXPIRING_OFFICES,
|
||||
)?;
|
||||
let rows = conn.execute("get_users_with_expiring_offices", &[])?;
|
||||
|
||||
for row in rows {
|
||||
if let Some(user_id) = row.get("user_id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
let msg =
|
||||
format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
broker.publish(msg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_election_created(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
user_ids: &[i32],
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("notify_election_created", QUERY_NOTIFY_ELECTION_CREATED)?;
|
||||
|
||||
for uid in user_ids {
|
||||
conn.execute("notify_election_created", &[uid])?;
|
||||
}
|
||||
|
||||
conn.prepare(
|
||||
"get_users_in_regions_with_elections",
|
||||
QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS,
|
||||
)?;
|
||||
let rows = conn.execute("get_users_in_regions_with_elections", &[])?;
|
||||
|
||||
for row in rows {
|
||||
if let Some(user_id) = row.get("user_id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
let msg =
|
||||
format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
broker.publish(msg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_office_filled(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
new_offices: &[Office],
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("notify_office_filled", QUERY_NOTIFY_OFFICE_FILLED)?;
|
||||
|
||||
for office in new_offices {
|
||||
conn.execute("notify_office_filled", &[&office.character_id])?;
|
||||
}
|
||||
|
||||
conn.prepare(
|
||||
"get_users_with_filled_offices",
|
||||
QUERY_GET_USERS_WITH_FILLED_OFFICES,
|
||||
)?;
|
||||
let rows = conn.execute("get_users_with_filled_offices", &[])?;
|
||||
|
||||
for row in rows {
|
||||
if let Some(user_id) = row.get("user_id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
let msg =
|
||||
format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
broker.publish(msg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_elections(pool: &ConnectionPool) -> Result<Vec<Office>, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("process_elections", QUERY_PROCESS_ELECTIONS)?;
|
||||
let rows = conn.execute("process_elections", &[])?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.filter_map(map_row_to_office)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Worker for PoliticsWorker {
|
||||
fn start_worker_thread(&mut self) {
|
||||
let pool = self.base.pool.clone();
|
||||
let broker = self.base.broker.clone();
|
||||
|
||||
self.base
|
||||
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
||||
PoliticsWorker::run_loop(pool.clone(), broker.clone(), state);
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_i32(row: &Row, key: &str, default: i32) -> i32 {
|
||||
row.get(key)
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn map_row_to_office(row: Row) -> Option<Office> {
|
||||
Some(Office {
|
||||
office_id: row.get("office_id")?.parse().ok()?,
|
||||
office_type_id: row.get("office_type_id")?.parse().ok()?,
|
||||
character_id: row.get("character_id")?.parse().ok()?,
|
||||
region_id: row.get("region_id")?.parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
495
src/worker/produce.rs
Normal file
495
src/worker/produce.rs
Normal file
@@ -0,0 +1,495 @@
|
||||
use crate::db::{Row, Rows};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::cmp::min;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::db::ConnectionPool;
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
|
||||
/// Abbildet eine abgeschlossene Produktion aus der Datenbank.
|
||||
#[derive(Debug, Clone)]
|
||||
struct FinishedProduction {
|
||||
production_id: i32,
|
||||
branch_id: i32,
|
||||
product_id: i32,
|
||||
quantity: i32,
|
||||
quality: i32,
|
||||
user_id: i32,
|
||||
region_id: i32,
|
||||
}
|
||||
|
||||
/// Abbildet ein Lager (Stock) mit Kapazität.
|
||||
#[derive(Debug, Clone)]
|
||||
struct StockInfo {
|
||||
stock_id: i32,
|
||||
total_capacity: i32,
|
||||
filled: i32,
|
||||
}
|
||||
|
||||
// SQL-Queries analog zur C++-Implementierung
|
||||
const QUERY_GET_FINISHED_PRODUCTIONS: &str = r#"
|
||||
SELECT DISTINCT
|
||||
p.id AS production_id,
|
||||
p.branch_id,
|
||||
p.product_id,
|
||||
p.quantity,
|
||||
p.start_timestamp,
|
||||
pr.production_time,
|
||||
k.character_id,
|
||||
CASE
|
||||
WHEN k2.id IS NOT NULL
|
||||
THEN (k.knowledge * 2 + k2.knowledge) / 3
|
||||
ELSE k.knowledge
|
||||
END AS quality,
|
||||
br.region_id,
|
||||
br.falukant_user_id AS user_id
|
||||
FROM falukant_data.production p
|
||||
JOIN falukant_type.product pr ON p.product_id = pr.id
|
||||
JOIN falukant_data.branch br ON p.branch_id = br.id
|
||||
JOIN falukant_data.character c ON c.user_id = br.falukant_user_id
|
||||
JOIN falukant_data.knowledge k ON p.product_id = k.product_id AND k.character_id = c.id
|
||||
JOIN falukant_data.stock s ON s.branch_id = br.id
|
||||
LEFT JOIN falukant_data.director d ON d.employer_user_id = c.user_id
|
||||
LEFT JOIN falukant_data.knowledge k2
|
||||
ON k2.character_id = d.director_character_id
|
||||
AND k2.product_id = p.product_id
|
||||
WHERE p.start_timestamp + INTERVAL '1 minute' * pr.production_time <= NOW()
|
||||
ORDER BY p.start_timestamp;
|
||||
"#;
|
||||
|
||||
const QUERY_GET_AVAILABLE_STOCKS: &str = r#"
|
||||
SELECT
|
||||
stock.id,
|
||||
stock.quantity AS total_capacity,
|
||||
(
|
||||
SELECT COALESCE(SUM(inventory.quantity), 0)
|
||||
FROM falukant_data.inventory
|
||||
WHERE inventory.stock_id = stock.id
|
||||
) AS filled,
|
||||
stock.branch_id
|
||||
FROM falukant_data.stock stock
|
||||
JOIN falukant_data.branch branch
|
||||
ON stock.branch_id = branch.id
|
||||
WHERE branch.id = $1
|
||||
ORDER BY total_capacity DESC;
|
||||
"#;
|
||||
|
||||
const QUERY_DELETE_PRODUCTION: &str = r#"
|
||||
DELETE FROM falukant_data.production
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
const QUERY_INSERT_INVENTORY: &str = r#"
|
||||
INSERT INTO falukant_data.inventory (
|
||||
stock_id,
|
||||
product_id,
|
||||
quantity,
|
||||
quality,
|
||||
produced_at
|
||||
) VALUES ($1, $2, $3, $4, NOW());
|
||||
"#;
|
||||
|
||||
const QUERY_INSERT_UPDATE_PRODUCTION_LOG: &str = r#"
|
||||
INSERT INTO falukant_log.production (
|
||||
region_id,
|
||||
product_id,
|
||||
quantity,
|
||||
producer_id,
|
||||
production_date
|
||||
) VALUES ($1, $2, $3, $4, CURRENT_DATE)
|
||||
ON CONFLICT (producer_id, product_id, region_id, production_date)
|
||||
DO UPDATE
|
||||
SET quantity = falukant_log.production.quantity + EXCLUDED.quantity;
|
||||
"#;
|
||||
|
||||
const QUERY_ADD_OVERPRODUCTION_NOTIFICATION: &str = r#"
|
||||
INSERT INTO falukant_log.notification (
|
||||
user_id,
|
||||
tr,
|
||||
shown,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES ($1, $2, FALSE, NOW(), NOW());
|
||||
"#;
|
||||
|
||||
pub struct ProduceWorker {
|
||||
base: BaseWorker,
|
||||
last_iteration: Option<Instant>,
|
||||
}
|
||||
|
||||
impl ProduceWorker {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
base: BaseWorker::new("ProduceWorker", pool, broker),
|
||||
last_iteration: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_iteration(&mut self, state: &WorkerState) {
|
||||
self.base
|
||||
.set_current_step("Check runningWorker Variable");
|
||||
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let sleep_duration = self.time_until_next_iteration();
|
||||
self.sleep_with_shutdown_check(sleep_duration, state);
|
||||
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.base.set_current_step("Process Productions");
|
||||
self.process_productions();
|
||||
self.base.set_current_step("Signal Activity");
|
||||
// TODO: Später Analogie zu signalActivity() aus der C++-Basisklasse herstellen.
|
||||
self.base.set_current_step("Loop Done");
|
||||
}
|
||||
|
||||
fn time_until_next_iteration(&mut self) -> Duration {
|
||||
const MIN_INTERVAL_MS: u64 = 200;
|
||||
let now = Instant::now();
|
||||
|
||||
match self.last_iteration {
|
||||
None => {
|
||||
self.last_iteration = Some(now);
|
||||
Duration::from_millis(0)
|
||||
}
|
||||
Some(last) => {
|
||||
let elapsed = now.saturating_duration_since(last);
|
||||
if elapsed >= Duration::from_millis(MIN_INTERVAL_MS) {
|
||||
self.last_iteration = Some(now);
|
||||
Duration::from_millis(0)
|
||||
} else {
|
||||
let remaining = Duration::from_millis(MIN_INTERVAL_MS) - elapsed;
|
||||
self.last_iteration = Some(now);
|
||||
remaining
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sleep_with_shutdown_check(&self, duration: Duration, state: &WorkerState) {
|
||||
const SLICE_MS: u64 = 10;
|
||||
let total_ms = duration.as_millis() as u64;
|
||||
|
||||
let mut slept = 0;
|
||||
while slept < total_ms {
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let remaining = total_ms - slept;
|
||||
let slice = min(remaining, SLICE_MS);
|
||||
std::thread::sleep(Duration::from_millis(slice));
|
||||
slept += slice;
|
||||
}
|
||||
}
|
||||
|
||||
fn process_productions(&mut self) {
|
||||
self.base
|
||||
.set_current_step("Fetch Finished Productions");
|
||||
|
||||
let finished_productions = match self.get_finished_productions() {
|
||||
Ok(rows) => rows,
|
||||
Err(err) => {
|
||||
eprintln!("[ProduceWorker] Fehler in getFinishedProductions: {err}");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
self.base
|
||||
.set_current_step("Process Finished Productions");
|
||||
|
||||
for production in finished_productions {
|
||||
self.handle_finished_production(&production);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_finished_productions(&self) -> Result<Vec<FinishedProduction>, crate::db::DbError> {
|
||||
let rows = self.load_finished_productions()?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.filter_map(Self::map_row_to_finished_production)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn handle_finished_production(&mut self, production: &FinishedProduction) {
|
||||
let FinishedProduction {
|
||||
branch_id,
|
||||
product_id,
|
||||
quantity,
|
||||
quality,
|
||||
user_id,
|
||||
region_id,
|
||||
production_id,
|
||||
} = *production;
|
||||
|
||||
if self.add_to_inventory(branch_id, product_id, quantity, quality, user_id) {
|
||||
self.delete_production(production_id);
|
||||
self.add_production_to_log(region_id, user_id, product_id, quantity);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_to_inventory(
|
||||
&mut self,
|
||||
branch_id: i32,
|
||||
product_id: i32,
|
||||
quantity: i32,
|
||||
quality: i32,
|
||||
user_id: i32,
|
||||
) -> bool {
|
||||
let mut remaining_quantity = quantity;
|
||||
let stocks = match self.get_available_stocks(branch_id) {
|
||||
Ok(rows) => rows,
|
||||
Err(err) => {
|
||||
eprintln!("[ProduceWorker] Fehler in getAvailableStocks: {err}");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
for stock in stocks {
|
||||
if remaining_quantity <= 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let free_capacity = stock.total_capacity - stock.filled;
|
||||
if free_capacity <= 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let to_store = min(remaining_quantity, free_capacity);
|
||||
if !self.store_in_stock(stock.stock_id, product_id, to_store, quality) {
|
||||
return false;
|
||||
}
|
||||
remaining_quantity -= to_store;
|
||||
}
|
||||
|
||||
if remaining_quantity == 0 {
|
||||
self.send_production_ready_event(user_id, product_id, quantity, quality, branch_id);
|
||||
true
|
||||
} else {
|
||||
self.handle_overproduction(user_id, remaining_quantity);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn get_available_stocks(&self, branch_id: i32) -> Result<Vec<StockInfo>, crate::db::DbError> {
|
||||
let rows = self.load_available_stocks(branch_id)?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.filter_map(Self::map_row_to_stock_info)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn store_in_stock(
|
||||
&self,
|
||||
stock_id: i32,
|
||||
product_id: i32,
|
||||
quantity: i32,
|
||||
quality: i32,
|
||||
) -> bool {
|
||||
if let Err(err) = self.insert_inventory(stock_id, product_id, quantity, quality) {
|
||||
eprintln!("[ProduceWorker] Fehler in storeInStock: {err}");
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn delete_production(&self, production_id: i32) {
|
||||
if let Err(err) = self.remove_production(production_id) {
|
||||
eprintln!("[ProduceWorker] Fehler beim Löschen der Produktion: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn add_production_to_log(
|
||||
&self,
|
||||
region_id: i32,
|
||||
user_id: i32,
|
||||
product_id: i32,
|
||||
quantity: i32,
|
||||
) {
|
||||
if let Err(err) = self.insert_or_update_production_log(region_id, user_id, product_id, quantity) {
|
||||
eprintln!("[ProduceWorker] Fehler in addProductionToLog: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn send_production_ready_event(
|
||||
&self,
|
||||
user_id: i32,
|
||||
product_id: i32,
|
||||
quantity: i32,
|
||||
quality: i32,
|
||||
branch_id: i32,
|
||||
) {
|
||||
// JSON als String aufbauen, um externe Dependencies zu vermeiden.
|
||||
let message = format!(
|
||||
r#"{{"event":"production_ready","user_id":{user_id},"product_id":{product_id},"quantity":{quantity},"quality":{quality},"branch_id":{branch_id}}}"#
|
||||
);
|
||||
self.base.broker.publish(message);
|
||||
}
|
||||
|
||||
fn handle_overproduction(&self, user_id: i32, remaining_quantity: i32) {
|
||||
if let Err(err) = self.insert_overproduction_notification(user_id, remaining_quantity) {
|
||||
eprintln!(
|
||||
"[ProduceWorker] Fehler beim Schreiben der Overproduction-Notification: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
let update_status =
|
||||
format!(r#"{{"event":"falukantUpdateStatus","user_id":{user_id}}}"#);
|
||||
self.base.broker.publish(update_status);
|
||||
}
|
||||
|
||||
fn load_finished_productions(&self) -> Result<Rows, crate::db::DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_finished_productions", QUERY_GET_FINISHED_PRODUCTIONS)?;
|
||||
conn.execute("get_finished_productions", &[])
|
||||
}
|
||||
|
||||
fn load_available_stocks(&self, branch_id: i32) -> Result<Rows, crate::db::DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_stocks", QUERY_GET_AVAILABLE_STOCKS)?;
|
||||
conn.execute("get_stocks", &[&branch_id])
|
||||
}
|
||||
|
||||
fn insert_inventory(
|
||||
&self,
|
||||
stock_id: i32,
|
||||
product_id: i32,
|
||||
quantity: i32,
|
||||
quality: i32,
|
||||
) -> Result<(), crate::db::DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("insert_inventory", QUERY_INSERT_INVENTORY)?;
|
||||
conn.execute("insert_inventory", &[&stock_id, &product_id, &quantity, &quality])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_production(&self, production_id: i32) -> Result<(), crate::db::DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("delete_production", QUERY_DELETE_PRODUCTION)?;
|
||||
conn.execute("delete_production", &[&production_id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_or_update_production_log(
|
||||
&self,
|
||||
region_id: i32,
|
||||
user_id: i32,
|
||||
product_id: i32,
|
||||
quantity: i32,
|
||||
) -> Result<(), crate::db::DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(
|
||||
"insert_update_production_log",
|
||||
QUERY_INSERT_UPDATE_PRODUCTION_LOG,
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"insert_update_production_log",
|
||||
&[®ion_id, &product_id, &quantity, &user_id],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_overproduction_notification(
|
||||
&self,
|
||||
user_id: i32,
|
||||
remaining_quantity: i32,
|
||||
) -> Result<(), crate::db::DbError> {
|
||||
let mut conn = self
|
||||
.base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| crate::db::DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(
|
||||
"add_overproduction_notification",
|
||||
QUERY_ADD_OVERPRODUCTION_NOTIFICATION,
|
||||
)?;
|
||||
|
||||
let notification = format!(
|
||||
r#"{{"tr":"production.overproduction","value":{}}}"#,
|
||||
remaining_quantity
|
||||
);
|
||||
|
||||
conn.execute(
|
||||
"add_overproduction_notification",
|
||||
&[&user_id, ¬ification],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_row_to_finished_production(row: Row) -> Option<FinishedProduction> {
|
||||
Some(FinishedProduction {
|
||||
production_id: row.get("production_id")?.parse().ok()?,
|
||||
branch_id: row.get("branch_id")?.parse().ok()?,
|
||||
product_id: row.get("product_id")?.parse().ok()?,
|
||||
quantity: row.get("quantity")?.parse().ok()?,
|
||||
quality: row.get("quality")?.parse().ok()?,
|
||||
user_id: row.get("user_id")?.parse().ok()?,
|
||||
region_id: row.get("region_id")?.parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_row_to_stock_info(row: Row) -> Option<StockInfo> {
|
||||
Some(StockInfo {
|
||||
stock_id: row.get("id")?.parse().ok()?,
|
||||
total_capacity: row.get("total_capacity")?.parse().ok()?,
|
||||
filled: row.get("filled")?.parse().ok()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Worker for ProduceWorker {
|
||||
fn start_worker_thread(&mut self) {
|
||||
let pool = self.base.pool.clone();
|
||||
let broker = self.base.broker.clone();
|
||||
|
||||
self.base
|
||||
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
||||
let mut worker = ProduceWorker::new(pool.clone(), broker.clone());
|
||||
while state.running_worker.load(Ordering::Relaxed) {
|
||||
worker.run_iteration(&state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
49
src/worker/simple.rs
Normal file
49
src/worker/simple.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::db::ConnectionPool;
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
|
||||
macro_rules! define_simple_worker {
|
||||
($name:ident) => {
|
||||
pub struct $name {
|
||||
base: BaseWorker,
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
base: BaseWorker::new(stringify!($name), pool, broker),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Worker for $name {
|
||||
fn start_worker_thread(&mut self) {
|
||||
self.base
|
||||
.start_worker_with_loop(|state: Arc<WorkerState>| {
|
||||
// Einfache Dummy-Schleife, bis echte Logik portiert ist
|
||||
while state.running_worker.load(Ordering::Relaxed) {
|
||||
if let Ok(mut step) = state.current_step.lock() {
|
||||
*step = format!("{}: idle", stringify!($name));
|
||||
}
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
203
src/worker/stockage_manager.rs
Normal file
203
src/worker/stockage_manager.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use crate::db::{ConnectionPool, DbError};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
|
||||
pub struct StockageManager {
|
||||
base: BaseWorker,
|
||||
}
|
||||
|
||||
// SQL-Queries analog zu `stockagemanager.h`
|
||||
const QUERY_GET_TOWNS: &str = r#"
|
||||
SELECT fdr.id
|
||||
FROM falukant_data.region fdr
|
||||
JOIN falukant_type.region ftr
|
||||
ON ftr.id = fdr.region_type_id
|
||||
WHERE ftr.label_tr = 'city';
|
||||
"#;
|
||||
|
||||
const QUERY_INSERT_STOCK: &str = r#"
|
||||
INSERT INTO falukant_data.buyable_stock (region_id, stock_type_id, quantity)
|
||||
SELECT
|
||||
$1 AS region_id,
|
||||
s.id AS stock_type_id,
|
||||
GREATEST(1, ROUND(RANDOM() * 5 * COUNT(br.id))) AS quantity
|
||||
FROM falukant_data.branch AS br
|
||||
CROSS JOIN falukant_type.stock AS s
|
||||
WHERE br.region_id = $1
|
||||
GROUP BY s.id
|
||||
ORDER BY RANDOM()
|
||||
LIMIT GREATEST(
|
||||
ROUND(RANDOM() * (SELECT COUNT(id) FROM falukant_type.stock)),
|
||||
1
|
||||
);
|
||||
"#;
|
||||
|
||||
const QUERY_CLEANUP_STOCK: &str = r#"
|
||||
DELETE FROM falukant_data.buyable_stock
|
||||
WHERE quantity <= 0;
|
||||
"#;
|
||||
|
||||
const QUERY_GET_REGION_USERS: &str = r#"
|
||||
SELECT c.user_id
|
||||
FROM falukant_data.character c
|
||||
WHERE c.region_id = $1
|
||||
AND c.user_id IS NOT NULL;
|
||||
"#;
|
||||
|
||||
impl StockageManager {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
base: BaseWorker::new("StockageManager", pool, broker),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
|
||||
let mut last_add_run: Option<Instant> = None;
|
||||
let mut last_cleanup_run: Option<Instant> = None;
|
||||
|
||||
while state.running_worker.load(Ordering::Relaxed) {
|
||||
let now = Instant::now();
|
||||
|
||||
// Entspricht addLocalStocks: alle 60 Sekunden prüfen & ggf. Stocks hinzufügen
|
||||
let should_add = match last_add_run {
|
||||
None => true,
|
||||
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(60),
|
||||
};
|
||||
|
||||
if should_add {
|
||||
if let Err(err) = Self::add_local_stocks(&pool, &broker) {
|
||||
eprintln!("[StockageManager] Fehler in addLocalStocks: {err}");
|
||||
}
|
||||
last_add_run = Some(now);
|
||||
}
|
||||
|
||||
// Cleanup regelmäßig ausführen (z.B. ebenfalls im 60s-Rhythmus)
|
||||
let should_cleanup = match last_cleanup_run {
|
||||
None => true,
|
||||
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(60),
|
||||
};
|
||||
|
||||
if should_cleanup {
|
||||
if let Err(err) = Self::cleanup_buyable_stock(&pool) {
|
||||
eprintln!("[StockageManager] Fehler bei stock cleanup: {err}");
|
||||
}
|
||||
last_cleanup_run = Some(now);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
|
||||
fn add_local_stocks(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> {
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let dist = Uniform::from(0.0..1.0);
|
||||
|
||||
let town_ids = Self::get_town_ids(pool)?;
|
||||
|
||||
for town_id in town_ids {
|
||||
// Wahrscheinlichkeit analog: round(dist * 2160) <= 1
|
||||
let roll: f64 = dist.sample(&mut rng) * 2160.0_f64;
|
||||
let chance = roll.round();
|
||||
if chance <= 1.0 {
|
||||
Self::add_stock_for_town(pool, broker, town_id)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_town_ids(pool: &ConnectionPool) -> Result<Vec<i32>, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("[StockageManager] DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_towns", QUERY_GET_TOWNS)?;
|
||||
let towns = conn.execute("get_towns", &[])?;
|
||||
|
||||
let mut ids = Vec::with_capacity(towns.len());
|
||||
for row in towns {
|
||||
if let Some(id) = row.get("id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
fn add_stock_for_town(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
town_id: i32,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("[StockageManager] DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("add_stock", QUERY_INSERT_STOCK)?;
|
||||
conn.execute("add_stock", &[&town_id])?;
|
||||
|
||||
// Benachrichtige alle User in der Region
|
||||
let users = Self::get_region_users(&mut conn, town_id)?;
|
||||
for user_id in users {
|
||||
let message = format!(
|
||||
r#"{{"event":"stock_change","user_id":{},"branch":{}}}"#,
|
||||
user_id, town_id
|
||||
);
|
||||
broker.publish(message);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_region_users(conn: &mut crate::db::DbConnection, region_id: i32) -> Result<Vec<i32>, DbError> {
|
||||
conn.prepare("get_region_users", QUERY_GET_REGION_USERS)?;
|
||||
let rows = conn.execute("get_region_users", &[®ion_id])?;
|
||||
|
||||
let mut result = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
if let Some(uid) = row.get("user_id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
result.push(uid);
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn cleanup_buyable_stock(pool: &ConnectionPool) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("[StockageManager] DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("cleanup_stock", QUERY_CLEANUP_STOCK)?;
|
||||
conn.execute("cleanup_stock", &[])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Worker for StockageManager {
|
||||
fn start_worker_thread(&mut self) {
|
||||
let pool = self.base.pool.clone();
|
||||
let broker = self.base.broker.clone();
|
||||
|
||||
self.base
|
||||
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
||||
StockageManager::run_loop(pool.clone(), broker.clone(), state);
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
968
src/worker/underground.rs
Normal file
968
src/worker/underground.rs
Normal file
@@ -0,0 +1,968 @@
|
||||
use crate::db::{ConnectionPool, DbError, Row, Rows};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::Rng;
|
||||
use serde_json::json;
|
||||
use serde_json::Value as Json;
|
||||
use std::cmp::{max, min};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
|
||||
pub struct UndergroundWorker {
|
||||
base: BaseWorker,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct HouseConditions {
|
||||
id: i32,
|
||||
roof: i32,
|
||||
floor: i32,
|
||||
wall: i32,
|
||||
windowc: i32,
|
||||
}
|
||||
|
||||
// Query-Konstanten (1:1 aus der C++-Version übernommen)
|
||||
const Q_SELECT_BY_PERFORMER: &str = r#"
|
||||
SELECT u.id,
|
||||
t.tr AS underground_type,
|
||||
u.performer_id,
|
||||
u.victim_id,
|
||||
to_char(u.created_at,'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') AS created_at,
|
||||
COALESCE(u.parameters::text,'{}') AS parameters,
|
||||
COALESCE(u.result::text,'null') AS result_text
|
||||
FROM falukant_data.underground u
|
||||
JOIN falukant_type.underground t ON t.tr = u.underground_type_id
|
||||
WHERE u.performer_id = $1
|
||||
ORDER BY u.created_at DESC;
|
||||
"#;
|
||||
|
||||
const Q_SELECT_PENDING: &str = r#"
|
||||
SELECT u.id,
|
||||
t.tr AS underground_type,
|
||||
u.performer_id,
|
||||
u.victim_id,
|
||||
COALESCE(u.parameters::text,'{}') AS parameters
|
||||
FROM falukant_data.underground u
|
||||
JOIN falukant_type.underground t ON t.tr = u.underground_type_id
|
||||
WHERE u.result IS NULL
|
||||
AND u.created_at <= NOW() - INTERVAL '1 day'
|
||||
ORDER BY u.created_at ASC
|
||||
LIMIT 200;
|
||||
"#;
|
||||
|
||||
const Q_UPDATE_RESULT: &str = r#"
|
||||
UPDATE falukant_data.underground
|
||||
SET result = $2::jsonb,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
const Q_SELECT_CHAR_USER: &str = r#"
|
||||
SELECT user_id
|
||||
FROM falukant_data.character
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
const Q_SELECT_HOUSE_BY_USER: &str = r#"
|
||||
SELECT id, roof_condition, floor_condition, wall_condition, window_condition
|
||||
FROM falukant_data.user_house
|
||||
WHERE user_id = $1
|
||||
LIMIT 1;
|
||||
"#;
|
||||
|
||||
const Q_UPDATE_HOUSE: &str = r#"
|
||||
UPDATE falukant_data.user_house
|
||||
SET roof_condition = $2,
|
||||
floor_condition = $3,
|
||||
wall_condition = $4,
|
||||
window_condition = $5
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
const Q_SELECT_STOCK_BY_BRANCH: &str = r#"
|
||||
SELECT id, stock_type_id, quantity
|
||||
FROM falukant_data.stock
|
||||
WHERE branch_id = $1
|
||||
ORDER BY quantity DESC;
|
||||
"#;
|
||||
|
||||
const Q_UPDATE_STOCK_QTY: &str = r#"
|
||||
UPDATE falukant_data.stock
|
||||
SET quantity = $2
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
const Q_SELECT_CHAR_HEALTH: &str = r#"
|
||||
SELECT health
|
||||
FROM falukant_data.character
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
const Q_UPDATE_CHAR_HEALTH: &str = r#"
|
||||
UPDATE falukant_data.character
|
||||
SET health = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
const Q_SELECT_FALUKANT_USER: &str = r#"
|
||||
SELECT id,
|
||||
money,
|
||||
COALESCE(main_branch_region_id, 0) AS main_branch_region_id
|
||||
FROM falukant_data.falukant_user
|
||||
WHERE user_id = $1
|
||||
LIMIT 1;
|
||||
"#;
|
||||
|
||||
// Query für Geldänderungen (lokale Variante von BaseWorker::change_falukant_user_money)
|
||||
const QUERY_UPDATE_MONEY: &str = r#"
|
||||
SELECT falukant_data.update_money($1, $2, $3);
|
||||
"#;
|
||||
|
||||
impl UndergroundWorker {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
base: BaseWorker::new("UndergroundWorker", pool, broker),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
|
||||
while state.running_worker.load(Ordering::Relaxed) {
|
||||
if let Err(err) = Self::tick(&pool, &broker) {
|
||||
eprintln!("[UndergroundWorker] Fehler in tick: {err}");
|
||||
}
|
||||
|
||||
// Entspricht ~60-Sekunden-Loop mit 1-Sekunden-Schritten
|
||||
for _ in 0..60 {
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tick(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> {
|
||||
let rows = Self::fetch_pending(pool)?;
|
||||
|
||||
for row in rows {
|
||||
let id = match row.get("id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
Some(id) => id,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
match Self::execute_row(pool, &row) {
|
||||
Ok(res) => {
|
||||
Self::update_result(pool, id, &res)?;
|
||||
let event = json!({
|
||||
"event": "underground_processed",
|
||||
"id": id,
|
||||
"type": row.get("underground_type").cloned().unwrap_or_default()
|
||||
});
|
||||
broker.publish(event.to_string());
|
||||
}
|
||||
Err(err) => {
|
||||
let error_res = json!({
|
||||
"status": "error",
|
||||
"message": err.to_string()
|
||||
});
|
||||
let _ = Self::update_result(pool, id, &error_res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_pending(pool: &ConnectionPool) -> Result<Rows, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ug_select_pending", Q_SELECT_PENDING)?;
|
||||
conn.execute("ug_select_pending", &[])
|
||||
}
|
||||
|
||||
fn execute_row(pool: &ConnectionPool, r: &Row) -> Result<Json, DbError> {
|
||||
let performer_id = parse_i32(r, "performer_id", -1);
|
||||
let victim_id = parse_i32(r, "victim_id", -1);
|
||||
let task_type = r.get("underground_type").cloned().unwrap_or_default();
|
||||
let params = r.get("parameters").cloned().unwrap_or_else(|| "{}".into());
|
||||
|
||||
Ok(Self::handle_task(pool, &task_type, performer_id, victim_id, ¶ms)?)
|
||||
}
|
||||
|
||||
fn handle_task(
|
||||
pool: &ConnectionPool,
|
||||
task_type: &str,
|
||||
performer_id: i32,
|
||||
victim_id: i32,
|
||||
params_json: &str,
|
||||
) -> Result<Json, DbError> {
|
||||
let p: Json = serde_json::from_str(params_json).unwrap_or_else(|_| json!({}));
|
||||
|
||||
match task_type {
|
||||
"spyin" => Self::spy_in(pool, performer_id, victim_id, &p),
|
||||
"assassin" => Self::assassin(pool, performer_id, victim_id, &p),
|
||||
"sabotage" => Self::sabotage(pool, performer_id, victim_id, &p),
|
||||
"corrupt_politician" => Ok(Self::corrupt_politician(performer_id, victim_id, &p)),
|
||||
"rob" => Self::rob(pool, performer_id, victim_id, &p),
|
||||
_ => Ok(json!({
|
||||
"status": "unknown_type",
|
||||
"type": task_type
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn spy_in(
|
||||
pool: &ConnectionPool,
|
||||
performer_id: i32,
|
||||
victim_id: i32,
|
||||
p: &Json,
|
||||
) -> Result<Json, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ug_select_by_performer", Q_SELECT_BY_PERFORMER)?;
|
||||
let rows = conn.execute("ug_select_by_performer", &[&victim_id])?;
|
||||
|
||||
let mut activities = Vec::new();
|
||||
for r in rows {
|
||||
let params: Json = r
|
||||
.get("parameters")
|
||||
.and_then(|s| serde_json::from_str(s).ok())
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let result_text = r.get("result_text").cloned().unwrap_or_else(|| "null".into());
|
||||
let result: Json = serde_json::from_str(&result_text).unwrap_or(Json::Null);
|
||||
|
||||
let mut status = "pending".to_string();
|
||||
if let Json::Object(obj) = &result {
|
||||
if let Some(Json::String(s)) = obj.get("status") {
|
||||
status = s.clone();
|
||||
} else {
|
||||
status = "done".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let activity = json!({
|
||||
"id": parse_i32(&r, "id", -1),
|
||||
"type": r.get("underground_type").cloned().unwrap_or_default(),
|
||||
"performed_by": parse_i32(&r, "performer_id", -1),
|
||||
"victim_id": parse_i32(&r, "victim_id", -1),
|
||||
"created_at": r.get("created_at").cloned().unwrap_or_default(),
|
||||
"parameters": params,
|
||||
"result": result,
|
||||
"status": status
|
||||
});
|
||||
|
||||
activities.push(activity);
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"status": "success",
|
||||
"action": "spyin",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"victim_illegal_activity_count": activities.len(),
|
||||
"victim_illegal_activities": activities
|
||||
}))
|
||||
}
|
||||
|
||||
fn assassin(
|
||||
pool: &ConnectionPool,
|
||||
performer_id: i32,
|
||||
victim_id: i32,
|
||||
p: &Json,
|
||||
) -> Result<Json, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("ug_select_char_health", Q_SELECT_CHAR_HEALTH)?;
|
||||
conn.prepare("ug_update_char_health", Q_UPDATE_CHAR_HEALTH)?;
|
||||
|
||||
let rows = conn.execute("ug_select_char_health", &[&victim_id])?;
|
||||
if rows.is_empty() {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"action": "assassin",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"message": "victim_not_found",
|
||||
"details": p
|
||||
}));
|
||||
}
|
||||
|
||||
let current = parse_i32(&rows[0], "health", 0);
|
||||
let mut rng = rand::thread_rng();
|
||||
let dist = Uniform::from(0..=current.max(0));
|
||||
let new_health = dist.sample(&mut rng);
|
||||
|
||||
conn.execute("ug_update_char_health", &[&victim_id, &new_health])?;
|
||||
|
||||
Ok(json!({
|
||||
"status": "success",
|
||||
"action": "assassin",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"previous_health": current,
|
||||
"new_health": new_health,
|
||||
"reduced_by": current - new_health
|
||||
}))
|
||||
}
|
||||
|
||||
fn sabotage(
|
||||
pool: &ConnectionPool,
|
||||
performer_id: i32,
|
||||
victim_id: i32,
|
||||
p: &Json,
|
||||
) -> Result<Json, DbError> {
|
||||
let target = p
|
||||
.get("target")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
match target.as_str() {
|
||||
"house" => Self::sabotage_house(pool, performer_id, victim_id, p),
|
||||
"storage" => Self::sabotage_storage(pool, performer_id, victim_id, p),
|
||||
_ => Ok(json!({
|
||||
"status": "error",
|
||||
"action": "sabotage",
|
||||
"message": "unknown_target",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_user_id_for_character(pool: &ConnectionPool, character_id: i32) -> Result<i32, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ug_select_char_user", Q_SELECT_CHAR_USER)?;
|
||||
let rows = conn.execute("ug_select_char_user", &[&character_id])?;
|
||||
|
||||
Ok(rows
|
||||
.get(0)
|
||||
.and_then(|r| r.get("user_id"))
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(-1))
|
||||
}
|
||||
|
||||
fn get_house_by_user(pool: &ConnectionPool, user_id: i32) -> Result<Option<HouseConditions>, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ug_select_house_by_user", Q_SELECT_HOUSE_BY_USER)?;
|
||||
let rows = conn.execute("ug_select_house_by_user", &[&user_id])?;
|
||||
|
||||
if rows.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let r = &rows[0];
|
||||
Ok(Some(HouseConditions {
|
||||
id: parse_i32(r, "id", -1),
|
||||
roof: parse_i32(r, "roof_condition", 0),
|
||||
floor: parse_i32(r, "floor_condition", 0),
|
||||
wall: parse_i32(r, "wall_condition", 0),
|
||||
windowc: parse_i32(r, "window_condition", 0),
|
||||
}))
|
||||
}
|
||||
|
||||
fn update_house(pool: &ConnectionPool, h: &HouseConditions) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ug_update_house", Q_UPDATE_HOUSE)?;
|
||||
|
||||
let roof = h.roof.clamp(0, 100);
|
||||
let floor = h.floor.clamp(0, 100);
|
||||
let wall = h.wall.clamp(0, 100);
|
||||
let windowc = h.windowc.clamp(0, 100);
|
||||
|
||||
conn.execute("ug_update_house", &[&h.id, &roof, &floor, &wall, &windowc])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sabotage_house(
|
||||
pool: &ConnectionPool,
|
||||
performer_id: i32,
|
||||
victim_id: i32,
|
||||
p: &Json,
|
||||
) -> Result<Json, DbError> {
|
||||
let user_id = Self::get_user_id_for_character(pool, victim_id)?;
|
||||
if user_id < 0 {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"action": "sabotage",
|
||||
"target": "house",
|
||||
"message": "victim_not_found",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p
|
||||
}));
|
||||
}
|
||||
|
||||
let mut house = match Self::get_house_by_user(pool, user_id)? {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"action": "sabotage",
|
||||
"target": "house",
|
||||
"message": "house_not_found",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
// Erlaubte Felder aus Params
|
||||
let mut allow: Vec<String> = Vec::new();
|
||||
if let Some(conds) = p.get("conditions").and_then(|v| v.as_array()) {
|
||||
for s in conds {
|
||||
if let Some(name) = s.as_str() {
|
||||
allow.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Statt Referenzen auf Felder zu speichern, arbeiten wir über Indizes,
|
||||
// um Borrowing-Probleme zu vermeiden.
|
||||
let all_fields = ["roof_condition", "floor_condition", "wall_condition", "window_condition"];
|
||||
let candidate_indices: Vec<usize> = (0..all_fields.len())
|
||||
.filter(|&idx| {
|
||||
allow.is_empty()
|
||||
|| allow
|
||||
.iter()
|
||||
.any(|name| name == all_fields[idx])
|
||||
})
|
||||
.collect();
|
||||
|
||||
if candidate_indices.is_empty() {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"action": "sabotage",
|
||||
"target": "house",
|
||||
"message": "no_conditions_selected",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p
|
||||
}));
|
||||
}
|
||||
|
||||
let k = random_int(1, candidate_indices.len() as i32) as usize;
|
||||
let picks = random_indices(candidate_indices.len(), k);
|
||||
|
||||
let mut changed = Vec::new();
|
||||
for i in picks {
|
||||
let idx = candidate_indices[i];
|
||||
let (name, value_ref) = match idx {
|
||||
0 => ("roof_condition", &mut house.roof),
|
||||
1 => ("floor_condition", &mut house.floor),
|
||||
2 => ("wall_condition", &mut house.wall),
|
||||
3 => ("window_condition", &mut house.windowc),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if *value_ref > 0 {
|
||||
let red = random_int(1, *value_ref);
|
||||
*value_ref = (*value_ref - red).clamp(0, 100);
|
||||
}
|
||||
changed.push(name.to_string());
|
||||
}
|
||||
|
||||
Self::update_house(pool, &house)?;
|
||||
|
||||
Ok(json!({
|
||||
"status": "success",
|
||||
"action": "sabotage",
|
||||
"target": "house",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"changed_conditions": changed,
|
||||
"new_conditions": {
|
||||
"roof_condition": house.roof,
|
||||
"floor_condition": house.floor,
|
||||
"wall_condition": house.wall,
|
||||
"window_condition": house.windowc
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn select_stock_by_branch(pool: &ConnectionPool, branch_id: i32) -> Result<Rows, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ug_select_stock_by_branch", Q_SELECT_STOCK_BY_BRANCH)?;
|
||||
conn.execute("ug_select_stock_by_branch", &[&branch_id])
|
||||
}
|
||||
|
||||
fn filter_by_stock_types(rows: &Rows, allowed: &[i32]) -> Rows {
|
||||
if allowed.is_empty() {
|
||||
return rows.clone();
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
for r in rows {
|
||||
if let Some(t) = r.get("stock_type_id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
if allowed.contains(&t) {
|
||||
out.push(r.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn update_stock_qty(pool: &ConnectionPool, id: i32, qty: i64) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ug_update_stock_qty", Q_UPDATE_STOCK_QTY)?;
|
||||
// beide Parameter explizit als ToSql-Traitobjekte typisieren, um Mischtypen zu erlauben
|
||||
use postgres::types::ToSql;
|
||||
let p1: &(dyn ToSql + Sync) = &id;
|
||||
let p2: &(dyn ToSql + Sync) = &qty;
|
||||
conn.execute("ug_update_stock_qty", &[p1, p2])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sabotage_storage(
|
||||
pool: &ConnectionPool,
|
||||
performer_id: i32,
|
||||
victim_id: i32,
|
||||
p: &Json,
|
||||
) -> Result<Json, DbError> {
|
||||
let branch_id = match p.get("branch_id").and_then(|v| v.as_i64()) {
|
||||
Some(id) => id as i32,
|
||||
None => {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"action": "sabotage",
|
||||
"target": "storage",
|
||||
"message": "branch_id_required",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
let mut allowed = Vec::new();
|
||||
if let Some(arr) = p.get("stock_type_ids").and_then(|v| v.as_array()) {
|
||||
for v in arr {
|
||||
if let Some(id) = v.as_i64() {
|
||||
allowed.push(id as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rows_all = Self::select_stock_by_branch(pool, branch_id)?;
|
||||
let mut rows = Self::filter_by_stock_types(&rows_all, &allowed);
|
||||
if rows.is_empty() {
|
||||
return Ok(json!({
|
||||
"status": "success",
|
||||
"action": "sabotage",
|
||||
"target": "storage",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"removed_total": 0,
|
||||
"affected_rows": []
|
||||
}));
|
||||
}
|
||||
|
||||
let mut total: i64 = 0;
|
||||
for r in &rows {
|
||||
if let Some(q) = r.get("quantity").and_then(|v| v.parse::<i64>().ok()) {
|
||||
total += q;
|
||||
}
|
||||
}
|
||||
if total <= 0 {
|
||||
return Ok(json!({
|
||||
"status": "success",
|
||||
"action": "sabotage",
|
||||
"target": "storage",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"removed_total": 0,
|
||||
"affected_rows": []
|
||||
}));
|
||||
}
|
||||
|
||||
let cap = total / 4;
|
||||
if cap <= 0 {
|
||||
return Ok(json!({
|
||||
"status": "success",
|
||||
"action": "sabotage",
|
||||
"target": "storage",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"removed_total": 0,
|
||||
"affected_rows": []
|
||||
}));
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut to_remove = random_ll(1, cap);
|
||||
rows.shuffle(&mut rng);
|
||||
|
||||
let mut affected = Vec::new();
|
||||
for r in rows {
|
||||
if to_remove == 0 {
|
||||
break;
|
||||
}
|
||||
let id = parse_i32(&r, "id", -1);
|
||||
let q = r
|
||||
.get("quantity")
|
||||
.and_then(|v| v.parse::<i64>().ok())
|
||||
.unwrap_or(0);
|
||||
if q <= 0 {
|
||||
continue;
|
||||
}
|
||||
let take = random_ll(1, min(q, to_remove));
|
||||
let newq = q - take;
|
||||
Self::update_stock_qty(pool, id, newq)?;
|
||||
to_remove -= take;
|
||||
let entry = json!({
|
||||
"id": id,
|
||||
"stock_type_id": parse_i32(&r, "stock_type_id", -1),
|
||||
"previous_quantity": q,
|
||||
"new_quantity": newq,
|
||||
"removed": take
|
||||
});
|
||||
affected.push(entry);
|
||||
}
|
||||
|
||||
let removed_total: i64 = affected
|
||||
.iter()
|
||||
.filter_map(|a| a.get("removed").and_then(|v| v.as_i64()))
|
||||
.sum();
|
||||
|
||||
Ok(json!({
|
||||
"status": "success",
|
||||
"action": "sabotage",
|
||||
"target": "storage",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"removed_total": removed_total,
|
||||
"affected_rows": affected
|
||||
}))
|
||||
}
|
||||
|
||||
fn corrupt_politician(
|
||||
performer_id: i32,
|
||||
victim_id: i32,
|
||||
p: &Json,
|
||||
) -> Json {
|
||||
json!({
|
||||
"status": "success",
|
||||
"action": "corrupt_politician",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p
|
||||
})
|
||||
}
|
||||
|
||||
fn rob(
|
||||
pool: &ConnectionPool,
|
||||
performer_id: i32,
|
||||
victim_id: i32,
|
||||
p: &Json,
|
||||
) -> Result<Json, DbError> {
|
||||
let user_id = Self::get_user_id_for_character(pool, victim_id)?;
|
||||
if user_id < 0 {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"action": "rob",
|
||||
"message": "victim_not_found",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p
|
||||
}));
|
||||
}
|
||||
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ug_select_falukant_user", Q_SELECT_FALUKANT_USER)?;
|
||||
let fu = conn.execute("ug_select_falukant_user", &[&user_id])?;
|
||||
if fu.is_empty() {
|
||||
return Ok(json!({
|
||||
"status": "error",
|
||||
"action": "rob",
|
||||
"message": "falukant_user_not_found",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p
|
||||
}));
|
||||
}
|
||||
|
||||
let falukant_user_id = parse_i32(&fu[0], "id", -1);
|
||||
let money = fu[0]
|
||||
.get("money")
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
let default_branch = parse_i32(&fu[0], "main_branch_region_id", 0);
|
||||
|
||||
let steal_goods = random_int(0, 1) == 1;
|
||||
|
||||
if steal_goods {
|
||||
let branch_id = p
|
||||
.get("branch_id")
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|v| v as i32)
|
||||
.unwrap_or(default_branch);
|
||||
|
||||
if branch_id <= 0 {
|
||||
return Ok(json!({
|
||||
"status": "success",
|
||||
"action": "rob",
|
||||
"mode": "goods",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"removed_total": 0,
|
||||
"affected_rows": []
|
||||
}));
|
||||
}
|
||||
|
||||
let rows_all = Self::select_stock_by_branch(pool, branch_id)?;
|
||||
let mut rows = rows_all;
|
||||
if rows.is_empty() {
|
||||
return Ok(json!({
|
||||
"status": "success",
|
||||
"action": "rob",
|
||||
"mode": "goods",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"removed_total": 0,
|
||||
"affected_rows": []
|
||||
}));
|
||||
}
|
||||
|
||||
let mut total: i64 = 0;
|
||||
for r in &rows {
|
||||
if let Some(q) = r.get("quantity").and_then(|v| v.parse::<i64>().ok()) {
|
||||
total += q;
|
||||
}
|
||||
}
|
||||
if total <= 0 {
|
||||
return Ok(json!({
|
||||
"status": "success",
|
||||
"action": "rob",
|
||||
"mode": "goods",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"removed_total": 0,
|
||||
"affected_rows": []
|
||||
}));
|
||||
}
|
||||
|
||||
let cap = max(1_i64, total / 2);
|
||||
let mut to_remove = random_ll(1, cap);
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
rows.shuffle(&mut rng);
|
||||
|
||||
let mut affected = Vec::new();
|
||||
for r in rows {
|
||||
if to_remove == 0 {
|
||||
break;
|
||||
}
|
||||
let id = parse_i32(&r, "id", -1);
|
||||
let q = r
|
||||
.get("quantity")
|
||||
.and_then(|v| v.parse::<i64>().ok())
|
||||
.unwrap_or(0);
|
||||
if q <= 0 {
|
||||
continue;
|
||||
}
|
||||
let take = random_ll(1, min(q, to_remove));
|
||||
let newq = q - take;
|
||||
Self::update_stock_qty(pool, id, newq)?;
|
||||
to_remove -= take;
|
||||
affected.push(json!({
|
||||
"id": id,
|
||||
"stock_type_id": parse_i32(&r, "stock_type_id", -1),
|
||||
"previous_quantity": q,
|
||||
"new_quantity": newq,
|
||||
"removed": take
|
||||
}));
|
||||
}
|
||||
|
||||
let removed: i64 = affected
|
||||
.iter()
|
||||
.filter_map(|a| a.get("removed").and_then(|v| v.as_i64()))
|
||||
.sum();
|
||||
|
||||
Ok(json!({
|
||||
"status": "success",
|
||||
"action": "rob",
|
||||
"mode": "goods",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"removed_total": removed,
|
||||
"affected_rows": affected
|
||||
}))
|
||||
} else {
|
||||
if money <= 0.0 {
|
||||
return Ok(json!({
|
||||
"status": "success",
|
||||
"action": "rob",
|
||||
"mode": "money",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"stolen": 0.0,
|
||||
"balance_before": 0.0,
|
||||
"balance_after": 0.0
|
||||
}));
|
||||
}
|
||||
|
||||
let rate = random_double(0.0, 0.18);
|
||||
let mut amount = (money * rate * 100.0).round() / 100.0;
|
||||
if amount < 0.01 {
|
||||
amount = 0.01;
|
||||
}
|
||||
if amount > money {
|
||||
amount = money;
|
||||
}
|
||||
|
||||
let _msg = json!({
|
||||
"event": "money_changed",
|
||||
"reason": "robbery",
|
||||
"delta": -amount,
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id
|
||||
});
|
||||
|
||||
if let Err(err) =
|
||||
change_falukant_user_money(pool, falukant_user_id, -amount, "robbery")
|
||||
{
|
||||
eprintln!(
|
||||
"[UndergroundWorker] Fehler bei change_falukant_user_money: {err}"
|
||||
);
|
||||
}
|
||||
// Event manuell publizieren
|
||||
// (BaseWorker kümmert sich aktuell nur um die DB-Änderung)
|
||||
// Hinweis: Wir haben keinen direkten Zugriff auf broker hier, daher wird das
|
||||
// Event nur im Rückgabe-JSON signalisiert.
|
||||
|
||||
let after = ((money - amount) * 100.0).round() / 100.0;
|
||||
|
||||
Ok(json!({
|
||||
"status": "success",
|
||||
"action": "rob",
|
||||
"mode": "money",
|
||||
"performer_id": performer_id,
|
||||
"victim_id": victim_id,
|
||||
"details": p,
|
||||
"stolen": amount,
|
||||
"rate": rate,
|
||||
"balance_before": money,
|
||||
"balance_after": after
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn update_result(pool: &ConnectionPool, id: i32, result: &Json) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ug_update_result", Q_UPDATE_RESULT)?;
|
||||
let result_text = result.to_string();
|
||||
conn.execute("ug_update_result", &[&id, &result_text])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Worker for UndergroundWorker {
|
||||
fn start_worker_thread(&mut self) {
|
||||
let pool = self.base.pool.clone();
|
||||
let broker = self.base.broker.clone();
|
||||
|
||||
self.base
|
||||
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
||||
UndergroundWorker::run_loop(pool.clone(), broker.clone(), state);
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktionen für Zufall und Parsing
|
||||
|
||||
fn random_int(lo: i32, hi: i32) -> i32 {
|
||||
let mut rng = rand::thread_rng();
|
||||
rng.gen_range(lo..=hi)
|
||||
}
|
||||
|
||||
fn random_ll(lo: i64, hi: i64) -> i64 {
|
||||
let mut rng = rand::thread_rng();
|
||||
rng.gen_range(lo..=hi)
|
||||
}
|
||||
|
||||
fn random_indices(n: usize, k: usize) -> Vec<usize> {
|
||||
let mut idx: Vec<usize> = (0..n).collect();
|
||||
let mut rng = rand::thread_rng();
|
||||
idx.shuffle(&mut rng);
|
||||
if k < idx.len() {
|
||||
idx.truncate(k);
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
fn random_double(lo: f64, hi: f64) -> f64 {
|
||||
let mut rng = rand::thread_rng();
|
||||
rng.gen_range(lo..hi)
|
||||
}
|
||||
|
||||
fn parse_i32(row: &Row, key: &str, default: i32) -> i32 {
|
||||
row.get(key)
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn change_falukant_user_money(
|
||||
pool: &ConnectionPool,
|
||||
falukant_user_id: i32,
|
||||
money_change: f64,
|
||||
action: &str,
|
||||
) -> Result<(), DbError> {
|
||||
use postgres::types::ToSql;
|
||||
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ug_update_money", QUERY_UPDATE_MONEY)?;
|
||||
let p1: &(dyn ToSql + Sync) = &falukant_user_id;
|
||||
let p2: &(dyn ToSql + Sync) = &money_change;
|
||||
let p3: &(dyn ToSql + Sync) = &action;
|
||||
conn.execute("ug_update_money", &[p1, p2, p3])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
1179
src/worker/user_character.rs
Normal file
1179
src/worker/user_character.rs
Normal file
File diff suppressed because it is too large
Load Diff
509
src/worker/value_recalculation.rs
Normal file
509
src/worker/value_recalculation.rs
Normal file
@@ -0,0 +1,509 @@
|
||||
use crate::db::{ConnectionPool, DbError, Row};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
|
||||
pub struct ValueRecalculationWorker {
|
||||
base: BaseWorker,
|
||||
}
|
||||
|
||||
// Produktwissen / Produktions-Logs
|
||||
const QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER: &str = r#"
|
||||
UPDATE falukant_data.knowledge k
|
||||
SET knowledge = LEAST(100, k.knowledge + 1)
|
||||
FROM falukant_data.character c
|
||||
JOIN falukant_log.production p
|
||||
ON DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day'
|
||||
WHERE c.id = k.character_id
|
||||
AND c.user_id = 18
|
||||
AND k.product_id = 10;
|
||||
"#;
|
||||
|
||||
const QUERY_DELETE_OLD_PRODUCTIONS: &str = r#"
|
||||
DELETE FROM falukant_log.production flp
|
||||
WHERE DATE(flp.production_timestamp) < CURRENT_DATE;
|
||||
"#;
|
||||
|
||||
const QUERY_GET_PRODUCERS_LAST_DAY: &str = r#"
|
||||
SELECT p.producer_id
|
||||
FROM falukant_log.production p
|
||||
WHERE DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day'
|
||||
GROUP BY producer_id;
|
||||
"#;
|
||||
|
||||
// Regionale Verkaufspreise
|
||||
const QUERY_UPDATE_REGION_SELL_PRICE: &str = r#"
|
||||
UPDATE falukant_data.town_product_worth tpw
|
||||
SET worth_percent =
|
||||
GREATEST(
|
||||
0,
|
||||
LEAST(
|
||||
CASE
|
||||
WHEN s.quantity > avg_sells THEN tpw.worth_percent - 1
|
||||
WHEN s.quantity < avg_sells THEN tpw.worth_percent + 1
|
||||
ELSE tpw.worth_percent
|
||||
END,
|
||||
100
|
||||
)
|
||||
)
|
||||
FROM (
|
||||
SELECT region_id,
|
||||
product_id,
|
||||
quantity,
|
||||
(SELECT AVG(quantity)
|
||||
FROM falukant_log.sell avs
|
||||
WHERE avs.product_id = s.product_id) AS avg_sells
|
||||
FROM falukant_log.sell s
|
||||
WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day'
|
||||
) s
|
||||
WHERE tpw.region_id = s.region_id
|
||||
AND tpw.product_id = s.product_id;
|
||||
"#;
|
||||
|
||||
const QUERY_DELETE_REGION_SELL_PRICE: &str = r#"
|
||||
DELETE FROM falukant_log.sell s
|
||||
WHERE DATE(s.sell_timestamp) < CURRENT_DATE;
|
||||
"#;
|
||||
|
||||
const QUERY_GET_SELL_REGIONS: &str = r#"
|
||||
SELECT s.region_id
|
||||
FROM falukant_log.sell s
|
||||
WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day'
|
||||
GROUP BY region_id;
|
||||
"#;
|
||||
|
||||
// Ehen / Beziehungen
|
||||
const QUERY_SET_MARRIAGES_BY_PARTY: &str = r#"
|
||||
WITH updated_relations AS (
|
||||
UPDATE falukant_data.relationship AS rel
|
||||
SET relationship_type_id = (
|
||||
SELECT id
|
||||
FROM falukant_type.relationship AS rt
|
||||
WHERE rt.tr = 'married'
|
||||
)
|
||||
WHERE rel.id IN (
|
||||
SELECT rel2.id
|
||||
FROM falukant_data.party AS p
|
||||
JOIN falukant_type.party AS pt
|
||||
ON pt.id = p.party_type_id
|
||||
AND pt.tr = 'wedding'
|
||||
JOIN falukant_data.falukant_user AS fu
|
||||
ON fu.id = p.falukant_user_id
|
||||
JOIN falukant_data.character AS c
|
||||
ON c.user_id = fu.id
|
||||
JOIN falukant_data.relationship AS rel2
|
||||
ON rel2.character1_id = c.id
|
||||
OR rel2.character2_id = c.id
|
||||
JOIN falukant_type.relationship AS rt2
|
||||
ON rt2.id = rel2.relationship_type_id
|
||||
AND rt2.tr = 'engaged'
|
||||
WHERE p.created_at <= NOW() - INTERVAL '1 day'
|
||||
)
|
||||
RETURNING character1_id, character2_id
|
||||
)
|
||||
SELECT
|
||||
c1.user_id AS character1_user,
|
||||
c2.user_id AS character2_user
|
||||
FROM updated_relations AS ur
|
||||
JOIN falukant_data.character AS c1
|
||||
ON c1.id = ur.character1_id
|
||||
JOIN falukant_data.character AS c2
|
||||
ON c2.id = ur.character2_id;
|
||||
"#;
|
||||
|
||||
// Lernen / Studium
|
||||
const QUERY_GET_STUDYINGS_TO_EXECUTE: &str = r#"
|
||||
SELECT
|
||||
l.id,
|
||||
l.associated_falukant_user_id,
|
||||
l.associated_learning_character_id,
|
||||
l.learn_all_products,
|
||||
l.learning_recipient_id,
|
||||
l.product_id,
|
||||
lr.tr
|
||||
FROM falukant_data.learning l
|
||||
JOIN falukant_type.learn_recipient lr
|
||||
ON lr.id = l.learning_recipient_id
|
||||
WHERE l.learning_is_executed = FALSE
|
||||
AND l.created_at + INTERVAL '1 day' < NOW();
|
||||
"#;
|
||||
|
||||
const QUERY_GET_OWN_CHARACTER_ID: &str = r#"
|
||||
SELECT id
|
||||
FROM falukant_data.character c
|
||||
WHERE c.user_id = $1;
|
||||
"#;
|
||||
|
||||
const QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE: &str = r#"
|
||||
UPDATE falukant_data.knowledge k
|
||||
SET knowledge = LEAST(100, k.knowledge + $1)
|
||||
WHERE k.character_id = $2
|
||||
AND k.product_id = $3;
|
||||
"#;
|
||||
|
||||
const QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE: &str = r#"
|
||||
UPDATE falukant_data.knowledge k
|
||||
SET knowledge = LEAST(100, k.knowledge + $1)
|
||||
WHERE k.character_id = $2;
|
||||
"#;
|
||||
|
||||
const QUERY_SET_LEARNING_DONE: &str = r#"
|
||||
UPDATE falukant_data.learning
|
||||
SET learning_is_executed = TRUE
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
impl ValueRecalculationWorker {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
base: BaseWorker::new("ValueRecalculationWorker", pool, broker),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
|
||||
// Wir nutzen hier einfach Intervall-Logik (täglich / halbtäglich),
|
||||
// statt exakte Uhrzeiten nachzubilden – Verhalten ist funktional ähnlich.
|
||||
let mut last_product = None;
|
||||
let mut last_sell_price = None;
|
||||
|
||||
loop {
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
// Produktwissen einmal täglich
|
||||
if should_run_interval(last_product, now, Duration::from_secs(24 * 3600)) {
|
||||
if let Err(err) = Self::calculate_product_knowledge_inner(&pool, &broker) {
|
||||
eprintln!("[ValueRecalculationWorker] Fehler in calculateProductKnowledge: {err}");
|
||||
}
|
||||
last_product = Some(now);
|
||||
}
|
||||
|
||||
// Regionale Verkaufspreise einmal täglich (gegen Mittag)
|
||||
if should_run_interval(last_sell_price, now, Duration::from_secs(24 * 3600)) {
|
||||
if let Err(err) = Self::calculate_regional_sell_price_inner(&pool, &broker) {
|
||||
eprintln!("[ValueRecalculationWorker] Fehler in calculateRegionalSellPrice: {err}");
|
||||
}
|
||||
last_sell_price = Some(now);
|
||||
}
|
||||
|
||||
// Ehen & Studium bei jedem Durchlauf
|
||||
if let Err(err) = Self::calculate_marriages_inner(&pool, &broker) {
|
||||
eprintln!("[ValueRecalculationWorker] Fehler in calculateMarriages: {err}");
|
||||
}
|
||||
if let Err(err) = Self::calculate_studying_inner(&pool, &broker) {
|
||||
eprintln!("[ValueRecalculationWorker] Fehler in calculateStudying: {err}");
|
||||
}
|
||||
|
||||
// 60-Sekunden-Wartezeit in kurze Scheiben aufteilen, damit ein Shutdown
|
||||
// (running_worker = false) schnell greift.
|
||||
const SLICE_MS: u64 = 500;
|
||||
let total_ms = 60_000;
|
||||
let mut slept = 0;
|
||||
while slept < total_ms {
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let remaining = total_ms - slept;
|
||||
let slice = SLICE_MS.min(remaining);
|
||||
std::thread::sleep(Duration::from_millis(slice));
|
||||
slept += slice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_product_knowledge_inner(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(
|
||||
"update_product_knowledge_user",
|
||||
QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER,
|
||||
)?;
|
||||
conn.execute("update_product_knowledge_user", &[])?;
|
||||
|
||||
conn.prepare("get_producers_last_day", QUERY_GET_PRODUCERS_LAST_DAY)?;
|
||||
let users = conn.execute("get_producers_last_day", &[])?;
|
||||
|
||||
for row in users {
|
||||
if let Some(user_id) = row.get("producer_id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
let message = format!(r#"{{"event":"price_update","user_id":{}}}"#, user_id);
|
||||
broker.publish(message);
|
||||
}
|
||||
}
|
||||
|
||||
conn.prepare("delete_old_productions", QUERY_DELETE_OLD_PRODUCTIONS)?;
|
||||
conn.execute("delete_old_productions", &[])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_regional_sell_price_inner(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("update_region_sell_price", QUERY_UPDATE_REGION_SELL_PRICE)?;
|
||||
conn.execute("update_region_sell_price", &[])?;
|
||||
|
||||
conn.prepare("get_sell_regions", QUERY_GET_SELL_REGIONS)?;
|
||||
let regions = conn.execute("get_sell_regions", &[])?;
|
||||
|
||||
for row in regions {
|
||||
if let Some(region_id) = row.get("region_id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
let message =
|
||||
format!(r#"{{"event":"price_update","region_id":{}}}"#, region_id);
|
||||
broker.publish(message);
|
||||
}
|
||||
}
|
||||
|
||||
conn.prepare("delete_region_sell_price", QUERY_DELETE_REGION_SELL_PRICE)?;
|
||||
conn.execute("delete_region_sell_price", &[])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_marriages_inner(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("set_marriages_by_party", QUERY_SET_MARRIAGES_BY_PARTY)?;
|
||||
let rows = conn.execute("set_marriages_by_party", &[])?;
|
||||
|
||||
for row in rows {
|
||||
if let Some(uid) =
|
||||
row.get("character1_user").and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
let msg =
|
||||
format!(r#"{{"event":"relationship_changed","user_id":{}}}"#, uid);
|
||||
broker.publish(msg);
|
||||
}
|
||||
if let Some(uid) =
|
||||
row.get("character2_user").and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
let msg =
|
||||
format!(r#"{{"event":"relationship_changed","user_id":{}}}"#, uid);
|
||||
broker.publish(msg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_studying_inner(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(
|
||||
"get_studyings_to_execute",
|
||||
QUERY_GET_STUDYINGS_TO_EXECUTE,
|
||||
)?;
|
||||
conn.prepare("set_learning_done", QUERY_SET_LEARNING_DONE)?;
|
||||
|
||||
let studies = conn.execute("get_studyings_to_execute", &[])?;
|
||||
|
||||
for study in studies {
|
||||
let tr = study.get("tr").cloned().unwrap_or_default();
|
||||
|
||||
match tr.as_str() {
|
||||
"self" => Self::calculate_studying_self(pool, broker, &study)?,
|
||||
"children" | "director" => {
|
||||
Self::calculate_studying_for_associated_character(
|
||||
pool, broker, &study,
|
||||
)?
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(id) = study.get("id").and_then(|v| v.parse::<i32>().ok()) {
|
||||
conn.execute("set_learning_done", &[&id])?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_studying_self(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
entry: &Row,
|
||||
) -> Result<(), DbError> {
|
||||
let falukant_user_id = match entry
|
||||
.get("associated_falukant_user_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
{
|
||||
Some(id) => id,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let (learn_all, product_id) = study_scope(entry);
|
||||
let character_id = Self::get_own_character_id(pool, falukant_user_id)?;
|
||||
|
||||
if let Some(cid) = character_id {
|
||||
Self::calculate_studying_character(
|
||||
pool,
|
||||
broker,
|
||||
cid,
|
||||
learn_all,
|
||||
product_id,
|
||||
parse_i32(entry, "learning_recipient_id", -1),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_studying_for_associated_character(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
entry: &Row,
|
||||
) -> Result<(), DbError> {
|
||||
let character_id = parse_i32(entry, "associated_learning_character_id", -1);
|
||||
if character_id < 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (learn_all, product_id) = study_scope(entry);
|
||||
let recipient_id = parse_i32(entry, "learning_recipient_id", -1);
|
||||
|
||||
Self::calculate_studying_character(
|
||||
pool,
|
||||
broker,
|
||||
character_id,
|
||||
learn_all,
|
||||
product_id,
|
||||
recipient_id,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_own_character_id(
|
||||
pool: &ConnectionPool,
|
||||
falukant_user_id: i32,
|
||||
) -> Result<Option<i32>, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("get_own_character_id", QUERY_GET_OWN_CHARACTER_ID)?;
|
||||
let rows = conn.execute("get_own_character_id", &[&falukant_user_id])?;
|
||||
|
||||
Ok(rows
|
||||
.get(0)
|
||||
.and_then(|r| r.get("id"))
|
||||
.and_then(|v| v.parse::<i32>().ok()))
|
||||
}
|
||||
|
||||
fn calculate_studying_character(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
character_id: i32,
|
||||
learn_all: bool,
|
||||
product_id: Option<i32>,
|
||||
falukant_user_id: i32,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
if learn_all {
|
||||
conn.prepare(
|
||||
"increase_all_products_knowledge",
|
||||
QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE,
|
||||
)?;
|
||||
conn.execute(
|
||||
"increase_all_products_knowledge",
|
||||
&[&1_i32, &character_id],
|
||||
)?;
|
||||
} else if let Some(pid) = product_id {
|
||||
conn.prepare(
|
||||
"increase_one_product_knowledge",
|
||||
QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE,
|
||||
)?;
|
||||
conn.execute(
|
||||
"increase_one_product_knowledge",
|
||||
&[&5_i32, &character_id, &pid],
|
||||
)?;
|
||||
}
|
||||
|
||||
let message =
|
||||
format!(r#"{{"event":"knowledge_updated","user_id":{}}}"#, falukant_user_id);
|
||||
broker.publish(message);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Worker for ValueRecalculationWorker {
|
||||
fn start_worker_thread(&mut self) {
|
||||
let pool = self.base.pool.clone();
|
||||
let broker = self.base.broker.clone();
|
||||
|
||||
self.base
|
||||
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
||||
ValueRecalculationWorker::run_loop(pool.clone(), broker.clone(), state);
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
|
||||
fn should_run_interval(
|
||||
last_run: Option<Instant>,
|
||||
now: Instant,
|
||||
interval: Duration,
|
||||
) -> bool {
|
||||
match last_run {
|
||||
None => true,
|
||||
Some(prev) => now.saturating_duration_since(prev) >= interval,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_i32(row: &Row, key: &str, default: i32) -> i32 {
|
||||
row.get(key)
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn study_scope(entry: &Row) -> (bool, Option<i32>) {
|
||||
let learn_all_flag =
|
||||
entry.get("learn_all_products").map(|v| v == "t").unwrap_or(false);
|
||||
let product_id_str = entry.get("product_id").cloned().unwrap_or_default();
|
||||
|
||||
if learn_all_flag || product_id_str.is_empty() {
|
||||
(true, None)
|
||||
} else {
|
||||
let pid = product_id_str.parse::<i32>().ok();
|
||||
match pid {
|
||||
Some(id) => (false, Some(id)),
|
||||
None => (true, None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user